Files
obsidian_ollama/src/main.ts
T
fegger 1ccd637149 fix: sanitize ChromaDB URL and update docker-compose CORS config
- src/semantic-cache.ts: Add robust URL sanitization in initialize().
  Trim whitespace and reject malformed URLs (e.g. empty host like
  'http://:8666') before passing to ChromaClient.

- src/main.ts: Validate ChromaDB URL in the settings tab onChange.
  Fall back to 'http://localhost:8000' if the value is empty or lacks
  '://'.

- docker-compose.yml: Add CHROMA_SERVER_CORS_ALLOW_ORIGINS=["*"] to
  allow cross-origin requests from Obsidian's app://obsidian.md origin.
2026-05-19 21:59:35 +02:00

250 lines
8.0 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Plugin, WorkspaceLeaf, App, Setting, Notice, PluginSettingTab } from 'obsidian';
import { ChatView } from './chat-view';
import { DEFAULT_SETTINGS } from './constants';
import { SemanticCacheService } from './semantic-cache';
import { PluginSettings } from './types';
export default class OllamaPlugin extends Plugin {
settings: PluginSettings = DEFAULT_SETTINGS;
semanticCache?: SemanticCacheService;
async onload() {
await this.loadSettings();
// Register the chat view
this.registerView(
'ollama-chat-view',
(leaf: WorkspaceLeaf) => new ChatView(leaf, this.settings)
);
// Add a ribbon icon in the left sidebar
this.addRibbonIcon('bot', 'Open Ollama Chat', async () => {
await this.activateChatView();
});
// Add a command to open the chat view
this.addCommand({
id: 'open-ollama-chat',
name: 'Open Ollama Chat',
callback: async () => {
await this.activateChatView();
},
});
// Add a command to clear the semantic cache
this.addCommand({
id: 'clear-semantic-cache',
name: 'Clear Semantic Cache',
callback: async () => {
await this.clearSemanticCache();
new Notice('Semantic cache cleared.');
},
});
// Add a settings tab
this.addSettingTab(new OllamaSettingTab(this.app, this));
// Initialize the semantic cache
if (this.settings.cacheConfig) {
this.semanticCache = new SemanticCacheService(
this.settings.ollamaUrl,
this.settings.cacheConfig
);
try {
await this.semanticCache.initialize();
} catch {
new Notice('Semantic cache initialization failed. Check console for details.');
}
}
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onunload() {
// Clean up any active semantic cache resources on plugin unload
// Using fire-and-forget pattern since onunload cannot be async per Obsidian API
if (this.semanticCache) {
void this.semanticCache.clearCache();
}
// No explicit unregisterView needed; relying on Obsidian lifecycle management.
}
async loadSettings() {
const loadedSettings = ((await this.loadData()) ?? {}) as Partial<PluginSettings>;
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings);
}
async saveSettings() {
await this.saveData(this.settings);
}
async activateChatView() {
const existing = this.app.workspace.getLeavesOfType('ollama-chat-view');
if (existing.length > 0) {
await this.app.workspace.revealLeaf(existing[0]);
} else {
const leaf = this.app.workspace.getRightLeaf(false);
if (leaf) {
await leaf.setViewState({
type: 'ollama-chat-view',
active: true,
});
}
}
}
async clearSemanticCache() {
if (this.semanticCache) {
await this.semanticCache.clearCache();
}
}
notifyChatViews() {
const leaves = this.app.workspace.getLeavesOfType('ollama-chat-view');
leaves.forEach((leaf) => {
if (leaf.view instanceof ChatView) {
leaf.view.updateSettings(this.settings);
}
});
}
}
class OllamaSettingTab extends PluginSettingTab {
plugin: OllamaPlugin;
constructor(app: App, plugin: OllamaPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Ollama Settings' });
new Setting(containerEl)
.setName('Ollama URL')
.setDesc('URL for your Ollama instance (default: http://localhost:11434)')
.addText((text) =>
text.setValue(this.plugin.settings.ollamaUrl).onChange(async (value) => {
this.plugin.settings.ollamaUrl = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Model')
.setDesc('Ollama model to use (default: llama3)')
.addText((text) =>
text.setValue(this.plugin.settings.model).onChange(async (value) => {
this.plugin.settings.model = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Vault Search Limit')
.setDesc('Maximum number of vault entries to include in context (default: 3)')
.addText((text) =>
text.setValue(String(this.plugin.settings.vaultSearchLimit)).onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed > 0) {
this.plugin.settings.vaultSearchLimit = parsed;
await this.plugin.saveSettings();
} else {
new Notice('Vault search limit must be a positive integer.');
}
})
);
new Setting(containerEl)
.setName('Max Message History')
.setDesc('Maximum number of messages to keep in conversation history (default: 50)')
.addText((text) =>
text.setValue(String(this.plugin.settings.maxMessageHistory)).onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed > 0) {
this.plugin.settings.maxMessageHistory = parsed;
await this.plugin.saveSettings();
} else {
new Notice('Max message history must be a positive integer.');
}
})
);
new Setting(containerEl)
.setName('Enable Semantic Cache')
.setDesc('Use semantic cache to store and retrieve previous responses')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.cacheConfig.enabled).onChange(async (value) => {
this.plugin.settings.cacheConfig.enabled = value;
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
})
);
new Setting(containerEl)
.setName('ChromaDB URL')
.setDesc('URL for your ChromaDB instance (default: http://localhost:8000)')
.addText((text) =>
text
.setValue(this.plugin.settings.cacheConfig.chromaURL || 'http://localhost:8000')
.onChange(async (value) => {
const trimmed = value.trim();
// Ensure a valid-looking URL; fall back to default if empty or malformed
this.plugin.settings.cacheConfig.chromaURL =
trimmed && trimmed.includes('://') ? trimmed : 'http://localhost:8000';
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Cache Embedding Model')
.setDesc('Ollama model used to generate embeddings for the semantic cache')
.addText((text) =>
text.setValue(this.plugin.settings.cacheConfig.embeddingModel).onChange(async (value) => {
this.plugin.settings.cacheConfig.embeddingModel = value;
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
})
);
new Setting(containerEl)
.setName('Cache Similarity Threshold')
.setDesc(
'Minimum cosine similarity (01) for a cache hit. Higher values require closer matches.'
)
.addText((text) =>
text
.setValue(String(this.plugin.settings.cacheConfig.similarityThreshold))
.onChange(async (value) => {
const parsed = parseFloat(value);
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) {
this.plugin.settings.cacheConfig.similarityThreshold = parsed;
await this.plugin.saveSettings();
} else {
new Notice('Similarity threshold must be a number between 0 and 1.');
}
})
);
new Setting(containerEl)
.setName('Clear Semantic Cache')
.setDesc('Delete all cached responses from ChromaDB')
.addButton((button) =>
button.setButtonText('Clear Cache').onClick(async () => {
try {
await this.plugin.clearSemanticCache();
new Notice('Semantic cache cleared.');
} catch {
new Notice('Failed to clear semantic cache. Is ChromaDB running?');
}
})
);
}
hide() {
// Clear the container to prevent duplicate elements
this.containerEl.empty();
}
}