From 9367811c5a85c54c25450e9d05da48e9b7894106 Mon Sep 17 00:00:00 2001 From: Florian Egger Date: Tue, 19 May 2026 18:23:24 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20switch=20to=20esbuild=20bundling=20?= =?UTF-8?q?=E2=80=94=20single=20main.js=20at=20plugin=20root?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root cause: Obsidian's plugin loader expects main.js at the plugin root alongside manifest.json. The previous 'dist/' output + shim approach caused 'Cannot find module ./dist/main.js' because dist/ was either missing or not resolved correctly in Obsidian's module loader. Changes: - build: use esbuild to bundle all source into a single main.js (61KB) tsc --noEmit for type checking; esbuild for the actual bundle - main.js: no longer a shim — it's the fully bundled plugin - package.json: added esbuild as devDependency; obsidian moved to devDeps - install.sh: remove dist/ copy step, add cleanup of old dist/ from vault - README.md: updated manual install steps to reflect bundling --- README.md | 5 +- install.sh | 9 +- main.js | 1805 ++++++++++++++++++++++++++++++++++++++++++++- package-lock.json | 592 ++++++++++++--- package.json | 3 +- 5 files changed, 2309 insertions(+), 105 deletions(-) diff --git a/README.md b/README.md index 01f770e..9f558ce 100755 --- a/README.md +++ b/README.md @@ -66,11 +66,12 @@ Then copy the plugin into your vault: mkdir -p /path/to/vault/.obsidian/plugins/ollama-plugin cp manifest.json /path/to/vault/.obsidian/plugins/ollama-plugin/ cp main.js /path/to/vault/.obsidian/plugins/ollama-plugin/ -cp -r dist /path/to/vault/.obsidian/plugins/ollama-plugin/ +# Remove old dist/ from previous installs (no longer needed with bundling) +rm -rf /path/to/vault/.obsidian/plugins/ollama-plugin/dist cp -r node_modules/chromadb /path/to/vault/.obsidian/plugins/ollama-plugin/node_modules/ # optional: only needed for semantic cache ``` -> **Note:** The `obsidian` npm package is a dev-only type stub — Obsidian provides its own API at runtime. The `chromadb` package is only needed if you enable the semantic cache feature. +> **Note:** The plugin is now bundled into a single `main.js` via esbuild. The `obsidian` npm package is a dev-only type stub — Obsidian provides its own API at runtime. The `chromadb` package is only needed if you enable the semantic cache feature. ### After installation diff --git a/install.sh b/install.sh index 9817ee6..0246ae2 100755 --- a/install.sh +++ b/install.sh @@ -92,10 +92,12 @@ step "Installing into vault" mkdir -p "$PLUGIN_DIR" -# Copy manifest, entry shim, and built output +# Remove old dist/ directory from previous installations (no longer needed with bundling) +rm -rf "$PLUGIN_DIR/dist" + +# Copy manifest and bundled entry point cp "$SCRIPT_DIR/manifest.json" "$PLUGIN_DIR/" cp "$SCRIPT_DIR/main.js" "$PLUGIN_DIR/" -cp -r "$SCRIPT_DIR/dist" "$PLUGIN_DIR/" # Copy only necessary runtime dependencies (obsidian and chromadb stubs) # The obsidian package is only used for type definitions at build time — @@ -112,10 +114,9 @@ info "Plugin installed to $PLUGIN_DIR" step "Verifying installation" -if [ -f "$PLUGIN_DIR/manifest.json" ] && [ -f "$PLUGIN_DIR/main.js" ] && [ -f "$PLUGIN_DIR/dist/main.js" ]; then +if [ -f "$PLUGIN_DIR/manifest.json" ] && [ -f "$PLUGIN_DIR/main.js" ]; then info "manifest.json ✓" info "main.js ✓" - info "dist/main.js ✓" else error "Installation verification failed — missing files in $PLUGIN_DIR" exit 1 diff --git a/main.js b/main.js index c01fb26..5b55966 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,1801 @@ -// Shim entry point — Obsidian expects main.js at the plugin root. -// Re-exports the compiled plugin from dist/, handling both default and named exports. -const plugin = require('./dist/main.js'); -module.exports = plugin.default || plugin; +"use strict"; +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/main.ts +var main_exports = {}; +__export(main_exports, { + default: () => OllamaPlugin +}); +module.exports = __toCommonJS(main_exports); +var import_obsidian4 = require("obsidian"); + +// src/chat-view.ts +var import_obsidian3 = require("obsidian"); + +// src/types.ts +var OllamaError = class _OllamaError extends Error { + constructor(message, type) { + super(message); + this.type = type; + Object.setPrototypeOf(this, _OllamaError.prototype); + } +}; +var NetworkError = class _NetworkError extends OllamaError { + constructor(message, statusCode) { + super(message, "network_error" /* NETWORK_ERROR */); + this.statusCode = statusCode; + Object.setPrototypeOf(this, _NetworkError.prototype); + } +}; +var ApiError = class _ApiError extends OllamaError { + constructor(message, statusCode) { + super(message, "api_error" /* API_ERROR */); + this.statusCode = statusCode; + Object.setPrototypeOf(this, _ApiError.prototype); + } +}; +var ValidationError = class _ValidationError extends OllamaError { + constructor(message, details) { + super(message, "validation_error" /* VALIDATION_ERROR */); + this.details = details; + Object.setPrototypeOf(this, _ValidationError.prototype); + } +}; +var StreamingError = class _StreamingError extends OllamaError { + constructor(message) { + super(message, "streaming_error" /* STREAMING_ERROR */); + Object.setPrototypeOf(this, _StreamingError.prototype); + } +}; +var ToolExecutionError = class _ToolExecutionError extends OllamaError { + constructor(message, toolName = "unknown") { + super(message, "tool_execution_error" /* TOOL_EXECUTION_ERROR */); + this.toolName = toolName; + Object.setPrototypeOf(this, _ToolExecutionError.prototype); + } +}; +var PathValidationError = class _PathValidationError extends OllamaError { + constructor(message, path = "") { + super(message, "path_validation_error" /* PATH_VALIDATION_ERROR */); + this.path = path; + Object.setPrototypeOf(this, _PathValidationError.prototype); + } +}; + +// src/utils.ts +var SEVERITY_ORDER = { + debug: 0 /* DEBUG */, + info: 1 /* INFO */, + warn: 2 /* WARN */, + error: 3 /* ERROR */ +}; +var _Logger = class _Logger { + static setLevel(level) { + if (typeof level === "string") { + const lowerLevel = level.toLowerCase(); + _Logger.minLevel = SEVERITY_ORDER[lowerLevel] ?? 0 /* DEBUG */; + } else { + _Logger.minLevel = level; + } + } + static debug(message, category = "general") { + if (0 /* DEBUG */ >= _Logger.minLevel) { + console.debug(`[${category}] DEBUG: ${message}`); + } + } + static info(message, category = "general") { + if (1 /* INFO */ >= _Logger.minLevel) { + console.info(`[${category}] INFO: ${message}`); + } + } + static warn(message, category = "general") { + if (2 /* WARN */ >= _Logger.minLevel) { + console.warn(`[${category}] WARN: ${message}`); + } + } + static error(message, category = "general") { + if (3 /* ERROR */ >= _Logger.minLevel) { + console.error(`[${category}] ERROR: ${message}`); + } + } +}; +_Logger.minLevel = 0 /* DEBUG */; +var Logger = _Logger; +var MAX_JSON_SIZE = 1e6; +var MAX_JSON_NESTING = 24; +function countNestingDepth(value, depth = 0) { + if (depth > MAX_JSON_NESTING) { + return depth; + } + if (Array.isArray(value)) { + return Math.max(...value.map((item) => countNestingDepth(item, depth + 1)), depth); + } + if (value !== null && typeof value === "object") { + const entries = Object.values(value); + if (entries.length === 0) return depth; + return Math.max(...entries.map((item) => countNestingDepth(item, depth + 1)), depth); + } + return depth; +} +function safeParseJson(jsonString) { + if (typeof jsonString !== "string") { + throw new Error("Input must be a string"); + } + if (jsonString.length > MAX_JSON_SIZE) { + throw new Error("JSON input too large"); + } + let parsed; + try { + parsed = JSON.parse(jsonString); + } catch { + throw new Error("Invalid JSON"); + } + const checkDangerousPatterns = (obj) => { + if (typeof obj !== "object" || obj === null) { + return false; + } + const dangerousKeys = ["constructor", "prototype", "__proto__"]; + if (dangerousKeys.some((key) => Object.keys(obj).includes(key))) { + return true; + } + const record = obj; + for (const key of Object.keys(obj)) { + if (checkDangerousPatterns(record[key])) { + return true; + } + } + return false; + }; + if (checkDangerousPatterns(parsed)) { + throw new Error("dangerous code pattern detected"); + } + if (countNestingDepth(parsed) > MAX_JSON_NESTING) { + throw new Error("JSON nesting too deep"); + } + return parsed; +} + +// src/semantic-cache.ts +var SemanticCacheService = class _SemanticCacheService { + constructor(ollamaURL, config) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.client = null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.collection = null; + this.ollamaURL = ollamaURL.replace(/\/+$/, ""); + this.config = config; + } + async initialize() { + if (!this.config.enabled) return; + try { + const { ChromaClient } = await import("chromadb"); + const chromaURL = this.config.chromaURL || "http://localhost:8000"; + this.client = new ChromaClient({ path: chromaURL }); + this.collection = await this.client.getOrCreateCollection({ + name: this.config.collectionName, + metadata: { "hnsw:space": "cosine" } + }); + Logger.info(`Semantic cache initialized: ${this.config.collectionName}`, "semantic-cache"); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.error(`Failed to initialize semantic cache: ${errorMessage}`, "semantic-cache"); + throw error; + } + } + async getCache(query) { + if (!this.config.enabled || !this.collection) return null; + try { + const results = await this.collection.query({ + query_embeddings: await this.generateEmbedding(query), + n_results: 1, + where: { source: "ollama" } + }); + if (results.ids[0] && results.ids[0].length > 0) { + if (results.distances[0] && results.distances[0][0] > this.config.similarityThreshold) { + return results.documents[0][0]; + } + } + return null; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.warn(`Cache lookup failed: ${errorMessage}`, "semantic-cache"); + return null; + } + } + static generateId() { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + return "cache_" + Date.now() + "_" + Math.random().toString(36).substring(2, 11); + } + async setCache(query, response) { + if (!this.config.enabled || !this.collection) return; + try { + await this.collection.upsert({ + ids: [_SemanticCacheService.generateId()], + documents: [response], + embeddings: await this.generateEmbedding(query), + metadatas: [{ source: "ollama" }] + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.warn(`Cache set failed: ${errorMessage}`, "semantic-cache"); + } + } + async clearCache() { + if (!this.config.enabled || !this.collection) return; + try { + await this.collection.reset(); + Logger.info("Semantic cache cleared", "semantic-cache"); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.error(`Failed to clear semantic cache: ${errorMessage}`, "semantic-cache"); + } + } + async generateEmbedding(text) { + const response = await fetch(`${this.ollamaURL}/api/embeddings`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + model: this.config.embeddingModel, + prompt: text + }) + }); + if (!response.ok) { + throw new Error(`Failed to generate embedding: ${response.status} ${response.statusText}`); + } + const data = await response.json(); + return data.embedding; + } +}; + +// src/ollama-client.ts +var OllamaClient = class { + constructor(baseURL, model, fetchFn, cacheConfig) { + this.maxRetries = 3; + this.maxMalformedChunks = 50; + this.currentStreamController = null; + this.baseURL = baseURL; + this.model = model; + this.fetchFn = fetchFn ?? fetch; + if (cacheConfig?.enabled) { + this.cacheService = new SemanticCacheService(baseURL, cacheConfig); + void this.cacheService.initialize(); + } + } + async initializeCache() { + if (this.cacheService) { + await this.cacheService.initialize(); + } + } + async clearCache() { + if (this.cacheService) { + await this.cacheService.clearCache(); + } + } + cancelStream() { + if (this.currentStreamController) { + this.currentStreamController.abort(); + this.currentStreamController = null; + } + } + async *streamChat(messages, tools = []) { + if (tools.length > 0) { + yield* this.streamChatWithRetry(messages, tools, 0); + return; + } + const lastUserMsg = [...messages].reverse().find((m) => m.role === "user"); + if (lastUserMsg && this.cacheService) { + const cached = await this.cacheService.getCache(lastUserMsg.content); + if (cached) { + yield { role: "assistant", content: cached, tool_calls: [] }; + return; + } + } + const chunks = []; + for await (const chunk of this.streamChatWithRetry(messages, tools, 0)) { + chunks.push(chunk); + yield chunk; + } + const fullContent = chunks.map((c) => c.content).join(""); + if (this.cacheService && lastUserMsg) { + void this.cacheService.setCache(lastUserMsg.content, fullContent); + } + } + async chat(messages, tools = []) { + if (tools.length > 0) { + return this.chatWithRetry(messages, tools, 0); + } + const lastUserMsg = [...messages].reverse().find((m) => m.role === "user"); + if (lastUserMsg && this.cacheService) { + const cached = await this.cacheService.getCache(lastUserMsg.content); + if (cached) { + return { role: "assistant", content: cached }; + } + } + const response = await this.chatWithRetry(messages, tools, 0); + if (this.cacheService && lastUserMsg) { + void this.cacheService.setCache(lastUserMsg.content, response.content); + } + return response; + } + async streamChatAsPromise(messages, tools = []) { + let content = ""; + let role = "assistant"; + let toolCalls; + for await (const chunk of this.streamChat(messages, tools)) { + role = chunk.role ?? role; + content += chunk.content ?? ""; + if (chunk.tool_calls) { + toolCalls = [...toolCalls ?? [], ...chunk.tool_calls]; + } + } + return { role, content, tool_calls: toolCalls }; + } + async *streamChatWithRetry(messages, tools = [], retryCount) { + const controller = new AbortController(); + this.currentStreamController = controller; + let reader = null; + try { + const response = await this.fetchFn(`${this.baseURL}/api/chat`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + model: this.model, + messages, + tools, + stream: true + }), + signal: controller.signal + }); + if (!response.ok) { + throw new ApiError(`Ollama API error: ${response.status}`, response.status); + } + if (!response.body) { + throw new Error("No response body"); + } + const contentType = response.headers?.get?.("content-type"); + if (contentType && !contentType.includes("application/x-ndjson")) { + throw new Error("Invalid response format"); + } + reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let malformedChunks = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + buffer += decoder.decode(value); + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + for (const line of lines) { + if (line.trim() === "") { + continue; + } + let parsed; + try { + parsed = this.parseChatResponse(line); + } catch (error) { + malformedChunks++; + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.warn( + `Skipped malformed chunk: ${line.slice(0, 50)}... - ${errorMessage}`, + "ollama-client" + ); + if (malformedChunks > this.maxMalformedChunks) { + throw new Error("Too many malformed chunks in Ollama response"); + } + continue; + } + if (parsed.error) { + const errorMsg = typeof parsed.error === "string" ? parsed.error : JSON.stringify(parsed.error); + throw new Error(`Ollama error: ${errorMsg}`); + } + yield this.normalizeMessage(parsed.message); + } + } + if (buffer.trim() !== "") { + let parsed = null; + try { + parsed = this.parseChatResponse(buffer); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.warn( + `Skipped malformed chunk: ${buffer.slice(0, 50)}... - ${errorMessage}`, + "ollama-client" + ); + } + if (parsed?.error) { + const errorMsg = typeof parsed.error === "string" ? parsed.error : JSON.stringify(parsed.error); + throw new Error(`Ollama error: ${errorMsg}`); + } + if (parsed?.message) { + yield this.normalizeMessage(parsed.message); + } + } + } catch (error) { + if (retryCount < this.maxRetries && this.isRetryableError(error, controller)) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.warn( + `Retrying after error (attempt ${retryCount + 1}): ${errorMessage}`, + "ollama-client" + ); + await new Promise((resolve) => setTimeout(resolve, 10 * Math.pow(2, retryCount))); + yield* this.streamChatWithRetry(messages, tools, retryCount + 1); + } else { + throw error; + } + } finally { + reader?.releaseLock(); + if (this.currentStreamController === controller) { + this.currentStreamController = null; + } + } + } + async chatWithRetry(messages, tools = [], retryCount) { + const controller = new AbortController(); + this.currentStreamController = controller; + try { + const response = await this.fetchFn(`${this.baseURL}/api/chat`, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + model: this.model, + messages, + tools, + stream: false + }), + signal: controller.signal + }); + if (!response.ok) { + throw new ApiError(`Ollama API error: ${response.status}`, response.status); + } + const data = await response.json(); + if (!this.isChatResponse(data)) { + return this.normalizeMessage(); + } + return this.normalizeMessage(data.message); + } catch (error) { + if (retryCount < this.maxRetries && this.isRetryableError(error, controller)) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.warn( + `Retrying after error (attempt ${retryCount + 1}): ${errorMessage}`, + "ollama-client" + ); + await new Promise((resolve) => setTimeout(resolve, 10 * Math.pow(2, retryCount))); + return this.chatWithRetry(messages, tools, retryCount + 1); + } else { + throw error; + } + } finally { + if (this.currentStreamController === controller) { + this.currentStreamController = null; + } + } + } + normalizeMessage(message) { + return { + role: message?.role ?? "assistant", + content: message?.content ?? "", + tool_calls: message?.tool_calls ?? [], + tool_call_id: message?.tool_call_id + }; + } + parseChatResponse(raw) { + const parsed = JSON.parse(raw); + if (!this.isChatResponse(parsed)) { + throw new Error("Invalid chat response"); + } + return parsed; + } + isChatResponse(data) { + if (typeof data !== "object" || data === null) { + return false; + } + const response = data; + return (response.error === void 0 || typeof response.error === "string") && (response.message === void 0 || this.isPartialMessage(response.message)); + } + isPartialMessage(data) { + if (typeof data !== "object" || data === null) { + return false; + } + const message = data; + const validRole = message.role === void 0 || message.role === "system" || message.role === "user" || message.role === "assistant" || message.role === "tool"; + return validRole && (message.content === void 0 || typeof message.content === "string") && (message.tool_calls === void 0 || Array.isArray(message.tool_calls)) && (message.tool_call_id === void 0 || typeof message.tool_call_id === "string"); + } + isRetryableError(error, controller) { + if (controller.signal.aborted) { + return false; + } + if (error instanceof ApiError && error.statusCode >= 400 && error.statusCode < 500) { + return false; + } + if (error instanceof Error) { + if (error.name === "AbortError") { + return false; + } + if (error.message.startsWith("Ollama error:") || error.message.includes("Too many malformed chunks") || error.message === "No response body" || error.message === "Invalid response format") { + return false; + } + } + return true; + } +}; + +// src/vault-indexer.ts +var STOP_WORDS = /* @__PURE__ */ new Set([ + "a", + "an", + "the", + "is", + "it", + "in", + "on", + "at", + "to", + "for", + "of", + "and", + "or", + "but", + "with", + "by", + "from", + "up", + "about", + "into", + "this", + "that", + "these", + "those", + "be", + "been", + "being", + "have", + "has", + "had", + "do", + "does", + "did", + "will", + "would", + "could", + "should", + "may", + "might", + "can", + "are", + "was", + "were", + "as", + "so", + "if", + "not", + "no", + "my", + "your", + "our", + "its", + "we", + "you", + "he", + "she", + "they" +]); +var CONTENT_PREVIEW_LENGTH = 500; +var VaultIndexer = class { + constructor(vault, cache) { + this.SCORING_WEIGHTS = { + TITLE: 5, + FRONTMATTER_TITLE: 4, + FRONTMATTER_TAGS: 3, + HEADINGS: 2, + CONTENT: 1 + }; + this.vault = vault; + this.cache = cache; + } + tokenize(text) { + return text.toLowerCase().replace(/[^\w\s]/g, "").split(/\s+/).filter((token) => token.length > 1 && !STOP_WORDS.has(token)); + } + tokenizeContent(content, file) { + const parsed = this.parseMarkdown(content); + const bodyWithoutFrontmatter = content.replace(/^---[\s\S]*?---\n/, ""); + const paragraphs = bodyWithoutFrontmatter.split(/\n\n+/).map((p) => p.trim()).filter((p) => p && !p.startsWith("#")); + const firstParagraph = paragraphs[0] || ""; + return { + title: parsed.title || file.basename, + headings: parsed.headings, + frontmatter: parsed.frontmatter, + firstParagraph, + content: parsed.content, + basename: file.basename + }; + } + calculateWeightedScore(tokenized, queryTokens) { + let score = 0; + for (const token of queryTokens) { + if (tokenized.frontmatter?.title && this.exactMatch(tokenized.frontmatter.title, token)) { + score += this.SCORING_WEIGHTS.FRONTMATTER_TITLE; + } + if (tokenized.basename && this.exactMatch(tokenized.basename, token)) { + score += this.SCORING_WEIGHTS.FRONTMATTER_TITLE; + } + if (tokenized.frontmatter?.tags && this.exactMatch(tokenized.frontmatter.tags, token)) { + score += this.SCORING_WEIGHTS.FRONTMATTER_TAGS; + } + if (tokenized.headings.some((h) => h.toLowerCase().includes(token.toLowerCase()))) { + score += this.SCORING_WEIGHTS.HEADINGS; + } + if (tokenized.content.toLowerCase().includes(token.toLowerCase())) { + score += this.SCORING_WEIGHTS.CONTENT; + } + if (tokenized.title && this.exactMatch(tokenized.title, token)) { + score += this.SCORING_WEIGHTS.TITLE; + } + } + return { score }; + } + async getVaultEntries() { + const files = this.vault.getMarkdownFiles(); + const entries = []; + for (const file of files) { + try { + const content = typeof this.vault.cachedRead === "function" ? await this.vault.cachedRead(file) : await this.vault.read(file); + const parsed = this.parseMarkdown(content); + entries.push({ + file, + title: parsed.frontmatter.title || file.basename, + frontmatter: parsed.frontmatter, + headings: parsed.headings, + content: parsed.content.slice(0, CONTENT_PREVIEW_LENGTH), + basename: file.basename, + score: 0 + }); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.warn(`Failed to read file ${file.path}: ${errorMessage}`, "vault-indexer"); + } + } + return entries; + } + async searchVault(query, limit = 3) { + if (!query || !query.trim()) { + return []; + } + const cacheKey = `query:${query.trim()}:limit:${limit}`; + if (this.cache) { + let cachedResults = null; + try { + cachedResults = await this.cache.get(cacheKey); + } catch { + cachedResults = null; + } + if (cachedResults) { + try { + const parsedResults = JSON.parse(cachedResults); + return parsedResults.slice(0, limit); + } catch { + } + } + } + const queryTokens = this.tokenize(query); + if (queryTokens.length === 0) { + return []; + } + const entries = await this.getVaultEntries(); + const scored = entries.map((entry) => { + const { score } = this.calculateWeightedScore( + { + title: entry.title, + headings: entry.headings, + frontmatter: entry.frontmatter, + firstParagraph: "", + content: entry.content, + basename: entry.basename + }, + queryTokens + ); + return { ...entry, score }; + }).filter((e) => e.score > 0); + scored.sort((a, b) => b.score - a.score); + const results = scored.slice(0, limit); + if (this.cache) { + try { + await this.cache.put(cacheKey, JSON.stringify(results)); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.warn( + `Failed to cache results for query "${query}": ${errorMessage}`, + "vault-indexer" + ); + } + } + return results; + } + stemToken(token) { + if (token.endsWith("ing") && token.length > 4) return token.slice(0, -3); + if (token.endsWith("ed") && token.length > 3) return token.slice(0, -2); + if (token.endsWith("s") && token.length > 2) return token.slice(0, -1); + return token; + } + exactMatch(text, queryToken) { + if (!text) return false; + const textLower = text.toLowerCase(); + const queryLower = queryToken.toLowerCase(); + const queryStem = this.stemToken(queryLower); + return textLower.includes(queryLower) || textLower.includes(queryStem); + } + parseMarkdown(content) { + const frontmatterRegex = /^---\n([\s\S]*?)\n---\n/; + const frontmatterMatch = content.match(frontmatterRegex); + const frontmatter = {}; + if (frontmatterMatch) { + try { + const lines = frontmatterMatch[1].trim().split("\n"); + for (const line of lines) { + const [key, ...valueParts] = line.split(":"); + if (!key) continue; + const value = valueParts.join(":").trim(); + if (key.trim() === "title" && value) frontmatter.title = value; + else if (key.trim() === "tags" && value) frontmatter.tags = value; + } + } catch { + Logger.warn("Failed to parse frontmatter", "vault-indexer"); + } + } + const titleMatch = content.match(/^# (.+)$/m); + const title = titleMatch ? titleMatch[1] : ""; + const headings = []; + const headingRegex = /^#{1,6} (.+)$/gm; + let headingMatch; + while ((headingMatch = headingRegex.exec(content)) !== null) { + headings.push(headingMatch[1]); + } + const bodyWithoutFrontmatter = frontmatterMatch ? content.substring(frontmatterMatch[0].length) : content; + const bodyText = bodyWithoutFrontmatter.replace(/#{1,6} .+/g, "").replace(/^\s*[\r\n]/gm, "").trim(); + return { frontmatter, title, headings, content: bodyText }; + } +}; + +// src/tool-executor.ts +var import_obsidian = require("obsidian"); +var INVALID_PATH_CHARS = /[<>:"|?*~]/; +var MAX_PATH_LENGTH = 200; +var FORBIDDEN_DIRS = [".obsidian", ".git"]; +var ToolExecutor = class { + constructor(vault, app) { + this.vault = vault; + this.app = app; + } + isSafePath(path) { + if (!path || path.trim().length === 0) { + return false; + } + if (path.length > MAX_PATH_LENGTH) { + return false; + } + if (INVALID_PATH_CHARS.test(path)) { + return false; + } + if (path.startsWith("/") || path.startsWith("\\")) { + return false; + } + if (/^[a-zA-Z]:/.test(path)) { + return false; + } + if (path.includes("\\")) { + return false; + } + const normalized = path.replace(/^(\.\/)+/, ""); + if (normalized.split("/").includes("..")) { + return false; + } + for (const dir of FORBIDDEN_DIRS) { + if (normalized.startsWith(`${dir}/`) || normalized.startsWith(`${dir}\\`)) { + return false; + } + if (normalized.includes(`/${dir}/`) || normalized.includes(`\\${dir}\\`)) { + return false; + } + } + return true; + } + async handleToolCall(toolCall) { + try { + const toolName = toolCall.function?.name; + const rawArgs = toolCall.function?.arguments; + if (!toolName) { + throw new Error("Tool name is required"); + } + let parsedArgs; + if (typeof rawArgs === "string") { + try { + parsedArgs = safeParseJson(rawArgs); + } catch { + throw new Error("Invalid JSON arguments"); + } + } else if (rawArgs && typeof rawArgs === "object") { + parsedArgs = rawArgs; + } else { + throw new Error("Arguments must be an object or JSON string"); + } + switch (toolName) { + case "create_file": + return await this.handleCreateFile(parsedArgs); + case "read_vault_file": + return await this.handleReadVaultFile(parsedArgs); + case "search_vault_files": + return this.handleSearchVaultFiles(parsedArgs); + default: + return { success: false, message: `Unknown tool: ${toolName}` }; + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(errorMessage); + } + } + async handleCreateFile(args) { + const path = args.path; + const content = args.content; + if (typeof path !== "string") { + throw new Error("Path must be a string"); + } + if (typeof content !== "string") { + throw new Error("Content must be a string"); + } + if (!this.isSafePath(path)) { + throw new Error("Invalid file path detected"); + } + try { + await this.vault.create(path, content); + return { success: true, message: "File created successfully" }; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + throw new Error(errorMessage); + } + } + async executeTool(name, args) { + return this.handleToolCall({ + id: crypto.randomUUID(), + type: "function", + function: { + name, + arguments: args + } + }); + } + async handleReadVaultFile(args) { + const path = args.path; + if (typeof path !== "string") { + throw new Error("Path must be a string"); + } + if (!this.isSafePath(path)) { + throw new Error("Invalid file path detected"); + } + const file = this.vault.getAbstractFileByPath(path); + if (!(file instanceof import_obsidian.TFile)) { + throw new Error(`File not found: ${path}`); + } + const content = await this.vault.cachedRead(file); + return { + success: true, + message: "File read successfully", + data: { path, content } + }; + } + handleSearchVaultFiles(args) { + const query = args.query; + const limitArg = args.limit; + if (typeof query !== "string") { + throw new Error("Query must be a string"); + } + const limit = typeof limitArg === "number" && Number.isFinite(limitArg) ? limitArg : 10; + const normalizedQuery = query.toLowerCase(); + const files = this.vault.getMarkdownFiles().filter((file) => file.path.toLowerCase().includes(normalizedQuery)).slice(0, limit).map((file) => ({ path: file.path, basename: file.basename })); + return { + success: true, + message: `Found ${files.length} matching files`, + data: files + }; + } +}; + +// src/conversation-state.ts +var ConversationStateManager = class { + constructor() { + this.shortTermContext = []; + this.mediumTermContext = []; + this.longTermContext = []; + this.maxShortTermTurns = 10; + this.maxMediumTermMessages = 20; + this.longTermContext = [ + { + role: "system", + content: `You are an assistant that can help answer questions using the contents of a vault. + The user can ask questions about their vault contents, and you should provide helpful responses based on the files. + When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool. + Only use the tools if you need to access vault content that is not already in the context.` + } + ]; + } + /** + * Updates the short-term context with a new message + * @param message The message to add to short-term context + */ + updateShortTermContext(message) { + this.shortTermContext.push(message); + if (this.shortTermContext.length > this.maxShortTermTurns) { + this.shortTermContext = this.shortTermContext.slice(-this.maxShortTermTurns); + } + } + /** + * Updates the medium-term context with a new message + * @param message The message to add to medium-term context + */ + updateMediumTermContext(message) { + this.mediumTermContext.push(message); + if (this.mediumTermContext.length > this.maxMediumTermMessages) { + this.mediumTermContext = this.mediumTermContext.slice(-this.maxMediumTermMessages); + } + } + /** + * Sets the user's persona or core knowledge as long-term context + * @param personaContent The persona or core knowledge content + */ + setPersona(personaContent) { + this.longTermContext = this.longTermContext.filter( + (msg) => msg.role !== "system" || !msg.content.includes( + "You are an assistant that can help answer questions using the contents of a vault" + ) + ); + this.longTermContext.push({ + role: "system", + content: personaContent + }); + } + /** + * Gets the combined conversation context for the current turn + * @param userMessage The user's current message + * @returns Complete conversation context with all three layers + */ + getConversationContext(_userMessage) { + return { + shortTermContext: this.shortTermContext, + mediumTermContext: this.mediumTermContext, + longTermContext: this.longTermContext + }; + } + /** + * Gets the complete messages array for sending to the LLM + * @param userMessage The user's current message + * @returns Complete message array for the LLM + */ + getCompleteMessages(userMessage) { + const userMessageWithContext = { + role: "user", + content: userMessage + }; + return [ + ...this.longTermContext, + ...this.mediumTermContext, + ...this.shortTermContext, + userMessageWithContext + ]; + } + /** + * Clears all conversation context + */ + clear() { + this.shortTermContext = []; + this.mediumTermContext = []; + this.longTermContext = [ + { + role: "system", + content: `You are an assistant that can help answer questions using the contents of a vault. + The user can ask questions about their vault contents, and you should provide helpful responses based on the files. + When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool. + Only use the tools if you need to access vault content that is not already in the context.` + } + ]; + } + /** + * Sets the medium-term context from a knowledge base query result + * @param queryResult The result from a knowledge base query + */ + setMediumTermContextFromQuery(queryResult) { + this.mediumTermContext = []; + if (queryResult.trim()) { + this.mediumTermContext.push({ + role: "system", + content: `Knowledge base results for current query: +${queryResult}` + }); + } + } + /** + * Gets the current short-term context + */ + getShortTermContext() { + return [...this.shortTermContext]; + } + /** + * Gets the current medium-term context + */ + getMediumTermContext() { + return [...this.mediumTermContext]; + } + /** + * Gets the current long-term context + */ + getLongTermContext() { + return [...this.longTermContext]; + } +}; + +// src/error-handler.ts +var import_obsidian2 = require("obsidian"); +var ErrorHandler = class { + static handleError(error, context) { + const message = this.getUserFriendlyMessage(error); + new import_obsidian2.Notice(message); + if (error instanceof Error) { + const ctx = context ? ` [${context}]` : ""; + console.error(`Ollama Plugin Error${ctx}: ${error.message}`); + if (error.stack) { + console.error(error.stack); + } + } else { + const ctx = context ? ` [${context}]` : ""; + console.error(`Ollama Plugin Error${ctx}:`, error); + } + } + static getUserFriendlyMessage(error) { + if (error instanceof OllamaError) { + return this.getUserFriendlyMessageFromOllamaError(error); + } + if (error instanceof Error) { + return this.getUserFriendlyMessageFromError(error); + } + return "An unexpected error occurred"; + } + static getUserFriendlyMessageFromOllamaError(error) { + switch (error.type) { + case "network_error" /* NETWORK_ERROR */: + return "Connection error. Please check if Ollama is running."; + case "api_error" /* API_ERROR */: + return `API error: ${error.message}`; + case "validation_error" /* VALIDATION_ERROR */: + return this.getUserFriendlyValidationMessage(error); + case "streaming_error" /* STREAMING_ERROR */: + return "Response too long. Please try a shorter request."; + case "tool_execution_error" /* TOOL_EXECUTION_ERROR */: + return `Tool error for ${error.toolName}. ${error.message}`; + case "path_validation_error" /* PATH_VALIDATION_ERROR */: + return `Invalid file path: ${error.path}`; + case "unknown_error" /* UNKNOWN_ERROR */: + return "An unexpected error occurred"; + default: + return "An unexpected error occurred"; + } + } + static getUserFriendlyValidationMessage(error) { + if (error instanceof ValidationError && error.details?.field) { + const fieldMsg = error.details.field.charAt(0).toUpperCase() + error.details.field.slice(1); + return `Invalid ${fieldMsg.toLowerCase()}. ${error.details.message ?? error.message}`; + } + return "Input validation error. Please correct your input."; + } + static getUserFriendlyMessageFromError(error) { + const msg = error.message.toLowerCase(); + if (msg.includes("timeout") || msg.includes("timed out") || msg.includes("time out")) { + return "Request timed out. Please check your Ollama connection."; + } + if (msg.includes("network") || msg.includes("connection") || msg.includes("fetch")) { + return "Connection error. Please check if Ollama is running."; + } + if (msg.includes("validation") || msg.includes("invalid")) { + return "Invalid input. Please correct your input."; + } + if (msg.includes("stream") || msg.includes("chunk")) { + return "Response too long. Please try a shorter request."; + } + if (msg.includes("tool") || msg.includes("function")) { + return "Tool error. Please try again."; + } + if (msg.includes("path") || msg.includes("file")) { + return "Invalid file path. Please check the path and try again."; + } + return "An unexpected error occurred"; + } + // -- Factory methods -- + static createNetworkError(message, statusCode) { + return new NetworkError(message, statusCode); + } + static createApiError(message, statusCode) { + return new ApiError(message, statusCode ?? 500); + } + static createValidationError(message, field) { + const details = field ? { field, message } : void 0; + return new ValidationError(message, details); + } + static createStreamingError(message) { + return new StreamingError(message); + } + static createToolExecutionError(message, toolName) { + return new ToolExecutionError(message, toolName ?? "unknown"); + } + static createPathValidationError(message, path) { + return new PathValidationError(message, path ?? ""); + } + static createUnknownError(message) { + return new OllamaError(message, "unknown_error" /* UNKNOWN_ERROR */); + } +}; + +// src/chat-view.ts +var ChatView = class extends import_obsidian3.ItemView { + constructor(leaf, settings) { + super(leaf); + // State + this.messages = []; + this.lastMessageEl = null; + this.newChatButton = null; + this.sendButton = null; + this.inputEl = null; + this.chatContainer = null; + this.sendButtonClickHandler = null; + this.inputKeyDownHandler = null; + this.newChatButtonClickHandler = null; + this.sendButtonClickWrapper = null; + this.inputKeyDownWrapper = null; + this.newChatButtonClickWrapper = null; + this.listenersAttached = false; + this.messages = []; + this.lastMessageEl = null; + this.newChatButton = null; + this.sendButton = null; + this.inputEl = null; + this.chatContainer = null; + this.sendButtonClickHandler = null; + this.inputKeyDownHandler = null; + this.newChatButtonClickHandler = null; + this.sendButtonClickWrapper = null; + this.inputKeyDownWrapper = null; + this.newChatButtonClickWrapper = null; + this.listenersAttached = false; + this.settings = settings; + this.ollamaClient = new OllamaClient( + settings.ollamaUrl, + settings.model, + void 0, + settings.cacheConfig + ); + this.vaultIndexer = new VaultIndexer(this.app.vault); + this.toolExecutor = new ToolExecutor(this.app.vault, this.app); + this.conversationStateManager = new ConversationStateManager(); + } + // Getters for testing + getSendButtonClickHandler() { + return this.sendButtonClickHandler; + } + getInputKeyDownHandler() { + return this.inputKeyDownHandler; + } + getNewChatButtonClickHandler() { + return this.newChatButtonClickHandler; + } + updateSettings(newSettings) { + this.settings = newSettings; + this.ollamaClient = new OllamaClient( + newSettings.ollamaUrl, + newSettings.model, + void 0, + newSettings.cacheConfig + ); + void this.ollamaClient.initializeCache().catch(() => { + new import_obsidian3.Notice( + "Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings." + ); + }); + } + async clearCache() { + await this.ollamaClient.clearCache(); + } + getViewType() { + return "ollama-chat-view"; + } + getDisplayText() { + return "Ollama Chat"; + } + async onOpen() { + try { + await this.ollamaClient.initializeCache(); + } catch { + new import_obsidian3.Notice( + "Semantic cache unavailable: could not connect to ChromaDB. Check the ChromaDB URL in settings." + ); + } + this.render(); + this.removeEventListeners(); + this.setupEventListeners(); + } + onSettingsChange(newSettings) { + this.updateSettings(newSettings); + } + async onClose() { + this.ollamaClient.cancelStream(); + this.removeEventListeners(); + this.cleanupStreamingResources(); + this.lastMessageEl = null; + this.sendButton = null; + this.inputEl = null; + this.chatContainer = null; + return Promise.resolve(); + } + cleanupStreamingResources() { + const streamingMessage = this.messages.find((msg) => msg.isStreaming); + if (streamingMessage && this.lastMessageEl && this.lastMessageEl.parentElement) { + this.lastMessageEl.parentElement.removeChild(this.lastMessageEl); + this.lastMessageEl = null; + } + } + render() { + const container = this.chatContainer || this.contentEl.createEl("div", { cls: "ollama-chat-container" }); + this.chatContainer = container; + const inputContainer = this.contentEl.querySelector(".ollama-input-container") || this.contentEl.createEl("div", { cls: "ollama-input-container" }); + const newChatContainer = this.contentEl.querySelector(".ollama-new-chat-container") || this.contentEl.createEl("div", { cls: "ollama-new-chat-container" }); + const messagesSnapshot = [...this.messages]; + const nonStreamingMessages = messagesSnapshot.filter((msg) => !msg.isStreaming); + const existingMessages = container.querySelectorAll(".ollama-message"); + for (const el of Array.from(existingMessages)) { + const id = el.getAttribute("data-msg-id"); + if (!id || !nonStreamingMessages.some((m) => m.id === id)) { + el.remove(); + } + } + for (const msg of nonStreamingMessages) { + const existingEl = container.querySelector(`.ollama-message[data-msg-id="${msg.id}"]`); + if (existingEl) { + const contentEl = existingEl.querySelector(".ollama-message-content"); + if (contentEl) { + contentEl.textContent = msg.content; + } + } else { + const messageEl = container.createEl("div", { cls: "ollama-message" }); + messageEl.setAttribute("data-msg-id", msg.id); + messageEl.createEl("div", { cls: "ollama-message-role", text: msg.role }); + const contentEl = messageEl.createEl("div", { cls: "ollama-message-content" }); + contentEl.textContent = msg.content; + } + } + const streamingMessage = messagesSnapshot.find((msg) => msg.isStreaming); + if (streamingMessage && this.lastMessageEl) { + const existingStreamingEl = container.querySelector( + `.ollama-message[data-msg-id="${streamingMessage.id}"]` + ); + if (!existingStreamingEl) { + container.appendChild(this.lastMessageEl); + } + } + if (!this.newChatButton) { + this.newChatButton = newChatContainer.createEl("button", { + cls: "ollama-new-chat-button", + text: "New Chat" + }); + } else { + newChatContainer.appendChild(this.newChatButton); + } + if (!this.inputEl) { + this.inputEl = inputContainer.createEl("textarea", { + cls: "ollama-input", + attr: { placeholder: "Type your message..." } + }); + } else { + inputContainer.appendChild(this.inputEl); + } + if (!this.sendButton) { + this.sendButton = inputContainer.createEl("button", { + cls: "ollama-send-button", + text: "Send" + }); + } else { + inputContainer.appendChild(this.sendButton); + } + this.contentEl.appendChild(newChatContainer); + this.contentEl.appendChild(inputContainer); + this.contentEl.appendChild(container); + this.inputEl.focus(); + } + setupEventListeners() { + if (this.listenersAttached) { + return; + } + this.sendButtonClickHandler = () => { + void this.handleUserInput(this.inputEl?.value); + }; + this.inputKeyDownHandler = (event) => { + if (event.key === "Enter" && !event.shiftKey) { + event.preventDefault(); + void this.handleUserInput(this.inputEl?.value); + } + }; + this.newChatButtonClickHandler = () => { + this.clearConversation(); + }; + if (this.sendButton && this.sendButtonClickHandler) { + this.sendButton.addEventListener("click", this.sendButtonClickHandler); + } + if (this.inputEl && this.inputKeyDownHandler) { + this.inputEl.addEventListener("keydown", this.inputKeyDownHandler); + } + if (this.newChatButton && this.newChatButtonClickHandler) { + this.newChatButton.addEventListener("click", this.newChatButtonClickHandler); + } + this.listenersAttached = true; + } + removeEventListeners() { + if (!this.listenersAttached) { + return; + } + if (this.sendButton && this.sendButtonClickHandler) { + this.sendButton.removeEventListener("click", this.sendButtonClickHandler); + } + if (this.inputEl && this.inputKeyDownHandler) { + this.inputEl.removeEventListener("keydown", this.inputKeyDownHandler); + } + if (this.newChatButton && this.newChatButtonClickHandler) { + this.newChatButton.removeEventListener("click", this.newChatButtonClickHandler); + } + this.listenersAttached = false; + } + clearConversation() { + this.messages = []; + this.conversationStateManager.clear(); + this.render(); + } + updateMessageById(id, updates) { + const index = this.messages.findIndex((m) => m.id === id); + if (index !== -1) { + this.messages[index] = { ...this.messages[index], ...updates }; + this.render(); + } + } + updateLastMessage(updates) { + const streamingMessage = this.messages.find((msg) => msg.isStreaming); + if (streamingMessage) { + const index = this.messages.findIndex((msg) => msg.id === streamingMessage.id); + if (index !== -1) { + this.messages[index] = { ...this.messages[index], ...updates }; + this.render(); + } + } + } + getTools() { + return [ + { + type: "function", + function: { + name: "read_vault_file", + description: "Reads the content of a file from the vault", + parameters: { + type: "object", + properties: { + path: { + type: "string", + description: "The path to the file to read" + }, + content: { + type: "string", + description: "The content of the file to read" + } + }, + required: ["path"] + } + } + }, + { + type: "function", + function: { + name: "search_vault_files", + description: "Searches for files in the vault that match a given query", + parameters: { + type: "object", + properties: { + query: { + type: "string", + description: "The search query to use" + }, + limit: { + type: "number", + description: "The maximum number of results to return" + } + }, + required: ["query"] + } + } + } + ]; + } + buildMessages(userMessageContent, tools) { + const systemContent = `You are an assistant that can help answer questions using the contents of a vault. + The user can ask questions about their vault contents, and you should provide helpful responses based on the files. + When a user asks for information, try to find relevant files using the search_vault_files tool and read their contents with the read_vault_file tool. + Only use the tools if you need to access vault content that is not already in the context.`; + const systemMessage = { + role: "system", + content: systemContent + }; + const userMessage = { + role: "user", + content: userMessageContent + }; + const messages = [systemMessage, userMessage]; + if (tools && tools.length > 0) { + messages.push({ + role: "assistant", + content: "I have access to the following tools to help answer your questions:" + }); + } + return messages; + } + async processToolCalls(toolCalls, messages, tools, fullResponse, assistantMessageId) { + const toolResults = (await Promise.all( + toolCalls.slice(0, MAX_TOOL_CALLS).map(async (toolCall) => { + try { + const toolResult = await this.toolExecutor.handleToolCall(toolCall); + return { ...toolResult, id: toolCall.id }; + } catch (error) { + ErrorHandler.handleError(error, "ChatView.handleUserInput"); + return null; + } + }) + )).filter((result) => result !== null); + const followUpMessages = toolResults.map((result) => { + return { + role: "tool", + content: JSON.stringify(result), + tool_call_id: result.id ?? "" + }; + }); + const followUp = { + role: "assistant", + content: "I have processed your request using the following tools. Here are the results:", + tool_calls: toolCalls + }; + if (followUpMessages.length > 0) { + const finalMessages = [...messages, followUp, ...followUpMessages]; + const response = await this.ollamaClient.chat(finalMessages, tools); + const finalResponse = response.content || fullResponse; + this.updateMessageById(assistantMessageId, { + content: finalResponse, + isStreaming: false + }); + } + } + async handleUserInput(inputValue) { + const userMessage = (inputValue ?? this.inputEl?.value ?? "").trim(); + if (!userMessage) { + return; + } + const MAX_CONTEXT_LENGTH = 2e3; + const tools = this.getTools(); + const messageId = crypto.randomUUID(); + const userMessageId = `${messageId}-user`; + const assistantMessageId = `${messageId}-assistant`; + const userChatMessage = { + id: userMessageId, + role: "user", + content: userMessage, + timestamp: Date.now() + }; + const assistantMessage = { + id: assistantMessageId, + role: "assistant", + content: "", + timestamp: Date.now(), + isStreaming: true + }; + const previousStreamingEl = this.lastMessageEl; + this.messages = [...this.messages, userChatMessage, assistantMessage]; + this.render(); + if (this.inputEl) { + this.inputEl.value = ""; + } + this.lastMessageEl = this.chatContainer?.querySelector(`.ollama-message[data-msg-id="${assistantMessageId}"]`) ?? null; + if (!this.lastMessageEl && previousStreamingEl) { + previousStreamingEl.classList.add("ollama-message"); + previousStreamingEl.setAttribute("data-msg-id", assistantMessageId); + this.contentEl.appendChild(previousStreamingEl); + this.lastMessageEl = previousStreamingEl; + } + try { + const entries = await this.vaultIndexer.searchVault(userMessage, this.settings.vaultSearchLimit); + const context = entries.map((entry) => `${entry.title} +${entry.content}`).join("\n\n").slice(0, MAX_CONTEXT_LENGTH); + const userMessageWithContext = context ? `Relevant vault context: +${context} + +User question: +${userMessage}` : userMessage; + const completeMessages = this.conversationStateManager.getCompleteMessages(userMessageWithContext); + const stream = this.ollamaClient.streamChat(completeMessages, tools); + let fullResponse = ""; + let toolCalls = []; + let chunkCount = 0; + for await (const chunk of stream) { + if (chunk.content) { + fullResponse += chunk.content; + this.updateLastMessage({ + content: fullResponse, + isStreaming: true + }); + } + if (chunk.tool_calls) { + toolCalls = [...toolCalls, ...chunk.tool_calls]; + } + chunkCount++; + if (chunkCount > MAX_STREAM_CHUNKS) { + break; + } + } + if (toolCalls.length > 0) { + await this.processToolCalls( + toolCalls, + completeMessages, + tools, + fullResponse, + assistantMessageId + ); + } + if (toolCalls.length === 0) { + this.updateMessageById(assistantMessageId, { + content: fullResponse, + isStreaming: false + }); + } + this.conversationStateManager.updateShortTermContext({ role: "user", content: userMessage }); + this.conversationStateManager.updateShortTermContext({ + role: "assistant", + content: fullResponse + }); + if (this.messages.length > this.settings.maxMessageHistory) { + this.messages = this.messages.slice(-this.settings.maxMessageHistory); + } + this.render(); + } catch (error) { + ErrorHandler.handleError(error, "ChatView.handleUserInput"); + this.updateMessageById(assistantMessageId, { + content: "An error occurred while processing your request.", + isStreaming: false + }); + } finally { + this.cleanupStreamingResources(); + } + } +}; +var MAX_STREAM_CHUNKS = 1e3; +var MAX_TOOL_CALLS = 5; + +// src/constants.ts +var DEFAULT_SETTINGS = { + ollamaUrl: "http://localhost:11434", + model: "llama3", + vaultSearchLimit: 3, + maxMessageHistory: 50, + lastIndexTime: 0, + cacheConfig: { + enabled: false, + similarityThreshold: 0.85, + collectionName: "ollama_semantic_cache", + embeddingModel: "nomic-embed-text", + chromaURL: "http://localhost:8000" + } +}; + +// src/main.ts +var OllamaPlugin = class extends import_obsidian4.Plugin { + constructor() { + super(...arguments); + this.settings = DEFAULT_SETTINGS; + } + async onload() { + await this.loadSettings(); + this.registerView( + "ollama-chat-view", + (leaf) => new ChatView(leaf, this.settings) + ); + this.addCommand({ + id: "open-ollama-chat", + name: "Open Ollama Chat", + callback: async () => { + await this.activateChatView(); + } + }); + this.addCommand({ + id: "clear-semantic-cache", + name: "Clear Semantic Cache", + callback: async () => { + await this.clearSemanticCache(); + new import_obsidian4.Notice("Semantic cache cleared."); + } + }); + this.addSettingTab(new OllamaSettingTab(this.app, this)); + if (this.settings.cacheConfig) { + this.semanticCache = new SemanticCacheService( + this.settings.ollamaUrl, + this.settings.cacheConfig + ); + try { + await this.semanticCache.initialize(); + } catch { + new import_obsidian4.Notice("Semantic cache initialization failed. Check console for details."); + } + } + } + // eslint-disable-next-line @typescript-eslint/no-misused-promises + onunload() { + if (this.semanticCache) { + void this.semanticCache.clearCache(); + } + } + async loadSettings() { + const loadedSettings = await this.loadData() ?? {}; + 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); + } + }); + } +}; +var OllamaSettingTab = class extends import_obsidian4.PluginSettingTab { + constructor(app, plugin) { + super(app, plugin); + this.plugin = plugin; + } + display() { + const { containerEl } = this; + containerEl.empty(); + containerEl.createEl("h2", { text: "Ollama Settings" }); + new import_obsidian4.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 import_obsidian4.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 import_obsidian4.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 import_obsidian4.Notice("Vault search limit must be a positive integer."); + } + }) + ); + new import_obsidian4.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 import_obsidian4.Notice("Max message history must be a positive integer."); + } + }) + ); + new import_obsidian4.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 import_obsidian4.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) => { + this.plugin.settings.cacheConfig.chromaURL = value; + await this.plugin.saveSettings(); + }) + ); + new import_obsidian4.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 import_obsidian4.Setting(containerEl).setName("Cache Similarity Threshold").setDesc( + "Minimum cosine similarity (0\u20131) 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 import_obsidian4.Notice("Similarity threshold must be a number between 0 and 1."); + } + }) + ); + new import_obsidian4.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 import_obsidian4.Notice("Semantic cache cleared."); + } catch { + new import_obsidian4.Notice("Failed to clear semantic cache. Is ChromaDB running?"); + } + }) + ); + } + hide() { + this.containerEl.empty(); + } +}; diff --git a/package-lock.json b/package-lock.json index b0c5e89..a261eee 100755 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,18 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "chromadb": "^1.5.3", - "node-fetch": "^3.3.2", - "obsidian": "^1.4.11" + "chromadb": "^1.5.3" }, "devDependencies": { "@types/jest": "^29.5.14", "@types/node": "^20.11.19", "@typescript-eslint/eslint-plugin": "^8.59.2", "@typescript-eslint/parser": "^8.59.2", + "esbuild": "^0.28.0", "eslint": "^8.56.0", "jest": "^29.7.0", "jest-environment-jsdom": "^30.3.0", + "obsidian": "^1.4.11", "prettier": "^3.2.5", "ts-jest": "^29.1.2", "typescript": "^5.3.3" @@ -567,6 +567,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -577,6 +578,7 @@ "version": "6.38.6", "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", + "dev": true, "license": "MIT", "peer": true, "dependencies": { @@ -701,6 +703,448 @@ "node": ">=18" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -1559,6 +2003,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "dev": true, "license": "MIT", "peer": true }, @@ -1676,6 +2121,7 @@ "version": "5.60.8", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, "license": "MIT", "dependencies": { "@types/tern": "*" @@ -1685,6 +2131,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, "license": "MIT" }, "node_modules/@types/graceful-fs": { @@ -1768,6 +2215,7 @@ "version": "0.23.9", "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "*" @@ -2635,6 +3083,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, "license": "MIT", "peer": true }, @@ -2667,15 +3116,6 @@ "node": ">=18" } }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/data-urls": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", @@ -2839,6 +3279,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3142,29 +3624,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3230,18 +3689,6 @@ "dev": true, "license": "ISC" }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4932,6 +5379,7 @@ "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -4958,44 +5406,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5044,6 +5454,7 @@ "version": "1.12.3", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", "integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==", + "dev": true, "license": "MIT", "dependencies": { "@types/codemirror": "5.60.8", @@ -5786,6 +6197,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "dev": true, "license": "MIT", "peer": true }, @@ -6192,6 +6604,7 @@ "version": "2.2.8", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "dev": true, "license": "MIT", "peer": true }, @@ -6218,15 +6631,6 @@ "makeerror": "1.0.12" } }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", diff --git a/package.json b/package.json index 66b33bd..f1cfbd5 100755 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "dist/main.js", "scripts": { "test": "jest", - "build": "tsc", + "build": "tsc --noEmit && node -e \"require('esbuild').build({entryPoints:['src/main.ts'],bundle:true,platform:'node',target:'es2020',outfile:'main.js',external:['obsidian','chromadb'],format:'cjs'})\"", "watch": "tsc --watch", "lint": "eslint src --ext .ts", "format": "prettier --write ." @@ -24,6 +24,7 @@ "@types/node": "^20.11.19", "@typescript-eslint/eslint-plugin": "^8.59.2", "@typescript-eslint/parser": "^8.59.2", + "esbuild": "^0.28.0", "eslint": "^8.56.0", "jest": "^29.7.0", "jest-environment-jsdom": "^30.3.0",