summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.rules/changelog/2026-03/28/07.md24
-rw-r--r--src/chat-view.ts80
-rw-r--r--src/ollama-client.ts128
-rw-r--r--src/tools.ts142
-rw-r--r--src/vault-context.ts4
5 files changed, 202 insertions, 176 deletions
diff --git a/.rules/changelog/2026-03/28/07.md b/.rules/changelog/2026-03/28/07.md
new file mode 100644
index 0000000..e982dc0
--- /dev/null
+++ b/.rules/changelog/2026-03/28/07.md
@@ -0,0 +1,24 @@
+# Fix 52 TypeScript strict-mode build errors
+
+## Files Changed
+- `src/chat-view.ts`
+- `src/vault-context.ts`
+
+## Changes
+
+### `never` type in catch block (chat-view.ts, 4 errors)
+- `currentBubble` was inferred as `never` inside the `catch` block because TypeScript loses track of closure-mutated `let` variables across `await` boundaries.
+- Fixed by capturing into a local `const` with an explicit `as HTMLDivElement | null` type assertion.
+
+### TS4111 index signature access (chat-view.ts, 46 errors)
+- `noPropertyAccessFromIndexSignature` is enabled; properties on `Record<string, unknown>` must use bracket notation.
+- Changed all dot-notation accesses (`args.file_path`, `args.operations`, `o.properties`, etc.) to bracket notation (`args['file_path']`, etc.) across:
+ - `appendToolCall` (edit_file branch)
+ - `showApprovalRequest` (edit_file, set_frontmatter, create_file branches)
+ - `renderBatchDeleteApproval`
+ - `renderBatchMoveApproval`
+ - `renderBatchSetFrontmatterApproval`
+ - `renderBatchEditApproval`
+
+### TS4111 frontmatter tags (vault-context.ts, 2 errors)
+- Changed `cache.frontmatter?.tags` to `cache.frontmatter?.['tags']` for the same index signature rule.
diff --git a/src/chat-view.ts b/src/chat-view.ts
index b673d26..55b730d 100644
--- a/src/chat-view.ts
+++ b/src/chat-view.ts
@@ -325,17 +325,19 @@ export class ChatView extends ItemView {
const isAbort = err instanceof DOMException && err.name === "AbortError";
// Clean up the streaming bubble
- if (currentBubble !== null) {
- currentBubble.removeClass("ai-pulse-streaming");
- const errorIcon = currentBubble.querySelector(".ai-pulse-loading-icon");
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+ const bubble = currentBubble as HTMLDivElement | null;
+ if (bubble !== null) {
+ bubble.removeClass("ai-pulse-streaming");
+ const errorIcon = bubble.querySelector(".ai-pulse-loading-icon");
if (errorIcon !== null) {
errorIcon.remove();
}
// Remove empty bubble, or remove partial bubble on abort
- if (currentBubble.textContent?.trim() === "" || isAbort) {
- currentBubble.remove();
+ if (bubble.textContent?.trim() === "" || isAbort) {
+ bubble.remove();
}
- this.bubbleContent.delete(currentBubble);
+ this.bubbleContent.delete(bubble);
}
// Only show error UI for real errors, not user-initiated aborts
@@ -467,9 +469,9 @@ export class ChatView extends ItemView {
if (event.toolName === "edit_file") {
// For edit_file, show old_text / new_text in dedicated labeled blocks
- const filePath = typeof event.args.file_path === "string" ? event.args.file_path : "";
- const oldText = typeof event.args.old_text === "string" ? event.args.old_text : "";
- const newText = typeof event.args.new_text === "string" ? event.args.new_text : "";
+ const filePath = typeof event.args['file_path'] === "string" ? event.args['file_path'] : "";
+ const oldText = typeof event.args['old_text'] === "string" ? event.args['old_text'] : "";
+ const newText = typeof event.args['new_text'] === "string" ? event.args['new_text'] : "";
if (filePath !== "") {
contentInner.createEl("div", { text: `File: ${filePath}`, cls: "ai-pulse-tool-call-label" });
@@ -542,8 +544,8 @@ export class ChatView extends ItemView {
const contentInner = collapseContent.createDiv({ cls: "ai-pulse-collapse-content-inner" });
if (event.toolName === "edit_file") {
- const oldText = typeof event.args.old_text === "string" ? event.args.old_text : "";
- const newText = typeof event.args.new_text === "string" ? event.args.new_text : "";
+ const oldText = typeof event.args['old_text'] === "string" ? event.args['old_text'] : "";
+ const newText = typeof event.args['new_text'] === "string" ? event.args['new_text'] : "";
contentInner.createEl("div", { text: "Old text:", cls: "ai-pulse-tool-call-label" });
contentInner.createEl("pre", {
@@ -557,7 +559,7 @@ export class ChatView extends ItemView {
cls: "ai-pulse-tool-call-result",
});
} else if (event.toolName === "set_frontmatter") {
- const props = event.args.properties;
+ const props = event.args['properties'];
const propsStr = typeof props === "object" && props !== null
? JSON.stringify(props, null, 2)
: typeof props === "string" ? props : "{}";
@@ -568,7 +570,7 @@ export class ChatView extends ItemView {
cls: "ai-pulse-tool-call-result",
});
} else if (event.toolName === "create_file") {
- const content = typeof event.args.content === "string" ? event.args.content : "";
+ const content = typeof event.args['content'] === "string" ? event.args['content'] : "";
contentInner.createEl("div", { text: "Content:", cls: "ai-pulse-tool-call-label" });
contentInner.createEl("pre", {
@@ -617,10 +619,10 @@ export class ChatView extends ItemView {
private renderBatchDeleteApproval(container: HTMLDivElement, args: Record<string, unknown>): void {
let filePaths: unknown[] = [];
- if (Array.isArray(args.file_paths)) {
- filePaths = args.file_paths;
- } else if (typeof args.file_paths === "string") {
- try { filePaths = JSON.parse(args.file_paths) as unknown[]; } catch { /* empty */ }
+ if (Array.isArray(args['file_paths'])) {
+ filePaths = args['file_paths'];
+ } else if (typeof args['file_paths'] === "string") {
+ try { filePaths = JSON.parse(args['file_paths']) as unknown[]; } catch { /* empty */ }
}
container.createEl("div", {
@@ -636,10 +638,10 @@ export class ChatView extends ItemView {
private renderBatchMoveApproval(container: HTMLDivElement, args: Record<string, unknown>): void {
let operations: unknown[] = [];
- if (Array.isArray(args.operations)) {
- operations = args.operations;
- } else if (typeof args.operations === "string") {
- try { operations = JSON.parse(args.operations) as unknown[]; } catch { /* empty */ }
+ if (Array.isArray(args['operations'])) {
+ operations = args['operations'];
+ } else if (typeof args['operations'] === "string") {
+ try { operations = JSON.parse(args['operations']) as unknown[]; } catch { /* empty */ }
}
container.createEl("div", {
@@ -654,8 +656,8 @@ export class ChatView extends ItemView {
continue;
}
const o = op as Record<string, unknown>;
- const from = typeof o.file_path === "string" ? o.file_path : "?";
- const to = typeof o.new_path === "string" ? o.new_path : "?";
+ const from = typeof o['file_path'] === "string" ? o['file_path'] : "?";
+ const to = typeof o['new_path'] === "string" ? o['new_path'] : "?";
const li = list.createEl("li");
li.createSpan({ text: from, cls: "ai-pulse-batch-path" });
li.createSpan({ text: " \u2192 " });
@@ -665,10 +667,10 @@ export class ChatView extends ItemView {
private renderBatchSetFrontmatterApproval(container: HTMLDivElement, args: Record<string, unknown>): void {
let operations: unknown[] = [];
- if (Array.isArray(args.operations)) {
- operations = args.operations;
- } else if (typeof args.operations === "string") {
- try { operations = JSON.parse(args.operations) as unknown[]; } catch { /* empty */ }
+ if (Array.isArray(args['operations'])) {
+ operations = args['operations'];
+ } else if (typeof args['operations'] === "string") {
+ try { operations = JSON.parse(args['operations']) as unknown[]; } catch { /* empty */ }
}
container.createEl("div", {
@@ -682,13 +684,13 @@ export class ChatView extends ItemView {
continue;
}
const o = op as Record<string, unknown>;
- const fp = typeof o.file_path === "string" ? o.file_path : "?";
+ const fp = typeof o['file_path'] === "string" ? o['file_path'] : "?";
let propsStr = "{}";
- if (typeof o.properties === "object" && o.properties !== null) {
- propsStr = JSON.stringify(o.properties, null, 2);
- } else if (typeof o.properties === "string") {
- propsStr = o.properties;
+ if (typeof o['properties'] === "object" && o['properties'] !== null) {
+ propsStr = JSON.stringify(o['properties'], null, 2);
+ } else if (typeof o['properties'] === "string") {
+ propsStr = o['properties'];
}
container.createEl("div", { text: fp, cls: "ai-pulse-tool-call-label ai-pulse-batch-file-header" });
@@ -698,10 +700,10 @@ export class ChatView extends ItemView {
private renderBatchEditApproval(container: HTMLDivElement, args: Record<string, unknown>): void {
let operations: unknown[] = [];
- if (Array.isArray(args.operations)) {
- operations = args.operations;
- } else if (typeof args.operations === "string") {
- try { operations = JSON.parse(args.operations) as unknown[]; } catch { /* empty */ }
+ if (Array.isArray(args['operations'])) {
+ operations = args['operations'];
+ } else if (typeof args['operations'] === "string") {
+ try { operations = JSON.parse(args['operations']) as unknown[]; } catch { /* empty */ }
}
container.createEl("div", {
@@ -715,9 +717,9 @@ export class ChatView extends ItemView {
continue;
}
const o = op as Record<string, unknown>;
- const fp = typeof o.file_path === "string" ? o.file_path : "?";
- const oldText = typeof o.old_text === "string" ? o.old_text : "";
- const newText = typeof o.new_text === "string" ? o.new_text : "";
+ const fp = typeof o['file_path'] === "string" ? o['file_path'] : "?";
+ const oldText = typeof o['old_text'] === "string" ? o['old_text'] : "";
+ const newText = typeof o['new_text'] === "string" ? o['new_text'] : "";
container.createEl("div", { text: fp, cls: "ai-pulse-tool-call-label ai-pulse-batch-file-header" });
diff --git a/src/ollama-client.ts b/src/ollama-client.ts
index c565356..b778798 100644
--- a/src/ollama-client.ts
+++ b/src/ollama-client.ts
@@ -56,17 +56,17 @@ function parseToolCalls(value: unknown): ToolCallResponse[] {
for (const item of value) {
if (typeof item !== "object" || item === null) continue;
const obj = item as Record<string, unknown>;
- const fn = obj.function;
+ const fn = obj["function"];
if (typeof fn !== "object" || fn === null) continue;
const fnObj = fn as Record<string, unknown>;
- if (typeof fnObj.name !== "string") continue;
+ if (typeof fnObj["name"] !== "string") continue;
result.push({
- ...(typeof obj.type === "string" ? { type: obj.type } : {}),
+ ...(typeof obj["type"] === "string" ? { type: obj["type"] } : {}),
function: {
- ...(typeof fnObj.index === "number" ? { index: fnObj.index } : {}),
- name: fnObj.name,
- arguments: typeof fnObj.arguments === "object" && fnObj.arguments !== null
- ? fnObj.arguments as Record<string, unknown>
+ ...(typeof fnObj["index"] === "number" ? { index: fnObj["index"] } : {}),
+ name: fnObj["name"],
+ arguments: typeof fnObj["arguments"] === "object" && fnObj["arguments"] !== null
+ ? fnObj["arguments"] as Record<string, unknown>
: {},
},
});
@@ -301,13 +301,13 @@ function preValidateTool(
// -- Individual tool validators ------------------------------------------------
function preValidateEditFile(app: App, args: Record<string, unknown>): string | null {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
- const oldText = typeof args.old_text === "string" ? args.old_text : "";
- const newText = typeof args.new_text === "string" ? args.new_text : "";
+ const oldText = typeof args["old_text"] === "string" ? args["old_text"] : "";
+ const newText = typeof args["new_text"] === "string" ? args["new_text"] : "";
if (oldText === newText) {
return "Error: old_text and new_text are identical — no change would occur. Provide different text for new_text, or skip this edit.";
}
@@ -321,7 +321,7 @@ function preValidateEditFile(app: App, args: Record<string, unknown>): string |
}
function preValidateCreateFile(app: App, args: Record<string, unknown>): string | null {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
@@ -335,7 +335,7 @@ function preValidateCreateFile(app: App, args: Record<string, unknown>): string
}
function preValidateDeleteFile(app: App, args: Record<string, unknown>): string | null {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
@@ -349,12 +349,12 @@ function preValidateDeleteFile(app: App, args: Record<string, unknown>): string
}
function preValidateMoveFile(app: App, args: Record<string, unknown>): string | null {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
- const newPath = typeof args.new_path === "string" ? args.new_path : "";
+ const newPath = typeof args["new_path"] === "string" ? args["new_path"] : "";
if (newPath === "") {
return "Error: new_path parameter is required.";
}
@@ -373,12 +373,12 @@ function preValidateMoveFile(app: App, args: Record<string, unknown>): string |
}
function preValidateSetFrontmatter(app: App, args: Record<string, unknown>): string | null {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
- let properties = args.properties;
+ let properties = args["properties"];
if (typeof properties === "string") {
try {
properties = JSON.parse(properties) as unknown;
@@ -404,7 +404,7 @@ function preValidateSetFrontmatter(app: App, args: Record<string, unknown>): str
// -- Batch tool validators -----------------------------------------------------
function preValidateBatchEditFile(app: App, args: Record<string, unknown>): string | null {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
if (operations === null || operations.length === 0) {
return "Error: operations parameter must be a non-empty array of {file_path, old_text, new_text} objects.";
}
@@ -413,8 +413,8 @@ function preValidateBatchEditFile(app: App, args: Record<string, unknown>): stri
const allNoOps = operations.every((op) => {
if (typeof op !== "object" || op === null) return false;
const o = op as Record<string, unknown>;
- const oldText = typeof o.old_text === "string" ? o.old_text : "";
- const newText = typeof o.new_text === "string" ? o.new_text : "";
+ const oldText = typeof o["old_text"] === "string" ? o["old_text"] : "";
+ const newText = typeof o["new_text"] === "string" ? o["new_text"] : "";
return oldText === newText;
});
if (allNoOps) {
@@ -425,7 +425,7 @@ function preValidateBatchEditFile(app: App, args: Record<string, unknown>): stri
const allMissing = operations.every((op) => {
if (typeof op !== "object" || op === null) return true;
const o = op as Record<string, unknown>;
- const fp = typeof o.file_path === "string" ? o.file_path : "";
+ const fp = typeof o["file_path"] === "string" ? o["file_path"] : "";
if (fp === "") return true;
const file = app.vault.getAbstractFileByPath(fp);
return file === null || !(file instanceof TFile);
@@ -438,7 +438,7 @@ function preValidateBatchEditFile(app: App, args: Record<string, unknown>): stri
}
function preValidateBatchDeleteFile(app: App, args: Record<string, unknown>): string | null {
- const filePaths = parseArrayArg(args.file_paths);
+ const filePaths = parseArrayArg(args["file_paths"]);
if (filePaths === null || filePaths.length === 0) {
return "Error: file_paths parameter must be a non-empty array of strings.";
}
@@ -457,7 +457,7 @@ function preValidateBatchDeleteFile(app: App, args: Record<string, unknown>): st
}
function preValidateBatchMoveFile(app: App, args: Record<string, unknown>): string | null {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
if (operations === null || operations.length === 0) {
return "Error: operations parameter must be a non-empty array of {file_path, new_path} objects.";
}
@@ -466,7 +466,7 @@ function preValidateBatchMoveFile(app: App, args: Record<string, unknown>): stri
const allSourcesMissing = operations.every((op) => {
if (typeof op !== "object" || op === null) return true;
const o = op as Record<string, unknown>;
- const fp = typeof o.file_path === "string" ? o.file_path : "";
+ const fp = typeof o["file_path"] === "string" ? o["file_path"] : "";
if (fp === "") return true;
const file = app.vault.getAbstractFileByPath(fp);
return file === null || !(file instanceof TFile);
@@ -479,7 +479,7 @@ function preValidateBatchMoveFile(app: App, args: Record<string, unknown>): stri
const allDestsExist = operations.every((op) => {
if (typeof op !== "object" || op === null) return false;
const o = op as Record<string, unknown>;
- const np = typeof o.new_path === "string" ? o.new_path : "";
+ const np = typeof o["new_path"] === "string" ? o["new_path"] : "";
if (np === "") return false;
return app.vault.getAbstractFileByPath(np) !== null;
});
@@ -491,7 +491,7 @@ function preValidateBatchMoveFile(app: App, args: Record<string, unknown>): stri
}
function preValidateBatchSetFrontmatter(app: App, args: Record<string, unknown>): string | null {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
if (operations === null || operations.length === 0) {
return "Error: operations parameter must be a non-empty array of {file_path, properties} objects.";
}
@@ -499,7 +499,7 @@ function preValidateBatchSetFrontmatter(app: App, args: Record<string, unknown>)
const allMissing = operations.every((op) => {
if (typeof op !== "object" || op === null) return true;
const o = op as Record<string, unknown>;
- const fp = typeof o.file_path === "string" ? o.file_path : "";
+ const fp = typeof o["file_path"] === "string" ? o["file_path"] : "";
if (fp === "") return true;
const file = app.vault.getAbstractFileByPath(fp);
return file === null || !(file instanceof TFile);
@@ -633,7 +633,7 @@ export async function testConnection(ollamaUrl: string): Promise<string> {
});
if (response.status === 200) {
- const version = (response.json as Record<string, unknown>).version;
+ const version = (response.json as Record<string, unknown>)["version"];
if (typeof version === "string") {
return version;
}
@@ -666,14 +666,14 @@ export async function listModels(ollamaUrl: string): Promise<string[]> {
method: "GET",
});
- const models = (response.json as Record<string, unknown>).models;
+ const models = (response.json as Record<string, unknown>)["models"];
if (!Array.isArray(models)) {
throw new Error("Unexpected response format: missing models array.");
}
return models.map((m: unknown) => {
if (typeof m === "object" && m !== null && "name" in m) {
- const name = (m as Record<string, unknown>).name;
+ const name = (m as Record<string, unknown>)["name"];
if (typeof name === "string") {
return name;
}
@@ -713,7 +713,7 @@ export async function showModel(ollamaUrl: string, model: string): Promise<Model
const json = response.json as Record<string, unknown>;
let contextLength = 4096; // fallback default
- const modelInfo = json.model_info as Record<string, unknown> | undefined;
+ const modelInfo = json["model_info"] as Record<string, unknown> | undefined;
if (modelInfo !== undefined && modelInfo !== null) {
for (const key of Object.keys(modelInfo)) {
if (key.endsWith(".context_length") || key === "context_length") {
@@ -772,7 +772,7 @@ export async function sendChatMessage(
};
if (tools !== undefined && tools.length > 0) {
- body.tools = tools;
+ body["tools"] = tools;
}
try {
@@ -783,14 +783,14 @@ export async function sendChatMessage(
body: JSON.stringify(body),
});
- const messageObj = (response.json as Record<string, unknown>).message;
+ const messageObj = (response.json as Record<string, unknown>)["message"];
if (typeof messageObj !== "object" || messageObj === null) {
throw new Error("Unexpected response format: missing message.");
}
const msg = messageObj as Record<string, unknown>;
- const content = typeof msg.content === "string" ? msg.content : "";
- const toolCalls = parseToolCalls(msg.tool_calls);
+ const content = typeof msg["content"] === "string" ? msg["content"] : "";
+ const toolCalls = parseToolCalls(msg["tool_calls"]);
return { content, toolCalls };
} catch (err: unknown) {
@@ -806,12 +806,12 @@ export async function sendChatMessage(
return chatAgentLoop({
messages: opts.messages,
- tools,
- app,
- userSystemPrompt,
- vaultContext,
- onToolCall,
- onApprovalRequest,
+ ...(tools !== undefined ? { tools } : {}),
+ ...(app !== undefined ? { app } : {}),
+ ...(userSystemPrompt !== undefined ? { userSystemPrompt } : {}),
+ ...(vaultContext !== undefined ? { vaultContext } : {}),
+ ...(onToolCall !== undefined ? { onToolCall } : {}),
+ ...(onApprovalRequest !== undefined ? { onApprovalRequest } : {}),
sendRequest,
});
}
@@ -922,12 +922,12 @@ export async function sendChatMessageStreaming(
return chatAgentLoop({
messages: opts.messages,
- tools,
- app,
- userSystemPrompt,
- vaultContext,
- onToolCall,
- onApprovalRequest,
+ ...(tools !== undefined ? { tools } : {}),
+ ...(app !== undefined ? { app } : {}),
+ ...(userSystemPrompt !== undefined ? { userSystemPrompt } : {}),
+ ...(vaultContext !== undefined ? { vaultContext } : {}),
+ ...(onToolCall !== undefined ? { onToolCall } : {}),
+ ...(onApprovalRequest !== undefined ? { onApprovalRequest } : {}),
sendRequest,
});
}
@@ -951,7 +951,7 @@ function buildMobileStrategy(
): ChatRequestStrategy {
return async (workingMessages) => {
// Bail out immediately if already aborted
- if (abortSignal?.aborted === true) {
+ if (abortSignal !== undefined && abortSignal.aborted) {
throw new DOMException("The operation was aborted.", "AbortError");
}
@@ -964,11 +964,11 @@ function buildMobileStrategy(
};
if (tools !== undefined && tools.length > 0) {
- body.tools = tools;
+ body["tools"] = tools;
}
if (options !== undefined) {
- body.options = options;
+ body["options"] = options;
}
try {
@@ -998,17 +998,17 @@ function buildMobileStrategy(
response = await requestPromise;
}
- const messageObj = (response.json as Record<string, unknown>).message;
+ const messageObj = (response.json as Record<string, unknown>)["message"];
if (typeof messageObj !== "object" || messageObj === null) {
throw new Error("Unexpected response format: missing message.");
}
const msg = messageObj as Record<string, unknown>;
- const content = typeof msg.content === "string" ? msg.content : "";
- const toolCalls = parseToolCalls(msg.tool_calls);
+ const content = typeof msg["content"] === "string" ? msg["content"] : "";
+ const toolCalls = parseToolCalls(msg["tool_calls"]);
// Check abort before delivering content to the UI
- if (abortSignal?.aborted === true) {
+ if (abortSignal !== undefined && abortSignal.aborted) {
throw new DOMException("The operation was aborted.", "AbortError");
}
@@ -1059,18 +1059,18 @@ function buildDesktopStreamingStrategy(
};
if (tools !== undefined && tools.length > 0) {
- body.tools = tools;
+ body["tools"] = tools;
}
if (options !== undefined) {
- body.options = options;
+ body["options"] = options;
}
const response = await fetch(`${ollamaUrl}/api/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
- signal: abortSignal,
+ signal: abortSignal ?? null,
});
if (!response.ok) {
@@ -1094,21 +1094,21 @@ function buildDesktopStreamingStrategy(
try {
for await (const chunk of withIdleTimeout(readNdjsonStream(reader, decoder), IDLE_TIMEOUT_MS)) {
// Check for mid-stream errors from Ollama
- if (typeof chunk.error === "string") {
- throw new Error(`Ollama error: ${chunk.error}`);
+ if (typeof chunk["error"] === "string") {
+ throw new Error(`Ollama error: ${chunk["error"]}`);
}
- const rawMsg: unknown = chunk.message;
+ const rawMsg: unknown = chunk["message"];
const msg = typeof rawMsg === "object" && rawMsg !== null
? rawMsg as Record<string, unknown>
: undefined;
if (msg !== undefined) {
- if (typeof msg.content === "string" && msg.content !== "") {
- content += msg.content;
- onChunk(msg.content);
+ if (typeof msg["content"] === "string" && msg["content"] !== "") {
+ content += msg["content"];
+ onChunk(msg["content"]);
}
- if (Array.isArray(msg.tool_calls)) {
- toolCalls.push(...parseToolCalls(msg.tool_calls));
+ if (Array.isArray(msg["tool_calls"])) {
+ toolCalls.push(...parseToolCalls(msg["tool_calls"]));
}
}
}
diff --git a/src/tools.ts b/src/tools.ts
index 57e21c1..df45b96 100644
--- a/src/tools.ts
+++ b/src/tools.ts
@@ -79,10 +79,10 @@ export interface ToolEntry {
* Returns a newline-separated list of vault file paths matching the query.
*/
async function executeSearchFiles(app: App, args: Record<string, unknown>): Promise<string> {
- const query = typeof args.query === "string" ? args.query.toLowerCase() : "";
+ const query = typeof args["query"] === "string" ? args["query"].toLowerCase() : "";
if (query === "") {
// Detect common misuse: model passed batch_search_files params to search_files
- if (args.queries !== undefined) {
+ if (args["queries"] !== undefined) {
return "Error: query parameter is required. You passed 'queries' (plural) — use search_files with a single 'query' string, or use batch_search_files for multiple queries.";
}
return "Error: query parameter is required.";
@@ -117,7 +117,7 @@ async function executeSearchFiles(app: App, args: Record<string, unknown>): Prom
* plus parsed frontmatter as a JSON block if present.
*/
async function executeReadFile(app: App, args: Record<string, unknown>): Promise<string> {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
@@ -155,7 +155,7 @@ async function executeReadFile(app: App, args: Record<string, unknown>): Promise
* Deletes a file by its vault path (moves to trash).
*/
async function executeDeleteFile(app: App, args: Record<string, unknown>): Promise<string> {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
@@ -179,15 +179,15 @@ async function executeDeleteFile(app: App, args: Record<string, unknown>): Promi
* Searches file contents for a text query, returning matching lines with context.
*/
async function executeGrepSearch(app: App, args: Record<string, unknown>): Promise<string> {
- const query = typeof args.query === "string" ? args.query : "";
+ const query = typeof args["query"] === "string" ? args["query"] : "";
if (query === "") {
- if (args.queries !== undefined) {
+ if (args["queries"] !== undefined) {
return "Error: query parameter is required. You passed 'queries' (plural) — use grep_search with a single 'query' string, or use batch_grep_search for multiple queries.";
}
return "Error: query parameter is required.";
}
- const filePattern = typeof args.file_pattern === "string" ? args.file_pattern.toLowerCase() : "";
+ const filePattern = typeof args["file_pattern"] === "string" ? args["file_pattern"].toLowerCase() : "";
const queryLower = query.toLowerCase();
const files = app.vault.getMarkdownFiles();
@@ -236,12 +236,12 @@ async function executeGrepSearch(app: App, args: Record<string, unknown>): Promi
* Creates a new file at the given vault path with the provided content.
*/
async function executeCreateFile(app: App, args: Record<string, unknown>): Promise<string> {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
- const content = typeof args.content === "string" ? args.content : "";
+ const content = typeof args["content"] === "string" ? args["content"] : "";
// Check if file already exists
const existing = app.vault.getAbstractFileByPath(filePath);
@@ -273,12 +273,12 @@ async function executeCreateFile(app: App, args: Record<string, unknown>): Promi
* Moves or renames a file, auto-updating all links.
*/
async function executeMoveFile(app: App, args: Record<string, unknown>): Promise<string> {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
- const newPath = typeof args.new_path === "string" ? args.new_path : "";
+ const newPath = typeof args["new_path"] === "string" ? args["new_path"] : "";
if (newPath === "") {
return "Error: new_path parameter is required.";
}
@@ -330,13 +330,13 @@ async function executeGetCurrentNote(app: App, _args: Record<string, unknown>):
* Performs a find-and-replace on the file content using vault.process() for atomicity.
*/
async function executeEditFile(app: App, args: Record<string, unknown>): Promise<string> {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
- const oldText = typeof args.old_text === "string" ? args.old_text : "";
- const newText = typeof args.new_text === "string" ? args.new_text : "";
+ const oldText = typeof args["old_text"] === "string" ? args["old_text"] : "";
+ const newText = typeof args["new_text"] === "string" ? args["new_text"] : "";
// Reject no-op edits where old_text and new_text are identical
if (oldText === newText) {
@@ -394,12 +394,12 @@ async function executeEditFile(app: App, args: Record<string, unknown>): Promise
* To remove a property, set its value to null.
*/
async function executeSetFrontmatter(app: App, args: Record<string, unknown>): Promise<string> {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
if (filePath === "") {
return "Error: file_path parameter is required.";
}
- let properties = args.properties;
+ let properties = args["properties"];
// The model may pass properties as a JSON string — parse it
if (typeof properties === "string") {
@@ -481,7 +481,7 @@ function parseArrayArg(value: unknown): unknown[] | null {
* Runs multiple search queries and returns combined results.
*/
async function executeBatchSearchFiles(app: App, args: Record<string, unknown>): Promise<string> {
- const queries = parseArrayArg(args.queries);
+ const queries = parseArrayArg(args["queries"]);
if (queries === null || queries.length === 0) {
return "Error: queries parameter must be a non-empty array of strings.";
}
@@ -502,7 +502,7 @@ async function executeBatchSearchFiles(app: App, args: Record<string, unknown>):
* Runs multiple content searches and returns combined results.
*/
async function executeBatchGrepSearch(app: App, args: Record<string, unknown>): Promise<string> {
- const queries = parseArrayArg(args.queries);
+ const queries = parseArrayArg(args["queries"]);
if (queries === null || queries.length === 0) {
return "Error: queries parameter must be a non-empty array of search query objects.";
}
@@ -516,8 +516,8 @@ async function executeBatchGrepSearch(app: App, args: Record<string, unknown>):
}
const queryObj = q as Record<string, unknown>;
const result = await executeGrepSearch(app, queryObj);
- const queryText = typeof queryObj.query === "string" ? queryObj.query : "";
- const filePattern = typeof queryObj.file_pattern === "string" ? ` (in "${queryObj.file_pattern}")` : "";
+ const queryText = typeof queryObj["query"] === "string" ? queryObj["query"] : "";
+ const filePattern = typeof queryObj["file_pattern"] === "string" ? ` (in "${queryObj["file_pattern"]}")` : "";
results.push(`--- Query ${i + 1}: "${queryText}"${filePattern} ---\n${result}`);
}
@@ -529,7 +529,7 @@ async function executeBatchGrepSearch(app: App, args: Record<string, unknown>):
* Deletes multiple files, continuing on failure and reporting per-file results.
*/
async function executeBatchDeleteFile(app: App, args: Record<string, unknown>): Promise<string> {
- const filePaths = parseArrayArg(args.file_paths);
+ const filePaths = parseArrayArg(args["file_paths"]);
if (filePaths === null || filePaths.length === 0) {
return "Error: file_paths parameter must be a non-empty array of strings.";
}
@@ -558,7 +558,7 @@ async function executeBatchDeleteFile(app: App, args: Record<string, unknown>):
* Moves/renames multiple files, continuing on failure.
*/
async function executeBatchMoveFile(app: App, args: Record<string, unknown>): Promise<string> {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
if (operations === null || operations.length === 0) {
return "Error: operations parameter must be a non-empty array of {file_path, new_path} objects.";
}
@@ -574,8 +574,8 @@ async function executeBatchMoveFile(app: App, args: Record<string, unknown>): Pr
continue;
}
const opObj = op as Record<string, unknown>;
- const filePath = typeof opObj.file_path === "string" ? opObj.file_path : "";
- const newPath = typeof opObj.new_path === "string" ? opObj.new_path : "";
+ const filePath = typeof opObj["file_path"] === "string" ? opObj["file_path"] : "";
+ const newPath = typeof opObj["new_path"] === "string" ? opObj["new_path"] : "";
const result = await executeMoveFile(app, { file_path: filePath, new_path: newPath });
if (result.startsWith("Error")) {
failures++;
@@ -594,7 +594,7 @@ async function executeBatchMoveFile(app: App, args: Record<string, unknown>): Pr
* Sets frontmatter on multiple files, continuing on failure.
*/
async function executeBatchSetFrontmatter(app: App, args: Record<string, unknown>): Promise<string> {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
if (operations === null || operations.length === 0) {
return "Error: operations parameter must be a non-empty array of {file_path, properties} objects.";
}
@@ -610,8 +610,8 @@ async function executeBatchSetFrontmatter(app: App, args: Record<string, unknown
continue;
}
const opObj = op as Record<string, unknown>;
- const filePath = typeof opObj.file_path === "string" ? opObj.file_path : "";
- const result = await executeSetFrontmatter(app, { file_path: filePath, properties: opObj.properties });
+ const filePath = typeof opObj["file_path"] === "string" ? opObj["file_path"] : "";
+ const result = await executeSetFrontmatter(app, { file_path: filePath, properties: opObj["properties"] });
if (result.startsWith("Error")) {
failures++;
} else {
@@ -629,7 +629,7 @@ async function executeBatchSetFrontmatter(app: App, args: Record<string, unknown
* Performs multiple file edits, continuing on failure.
*/
async function executeBatchEditFile(app: App, args: Record<string, unknown>): Promise<string> {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
if (operations === null || operations.length === 0) {
return "Error: operations parameter must be a non-empty array of {file_path, old_text, new_text} objects.";
}
@@ -645,11 +645,11 @@ async function executeBatchEditFile(app: App, args: Record<string, unknown>): Pr
continue;
}
const opObj = op as Record<string, unknown>;
- const filePath = typeof opObj.file_path === "string" ? opObj.file_path : "";
+ const filePath = typeof opObj["file_path"] === "string" ? opObj["file_path"] : "";
const result = await executeEditFile(app, {
file_path: filePath,
- old_text: opObj.old_text,
- new_text: opObj.new_text,
+ old_text: opObj["old_text"],
+ new_text: opObj["new_text"],
});
if (result.startsWith("Error")) {
failures++;
@@ -673,8 +673,8 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(searchFilesCtx as Record<string, unknown>),
summarize: (args) => {
- const query = typeof args.query === "string" ? args.query : "";
- if (query === "" && args.queries !== undefined) {
+ const query = typeof args["query"] === "string" ? args["query"] : "";
+ if (query === "" && args["queries"] !== undefined) {
return "(wrong params: used 'queries' instead of 'query')";
}
return `"${query}"`;
@@ -697,7 +697,7 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(readFileCtx as Record<string, unknown>),
summarize: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
return `"/${filePath}"`;
},
summarizeResult: (result) => {
@@ -712,11 +712,11 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(deleteFileCtx as Record<string, unknown>),
approvalMessage: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "unknown";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "unknown";
return `Delete "${filePath}"?`;
},
summarize: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
return `"/${filePath}"`;
},
summarizeResult: (result) => {
@@ -744,11 +744,11 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(editFileCtx as Record<string, unknown>),
approvalMessage: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "unknown";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "unknown";
return `Edit "${filePath}"?`;
},
summarize: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
return `"/${filePath}"`;
},
summarizeResult: (result) => {
@@ -765,9 +765,9 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(grepSearchCtx as Record<string, unknown>),
summarize: (args) => {
- const query = typeof args.query === "string" ? args.query : "";
- const filePattern = typeof args.file_pattern === "string" ? args.file_pattern : "";
- if (query === "" && args.queries !== undefined) {
+ const query = typeof args["query"] === "string" ? args["query"] : "";
+ const filePattern = typeof args["file_pattern"] === "string" ? args["file_pattern"] : "";
+ if (query === "" && args["queries"] !== undefined) {
return "(wrong params: used 'queries' instead of 'query')";
}
const suffix = filePattern !== "" ? ` in "${filePattern}"` : "";
@@ -790,11 +790,11 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(createFileCtx as Record<string, unknown>),
approvalMessage: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "unknown";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "unknown";
return `Create "${filePath}"?`;
},
summarize: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
return `"/${filePath}"`;
},
summarizeResult: (result) => {
@@ -811,13 +811,13 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(moveFileCtx as Record<string, unknown>),
approvalMessage: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "unknown";
- const newPath = typeof args.new_path === "string" ? args.new_path : "unknown";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "unknown";
+ const newPath = typeof args["new_path"] === "string" ? args["new_path"] : "unknown";
return `Move "${filePath}" to "${newPath}"?`;
},
summarize: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
- const newPath = typeof args.new_path === "string" ? args.new_path : "";
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
+ const newPath = typeof args["new_path"] === "string" ? args["new_path"] : "";
return `"/${filePath}" \u2192 "/${newPath}"`;
},
summarizeResult: (result) => {
@@ -834,16 +834,16 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(setFrontmatterCtx as Record<string, unknown>),
approvalMessage: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "unknown";
- const props = typeof args.properties === "object" && args.properties !== null
- ? Object.keys(args.properties as Record<string, unknown>)
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "unknown";
+ const props = typeof args["properties"] === "object" && args["properties"] !== null
+ ? Object.keys(args["properties"] as Record<string, unknown>)
: [];
return `Update frontmatter in "${filePath}"? Properties: ${props.join(", ")}`;
},
summarize: (args) => {
- const filePath = typeof args.file_path === "string" ? args.file_path : "";
- const props = typeof args.properties === "object" && args.properties !== null
- ? Object.keys(args.properties as Record<string, unknown>)
+ const filePath = typeof args["file_path"] === "string" ? args["file_path"] : "";
+ const props = typeof args["properties"] === "object" && args["properties"] !== null
+ ? Object.keys(args["properties"] as Record<string, unknown>)
: [];
return `"/${filePath}" \u2014 ${props.join(", ")}`;
},
@@ -862,7 +862,7 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(batchSearchFilesCtx as Record<string, unknown>),
summarize: (args) => {
- const queries = parseArrayArg(args.queries);
+ const queries = parseArrayArg(args["queries"]);
const count = queries !== null ? queries.length : 0;
return `${count} search quer${count === 1 ? "y" : "ies"}`;
},
@@ -876,7 +876,7 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(batchGrepSearchCtx as Record<string, unknown>),
summarize: (args) => {
- const queries = parseArrayArg(args.queries);
+ const queries = parseArrayArg(args["queries"]);
const count = queries !== null ? queries.length : 0;
return `${count} content search${count === 1 ? "" : "es"}`;
},
@@ -890,13 +890,13 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(batchDeleteFileCtx as Record<string, unknown>),
approvalMessage: (args) => {
- const filePaths = parseArrayArg(args.file_paths);
+ const filePaths = parseArrayArg(args["file_paths"]);
if (filePaths === null || filePaths.length === 0) return "Delete files?";
const list = filePaths.map((fp) => ` \u2022 ${typeof fp === "string" ? fp : "(invalid)"}`);
return `Delete ${filePaths.length} file${filePaths.length === 1 ? "" : "s"}?\n${list.join("\n")}`;
},
summarize: (args) => {
- const filePaths = parseArrayArg(args.file_paths);
+ const filePaths = parseArrayArg(args["file_paths"]);
const count = filePaths !== null ? filePaths.length : 0;
return `${count} file${count === 1 ? "" : "s"}`;
},
@@ -912,19 +912,19 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(batchMoveFileCtx as Record<string, unknown>),
approvalMessage: (args) => {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
if (operations === null || operations.length === 0) return "Move files?";
const list = operations.map((op) => {
if (typeof op !== "object" || op === null) return " \u2022 (invalid entry)";
const o = op as Record<string, unknown>;
- const from = typeof o.file_path === "string" ? o.file_path : "?";
- const to = typeof o.new_path === "string" ? o.new_path : "?";
+ const from = typeof o["file_path"] === "string" ? o["file_path"] : "?";
+ const to = typeof o["new_path"] === "string" ? o["new_path"] : "?";
return ` \u2022 ${from} \u2192 ${to}`;
});
return `Move ${operations.length} file${operations.length === 1 ? "" : "s"}?\n${list.join("\n")}`;
},
summarize: (args) => {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
const count = operations !== null ? operations.length : 0;
return `${count} file${count === 1 ? "" : "s"}`;
},
@@ -940,18 +940,18 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(batchSetFrontmatterCtx as Record<string, unknown>),
approvalMessage: (args) => {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
if (operations === null || operations.length === 0) return "Update frontmatter?";
const list = operations.map((op) => {
if (typeof op !== "object" || op === null) return " \u2022 (invalid entry)";
const o = op as Record<string, unknown>;
- const fp = typeof o.file_path === "string" ? o.file_path : "?";
+ const fp = typeof o["file_path"] === "string" ? o["file_path"] : "?";
let propsStr = "";
- if (typeof o.properties === "object" && o.properties !== null) {
- propsStr = Object.keys(o.properties as Record<string, unknown>).join(", ");
- } else if (typeof o.properties === "string") {
+ if (typeof o["properties"] === "object" && o["properties"] !== null) {
+ propsStr = Object.keys(o["properties"] as Record<string, unknown>).join(", ");
+ } else if (typeof o["properties"] === "string") {
try {
- const parsed = JSON.parse(o.properties) as Record<string, unknown>;
+ const parsed = JSON.parse(o["properties"]) as Record<string, unknown>;
propsStr = Object.keys(parsed).join(", ");
} catch { propsStr = "(properties)"; }
}
@@ -960,7 +960,7 @@ export const TOOL_REGISTRY: ToolEntry[] = [
return `Update frontmatter on ${operations.length} file${operations.length === 1 ? "" : "s"}?\n${list.join("\n")}`;
},
summarize: (args) => {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
const count = operations !== null ? operations.length : 0;
return `${count} file${count === 1 ? "" : "s"}`;
},
@@ -976,18 +976,18 @@ export const TOOL_REGISTRY: ToolEntry[] = [
{
...asToolContext(batchEditFileCtx as Record<string, unknown>),
approvalMessage: (args) => {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
if (operations === null || operations.length === 0) return "Edit files?";
const list = operations.map((op) => {
if (typeof op !== "object" || op === null) return " \u2022 (invalid entry)";
const o = op as Record<string, unknown>;
- const fp = typeof o.file_path === "string" ? o.file_path : "?";
+ const fp = typeof o["file_path"] === "string" ? o["file_path"] : "?";
return ` \u2022 ${fp}`;
});
return `Edit ${operations.length} file${operations.length === 1 ? "" : "s"}?\n${list.join("\n")}`;
},
summarize: (args) => {
- const operations = parseArrayArg(args.operations);
+ const operations = parseArrayArg(args["operations"]);
const count = operations !== null ? operations.length : 0;
return `${count} file${count === 1 ? "" : "s"}`;
},
diff --git a/src/vault-context.ts b/src/vault-context.ts
index 6ac55ca..8cdda81 100644
--- a/src/vault-context.ts
+++ b/src/vault-context.ts
@@ -84,8 +84,8 @@ function buildTagTaxonomy(app: App): string {
}
// Frontmatter tags
- if (cache.frontmatter?.tags !== undefined) {
- const fmTags = cache.frontmatter.tags;
+ if (cache.frontmatter?.['tags'] !== undefined) {
+ const fmTags = cache.frontmatter['tags'];
if (Array.isArray(fmTags)) {
for (const raw of fmTags) {
const tag = typeof raw === "string"