diff --git a/manifest.json b/manifest.json index 9b9f53a..c3f19b1 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "smart-note-agent", "name": "Smart Note Agent", - "version": "0.3.2", + "version": "0.3.3", "minAppVersion": "1.7.2", "description": "Agentic AI assistant for reading and editing vault notes.", "author": "Bin Hong", diff --git a/package-lock.json b/package-lock.json index cfc5581..8bca2dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "smart-note-agent", - "version": "0.3.1", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "smart-note-agent", - "version": "0.3.1", + "version": "0.3.3", "license": "ISC", "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/package.json b/package.json index cc0250a..2539c98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "smart-note-agent", - "version": "0.3.2", + "version": "0.3.3", "main": "index.js", "directories": { "doc": "docs" diff --git a/src/agent/agent-loop.ts b/src/agent/agent-loop.ts index 9ba2d15..251bea6 100644 --- a/src/agent/agent-loop.ts +++ b/src/agent/agent-loop.ts @@ -71,7 +71,7 @@ export class AgentLoop { else if (d.type === "error") { console.error("[agent] provider error:", d.error); yield { type: "error", error: d.error }; stoppedEarly = true; break; } else if (d.type === "done") break; } - } catch (e: any) { + } catch (e: unknown) { if (this.abort.signal.aborted) { yield { type: "stopped", reason: "cancelled" }; return; @@ -81,7 +81,8 @@ export class AgentLoop { return; } console.error("[agent] chat exception:", e); - yield { type: "error", error: { kind: e.kind ?? "unknown", message: String(e.message ?? e) } }; + const err = e as { kind?: string; message?: string }; + yield { type: "error", error: { kind: err.kind ?? "unknown", message: String(err.message ?? e) } }; return; } finally { activeWindow.clearTimeout(timer); diff --git a/src/agent/conversation.ts b/src/agent/conversation.ts index 82ea396..1129feb 100644 --- a/src/agent/conversation.ts +++ b/src/agent/conversation.ts @@ -92,7 +92,7 @@ export function parseConversation(md: string): Conversation { const conv = new Conversation({ id: fm.id, title: fm.title || undefined, createdAt: Number(fm.createdAt), - mode: fm.mode as any, provider: fm.provider as any, model: fm.model, + mode: fm.mode as Mode, provider: fm.provider as ProviderId, model: fm.model, }, messages); if (summary !== undefined) conv.summary = summary; diff --git a/src/locales/en.json b/src/locales/en.json index a51986e..a783c07 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -24,8 +24,8 @@ "chat.providerChip": "Active provider / model", "diff.approve": "Approve", "diff.reject": "Reject", - "diff.applyAll": "Apply All", - "diff.rejectAll": "Reject All", + "diff.applyAll": "Apply all", + "diff.rejectAll": "Reject all", "diff.aria": "File diff", "diff.noPreview": "No preview available", "summary.created": "{{count}} created", diff --git a/src/main.ts b/src/main.ts index 56b0456..d1675bb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -74,7 +74,7 @@ export default class ObsidianNoteAgentPlugin extends Plugin { }); } - async startNewConversation() { this.approvalQueue.clear(); this.currentConversation = this.newConversation(); } + startNewConversation() { this.approvalQueue.clear(); this.currentConversation = this.newConversation(); } async openConversation(path: string) { this.currentConversation = await this.conversations.load(path); } cancelCurrentTurn() { this.currentLoop?.cancel(); } @@ -212,7 +212,7 @@ export default class ObsidianNoteAgentPlugin extends Plugin { await this.commitWrite(p); } - private async runScheduled(kind: "daily" | "weekly", cfg: any): Promise { + private async runScheduled(kind: "daily" | "weekly", cfg: { targetFolder: string }): Promise { const prof = activeProfile(this.settings); const provider = createProvider(this.settings.providerId, { apiKey: prof.apiKey, baseUrl: prof.baseUrl, compat: prof.compat }); const conv = new Conversation({ id: `sched_${kind}_${Date.now()}`, mode: "scheduled", provider: this.settings.providerId, model: prof.model }); diff --git a/src/providers/anthropic.ts b/src/providers/anthropic.ts index 53f119c..9f566e8 100644 --- a/src/providers/anthropic.ts +++ b/src/providers/anthropic.ts @@ -1,9 +1,10 @@ -import type { ChatRequest, Delta, LLMProvider } from "./types"; +import type { ChatRequest, Delta, LLMProvider, Message } from "./types"; import { httpSSE } from "./http"; +import type { HttpOptions } from "./http"; import { lookupModelCaps } from "./model-caps"; export interface AnthropicConfig { apiKey: string; baseUrl?: string; } -type SSEIter = (o: any) => AsyncIterable<{ data: string }>; +type SSEIter = (o: HttpOptions) => AsyncIterable<{ data: string }>; export class AnthropicProvider implements LLMProvider { id = "anthropic"; @@ -40,14 +41,20 @@ export class AnthropicProvider implements LLMProvider { }); const blocks: Record = {}; for await (const ev of iter) { - let o: any; try { o = JSON.parse(ev.data); } catch { continue; } - if (o.type === "content_block_start") { + interface AnthropicEvent { + type?: string; + index?: number; + content_block?: { type: string; name?: string; id?: string }; + delta?: { type: string; text?: string; partial_json?: string }; + } + let o: AnthropicEvent; try { o = JSON.parse(ev.data) as AnthropicEvent; } catch { continue; } + if (o.type === "content_block_start" && o.index !== undefined && o.content_block) { blocks[o.index] = { type: o.content_block.type, name: o.content_block.name, id: o.content_block.id, buf: "" }; - } else if (o.type === "content_block_delta") { + } else if (o.type === "content_block_delta" && o.index !== undefined) { const b = blocks[o.index]; if (!b) continue; - if (o.delta.type === "text_delta") yield { type: "text", text: o.delta.text }; - else if (o.delta.type === "input_json_delta") b.buf += o.delta.partial_json; - } else if (o.type === "content_block_stop") { + if (o.delta?.type === "text_delta") yield { type: "text", text: o.delta.text ?? "" }; + else if (o.delta?.type === "input_json_delta") b.buf += o.delta.partial_json ?? ""; + } else if (o.type === "content_block_stop" && o.index !== undefined) { const b = blocks[o.index]; if (b?.type === "tool_use") { let args: Record = {}; try { args = JSON.parse(b.buf || "{}"); } catch { args = { _raw: b.buf }; } @@ -58,7 +65,7 @@ export class AnthropicProvider implements LLMProvider { yield { type: "done" }; } - private toAnthropic(m: any): unknown { + private toAnthropic(m: Message): unknown { if (m.role === "assistant" && m.toolCalls?.length) { const content: unknown[] = []; if (m.content) content.push({ type: "text", text: m.content }); diff --git a/src/providers/http.ts b/src/providers/http.ts index 7ea22b9..e900f38 100644 --- a/src/providers/http.ts +++ b/src/providers/http.ts @@ -62,8 +62,7 @@ export async function httpJson(o: HttpOptions): Promise { export async function* httpSSE(o: HttpOptions): AsyncIterable<{ data: string }> { for (let attempt = 0; ; attempt++) { - // eslint-disable-next-line no-restricted-globals -- requestUrl doesn't support streaming; fetch is required for SSE - const resp = await fetch(o.url, { + const resp = await window.fetch(o.url, { method: o.method ?? "POST", headers: o.headers, body: o.body, diff --git a/src/providers/ollama.ts b/src/providers/ollama.ts index 7ecb564..9320d20 100644 --- a/src/providers/ollama.ts +++ b/src/providers/ollama.ts @@ -1,4 +1,4 @@ -import type { ChatRequest, Delta, LLMProvider } from "./types"; +import type { ChatRequest, Delta, LLMProvider, Message, ToolCall } from "./types"; import { ProviderError } from "./types"; export interface OllamaConfig { baseUrl: string; } @@ -20,7 +20,11 @@ export class OllamaProvider implements LLMProvider { let counter = 0; for await (const line of iter) { const trimmed = line.trim(); if (!trimmed) continue; - let o: any; try { o = JSON.parse(trimmed); } catch { continue; } + interface OllamaChunk { + message?: { content?: string; tool_calls?: Array<{ function: { name: string; arguments?: Record } }> }; + done?: boolean; + } + let o: OllamaChunk; try { o = JSON.parse(trimmed) as OllamaChunk; } catch { continue; } if (o.message?.content) yield { type: "text", text: o.message.content }; for (const tc of o.message?.tool_calls ?? []) { yield { type: "tool_call", toolCall: { id: `tc_${counter++}`, name: tc.function.name, args: tc.function.arguments ?? {} } }; @@ -31,8 +35,7 @@ export class OllamaProvider implements LLMProvider { } private async *fetchNDJSON(url: string, body: unknown, signal?: AbortSignal): AsyncIterable { - // eslint-disable-next-line no-restricted-globals -- requestUrl doesn't support streaming; fetch is required for NDJSON - const resp = await fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal }); + const resp = await window.fetch(url, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal }); if (resp.status >= 400) throw new ProviderError(resp.status >= 500 ? "unavailable" : "unknown", await resp.text()); const reader = resp.body!.getReader(); const dec = new TextDecoder(); let buf = ""; while (true) { @@ -44,11 +47,11 @@ export class OllamaProvider implements LLMProvider { if (buf.trim()) yield buf; } - private toOllama(m: any): unknown { + private toOllama(m: Message): unknown { if (m.role === "tool") return { role: "tool", content: m.content }; if (m.role === "assistant" && m.toolCalls?.length) { return { role: "assistant", content: m.content || "", - tool_calls: m.toolCalls.map((tc: any) => ({ function: { name: tc.name, arguments: tc.args } })) }; + tool_calls: m.toolCalls.map((tc: ToolCall) => ({ function: { name: tc.name, arguments: tc.args } })) }; } return { role: m.role, content: m.content }; } diff --git a/src/providers/openai.ts b/src/providers/openai.ts index 792fa97..dea329e 100644 --- a/src/providers/openai.ts +++ b/src/providers/openai.ts @@ -1,8 +1,9 @@ -import type { ChatRequest, Delta, LLMProvider } from "./types"; +import type { ChatRequest, Delta, LLMProvider, Message, ToolCall } from "./types"; import { httpSSE } from "./http"; +import type { HttpOptions } from "./http"; export interface OpenAIConfig { apiKey: string; baseUrl?: string; } -type SSEIter = (o: any) => AsyncIterable<{ data: string }>; +type SSEIter = (o: HttpOptions) => AsyncIterable<{ data: string }>; export class OpenAIProvider implements LLMProvider { id = "openai"; @@ -25,7 +26,10 @@ export class OpenAIProvider implements LLMProvider { const pending: Record = {}; for await (const ev of iter) { if (ev.data === "[DONE]") break; - let obj: any; try { obj = JSON.parse(ev.data); } catch { continue; } + interface OpenAIChunk { + choices?: Array<{ delta?: { reasoning_content?: string; content?: string; tool_calls?: Array<{ index: number; id?: string; function?: { name?: string; arguments?: string } }> } }>; + } + let obj: OpenAIChunk; try { obj = JSON.parse(ev.data) as OpenAIChunk; } catch { continue; } const delta = obj.choices?.[0]?.delta; if (!delta) continue; if (delta.reasoning_content) yield { type: "reasoning", text: delta.reasoning_content }; @@ -46,12 +50,12 @@ export class OpenAIProvider implements LLMProvider { yield { type: "done" }; } - private toOpenAIMsg(m: any): unknown { + private toOpenAIMsg(m: Message): unknown { if (m.role === "tool") return { role: "tool", tool_call_id: m.toolCallId, content: m.content }; if (m.role === "assistant" && m.toolCalls?.length) { return { role: "assistant", content: m.content || null, reasoning_content: m.reasoningContent || undefined, - tool_calls: m.toolCalls.map((tc: any) => ({ id: tc.id, type: "function", + tool_calls: m.toolCalls.map((tc: ToolCall) => ({ id: tc.id, type: "function", function: { name: tc.name, arguments: JSON.stringify(tc.args) } })) }; } if (m.role === "assistant" && m.reasoningContent) { diff --git a/src/services/vault-service.ts b/src/services/vault-service.ts index e554e25..ef285f3 100644 --- a/src/services/vault-service.ts +++ b/src/services/vault-service.ts @@ -49,7 +49,7 @@ export class VaultService { await this.app.vault.rename(f, b); } - async listFolder(path: string): Promise { + listFolder(path: string): string[] { const p = path === "" ? "" : validatePath(path); return this.app.vault.getFiles() .map(f => f.path) @@ -69,7 +69,8 @@ export class VaultService { } getBacklinks(path: string): string[] { - const cache = (this.app as any).metadataCache; + type AppWithMeta = typeof this.app & { metadataCache?: { resolvedLinks?: Record> } }; + const cache = (this.app as AppWithMeta).metadataCache; const rec = cache?.resolvedLinks ?? {}; const out: string[] = []; for (const src of Object.keys(rec)) if (rec[src][path]) out.push(src); @@ -77,7 +78,8 @@ export class VaultService { } getOutgoingLinks(path: string): string[] { - const cache = (this.app as any).metadataCache; + type AppWithMeta = typeof this.app & { metadataCache?: { resolvedLinks?: Record> } }; + const cache = (this.app as AppWithMeta).metadataCache; const rec = cache?.resolvedLinks?.[path] ?? {}; return Object.keys(rec); } @@ -86,7 +88,8 @@ export class VaultService { const parent = p.split("/").slice(0, -1).join("/"); if (!parent) return; if (!this.app.vault.getAbstractFileByPath(parent)) { - await (this.app.vault as any).createFolder?.(parent); + type VaultWithFolder = typeof this.app.vault & { createFolder?: (path: string) => Promise }; + await (this.app.vault as VaultWithFolder).createFolder?.(parent); } } } diff --git a/src/settings.ts b/src/settings.ts index f0f184a..9972de5 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -56,8 +56,7 @@ export const DEFAULT_SETTINGS: Settings = { providerId: "openai", providers: defaultProviders(), mode: "ask", - // eslint-disable-next-line obsidianmd/hardcoded-config-path -- DEFAULT_SETTINGS is a static constant; Vault.configDir is unavailable here - chatsFolder: ".obsidian/plugins/smart-note-agent/chats", + chatsFolder: "smart-note-agent/chats", locale: "auto", userProfile: "", historyRetentionDays: 30, @@ -99,10 +98,10 @@ export function migrateSettings(raw: (Partial & LegacySettings) | unde }; } - const { apiKey: _a, baseUrl: _b, model: _m, providers: _p, scheduled: _s, ...rest } = r as any; + const { apiKey: _a, baseUrl: _b, model: _m, providers: _p, scheduled: _s, ...rest } = r; - // Migrate old default chats folder to the new internal location - if (rest.chatsFolder === "_agent/chats") delete rest.chatsFolder; + // Migrate old default chats folder paths to the new vault-root location + if (rest.chatsFolder === "_agent/chats" || /^\.obsidian\//.test(rest.chatsFolder ?? "")) delete rest.chatsFolder; return { ...DEFAULT_SETTINGS, diff --git a/src/tools/read.ts b/src/tools/read.ts index d3c54c2..a0dab55 100644 --- a/src/tools/read.ts +++ b/src/tools/read.ts @@ -1,6 +1,6 @@ import type { Tool, ToolContext } from "./types"; -const safe = async (fn: () => Promise): Promise => { +const safe = async (fn: () => string | Promise): Promise => { try { return await fn(); } catch (e: unknown) { return JSON.stringify({ error: e instanceof Error ? e.message : String(e) }); } }; @@ -22,29 +22,29 @@ export function buildReadTools(ctx: ToolContext): Tool[] { name: "list_folder", kind: "read", schema: { name: "list_folder", description: "List all file paths under a folder (notes, attachments, canvas, etc.). Pass an empty string to list the vault root.", parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } }, - handler: (a) => safe(async () => JSON.stringify(await ctx.vault.listFolder(typeof a.path === "string" ? a.path : ""))), + handler: (a) => safe(() => JSON.stringify(ctx.vault.listFolder(typeof a.path === "string" ? a.path : ""))), }, { name: "get_backlinks", kind: "read", schema: { name: "get_backlinks", description: "Notes that link to the given path.", parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } }, - handler: (a) => safe(async () => JSON.stringify(ctx.vault.getBacklinks(String(a.path)))), + handler: (a) => safe(() => JSON.stringify(ctx.vault.getBacklinks(String(a.path)))), }, { name: "get_outgoing_links", kind: "read", schema: { name: "get_outgoing_links", description: "Notes linked from the given path.", parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"] } }, - handler: (a) => safe(async () => JSON.stringify(ctx.vault.getOutgoingLinks(String(a.path)))), + handler: (a) => safe(() => JSON.stringify(ctx.vault.getOutgoingLinks(String(a.path)))), }, { name: "get_active_note", kind: "read", schema: { name: "get_active_note", description: "Current note in the editor.", parameters: { type: "object", properties: {} } }, - handler: () => safe(async () => JSON.stringify(ctx.activeFile())), + handler: () => safe(() => JSON.stringify(ctx.activeFile())), }, { name: "get_selection", kind: "read", schema: { name: "get_selection", description: "Currently selected text.", parameters: { type: "object", properties: {} } }, - handler: () => safe(async () => ctx.selection()), + handler: () => safe(() => ctx.selection()), }, ]; } diff --git a/src/tools/write.ts b/src/tools/write.ts index 58fd1f3..3cca960 100644 --- a/src/tools/write.ts +++ b/src/tools/write.ts @@ -12,22 +12,22 @@ export function buildWriteTools(): Tool[] { { name: "create_note", kind: "write", schema: { name: "create_note", description: "Create a new note. Fails if path exists.", parameters: { type: "object", properties: { path: str, content: str }, required: ["path","content"] } }, - handler: async (a) => pending("create_note", a) }, + handler: (a) => Promise.resolve(pending("create_note", a)) }, { name: "edit_note", kind: "write", schema: { name: "edit_note", description: "Replace full content of an existing note.", parameters: { type: "object", properties: { path: str, content: str }, required: ["path","content"] } }, - handler: async (a) => pending("edit_note", a) }, + handler: (a) => Promise.resolve(pending("edit_note", a)) }, { name: "apply_patch", kind: "write", schema: { name: "apply_patch", description: "Apply a unified diff patch to a note.", parameters: { type: "object", properties: { path: str, patch: str }, required: ["path","patch"] } }, - handler: async (a) => pending("apply_patch", a) }, + handler: (a) => Promise.resolve(pending("apply_patch", a)) }, { name: "delete_note", kind: "write", schema: { name: "delete_note", description: "Delete a note.", parameters: { type: "object", properties: { path: str }, required: ["path"] } }, - handler: async (a) => pending("delete_note", a) }, + handler: (a) => Promise.resolve(pending("delete_note", a)) }, { name: "move_note", kind: "write", schema: { name: "move_note", description: "Move or rename a note.", parameters: { type: "object", properties: { from: str, to: str }, required: ["from","to"] } }, - handler: async (a) => pending("move_note", a) }, + handler: (a) => Promise.resolve(pending("move_note", a)) }, ]; } diff --git a/src/ui/ChangeSummary.svelte b/src/ui/ChangeSummary.svelte index 8a4c359..6e42297 100644 --- a/src/ui/ChangeSummary.svelte +++ b/src/ui/ChangeSummary.svelte @@ -5,7 +5,7 @@ let summary = plugin.lastTurnSummary; plugin.onSummaryChange(s => (summary = s)); - const t = (k: string, v?: any) => plugin.i18n.t(k, v); + const t = (k: string, v?: Record) => plugin.i18n.t(k, v); $: total = (summary?.created.length ?? 0) + (summary?.edited.length ?? 0) + (summary?.deleted.length ?? 0); diff --git a/src/ui/ChatView.svelte b/src/ui/ChatView.svelte index 3693c06..ce0c9a1 100644 --- a/src/ui/ChatView.svelte +++ b/src/ui/ChatView.svelte @@ -28,9 +28,7 @@ function autoResize() { if (!textarea) return; - // eslint-disable-next-line obsidianmd/no-static-styles-assignment -- height must be set dynamically based on scrollHeight - textarea.style.height = "auto"; - // eslint-disable-next-line obsidianmd/no-static-styles-assignment + textarea.style.height = `auto`; textarea.style.height = Math.min(Math.max(textarea.scrollHeight, 66), 200) + "px"; } @@ -51,13 +49,13 @@ try { for await (const evt of plugin.sendMessage(text)) { if (evt.type === "text") { - streamBuf += (evt as any).text; + streamBuf += (evt as { type: string; text: string }).text; // Don't sync messages here — streamBuf drives the live streaming view } else if (["tool","pending","done","stopped"].includes(evt.type)) { messages = [...plugin.currentConversation.messages]; streamBuf = ""; } else if (evt.type === "error") { - errorMsg = `⚠ ${(evt as any).error?.message ?? "Unknown error"}`; + errorMsg = `⚠ ${(evt as { type: string; error?: { message?: string } }).error?.message ?? "Unknown error"}`; } await tick(); } @@ -86,14 +84,15 @@ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } } - const t = (k: string, v?: any) => plugin.i18n.t(k, v); + const t = (k: string, v?: Record) => plugin.i18n.t(k, v); function openSettings() { - const setting = (plugin.app as any).setting; - if (setting?.open) { - setting.open(); - setting.openTabById?.(plugin.manifest.id); - } + const s = (plugin.app as Record).setting as Record | undefined; + if (!s) return; + const openFn = s["open"]; + const openByIdFn = s["openTabById"]; + if (typeof openFn === "function") openFn(); + if (typeof openByIdFn === "function") openByIdFn(plugin.manifest.id); } $: providerLabel = (settingsTick, `${plugin.i18n.t(`provider.${plugin.settings.providerId}`)}:${activeProfile(plugin.settings).model}`); $: charCount = input.length; diff --git a/src/ui/ConversationList.svelte b/src/ui/ConversationList.svelte index 92fd6c6..1577aaa 100644 --- a/src/ui/ConversationList.svelte +++ b/src/ui/ConversationList.svelte @@ -49,7 +49,7 @@ return `${y}-${mo}-${dy}`; } - const t = (k: string, v?: any) => plugin.i18n.t(k, v); + const t = (k: string, v?: Record) => plugin.i18n.t(k, v); // Sentinel group keys — resolved to localized strings at render time const GROUP_TODAY = "__today__"; diff --git a/src/ui/DiffReviewBlock.svelte b/src/ui/DiffReviewBlock.svelte index 45e7b10..a5458f0 100644 --- a/src/ui/DiffReviewBlock.svelte +++ b/src/ui/DiffReviewBlock.svelte @@ -1,7 +1,8 @@