From 6ca2dc01ea4f4da9133cc9348986a03e01ed4742 Mon Sep 17 00:00:00 2001 From: Zero Liu Date: Thu, 14 May 2026 02:08:45 -0700 Subject: [PATCH] chore(eslint): enable no-explicit-any; fix ~395 violations (#2452) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(eslint): enable no-explicit-any; fix ~395 violations Switches @typescript-eslint/no-explicit-any from "off" to "error" and replaces explicit `any` with proper types or `unknown` + narrowing across ~100 source files and 15 test files. Eleven eslint-disable comments remain: one for a BaseLanguageModel prototype patch and ten for Orama API surfaces where typed alternatives poison Orama's internal inference to `never`. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(scripts): cast chainContext to ChainManager in printPromptDebugEntry The earlier `as any` → `as unknown` rewrite left buildAgentPromptDebugReport's chainManager argument typed as `unknown`, which CI's `tsc -noEmit` (without `--skipLibCheck`) flagged. Restore the cast through the real ChainManager type. Co-Authored-By: Claude Opus 4.7 (1M context) * chore(eslint): fix remaining no-explicit-any and unnecessary-type-assertion errors Co-Authored-By: Claude Opus 4.7 (1M context) * chore(eslint): drop redundant no-explicit-any rule Already set to "error" by typescript-eslint/recommendedTypeChecked via obsidianmd's recommended config. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) --- eslint.config.mjs | 1 - scripts/printPromptDebugEntry.ts | 3 +- scripts/stubs/obsidian.ts | 2 +- src/LLMProviders/BedrockChatModel.test.ts | 8 +- src/LLMProviders/BedrockChatModel.ts | 209 ++++++++------ src/LLMProviders/ChatLMStudio.ts | 6 +- src/LLMProviders/ChatOpenRouter.ts | 50 ++-- src/LLMProviders/CustomOpenAIEmbeddings.ts | 19 +- src/LLMProviders/brevilabsClient.ts | 16 +- .../chainRunner/AutonomousAgentChainRunner.ts | 25 +- .../chainRunner/BaseChainRunner.ts | 29 +- .../chainRunner/CopilotPlusChainRunner.ts | 157 +++++++---- .../chainRunner/LLMChainRunner.ts | 18 +- .../chainRunner/VaultQAChainRunner.ts | 46 +-- .../utils/ActionBlockStreamer.test.ts | 6 +- .../chainRunner/utils/ActionBlockStreamer.ts | 16 +- .../chainRunner/utils/ThinkBlockStreamer.ts | 47 +++- .../utils/chatHistoryUtils.test.ts | 2 +- .../chainRunner/utils/chatHistoryUtils.ts | 42 +-- .../chainRunner/utils/finishReasonDetector.ts | 55 ++-- .../chainRunner/utils/modelAdapter.ts | 10 +- .../utils/promptDebugService.test.ts | 2 +- .../utils/promptPayloadRecorder.ts | 16 +- .../chainRunner/utils/searchResultUtils.ts | 106 ++++--- .../chainRunner/utils/toolExecution.ts | 16 +- .../chainRunner/utils/toolPromptDebugger.ts | 2 +- src/LLMProviders/chatModelManager.ts | 47 ++-- src/LLMProviders/embeddingManager.ts | 4 +- .../githubCopilot/GitHubCopilotChatModel.ts | 6 +- src/LLMProviders/memoryManager.ts | 9 +- src/LLMProviders/selfHostServices.test.ts | 2 +- src/aiParams.ts | 3 +- src/cache/fileCache.ts | 2 +- src/chainFactory.ts | 2 +- src/commands/CustomCommandSettingsModal.tsx | 2 +- src/commands/index.ts | 18 +- src/components/Chat.tsx | 5 +- .../chat-components/AtMentionTypeahead.tsx | 5 +- .../chat-components/ChatContextMenu.tsx | 13 +- .../chat-components/ChatControls.tsx | 18 +- src/components/chat-components/ChatInput.tsx | 23 +- .../chat-components/ChatSettingsPopover.tsx | 2 +- .../ChatSingleMessage.test.tsx | 4 +- .../chat-components/ContextControl.tsx | 15 +- .../chat-components/LexicalEditor.tsx | 2 +- .../chat-components/pills/FolderPillNode.tsx | 2 +- .../chat-components/pills/ToolPillNode.tsx | 2 +- .../chat-components/pills/URLPillNode.tsx | 2 +- .../chat-components/plugins/FocusPlugin.tsx | 3 +- .../plugins/FolderPillSyncPlugin.tsx | 3 +- .../plugins/GenericPillSyncPlugin.tsx | 4 +- .../plugins/NotePillSyncPlugin.tsx | 11 +- .../plugins/PillDeletionPlugin.tsx | 5 +- .../plugins/ToolPillSyncPlugin.tsx | 3 +- .../plugins/URLPillSyncPlugin.tsx | 3 +- .../utils/lexicalTextUtils.test.ts | 4 +- src/components/modals/SourcesModal.tsx | 54 ++-- .../modals/project/AddProjectModal.tsx | 4 +- .../modals/project/context-manage-modal.tsx | 4 +- src/components/ui/ModelParametersEditor.tsx | 2 +- src/components/ui/mobile-card.tsx | 2 +- src/context/ChatHistoryCompactor.test.ts | 2 +- src/context/L2ContextCompactor.test.ts | 2 +- src/contextProcessor.dataview.test.ts | 41 ++- src/contextProcessor.embeds.test.ts | 6 +- src/contextProcessor.ts | 68 +++-- src/core/ChatManager.ts | 3 +- src/core/ChatPersistenceManager.ts | 14 +- src/core/MessageRepository.test.ts | 2 +- src/core/MessageRepository.ts | 5 +- src/errorFormat.ts | 9 +- src/hooks/use-streaming-chat-session.ts | 2 +- src/lib/plugins/colorOpacityPlugin.ts | 26 +- src/main.ts | 6 +- src/memory/UserMemoryManager.test.ts | 4 +- src/miyo/MiyoServiceDiscovery.ts | 2 +- src/plusUtils.ts | 4 +- src/search/chunkedStorage.ts | 49 ++-- src/search/dbOperations.ts | 41 ++- src/search/findRelevantNotes.ts | 2 + src/search/hybridRetriever.ts | 4 +- src/search/indexBackend/OramaIndexBackend.ts | 3 +- src/search/indexOperations.ts | 45 +-- src/search/v3/MergedSemanticRetriever.ts | 6 +- src/search/v3/QueryExpander.ts | 12 +- src/search/v3/SearchCore.test.ts | 2 +- src/search/v3/TieredLexicalRetriever.ts | 7 +- src/search/v3/chunks.test.ts | 20 +- src/search/v3/engines/FullTextEngine.test.ts | 16 +- src/search/vectorStoreManager.ts | 1 + src/settings/providerModels.ts | 42 ++- .../v2/components/ModelEditDialog.tsx | 2 +- src/state/ChatUIState.ts | 3 +- .../systemPromptManager.test.ts | 4 +- .../systemPromptRegister.test.ts | 4 +- src/tools/FileParserManager.ts | 17 +- src/tools/FileTreeTools.test.ts | 111 ++++---- src/tools/FileTreeTools.ts | 8 +- src/tools/SearchTools.ts | 16 +- src/tools/ToolResultFormatter.ts | 266 +++++++++++------- src/tools/allTools.validation.test.ts | 9 +- src/tools/toolManager.ts | 2 +- src/types.ts | 2 +- src/types/message.ts | 8 +- src/utils.ts | 64 +++-- src/utils/debounce.ts | 2 + src/utils/rateLimitUtils.ts | 12 +- 107 files changed, 1322 insertions(+), 853 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index d38e4719..b820dc6c 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -226,7 +226,6 @@ export default [ }, }, rules: { - "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-empty-function": "off", "@typescript-eslint/ban-ts-comment": "off", "@typescript-eslint/no-unused-vars": ["error", { args: "none" }], diff --git a/scripts/printPromptDebugEntry.ts b/scripts/printPromptDebugEntry.ts index d39d9e01..2bddd654 100644 --- a/scripts/printPromptDebugEntry.ts +++ b/scripts/printPromptDebugEntry.ts @@ -1,3 +1,4 @@ +import type ChainManager from "@/LLMProviders/chainManager"; import MemoryManager from "@/LLMProviders/memoryManager"; import { ModelAdapterFactory } from "@/LLMProviders/chainRunner/utils/modelAdapter"; import { buildAgentPromptDebugReport } from "@/LLMProviders/chainRunner/utils/promptDebugService"; @@ -116,7 +117,7 @@ export async function run(args: string[]): Promise { const chainContext = { memoryManager, userMemoryManager, - } as any; + } as unknown as ChainManager; const adapter = ModelAdapterFactory.createAdapter({ modelName: "gpt-4", diff --git a/scripts/stubs/obsidian.ts b/scripts/stubs/obsidian.ts index 3c341ea0..08526fca 100644 --- a/scripts/stubs/obsidian.ts +++ b/scripts/stubs/obsidian.ts @@ -88,6 +88,6 @@ export class Modal { } } -export function parseYaml(_: string): any { +export function parseYaml(_: string): unknown { return {}; } diff --git a/src/LLMProviders/BedrockChatModel.test.ts b/src/LLMProviders/BedrockChatModel.test.ts index 3c0d7836..dc1f6fb4 100644 --- a/src/LLMProviders/BedrockChatModel.test.ts +++ b/src/LLMProviders/BedrockChatModel.test.ts @@ -229,7 +229,7 @@ describe("BedrockChatModel streaming decode", () => { // Check that content is an array with thinking type expect(Array.isArray(chunk?.message.content)).toBe(true); - const content = chunk?.message.content as any[]; + const content = chunk?.message.content as unknown[]; expect(content).toHaveLength(1); expect(content[0]).toEqual({ type: "thinking", @@ -276,7 +276,7 @@ describe("BedrockChatModel streaming decode", () => { // Check that content is an array with text type expect(Array.isArray(chunk?.message.content)).toBe(true); - const content = chunk?.message.content as any[]; + const content = chunk?.message.content as unknown[]; expect(content).toHaveLength(1); expect(content[0]).toEqual({ type: "text", @@ -317,7 +317,7 @@ describe("BedrockChatModel streaming decode", () => { expect(thinkingResult.deltaChunks).toHaveLength(1); const thinkingChunk = thinkingResult.deltaChunks[0]; - expect((thinkingChunk?.message.content as any[])[0]?.type).toBe("thinking"); + expect((thinkingChunk?.message.content as Array<{ type: string }>)[0]?.type).toBe("thinking"); // Second chunk: text const textPayload = JSON.stringify({ @@ -346,7 +346,7 @@ describe("BedrockChatModel streaming decode", () => { expect(textResult.deltaChunks).toHaveLength(1); const textChunk = textResult.deltaChunks[0]; - expect((textChunk?.message.content as any[])[0]?.type).toBe("text"); + expect((textChunk?.message.content as Array<{ type: string }>)[0]?.type).toBe("text"); }); it("extractStreamText can fallback to extract thinking content", () => { diff --git a/src/LLMProviders/BedrockChatModel.ts b/src/LLMProviders/BedrockChatModel.ts index f8dabc7a..3464e25d 100644 --- a/src/LLMProviders/BedrockChatModel.ts +++ b/src/LLMProviders/BedrockChatModel.ts @@ -133,12 +133,18 @@ export class BedrockChatModel extends BaseChatModel /** * Convert LangChain tools to Claude's tool format for Bedrock. */ - private convertToolsToClaude(tools: StructuredToolInterface[]): any[] { + private convertToolsToClaude(tools: StructuredToolInterface[]): Array<{ + name: string; + description: string; + input_schema: Record; + }> { return tools.map((tool) => { - let inputSchema: any = { type: "object", properties: {} }; + let inputSchema: Record = { type: "object", properties: {} }; if (tool.schema) { // Use LangChain's schema conversion utilities - inputSchema = isInteropZodSchema(tool.schema) ? toJsonSchema(tool.schema) : tool.schema; + inputSchema = ( + isInteropZodSchema(tool.schema) ? toJsonSchema(tool.schema) : tool.schema + ) as Record; } return { name: tool.name, @@ -151,19 +157,26 @@ export class BedrockChatModel extends BaseChatModel /** * Extract tool calls from Claude's response format. */ - private extractToolCalls(data: any): any[] | undefined { - if (!Array.isArray(data?.content)) return undefined; + private extractToolCalls( + data: unknown + ): + | Array<{ id: string; name: string; args: Record; type: "tool_call" }> + | undefined { + const dataObj = data as Record | null | undefined; + if (!Array.isArray(dataObj?.content)) return undefined; - const toolUseBlocks: any[] = data.content.filter((block: any) => block.type === "tool_use"); + const toolUseBlocks = (dataObj.content as Record[]).filter( + (block) => block.type === "tool_use" + ); if (toolUseBlocks.length === 0) return undefined; - return toolUseBlocks.map((block: any) => ({ + return toolUseBlocks.map((block) => ({ id: block.id as string, name: block.name as string, args: (block.input || {}) as Record, type: "tool_call" as const, - })) as Array<{ id: string; name: string; args: Record; type: "tool_call" }>; + })); } async _generate( @@ -188,7 +201,7 @@ export class BedrockChatModel extends BaseChatModel throw new Error(`Amazon Bedrock request failed with status ${response.status}: ${errorText}`); } - const data = await response.json(); + const data = (await response.json()) as Record; const text = this.extractText(data); const toolCalls = this.extractToolCalls(data); @@ -306,13 +319,14 @@ export class BedrockChatModel extends BaseChatModel byteBuffer = new Uint8Array(remainingBytes); for (const messagePayload of messages) { - const outerEvent = this.safeJsonParse(messagePayload); - if (!outerEvent) { + const parsedOuter = this.safeJsonParse(messagePayload); + if (!parsedOuter || typeof parsedOuter !== "object") { logWarn( `[${requestId}] Failed to parse event JSON: ${messagePayload.slice(0, 100)}...` ); continue; } + const outerEvent = parsedOuter as Record; // Handle AWS EventStream format where bytes field is at top level let eventToProcess = outerEvent; @@ -404,7 +418,7 @@ export class BedrockChatModel extends BaseChatModel } } - private safeJsonParse(value: string): any { + private safeJsonParse(value: string): unknown { try { return JSON.parse(value); } catch { @@ -420,18 +434,21 @@ export class BedrockChatModel extends BaseChatModel * @returns Claude-style content array or null if not classifiable */ private buildContentItemsFromDelta( - event: any + event: Record ): Array<{ type: string; text?: string; thinking?: string }> | null { if (!event || typeof event !== "object") { return null; } // Check for content_block_delta format (nested delta) - const delta = event.content_block_delta?.delta || event.contentBlockDelta?.delta || event.delta; + const contentBlockDelta = event.content_block_delta as Record | undefined; + const contentBlockDeltaCamel = event.contentBlockDelta as Record | undefined; + const deltaUnknown = contentBlockDelta?.delta ?? contentBlockDeltaCamel?.delta ?? event.delta; - if (!delta || typeof delta !== "object") { + if (!deltaUnknown || typeof deltaUnknown !== "object") { return null; } + const delta = deltaUnknown as Record; const deltaType = delta.type; @@ -461,27 +478,29 @@ export class BedrockChatModel extends BaseChatModel * Returns tool_call_chunks format that LangChain can concatenate. */ private extractToolCallChunk( - event: any + event: Record ): { id?: string; index: number; name?: string; args?: string } | null { if (!event || typeof event !== "object") { return null; } // Handle content_block_start with tool_use - initial tool call info - if (event.type === "content_block_start" && event.content_block?.type === "tool_use") { + const contentBlock = event.content_block as Record | undefined; + if (event.type === "content_block_start" && contentBlock?.type === "tool_use") { return { - id: event.content_block.id, - index: event.index ?? 0, - name: event.content_block.name, + id: contentBlock.id as string | undefined, + index: typeof event.index === "number" ? event.index : 0, + name: contentBlock.name as string | undefined, args: "", }; } // Handle content_block_delta with input_json_delta - partial tool args - if (event.type === "content_block_delta" && event.delta?.type === "input_json_delta") { + const eventDelta = event.delta as Record | undefined; + if (event.type === "content_block_delta" && eventDelta?.type === "input_json_delta") { return { - index: event.index ?? 0, - args: event.delta.partial_json || "", + index: typeof event.index === "number" ? event.index : 0, + args: typeof eventDelta.partial_json === "string" ? eventDelta.partial_json : "", }; } @@ -489,7 +508,7 @@ export class BedrockChatModel extends BaseChatModel } private async processStreamEvent( - event: any, + event: Record, runManager: CallbackManagerForLLMRun | undefined, currentUsage?: Record, currentStopReason?: string @@ -506,23 +525,25 @@ export class BedrockChatModel extends BaseChatModel let hasText = false; const debugSummaries: string[] = []; - if (event?.type === "chunk" && typeof event.chunk?.bytes === "string") { - const decodedPayloads = this.decodeChunkBytes(event.chunk.bytes as string); + const eventChunk = event.chunk as Record | undefined; + if (event?.type === "chunk" && typeof eventChunk?.bytes === "string") { + const decodedPayloads = this.decodeChunkBytes(eventChunk.bytes); for (const payload of decodedPayloads) { - const innerEvent = this.safeJsonParse(payload); - if (!innerEvent) { + const parsedInner = this.safeJsonParse(payload); + if (!parsedInner || typeof parsedInner !== "object") { debugSummaries.push(`Failed to parse inner payload: ${this.describePayload(payload)}`); continue; } + const innerEvent = parsedInner as Record; - const chunkMetadata = this.buildChunkMetadata(innerEvent as Record); + const chunkMetadata = this.buildChunkMetadata(innerEvent); // Try to build structured content (Claude-style arrays) - const contentItems = this.buildContentItemsFromDelta(innerEvent as Record); + const contentItems = this.buildContentItemsFromDelta(innerEvent); // Check for tool call chunks first (content_block_start with tool_use, or input_json_delta) - const toolCallChunk = this.extractToolCallChunk(innerEvent as Record); + const toolCallChunk = this.extractToolCallChunk(innerEvent); if (toolCallChunk) { const messageChunk = new AIMessageChunk({ content: "", @@ -595,9 +616,10 @@ export class BedrockChatModel extends BaseChatModel } else { // Only log if it's an unexpected event type that should have had content // But don't warn for tool-related events which are handled above + const innerDelta = innerEvent.delta as Record | undefined; if ( innerEvent.type === "content_block_delta" && - innerEvent.delta?.type !== "input_json_delta" + innerDelta?.type !== "input_json_delta" ) { const summary = `No content in content_block_delta event: ${this.describeEvent(innerEvent)}`; debugSummaries.push(summary); @@ -617,13 +639,13 @@ export class BedrockChatModel extends BaseChatModel } } } else { - const chunkMetadata = this.buildChunkMetadata(event as Record); + const chunkMetadata = this.buildChunkMetadata(event); // Try to build structured content (Claude-style arrays) - const contentItems = this.buildContentItemsFromDelta(event as Record); + const contentItems = this.buildContentItemsFromDelta(event); // Check for tool call chunks first - const toolCallChunk = this.extractToolCallChunk(event as Record); + const toolCallChunk = this.extractToolCallChunk(event); if (toolCallChunk) { const messageChunk = new AIMessageChunk({ content: "", @@ -942,18 +964,28 @@ export class BedrockChatModel extends BaseChatModel return metadata; } - private extractStreamText(event: any): string | null { + private extractStreamText(event: Record): string | null { if (!event || typeof event !== "object") { return null; } + const evDelta = event.delta as Record | undefined; + const evContentBlockDelta = event.content_block_delta as Record | undefined; + const evContentBlockDeltaCamel = event.contentBlockDelta as Record | undefined; + const evContentBlockDeltaInner = evContentBlockDelta?.delta as + | Record + | undefined; + const evContentBlockDeltaCamelInner = evContentBlockDeltaCamel?.delta as + | Record + | undefined; + // Check for thinking/reasoning fields first (fallback for non-streaming or debugging) // This ensures thinking content is never silently dropped const thinkingCandidates: Array = [ - event.delta?.thinking, - event.content_block_delta?.delta?.thinking, - event.contentBlockDelta?.delta?.thinking, - event.delta?.reasoning_content, // Deepseek compatibility + evDelta?.thinking, + evContentBlockDeltaInner?.thinking, + evContentBlockDeltaCamelInner?.thinking, + evDelta?.reasoning_content, // Deepseek compatibility event.reasoning_content, ]; @@ -977,19 +1009,27 @@ export class BedrockChatModel extends BaseChatModel } } + const evMessage = event.message as Record | undefined; + const evMessageStop = event.messageStop as Record | undefined; + const evMessageStopSnake = event.message_stop as Record | undefined; + const evMessageStopMsg = evMessageStop?.message as Record | undefined; + const evMessageStopSnakeMsg = evMessageStopSnake?.message as + | Record + | undefined; + const nestedCandidates: Array = [ - event.delta?.text, - event.delta?.output_text, - event.delta?.content, - event.contentBlockDelta?.delta?.text, - event.contentBlockDelta?.delta?.output_text, - event.contentBlockDelta?.delta?.content, - event.content_block_delta?.delta?.text, - event.content_block_delta?.delta?.output_text, - event.content_block_delta?.delta?.content, - event.message?.content, - event.messageStop?.message?.content, - event.message_stop?.message?.content, + evDelta?.text, + evDelta?.output_text, + evDelta?.content, + evContentBlockDeltaCamelInner?.text, + evContentBlockDeltaCamelInner?.output_text, + evContentBlockDeltaCamelInner?.content, + evContentBlockDeltaInner?.text, + evContentBlockDeltaInner?.output_text, + evContentBlockDeltaInner?.content, + evMessage?.content, + evMessageStopMsg?.content, + evMessageStopSnakeMsg?.content, event.content, ]; @@ -1014,20 +1054,24 @@ export class BedrockChatModel extends BaseChatModel if (Array.isArray(candidate)) { const combined = candidate - .map((part: any): string => { + .map((part: unknown): string => { if (typeof part === "string") { return part; } if (part && typeof part === "object") { - if (typeof part.text === "string") { - return part.text as string; + const partObj = part as Record; + if (typeof partObj.text === "string") { + return partObj.text; } - if (typeof part.value === "string") { - return part.value as string; + if (typeof partObj.value === "string") { + return partObj.value; } - if (Array.isArray(part.content)) { - return (part.content as any[]) - .map((sub: any) => (typeof sub?.text === "string" ? (sub.text as string) : "")) + if (Array.isArray(partObj.content)) { + return (partObj.content as unknown[]) + .map((sub: unknown) => { + const subObj = sub as Record | null | undefined; + return typeof subObj?.text === "string" ? subObj.text : ""; + }) .join(""); } } @@ -1071,7 +1115,7 @@ export class BedrockChatModel extends BaseChatModel return null; } - private extractUsage(event: any): Record | undefined { + private extractUsage(event: Record): Record | undefined { if (!event || typeof event !== "object") { return undefined; } @@ -1093,29 +1137,31 @@ export class BedrockChatModel extends BaseChatModel } if (event.messageStop && typeof event.messageStop === "object") { - return this.extractUsage(event.messageStop); + return this.extractUsage(event.messageStop as Record); } if (event.message_stop && typeof event.message_stop === "object") { - return this.extractUsage(event.message_stop); + return this.extractUsage(event.message_stop as Record); } return undefined; } - private extractStopReason(event: any): string | undefined { + private extractStopReason(event: Record): string | undefined { if (!event || typeof event !== "object") { return undefined; } + const evMessageStop = event.messageStop as Record | undefined; + const evMessageStopSnake = event.message_stop as Record | undefined; const stopReason = event.stop_reason || event.stopReason || event.completionReason || event.completion_reason || event.reason || - event.messageStop?.stopReason || - event.message_stop?.stop_reason || + evMessageStop?.stopReason || + evMessageStopSnake?.stop_reason || (event.type === "message_stop" ? event.reason : undefined); return typeof stopReason === "string" ? stopReason : undefined; @@ -1342,9 +1388,13 @@ export class BedrockChatModel extends BaseChatModel type: "text", text: block.text, }); - } else if (block.type === "image_url" && block.image_url?.url) { + } else if ( + block.type === "image_url" && + (block.image_url as Record | undefined)?.url + ) { // Image block in OpenAI format - convert to Claude format - const claudeImage = this.convertImageContent(block.image_url.url as string); + const imageUrlBlock = block.image_url as Record; + const claudeImage = this.convertImageContent(imageUrlBlock.url as string); if (claudeImage) { contentBlocks.push(claudeImage); } @@ -1423,7 +1473,7 @@ export class BedrockChatModel extends BaseChatModel */ private normaliseMessageContent( message: BaseMessage - ): string | Array<{ type: string; [key: string]: any }> { + ): string | Array<{ type: string; [key: string]: unknown }> { const { content } = message; // Handle string content (simple text message) @@ -1463,7 +1513,7 @@ export class BedrockChatModel extends BaseChatModel } return null; }) - .filter((part): part is { type: string; [key: string]: any } => part !== null); + .filter((part): part is { type: string; [key: string]: unknown } => part !== null); } // No images, flatten to string @@ -1494,20 +1544,21 @@ export class BedrockChatModel extends BaseChatModel return ""; } - private extractText(data: any): string { + private extractText(data: Record): string { if (typeof data?.outputText === "string") { - return data.outputText as string; + return data.outputText; } if (Array.isArray(data?.content)) { - return (data.content as any[]) - .map((item: any): string => { + return (data.content as unknown[]) + .map((item: unknown): string => { if (!item) return ""; if (typeof item === "string") return item; if (typeof item === "object") { - if (typeof item.text === "string") return item.text as string; - if (item.text && typeof item.text === "object" && "text" in item.text) { - return (item.text.text as string | undefined) ?? ""; + const itemObj = item as Record; + if (typeof itemObj.text === "string") return itemObj.text; + if (itemObj.text && typeof itemObj.text === "object" && "text" in itemObj.text) { + return ((itemObj.text as Record).text as string | undefined) ?? ""; } } return ""; @@ -1516,11 +1567,11 @@ export class BedrockChatModel extends BaseChatModel } if (typeof data?.completion === "string") { - return data.completion as string; + return data.completion; } if (typeof data?.resultText === "string") { - return data.resultText as string; + return data.resultText; } return ""; diff --git a/src/LLMProviders/ChatLMStudio.ts b/src/LLMProviders/ChatLMStudio.ts index 748c7172..287cf1a2 100644 --- a/src/LLMProviders/ChatLMStudio.ts +++ b/src/LLMProviders/ChatLMStudio.ts @@ -11,14 +11,14 @@ import { ChatOpenAI } from "@langchain/openai"; export interface ChatLMStudioInput { modelName?: string; apiKey?: string; - configuration?: any; + configuration?: Record; temperature?: number; maxTokens?: number; topP?: number; frequencyPenalty?: number; streaming?: boolean; streamUsage?: boolean; - [key: string]: any; + [key: string]: unknown; } /** @@ -79,7 +79,7 @@ export class ChatLMStudio extends ChatOpenAI { // `text: { format: undefined }` (serializes to `text: {}`) which LM Studio // rejects with "Required: text.format". modelKwargs: { - ...fields.modelKwargs, + ...(fields.modelKwargs as Record | undefined), text: { format: { type: "text" } }, }, }); diff --git a/src/LLMProviders/ChatOpenRouter.ts b/src/LLMProviders/ChatOpenRouter.ts index 7f212012..c2f52193 100644 --- a/src/LLMProviders/ChatOpenRouter.ts +++ b/src/LLMProviders/ChatOpenRouter.ts @@ -1,5 +1,5 @@ import { BaseChatModelParams } from "@langchain/core/language_models/chat_models"; -import { AIMessageChunk } from "@langchain/core/messages"; +import { AIMessageChunk, BaseMessage } from "@langchain/core/messages"; import type { UsageMetadata } from "@langchain/core/messages"; import { ChatGenerationChunk } from "@langchain/core/outputs"; import { ChatOpenAI } from "@langchain/openai"; @@ -44,7 +44,12 @@ export interface ChatOpenRouterInput extends BaseChatModelParams { // All other ChatOpenAI parameters modelName?: string; apiKey?: string; - configuration?: any; + configuration?: { + baseURL?: string; + defaultHeaders?: Record; + fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise; + [key: string]: unknown; + }; temperature?: number; maxTokens?: number; topP?: number; @@ -52,7 +57,7 @@ export interface ChatOpenRouterInput extends BaseChatModelParams { streaming?: boolean; maxRetries?: number; maxConcurrency?: number; - [key: string]: any; + [key: string]: unknown; } export class ChatOpenRouter extends ChatOpenAI { @@ -101,7 +106,7 @@ export class ChatOpenRouter extends ChatOpenAI { * * @see https://openrouter.ai/docs/features/prompt-caching */ - override invocationParams(options?: this["ParsedCallOptions"]): any { + override invocationParams(options?: this["ParsedCallOptions"]): Record { const baseParams = super.invocationParams(options); // Only inject cache_control for OpenRouter endpoints. LM Studio, Copilot Plus, and @@ -150,15 +155,15 @@ export class ChatOpenRouter extends ChatOpenAI { * LangChain filters out reasoning_details, so we bypass it completely */ override async *_streamResponseChunks( - messages: any[], + messages: BaseMessage[], options: this["ParsedCallOptions"], - _runManager?: any + _runManager?: { handleLLMNewToken: (token: string) => Promise } ): AsyncGenerator { const params = this.invocationParams(options); const openaiMessages = this.toOpenRouterMessages(messages); const stream = (await this.openaiClient.chat.completions.create({ - ...(params as Record), + ...params, messages: openaiMessages, stream: true, stream_options: { @@ -190,7 +195,7 @@ export class ChatOpenRouter extends ChatOpenAI { const messageChunk = this.buildMessageChunk({ rawChunk, - delta, + delta: delta as unknown as Record, content, finishReason: choice.finish_reason, reasoningDetails, @@ -230,9 +235,13 @@ export class ChatOpenRouter extends ChatOpenAI { * @param messages LangChain messages passed into the model * @returns Messages formatted for the OpenRouter API */ - private toOpenRouterMessages(messages: any[]): OpenRouterMessageParam[] { + private toOpenRouterMessages(messages: BaseMessage[]): OpenRouterMessageParam[] { return messages.map((msg) => { - const role = typeof msg._getType === "function" ? msg._getType() : (msg.role ?? "user"); + const msgRecord = msg as unknown as Record; + const role = + typeof msg._getType === "function" + ? msg._getType() + : ((msgRecord.role as string) ?? "user"); const mappedRole = role === "human" ? "user" @@ -240,12 +249,12 @@ export class ChatOpenRouter extends ChatOpenAI { ? "assistant" : (role as OpenAI.ChatCompletionRole); - if (msg.tool_call_id) { + if (msgRecord.tool_call_id) { return { role: "tool", content: msg.content, - tool_call_id: msg.tool_call_id, - }; + tool_call_id: msgRecord.tool_call_id as string, + } as OpenRouterMessageParam; } if (msg.additional_kwargs?.function_call) { @@ -280,7 +289,7 @@ export class ChatOpenRouter extends ChatOpenAI { */ private buildMessageChunk(config: { rawChunk: OpenRouterChatChunk; - delta: Record; + delta: Record; content: string; finishReason: string | null | undefined; reasoningText?: string; @@ -376,11 +385,14 @@ export class ChatOpenRouter extends ChatOpenAI { * @param choice Chunk choice object from the OpenRouter stream * @returns Array of reasoning detail entries, if present */ - private extractReasoningDetails(choice: any): unknown[] | undefined { + private extractReasoningDetails( + choice: OpenAI.ChatCompletionChunk.Choice + ): unknown[] | undefined { + const choiceRecord = choice as unknown as Record>; const candidate = - choice?.delta?.reasoning_details ?? - choice?.message?.reasoning_details ?? - choice?.reasoning_details; + choiceRecord?.delta?.reasoning_details ?? + choiceRecord?.message?.reasoning_details ?? + (choice as unknown as Record)?.reasoning_details; if (!Array.isArray(candidate)) { return undefined; @@ -428,7 +440,7 @@ export class ChatOpenRouter extends ChatOpenAI { * @returns Tool call chunk array compatible with LangChain */ private extractToolCallChunks( - toolCalls: any + toolCalls: unknown ): | Array<{ name?: string; args?: string; id?: string; index?: number; type: "tool_call_chunk" }> | undefined { diff --git a/src/LLMProviders/CustomOpenAIEmbeddings.ts b/src/LLMProviders/CustomOpenAIEmbeddings.ts index e0fc01c6..34243db1 100644 --- a/src/LLMProviders/CustomOpenAIEmbeddings.ts +++ b/src/LLMProviders/CustomOpenAIEmbeddings.ts @@ -1,10 +1,10 @@ -import { OpenAIEmbeddings, type OpenAIEmbeddingsParams } from "@langchain/openai"; +import { OpenAIEmbeddings } from "@langchain/openai"; export class CustomOpenAIEmbeddings extends OpenAIEmbeddings { - private customConfig: any; + private customConfig: Record; - constructor(config: any) { - super(config as OpenAIEmbeddingsParams); + constructor(config: Record) { + super(config); // Store the config for our custom methods this.customConfig = config; } @@ -29,17 +29,20 @@ export class CustomOpenAIEmbeddings extends OpenAIEmbeddings { }; // Get the correct baseURL, apiKey, and fetch function from the configuration - const baseURL = this.customConfig.configuration?.baseURL || "https://api.openai.com/v1"; + const configuration = this.customConfig.configuration as + | { baseURL?: string; fetch?: typeof fetch } + | undefined; + const baseURL = configuration?.baseURL || "https://api.openai.com/v1"; const url = `${baseURL}/embeddings`; - const apiKey = this.customConfig.apiKey; - const fetchFn = this.customConfig.configuration?.fetch || fetch; + const apiKey = this.customConfig.apiKey as string; + const fetchFn = configuration?.fetch || fetch; const response = await fetchFn(url, { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", - ...(this.customConfig.headers || {}), + ...((this.customConfig.headers as Record) || {}), }, body: JSON.stringify(requestBody), }); diff --git a/src/LLMProviders/brevilabsClient.ts b/src/LLMProviders/brevilabsClient.ts index 3c14c910..7b13653b 100644 --- a/src/LLMProviders/brevilabsClient.ts +++ b/src/LLMProviders/brevilabsClient.ts @@ -23,8 +23,8 @@ export interface RerankResponse { } export interface ToolCall { - tool: any; - args: any; + tool: unknown; + args: unknown; } export interface Url4llmResponse { @@ -38,7 +38,7 @@ export interface Pdf4llmResponse { } export interface Docs4llmResponse { - response: any; + response: unknown; elapsed_time_ms: number; } @@ -64,7 +64,7 @@ export interface Youtube4llmResponse { } export interface Twitter4llmResponse { - response: any; + response: string; elapsed_time_ms: number; } @@ -98,7 +98,7 @@ export class BrevilabsClient { private async makeRequest( endpoint: string, - body: any, + body: Record, method = "POST", excludeAuthHeader = false, skipLicenseCheck = false @@ -112,7 +112,7 @@ export class BrevilabsClient { const url = new URL(`${BREVILABS_API_BASE_URL}${endpoint}`); if (method === "GET") { // Add query parameters for GET requests - Object.entries(body as Record).forEach(([key, value]) => { + Object.entries(body).forEach(([key, value]) => { url.searchParams.append(key, value as string); }); } @@ -194,10 +194,10 @@ export class BrevilabsClient { * unknown error. */ async validateLicenseKey( - context?: Record + context?: Record ): Promise<{ isValid: boolean | undefined; plan?: string }> { // Build the request body with proper structure - const requestBody: Record = { + const requestBody: Record = { license_key: await getDecryptedKey(getSettings().plusLicenseKey), }; diff --git a/src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts b/src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts index fa529b68..965968b3 100644 --- a/src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts +++ b/src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts @@ -58,7 +58,7 @@ type AgentSource = { title: string; path: string; score: number; - explanation?: any; + explanation?: unknown; }; /** @@ -476,11 +476,11 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner { this.lastDisplayedContent = ""; return loopResult.finalResponse; - } catch (error: any) { + } catch (error: unknown) { // Always stop the reasoning timer on error this.stopReasoningTimer(); - if (error.name === "AbortError" || abortController.signal.aborted) { + if ((error as { name?: string }).name === "AbortError" || abortController.signal.aborted) { logInfo("Autonomous agent stream aborted by user", { reason: abortController.signal.reason, }); @@ -538,7 +538,11 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner { */ private async prepareAgentConversation( userMessage: ChatMessage, - chatModel: any, + chatModel: BaseChatModel & { + modelName?: string; + model?: string; + bindTools?: (tools: unknown[]) => unknown; + }, _updateLoadingMessage?: (message: string) => void // Unused, kept for potential future use ): Promise { const messages: BaseMessage[] = []; @@ -622,7 +626,7 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner { // Extract user content (L3 smart references + L5) from base messages const userMessageContent = baseMessages.find((m) => m.role === "user"); if (userMessageContent) { - const isMultimodal = this.isMultimodalModel(chatModel as BaseChatModel); + const isMultimodal = this.isMultimodalModel(chatModel); const content: string | MessageContent[] = isMultimodal ? await this.buildMessageContent(userMessageContent.content, userMessage) : userMessageContent.content; @@ -713,7 +717,8 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner { // 2. Model returned only thinking/reasoning content that gets filtered let finalContent = content; if (!finalContent || finalContent.trim() === "") { - const rawToolCallChunks = (aiMessage as any).tool_call_chunks ?? []; + const rawToolCallChunks = + (aiMessage as { tool_call_chunks?: unknown[] }).tool_call_chunks ?? []; logWarn( `[Agent] Empty response detected (iteration ${iteration}). ` + `Content length: ${content?.length ?? 0}, ` + @@ -1048,7 +1053,7 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner { if (chunkContent) rawContent += chunkContent; // Process chunk through ThinkBlockStreamer to strip thinking content - thinkStreamer.processChunk(chunk); + thinkStreamer.processChunk(chunk as Parameters[0]); } // Close the streamer to finalize content (handles unclosed think blocks, etc.) @@ -1083,9 +1088,9 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner { aiMessage, streamingResult, }; - } catch (error: any) { - logError(`Stream error: ${error.message}`); - if (error.name === "AbortError" || abortController.signal.aborted) { + } catch (error: unknown) { + logError(`Stream error: ${(error as Error).message}`); + if ((error as { name?: string }).name === "AbortError" || abortController.signal.aborted) { const streamingResult = thinkStreamer.close(); return { content: streamingResult.content, diff --git a/src/LLMProviders/chainRunner/BaseChainRunner.ts b/src/LLMProviders/chainRunner/BaseChainRunner.ts index c12915be..8f58b987 100644 --- a/src/LLMProviders/chainRunner/BaseChainRunner.ts +++ b/src/LLMProviders/chainRunner/BaseChainRunner.ts @@ -110,8 +110,9 @@ export abstract class BaseChainRunner implements ChainRunner { updateCurrentAiMessage(""); } // Log compact memory summary and a truncated final response (~300 chars) - const historyMessages = (this.chainManager.memoryManager.getMemory().chatHistory as any) - .messages; + const historyMessages = ( + this.chainManager.memoryManager.getMemory().chatHistory as { messages?: unknown[] } + ).messages; logInfo("Chat memory updated:\n", { turns: Array.isArray(historyMessages) ? historyMessages.length : 0, }); @@ -120,8 +121,8 @@ export abstract class BaseChainRunner implements ChainRunner { try { const { parseToolCallMarkers } = await import("./utils/toolCallParser"); const parsed = parseToolCallMarkers(fullAIResponse); - let textOnly = parsed.segments - .map((seg: { type: string; content: string }) => (seg.type === "text" ? seg.content : "")) + let textOnly = (parsed.segments as { type: string; content: string }[]) + .map((seg) => (seg.type === "text" ? seg.content : "")) .join("") .trim(); if (!textOnly) textOnly = fullAIResponse || ""; @@ -146,15 +147,16 @@ export abstract class BaseChainRunner implements ChainRunner { * @param error - Raw provider error object. * @param processErrorChunk - Callback used to stream error text to the UI. */ - protected async handleError(error: any, processErrorChunk: (message: string) => void) { + protected async handleError(error: unknown, processErrorChunk: (message: string) => void) { const msg = err2String(error); logError("Error during LLM invocation:", msg); - const errorData = error?.response?.data?.error || msg; - const errorCode = errorData?.code || msg; + const errorData = + (error as { response?: { data?: { error?: unknown } } })?.response?.data?.error || msg; + const errorCode = (errorData as { code?: string })?.code || msg; let errorMessage = ""; // Check for specific error messages - if (error?.message?.includes("Invalid license key")) { + if ((error as { message?: string })?.message?.includes("Invalid license key")) { errorMessage = "Invalid Copilot Plus license key. Please check your license key in settings."; } else if (errorCode === "model_not_found") { errorMessage = @@ -200,7 +202,16 @@ export abstract class BaseChainRunner implements ChainRunner { * @returns True if the error indicates an authentication problem. */ private isAuthenticationError(error: unknown, normalizedMessage: string): boolean { - const responseError = (error as { response?: { status?: number; data?: any } })?.response; + const responseError = ( + error as { + response?: { + status?: number; + data?: { + error?: { status?: number | string; code?: string; message?: string; type?: string }; + }; + }; + } + )?.response; const errorData = responseError?.data?.error ?? (error as { error?: unknown })?.error; const rawStatus = responseError?.status ?? (errorData as { status?: number | string })?.status; const statusCode = typeof rawStatus === "string" ? Number.parseInt(rawStatus, 10) : rawStatus; diff --git a/src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts b/src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts index 355679e6..ece145f9 100644 --- a/src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts +++ b/src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts @@ -50,6 +50,7 @@ import { isFilterOnlyResults, isTimeDominantResults, logSearchResultsDebugTable, + type SearchDoc, } from "./utils/searchResultUtils"; import { buildLocalSearchInnerContent, @@ -68,8 +69,8 @@ import ProjectManager from "@/LLMProviders/projectManager"; import { isProjectMode } from "@/aiParams"; type ToolCallWithExecutor = { - tool: any; - args: any; + tool: StructuredTool; + args: Record; }; export class CopilotPlusChainRunner extends BaseChainRunner { @@ -113,7 +114,10 @@ export class CopilotPlusChainRunner extends BaseChainRunner { const availableTools = this.getAvailableToolsForPlanning(); // Check if model supports native tool calling - if (typeof (chatModel as any).bindTools !== "function") { + const modelWithTools = chatModel as BaseChatModel & { + bindTools?: (tools: StructuredTool[]) => unknown; + }; + if (typeof modelWithTools.bindTools !== "function") { logWarn("[CopilotPlus] Model does not support native tool calling, skipping tool planning"); return { toolCalls: [], @@ -122,7 +126,7 @@ export class CopilotPlusChainRunner extends BaseChainRunner { } // Bind tools to the model for native function calling - const boundModel = (chatModel as any).bindTools(availableTools); + const boundModel = modelWithTools.bindTools(availableTools); // Build a lightweight planning prompt (no XML format instructions needed) const planningPrompt = `You are a helpful AI assistant. Analyze the user's message and determine if any tools should be called. @@ -160,8 +164,10 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; // token estimation entirely. Actual token usage comes from API response metadata. let response: AIMessage; { - const stream: AsyncIterable = await withSuppressedTokenWarnings( - () => boundModel.stream(planningMessages) as Promise> + const stream: AsyncIterable = await withSuppressedTokenWarnings(() => + ( + boundModel as { stream: (msgs: unknown) => Promise> } + ).stream(planningMessages) ); let aggregated: AIMessageChunk | undefined; for await (const chunk of stream) { @@ -248,7 +254,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; private async processAtCommands( userMessage: string, existingToolCalls: ToolCallWithExecutor[], - context: { salientTerms: string[]; timeRange?: any } + context: { salientTerms: string[]; timeRange?: unknown } ): Promise { const message = userMessage.toLowerCase(); const cleanQuery = this.removeAtCommands(userMessage); @@ -546,8 +552,8 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; } protected hasCapability(model: BaseChatModel, capability: ModelCapability): boolean { - const modelName: string = - ((model as any).modelName as string) || ((model as any).model as string) || ""; + const modelWithName = model as BaseChatModel & { modelName?: string; model?: string }; + const modelName: string = modelWithName.modelName || modelWithName.model || ""; const customModel = this.chainManager.chatModelManager.findModelByName(modelName); return customModel?.capabilities?.includes(capability) ?? false; } @@ -571,7 +577,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; private async streamMultimodalResponse( textContent: string, userMessage: ChatMessage, - allToolOutputs: any[], + allToolOutputs: { tool: string; output: unknown }[], abortController: AbortController, thinkStreamer: ThinkBlockStreamer, originalUserQuestion: string, @@ -585,7 +591,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; const isMultimodalCurrent = this.isMultimodalModel(chatModel); // Create messages array - const messages: { role: string; content: any }[] = []; + const messages: { role: string; content: string | MessageContent[] }[] = []; // Envelope-based context construction (required) const envelope = userMessage.contextEnvelope; @@ -714,7 +720,9 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; }); break; } - for await (const processedChunk of actionStreamer.processChunk(chunk)) { + for await (const processedChunk of actionStreamer.processChunk( + chunk as unknown as Record + )) { thinkStreamer.processChunk(processedChunk); } } @@ -740,7 +748,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; const excludeThinking = !hasReasoning; const thinkStreamer = new ThinkBlockStreamer(updateCurrentAiMessage, excludeThinking); - let sources: { title: string; path: string; score: number; explanation?: any }[] = []; + let sources: { title: string; path: string; score: number; explanation?: unknown }[] = []; const isPlusUser = await checkIsPlusUser({ isCopilotPlus: true, @@ -783,7 +791,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; // Execute getTimeRangeMs immediately if present (needed for localSearch timeRange) // We execute it once here and remove it from toolCalls to avoid double execution - let timeRange: any = undefined; + let timeRange: unknown = undefined; const timeRangeCall = planningResult.toolCalls.find( (tc) => tc.tool.name === "getTimeRangeMs" ); @@ -794,7 +802,12 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; ); // Parse result if it's a JSON string (LangChain tools return strings) // Extract epoch values from TimeInfo objects - localSearch expects {startTime: number, endTime: number} - const extractEpochValues = (result: any): unknown => { + type TimeInfoResult = { + startTime?: { epoch?: number }; + endTime?: { epoch?: number }; + error?: unknown; + }; + const extractEpochValues = (result: TimeInfoResult): unknown => { if (result?.startTime?.epoch !== undefined && result?.endTime?.epoch !== undefined) { return { startTime: result.startTime.epoch, @@ -806,7 +819,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; if (typeof timeRangeResult === "string") { try { - const parsed = JSON.parse(timeRangeResult); + const parsed = JSON.parse(timeRangeResult) as TimeInfoResult; // Only use result if it's not an error if (!parsed.error) { timeRange = extractEpochValues(parsed); @@ -814,8 +827,11 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; } catch { logWarn("[CopilotPlus] Failed to parse getTimeRangeMs result:", timeRangeResult); } - } else if (timeRangeResult && !timeRangeResult.error) { - timeRange = extractEpochValues(timeRangeResult); + } else if (timeRangeResult) { + const typedResult = timeRangeResult as TimeInfoResult; + if (!typedResult.error) { + timeRange = extractEpochValues(typedResult); + } } logInfo("[CopilotPlus] Executed getTimeRangeMs, result:", timeRange); } @@ -839,7 +855,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; salientTerms: planningResult.salientTerms, timeRange, }); - } catch (error: any) { + } catch (error: unknown) { return this.handleResponse( getApiErrorMessage(error), userMessage, @@ -887,12 +903,15 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; cleanedUserMessage, updateLoadingMessage ); - } catch (error: any) { + } catch (error: unknown) { // Reset loading message to default updateLoadingMessage?.(LOADING_MESSAGES.DEFAULT); // Check if the error is due to abort signal - if (error.name === "AbortError" || abortController.signal.aborted) { + if ( + (error instanceof Error && error.name === "AbortError") || + abortController.signal.aborted + ) { logInfo("CopilotPlus stream aborted by user", { reason: abortController.signal.reason }); // Don't show error message for user-initiated aborts } else { @@ -924,7 +943,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; const fallbackSources = this.lastCitationSources && this.lastCitationSources.length > 0 ? this.lastCitationSources - : ((sources as any[]) || []).map((source) => ({ title: source.title, path: source.path })); + : (sources || []).map((source) => ({ title: source.title, path: source.path })); fullAIResponse = addFallbackSources( fullAIResponse, @@ -947,14 +966,14 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; } private async executeToolCalls( - toolCalls: any[], + toolCalls: ToolCallWithExecutor[], updateLoadingMessage?: (message: string) => void ): Promise<{ - toolOutputs: { tool: string; output: any }[]; - sources: { title: string; path: string; score: number; explanation?: any }[]; + toolOutputs: { tool: string; output: unknown }[]; + sources: { title: string; path: string; score: number; explanation?: unknown }[]; }> { - const toolOutputs = []; - const allSources: { title: string; path: string; score: number; explanation?: any }[] = []; + const toolOutputs: { tool: string; output: unknown }[] = []; + const allSources: { title: string; path: string; score: number; explanation?: unknown }[] = []; // TODO: remove this hack until better solution in place (logan, wenzheng) // Skip getFileTree if localSearch is already being called to avoid redundant work @@ -1002,17 +1021,32 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; // Persist citation lines built for this turn to reuse in fallback private lastCitationSources: { title?: string; path?: string }[] | null = null; - protected getTimeExpression(toolCalls: any[]): string { + protected getTimeExpression(toolCalls: ToolCallWithExecutor[]): string { const timeRangeCall = toolCalls.find((call) => call.tool.name === "getTimeRangeMs"); return timeRangeCall ? (timeRangeCall.args.timeExpression as string) : ""; } - private prepareLocalSearchResult(documents: any[], timeExpression: string): string { + private prepareLocalSearchResult(documents: unknown[], timeExpression: string): string { const settings = getSettings(); + // Type alias for the shape we need from search result documents + type SearchDoc = { + includeInContext?: boolean; + mtime?: number; + content?: string; + source?: string; + isFilterResult?: boolean; + title?: string; + path?: string; + __sourceId?: number; + }; + + // Cast once to a typed array we can work with throughout this method + const typedDocs = documents as SearchDoc[]; + // Filter documents that should be included in context // Use !== false to be consistent with formatSearchResultsForLLM and logSearchResultsDebugTable - const includedDocs = documents.filter((doc) => doc.includeInContext !== false); + const includedDocs = typedDocs.filter((doc) => doc.includeInContext !== false); // Generate quality summary across all docs combined const qualitySummary = generateQualitySummary(includedDocs); @@ -1022,12 +1056,12 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; // timeDominant is checked independently of filterOnly: time-range queries often include // daily notes with source "title-match" (from getTitleMatches), which are not in // FILTER_SOURCES, so filterOnly would be false even for pure time-range result sets. - const filterOnly = isFilterOnlyResults(includedDocs as Array<{ source?: string }>); - const timeDominant = isTimeDominantResults(includedDocs as Array<{ source?: string }>); + const filterOnly = isFilterOnlyResults(includedDocs); + const timeDominant = isTimeDominantResults(includedDocs); // Determine tier split based on result type - let tier1Docs: any[]; - let tier2Docs: any[]; + let tier1Docs: SearchDoc[]; + let tier2Docs: SearchDoc[]; if (timeDominant) { // Sort by mtime desc; most recent get full content, older get metadata-only const sorted = [...includedDocs].sort((a, b) => (b.mtime || 0) - (a.mtime || 0)); @@ -1049,36 +1083,39 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; } // Calculate total content length for tier 1 only (safety net truncation) - const totalContentLength = tier1Docs.reduce( - (sum, doc): number => sum + ((doc.content?.length as number) || 0), - 0 - ); + const totalContentLength = tier1Docs.reduce((sum, doc) => { + return sum + (doc.content ? doc.content.length : 0); + }, 0); // If total content length exceeds threshold, truncate content proportionally - let processedDocs = tier1Docs; + let processedDocs: SearchDoc[] = tier1Docs; if (totalContentLength > MAX_CHARS_FOR_LOCAL_SEARCH_CONTEXT) { const truncationRatio = MAX_CHARS_FOR_LOCAL_SEARCH_CONTEXT / totalContentLength; logInfo( "Truncating document contents to fit context length. Truncation ratio:", truncationRatio ); - processedDocs = tier1Docs.map((doc): any => ({ - ...doc, - content: - doc.content?.slice(0, Math.floor((doc.content?.length || 0) * truncationRatio)) || "", - })); + processedDocs = tier1Docs.map( + (doc): SearchDoc => ({ + ...doc, + content: + doc.content?.slice(0, Math.floor((doc.content?.length || 0) * truncationRatio)) || "", + }) + ); } // Assign stable source ids (continuous across both groups) and sanitize content - const withIds = processedDocs.map((doc, idx): any => ({ - ...doc, - __sourceId: idx + 1, - content: sanitizeContentForCitations((doc.content as string) || ""), - })); + const withIds: SearchDoc[] = processedDocs.map( + (doc, idx): SearchDoc => ({ + ...doc, + __sourceId: idx + 1, + content: sanitizeContentForCitations((doc.content as string) || ""), + }) + ); // Split into filter and search docs by isFilterResult flag - const filterDocs = withIds.filter((d: any) => d.isFilterResult === true); - const searchDocs = withIds.filter((d: any) => d.isFilterResult !== true); + const filterDocs = withIds.filter((d) => d.isFilterResult === true); + const searchDocs = withIds.filter((d) => d.isFilterResult !== true); // Use split formatter if there are filter results, otherwise fall back to unified format. // For tag-only queries (tier1 empty), output metadata-only directly. @@ -1108,14 +1145,14 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; // Build a compact, unnumbered source catalog to avoid bias const sourceEntries: SourceCatalogEntry[] = withIds .slice(0, Math.min(20, withIds.length)) - .map((d: any) => ({ + .map((d) => ({ title: d.title || d.path || "Untitled", path: d.path || d.title || "", })); const catalogLines = formatSourceCatalog(sourceEntries); // Also keep a numbered mapping for fallback use only (if model emits footnotes but forgets Sources) - this.lastCitationSources = withIds.slice(0, Math.min(20, withIds.length)).map((d: any) => { + this.lastCitationSources = withIds.slice(0, Math.min(20, withIds.length)).map((d) => { const title = d.title || d.path || "Untitled"; return { title, @@ -1151,9 +1188,9 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; ): { formattedForLLM: string; formattedForDisplay: string; - sources: { title: string; path: string; score: number; explanation?: any }[]; + sources: { title: string; path: string; score: number; explanation?: unknown }[]; } { - let sources: { title: string; path: string; score: number; explanation?: any }[] = []; + let sources: { title: string; path: string; score: number; explanation?: unknown }[] = []; let formattedForLLM: string; let formattedForDisplay: string; @@ -1179,7 +1216,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; } // Log a concise debug table of results with explanations (title, ctime, mtime) - logSearchResultsDebugTable(searchResults); + logSearchResultsDebugTable(searchResults as SearchDoc[]); // Extract sources with explanation for UI display sources = extractSourcesFromSearchResults(searchResults); @@ -1208,15 +1245,13 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; * Formats all tool outputs uniformly for user message. * All tools (localSearch, webSearch, getFileTree, etc.) are treated the same. */ - private formatAllToolOutputs(toolOutputs: any[]): string { + private formatAllToolOutputs(toolOutputs: { tool: string; output: unknown }[]): string { if (toolOutputs.length === 0) return ""; const formattedOutputs = toolOutputs .map((output) => { - let content = output.output; - if (typeof content !== "string") { - content = JSON.stringify(content); - } + const content: string = + typeof output.output === "string" ? output.output : JSON.stringify(output.output); return `<${output.tool}>\n${content}\n`; }) .join("\n\n"); diff --git a/src/LLMProviders/chainRunner/LLMChainRunner.ts b/src/LLMProviders/chainRunner/LLMChainRunner.ts index 4c6351ec..90e76003 100644 --- a/src/LLMProviders/chainRunner/LLMChainRunner.ts +++ b/src/LLMProviders/chainRunner/LLMChainRunner.ts @@ -15,7 +15,9 @@ export class LLMChainRunner extends BaseChainRunner { * Construct messages array using envelope-based context (L1-L5 layers) * Requires context envelope - throws error if unavailable */ - private async constructMessages(userMessage: ChatMessage): Promise { + private async constructMessages( + userMessage: ChatMessage + ): Promise<{ role: string; content: string | unknown[] }[]> { // Require envelope for LLM chain if (!userMessage.contextEnvelope) { throw new Error( @@ -32,7 +34,7 @@ export class LLMChainRunner extends BaseChainRunner { debug: false, }); - const messages: { role: string; content: any }[] = []; + const messages: { role: string; content: string | unknown[] }[] = []; // Add system message (L1) const systemMessage = baseMessages.find((m) => m.role === "system"); @@ -115,9 +117,13 @@ export class LLMChainRunner extends BaseChainRunner { // Stream with abort signal const chatStream = await withSuppressedTokenWarnings(() => - this.chainManager.chatModelManager.getChatModel().stream(messages, { - signal: abortController.signal, - }) + this.chainManager.chatModelManager.getChatModel().stream( + // ProviderMessage[] format matches what getChatModel().stream() accepts at runtime + messages as never, + { + signal: abortController.signal, + } + ) ); for await (const chunk of chatStream) { @@ -125,7 +131,7 @@ export class LLMChainRunner extends BaseChainRunner { logInfo("Stream iteration aborted", { reason: abortController.signal.reason }); break; } - streamer.processChunk(chunk); + streamer.processChunk(chunk as Parameters[0]); } } catch (error: unknown) { // Check if the error is due to abort signal diff --git a/src/LLMProviders/chainRunner/VaultQAChainRunner.ts b/src/LLMProviders/chainRunner/VaultQAChainRunner.ts index 5f1f1933..2ae374ff 100644 --- a/src/LLMProviders/chainRunner/VaultQAChainRunner.ts +++ b/src/LLMProviders/chainRunner/VaultQAChainRunner.ts @@ -138,22 +138,26 @@ export class VaultQAChainRunner extends BaseChainRunner { // Format documents as context with XML tags // Sanitize content to remove pre-existing citation markers - const context = retrievedDocs - .map((doc: any) => { + const context = ( + retrievedDocs as { metadata?: { title?: string; path?: string }; pageContent?: string }[] + ) + .map((doc) => { const title = doc.metadata?.title || "Untitled"; const path = doc.metadata?.path || title; - return `<${RETRIEVED_DOCUMENT_TAG}>\n${title}\n${path}\n\n${sanitizeContentForCitations(doc.pageContent as string)}\n\n`; + return `<${RETRIEVED_DOCUMENT_TAG}>\n${title}\n${path}\n\n${sanitizeContentForCitations(doc.pageContent ?? "")}\n\n`; }) .join("\n\n"); // Step 6: Build messages array with envelope-aware logic - const messages: any[] = []; + const messages: { role: string; content: string | unknown[] }[] = []; const chatModel = this.chainManager.chatModelManager.getChatModel(); // Prepare RAG context and citation instructions - const sourceEntries: SourceCatalogEntry[] = retrievedDocs + const sourceEntries: SourceCatalogEntry[] = ( + retrievedDocs as { metadata?: { title?: string; path?: string } }[] + ) .slice(0, Math.max(5, Math.min(20, retrievedDocs.length))) - .map((d: any) => ({ + .map((d) => ({ title: d.metadata?.title || d.metadata?.path || "Untitled", path: d.metadata?.path || d.metadata?.title || "", })); @@ -190,7 +194,7 @@ export class VaultQAChainRunner extends BaseChainRunner { } // Insert L4 (chat history) between system and user - await loadAndAddChatHistory(memory, messages as { role: string; content: any }[]); + await loadAndAddChatHistory(memory, messages); // Add user message with RAG prepended // User message now contains: RAG results + citations + L3 smart references + L5 @@ -204,12 +208,14 @@ export class VaultQAChainRunner extends BaseChainRunner { // Handle multimodal content if present if (userMessage.content && Array.isArray(userMessage.content)) { - const updatedContent = userMessage.content.map((item: any): any => { - if (item.type === "text") { - return { ...item, text: enhancedUserContent }; + const updatedContent = userMessage.content.map( + (item: { type?: string }): { type?: string; [key: string]: unknown } => { + if (item.type === "text") { + return { ...item, text: enhancedUserContent }; + } + return { ...item }; } - return item; - }); + ); messages.push({ role: "user", content: updatedContent, @@ -234,9 +240,13 @@ export class VaultQAChainRunner extends BaseChainRunner { // Stream with abort signal const chatStream = await withSuppressedTokenWarnings(() => - this.chainManager.chatModelManager.getChatModel().stream(messages, { - signal: abortController.signal, - }) + this.chainManager.chatModelManager.getChatModel().stream( + // ProviderMessage[] format matches what getChatModel().stream() accepts at runtime + messages as never, + { + signal: abortController.signal, + } + ) ); for await (const chunk of chatStream) { @@ -244,11 +254,11 @@ export class VaultQAChainRunner extends BaseChainRunner { logInfo("VaultQA stream iteration aborted", { reason: abortController.signal.reason }); break; } - streamer.processChunk(chunk); + streamer.processChunk(chunk as Parameters[0]); } - } catch (error: any) { + } catch (error: unknown) { // Check if the error is due to abort signal - if (error.name === "AbortError" || abortController.signal.aborted) { + if ((error as { name?: string }).name === "AbortError" || abortController.signal.aborted) { logInfo("VaultQA stream aborted by user", { reason: abortController.signal.reason }); // Don't show error message for user-initiated aborts } else { diff --git a/src/LLMProviders/chainRunner/utils/ActionBlockStreamer.test.ts b/src/LLMProviders/chainRunner/utils/ActionBlockStreamer.test.ts index b012c039..a43d0acd 100644 --- a/src/LLMProviders/chainRunner/utils/ActionBlockStreamer.test.ts +++ b/src/LLMProviders/chainRunner/utils/ActionBlockStreamer.test.ts @@ -10,7 +10,7 @@ const MockedToolManager = ToolManager as jest.Mocked; const MockedToolResultFormatter = ToolResultFormatter as jest.Mocked; describe("ActionBlockStreamer", () => { - let writeFileTool: any; + let writeFileTool: unknown; let streamer: ActionBlockStreamer; beforeEach(() => { @@ -24,8 +24,8 @@ describe("ActionBlockStreamer", () => { }); // Helper function to process chunks and collect results - async function processChunks(chunks: { content: string | null }[]): Promise { - const outputContents: any[] = []; + async function processChunks(chunks: { content: string | null }[]): Promise { + const outputContents: unknown[] = []; for (const chunk of chunks) { for await (const result of streamer.processChunk(chunk)) { // Always push the content, even if it's null, undefined, or empty string diff --git a/src/LLMProviders/chainRunner/utils/ActionBlockStreamer.ts b/src/LLMProviders/chainRunner/utils/ActionBlockStreamer.ts index 5e1a08b2..a15aaa2d 100644 --- a/src/LLMProviders/chainRunner/utils/ActionBlockStreamer.ts +++ b/src/LLMProviders/chainRunner/utils/ActionBlockStreamer.ts @@ -14,7 +14,7 @@ export class ActionBlockStreamer { constructor( private toolManager: typeof ToolManager, - private writeFileTool: any + private writeFileTool: unknown ) {} private findCompleteBlock(str: string) { @@ -32,21 +32,23 @@ export class ActionBlockStreamer { }; } - async *processChunk(chunk: any): AsyncGenerator { + async *processChunk( + chunk: Record + ): AsyncGenerator, void, unknown> { // Handle different chunk formats let chunkContent = ""; // Handle Claude thinking model array-based content if (Array.isArray(chunk.content)) { - for (const item of chunk.content) { + for (const item of chunk.content as Array<{ type?: string; text?: unknown }>) { if (item.type === "text" && item.text != null) { - chunkContent += item.text; + chunkContent += typeof item.text === "string" ? item.text : ""; } } } // Handle standard string content else if (chunk.content != null) { - chunkContent = chunk.content; + chunkContent = typeof chunk.content === "string" ? chunk.content : ""; } // Add to buffer @@ -79,8 +81,8 @@ export class ActionBlockStreamer { // Format tool result using ToolResultFormatter for consistency with agent mode const formattedResult = ToolResultFormatter.format("writeFile", result as string); yield { ...chunk, content: `\n${formattedResult}\n` }; - } catch (err: any) { - yield { ...chunk, content: `\nError: ${err?.message || err}\n` }; + } catch (err: unknown) { + yield { ...chunk, content: `\nError: ${(err as Error)?.message ?? String(err)}\n` }; } // Remove processed block from buffer diff --git a/src/LLMProviders/chainRunner/utils/ThinkBlockStreamer.ts b/src/LLMProviders/chainRunner/utils/ThinkBlockStreamer.ts index 64ea2b1a..0d527553 100644 --- a/src/LLMProviders/chainRunner/utils/ThinkBlockStreamer.ts +++ b/src/LLMProviders/chainRunner/utils/ThinkBlockStreamer.ts @@ -120,7 +120,7 @@ export class ThinkBlockStreamer { } } - private handleClaudeChunk(content: any[]) { + private handleClaudeChunk(content: Array<{ type?: string; text?: string; thinking?: string }>) { let textContent = ""; let hasThinkingContent = false; for (const item of content) { @@ -157,10 +157,13 @@ export class ThinkBlockStreamer { return hasThinkingContent; } - private handleDeepseekChunk(chunk: any) { + private handleDeepseekChunk(chunk: { + content?: string; + additional_kwargs?: { reasoning_content?: string }; + }) { // Handle standard string content if (typeof chunk.content === "string") { - this.fullResponse += stripSpecialTokens(chunk.content as string); + this.fullResponse += stripSpecialTokens(chunk.content); } // Handle deepseek reasoning/thinking content @@ -200,7 +203,13 @@ export class ThinkBlockStreamer { * - Models that only populate reasoning_details (without delta.reasoning) won't show thinking * - This is acceptable for now as most models use delta.reasoning for streaming */ - private handleOpenRouterChunk(chunk: any) { + private handleOpenRouterChunk(chunk: { + content?: string; + additional_kwargs?: { + delta?: { reasoning?: string }; + reasoning_details?: unknown[]; + }; + }) { // Only process delta.reasoning (streaming), ignore reasoning_details entirely if (chunk.additional_kwargs?.delta?.reasoning) { // Skip thinking content if excludeThinking is enabled @@ -233,7 +242,14 @@ export class ThinkBlockStreamer { * Accumulate native tool call chunks during streaming. * LangChain providers send tool_call_chunks with incremental data. */ - private handleToolCallChunks(chunk: any) { + private handleToolCallChunks(chunk: { + tool_call_chunks?: Array<{ + index?: number; + id?: string; + name?: string; + args?: string; + }>; + }) { // Check for tool_call_chunks in the chunk (LangChain streaming format) const toolCallChunks = chunk.tool_call_chunks; if (!toolCallChunks || !Array.isArray(toolCallChunks)) { @@ -253,7 +269,17 @@ export class ThinkBlockStreamer { } } - processChunk(chunk: any) { + processChunk(chunk: { + response_metadata?: Record; + usage_metadata?: { input_tokens?: number; output_tokens?: number; total_tokens?: number }; + tool_call_chunks?: Array<{ index?: number; id?: string; name?: string; args?: string }>; + content?: string | Array<{ type?: string; text?: string; thinking?: string }>; + additional_kwargs?: { + reasoning_content?: string; + delta?: { reasoning?: string }; + reasoning_details?: unknown[]; + }; + }) { // Detect truncation using multi-provider detector const truncationResult = detectTruncation(chunk); if (truncationResult.wasTruncated) { @@ -290,16 +316,17 @@ export class ThinkBlockStreamer { // Route based on the actual chunk format if (Array.isArray(chunk.content)) { // Claude format with content array - this.handleClaudeChunk(chunk.content as any[]); + // chunk.content is Array<...> in this branch (checked by Array.isArray guard above) + this.handleClaudeChunk(chunk.content); } else if (chunk.additional_kwargs?.reasoning_content) { // Deepseek format with reasoning_content - this.handleDeepseekChunk(chunk); + this.handleDeepseekChunk(chunk as Parameters[0]); } else if (isThinkingChunk) { // OpenRouter format with delta.reasoning or reasoning_details - this.handleOpenRouterChunk(chunk); + this.handleOpenRouterChunk(chunk as Parameters[0]); } else { // Default case: regular content or other formats - this.handleDeepseekChunk(chunk); + this.handleDeepseekChunk(chunk as Parameters[0]); } // Handle text-level think tags (e.g., from nvidia/nemotron models) diff --git a/src/LLMProviders/chainRunner/utils/chatHistoryUtils.test.ts b/src/LLMProviders/chainRunner/utils/chatHistoryUtils.test.ts index 69b11802..b6788b26 100644 --- a/src/LLMProviders/chainRunner/utils/chatHistoryUtils.test.ts +++ b/src/LLMProviders/chainRunner/utils/chatHistoryUtils.test.ts @@ -148,7 +148,7 @@ describe("chatHistoryUtils", () => { }, ]; - const messages: { role: string; content: any }[] = []; + const messages: { role: string; content: string | unknown[] }[] = []; addChatHistoryToMessages(rawHistory, messages); expect(messages).toEqual([ diff --git a/src/LLMProviders/chainRunner/utils/chatHistoryUtils.ts b/src/LLMProviders/chainRunner/utils/chatHistoryUtils.ts index ac8c1c21..063c55b6 100644 --- a/src/LLMProviders/chainRunner/utils/chatHistoryUtils.ts +++ b/src/LLMProviders/chainRunner/utils/chatHistoryUtils.ts @@ -6,7 +6,7 @@ import { logInfo } from "@/logger"; export interface ProcessedMessage { role: "user" | "assistant"; - content: any; // string or MessageContent[] + content: string | unknown[]; // string or MessageContent[] } /** @@ -16,28 +16,29 @@ export interface ProcessedMessage { * @param rawHistory Array of messages from memory.loadMemoryVariables() * @returns Array of processed messages safe for LLM consumption */ -export function processRawChatHistory(rawHistory: any[]): ProcessedMessage[] { +export function processRawChatHistory(rawHistory: unknown[]): ProcessedMessage[] { const messages: ProcessedMessage[] = []; for (const message of rawHistory) { if (!message) continue; + const msg = message as Record; // Check if this is a BaseMessage with _getType method - if (typeof message._getType === "function") { - const messageType = message._getType(); + if (typeof msg._getType === "function") { + const messageType = (msg._getType as () => string)(); // Only process human and AI messages if (messageType === "human") { - messages.push({ role: "user", content: message.content }); + messages.push({ role: "user", content: msg.content as string | unknown[] }); } else if (messageType === "ai") { - messages.push({ role: "assistant", content: message.content }); + messages.push({ role: "assistant", content: msg.content as string | unknown[] }); } // Skip system messages and unknown types - } else if (message.content !== undefined) { + } else if (msg.content !== undefined) { // Fallback for other message formats - try to infer role - const role = inferMessageRole(message); + const role = inferMessageRole(msg); if (role) { - messages.push({ role, content: message.content }); + messages.push({ role, content: msg.content as string | unknown[] }); } } } @@ -49,7 +50,7 @@ export function processRawChatHistory(rawHistory: any[]): ProcessedMessage[] { * Try to infer the role from various message format properties * @returns 'user' | 'assistant' | null */ -function inferMessageRole(message: any): "user" | "assistant" | null { +function inferMessageRole(message: Record): "user" | "assistant" | null { // Check various properties that might indicate the role if (message.role === "human" || message.role === "user" || message.sender === "user") { return "user"; @@ -69,8 +70,8 @@ function inferMessageRole(message: any): "user" | "assistant" | null { * @param messages Target messages array to add to */ export function addChatHistoryToMessages( - rawHistory: any[], - messages: Array<{ role: string; content: any }> + rawHistory: unknown[], + messages: Array<{ role: string; content: string | unknown[] }> ): void { const processedHistory = processRawChatHistory(rawHistory); for (const msg of processedHistory) { @@ -87,14 +88,17 @@ export interface ChatHistoryEntry { * Extract text content from potentially multimodal message content. * Replaces non-text content (images) with placeholder. */ -function extractTextContent(content: any): string { +function extractTextContent(content: string | unknown[]): string { if (typeof content === "string") { return content; } else if (Array.isArray(content)) { // Extract text from multimodal content, skip image_url payloads const textParts: string = content - .filter((item: any) => item.type === "text") - .map((item: any): string => (item.text as string) || "") + .filter( + (item): item is { type: string; text?: string } => + typeof item === "object" && item !== null && (item as { type?: unknown }).type === "text" + ) + .map((item): string => item.text || "") .join(" "); return textParts || "[Image content]"; } @@ -229,13 +233,15 @@ export function extractConversationTurns(processedHistory: ProcessedMessage[]): * @returns The processed history that was added */ export async function loadAndAddChatHistory( - memory: any, - messages: Array<{ role: string; content: any }> + memory: { + loadMemoryVariables: (vars: Record) => Promise<{ history?: unknown[] }>; + }, + messages: Array<{ role: string; content: string | unknown[] }> ): Promise { const memoryVariables = await memory.loadMemoryVariables({}); const rawHistory = memoryVariables.history || []; - const processedHistory = rawHistory.length ? processRawChatHistory(rawHistory as any[]) : []; + const processedHistory = rawHistory.length ? processRawChatHistory(rawHistory) : []; // Add history messages directly (already compacted at save time) for (const msg of processedHistory) { diff --git a/src/LLMProviders/chainRunner/utils/finishReasonDetector.ts b/src/LLMProviders/chainRunner/utils/finishReasonDetector.ts index 244863d5..5118c3f5 100644 --- a/src/LLMProviders/chainRunner/utils/finishReasonDetector.ts +++ b/src/LLMProviders/chainRunner/utils/finishReasonDetector.ts @@ -28,7 +28,9 @@ export interface FinishReasonResult { * @param chunk The streaming chunk from the LLM (AIMessageChunk) * @returns FinishReasonResult with truncation status and details */ -export function detectTruncation(chunk: any): FinishReasonResult { +export function detectTruncation(chunk: { + response_metadata?: Record; +}): FinishReasonResult { const metadata = chunk.response_metadata || {}; // OpenAI, DeepSeek, Mistral, Groq use "length" @@ -69,7 +71,10 @@ export function detectTruncation(chunk: any): FinishReasonResult { * @param chunk The streaming chunk from the LLM * @returns Token usage object or null if not available */ -export function extractTokenUsage(chunk: any): { +export function extractTokenUsage(chunk: { + response_metadata?: Record; + usage_metadata?: { input_tokens?: number; output_tokens?: number; total_tokens?: number }; +}): { inputTokens?: number; outputTokens?: number; totalTokens?: number; @@ -78,31 +83,47 @@ export function extractTokenUsage(chunk: any): { // OpenAI format: tokenUsage with camelCase if (metadata.tokenUsage) { + const tu = metadata.tokenUsage as { + promptTokens?: number; + completionTokens?: number; + totalTokens?: number; + }; return { - inputTokens: metadata.tokenUsage.promptTokens, - outputTokens: metadata.tokenUsage.completionTokens, - totalTokens: metadata.tokenUsage.totalTokens, + inputTokens: tu.promptTokens, + outputTokens: tu.completionTokens, + totalTokens: tu.totalTokens, }; } // Anthropic/Bedrock/others format: usage with snake_case or camelCase if (metadata.usage) { + const u = metadata.usage as { + input_tokens?: number; + inputTokens?: number; + inputTokenCount?: number; + prompt_tokens?: number; + output_tokens?: number; + outputTokens?: number; + outputTokenCount?: number; + completion_tokens?: number; + total_tokens?: number; + totalTokens?: number; + }; return { inputTokens: - metadata.usage.input_tokens || - metadata.usage.inputTokens || // Bedrock camelCase - metadata.usage.inputTokenCount || // Bedrock invocationMetrics - metadata.usage.prompt_tokens, + u.input_tokens || + u.inputTokens || // Bedrock camelCase + u.inputTokenCount || // Bedrock invocationMetrics + u.prompt_tokens, outputTokens: - metadata.usage.output_tokens || - metadata.usage.outputTokens || // Bedrock camelCase - metadata.usage.outputTokenCount || // Bedrock invocationMetrics - metadata.usage.completion_tokens, + u.output_tokens || + u.outputTokens || // Bedrock camelCase + u.outputTokenCount || // Bedrock invocationMetrics + u.completion_tokens, totalTokens: - metadata.usage.total_tokens || - metadata.usage.totalTokens || // Bedrock camelCase - (metadata.usage.input_tokens || metadata.usage.inputTokenCount || 0) + - (metadata.usage.output_tokens || metadata.usage.outputTokenCount || 0), + u.total_tokens || + u.totalTokens || // Bedrock camelCase + (u.input_tokens || u.inputTokenCount || 0) + (u.output_tokens || u.outputTokenCount || 0), }; } diff --git a/src/LLMProviders/chainRunner/utils/modelAdapter.ts b/src/LLMProviders/chainRunner/utils/modelAdapter.ts index 7777f015..eb4b13ef 100644 --- a/src/LLMProviders/chainRunner/utils/modelAdapter.ts +++ b/src/LLMProviders/chainRunner/utils/modelAdapter.ts @@ -75,7 +75,7 @@ export interface ModelAdapter { * @param response - The model's response text containing tool calls * @returns Array of parsed tool calls */ - parseToolCalls?(response: string): any[]; + parseToolCalls?(response: string): unknown[]; /** * Check if model needs special handling @@ -288,7 +288,7 @@ class GPTModelAdapter extends BaseModelAdapter { * Check if this is a GPT-5 model * @returns True if the model is in the GPT-5 family */ - protected isGPT5Model(): boolean { + isGPT5Model(): boolean { return this.modelName.includes("gpt-5") || this.modelName.includes("gpt5"); } buildSystemPromptSections( @@ -675,8 +675,8 @@ REMEMBER: It is better to say "I only searched your notes, not the web" than to export class ModelAdapterFactory { static createAdapter(model: BaseChatModel): ModelAdapter { const modelName: string = ( - ((model as any).modelName as string) || - ((model as any).model as string) || + (model as { modelName?: string }).modelName || + (model as { model?: string }).model || "" ).toLowerCase(); @@ -686,7 +686,7 @@ export class ModelAdapterFactory { if (modelName.includes("gpt")) { const adapter = new GPTModelAdapter(modelName); // Log if it's a GPT-5 model for debugging - if ((adapter as any).isGPT5Model()) { + if (adapter.isGPT5Model()) { logInfo("Using GPTModelAdapter with GPT-5 specific enhancements"); } else { logInfo("Using GPTModelAdapter"); diff --git a/src/LLMProviders/chainRunner/utils/promptDebugService.test.ts b/src/LLMProviders/chainRunner/utils/promptDebugService.test.ts index 67a9f040..1e460c21 100644 --- a/src/LLMProviders/chainRunner/utils/promptDebugService.test.ts +++ b/src/LLMProviders/chainRunner/utils/promptDebugService.test.ts @@ -9,7 +9,7 @@ const createAdapter = () => ({ basePrompt: string, toolDescriptions: string, toolNames?: string[], - toolMetadata?: any[] + toolMetadata?: unknown[] ): PromptSection[] => [ { id: "system", diff --git a/src/LLMProviders/chainRunner/utils/promptPayloadRecorder.ts b/src/LLMProviders/chainRunner/utils/promptPayloadRecorder.ts index 4875ac52..6fd19834 100644 --- a/src/LLMProviders/chainRunner/utils/promptPayloadRecorder.ts +++ b/src/LLMProviders/chainRunner/utils/promptPayloadRecorder.ts @@ -85,14 +85,14 @@ function buildLayeredViewFromMessages( }; // Helper to extract text content from message - const getTextContent = (msg: any): string => { + const getTextContent = (msg: { role?: unknown; content?: unknown }): string => { if (typeof msg.content === "string") { - return msg.content as string; + return msg.content; } if (Array.isArray(msg.content)) { - const textParts: string[] = msg.content - .filter((item: any) => item.type === "text") - .map((item: any): string => item.text as string); + const textParts: string[] = (msg.content as Array<{ type?: string; text?: string }>) + .filter((item) => item.type === "text") + .map((item): string => item.text ?? ""); return textParts.join("\n"); } return ""; @@ -103,7 +103,7 @@ function buildLayeredViewFromMessages( let historyCount = 0; for (let i = 0; i < messageArray.length; i++) { - const msg: any = messageArray[i]; + const msg = messageArray[i] as { role?: unknown; content?: unknown }; const content = getTextContent(msg); if (msg.role === "system") { @@ -194,7 +194,9 @@ function buildLayeredViewFromMessages( } // Last user message - const lastMsg: any = messageArray[messageArray.length - 1]; + const lastMsg = messageArray[messageArray.length - 1] as + | { role?: unknown; content?: unknown } + | undefined; if (lastMsg && lastMsg.role === "user") { lines.push("━━━ USER MESSAGE ━━━"); lines.push(""); diff --git a/src/LLMProviders/chainRunner/utils/searchResultUtils.ts b/src/LLMProviders/chainRunner/utils/searchResultUtils.ts index 9a147203..bf1d55c8 100644 --- a/src/LLMProviders/chainRunner/utils/searchResultUtils.ts +++ b/src/LLMProviders/chainRunner/utils/searchResultUtils.ts @@ -1,6 +1,27 @@ import { logInfo, logWarn, logMarkdownBlock, logTable } from "@/logger"; import { sanitizeContentForCitations } from "@/LLMProviders/chainRunner/utils/citationUtils"; +/** + * Raw document shape returned by search/retrieval tools. + * Fields are optional because different providers may omit some. + */ +export interface SearchDoc { + title?: string; + path?: string; + content?: string; + mtime?: string | number; + rerank_score?: number; + score?: number; + explanation?: unknown; + includeInContext?: boolean; + __sourceId?: string | number; + collection_name?: string; + source_id?: string | number; + matchType?: string; + source?: string; + chunkId?: string; +} + /** * Quality summary for search results. * Helps the LLM evaluate whether results are adequate or if re-search is needed. @@ -20,7 +41,7 @@ export interface QualitySummary { * @param searchResults - Array of search results with scores * @returns Quality summary with counts by relevance tier */ -export function generateQualitySummary(searchResults: any[]): QualitySummary { +export function generateQualitySummary(searchResults: SearchDoc[]): QualitySummary { if (!Array.isArray(searchResults) || searchResults.length === 0) { return { high: 0, medium: 0, low: 0, total: 0, averageScore: 0 }; } @@ -83,7 +104,9 @@ export function formatSearchResultsForLLM(searchResults: unknown): string { } // Filter documents that should be included in context - const includedDocs = searchResults.filter((doc) => doc.includeInContext !== false); + const includedDocs = (searchResults as SearchDoc[]).filter( + (doc) => doc.includeInContext !== false + ); if (includedDocs.length === 0) { return "No relevant documents found."; @@ -91,7 +114,7 @@ export function formatSearchResultsForLLM(searchResults: unknown): string { // Format each document with essential metadata const formattedDocs = includedDocs - .map((doc: any, idx: number) => { + .map((doc, idx: number) => { const title = doc.title || "Untitled"; const path = doc.path || ""; // Optional stable source id if provided by caller; fallback to order @@ -100,7 +123,7 @@ export function formatSearchResultsForLLM(searchResults: unknown): string { // Safely handle mtime - check validity before converting let modified: string | null = null; if (doc.mtime) { - const date = new Date(doc.mtime as string | number | Date); + const date = new Date(doc.mtime); if (!isNaN(date.getTime())) { modified = date.toISOString(); } @@ -156,12 +179,12 @@ export function formatSearchResultStringForLLM(resultString: string): string { */ export function extractSourcesFromSearchResults( searchResults: unknown -): { title: string; path: string; score: number; explanation?: any }[] { +): { title: string; path: string; score: number; explanation?: unknown }[] { if (!Array.isArray(searchResults)) { return []; } - return searchResults.map((doc: any) => ({ + return (searchResults as SearchDoc[]).map((doc) => ({ title: doc.title || doc.path || "Untitled", path: doc.path || doc.title || "", score: doc.rerank_score || doc.score || 0, @@ -189,19 +212,20 @@ function toIsoString(ts: unknown): string { * Create a concise, single-line summary of an explanation object. * Includes lexical matches, semantic score, folder/graph boosts, and score adjustments. */ -export function summarizeExplanation(explanation: any): string { +export function summarizeExplanation(explanation: unknown): string { if (!explanation) return ""; const parts: string[] = []; + const exp = explanation as Record; try { // Lexical matches summary - if (Array.isArray(explanation.lexicalMatches) && explanation.lexicalMatches.length > 0) { + if (Array.isArray(exp.lexicalMatches) && exp.lexicalMatches.length > 0) { const fields = new Set(); const terms = new Set(); - for (const m of explanation.lexicalMatches) { - if (m?.field) fields.add(String(m.field)); - if (m?.query) terms.add(String(m.query)); + for (const m of exp.lexicalMatches as Array<{ field?: unknown; query?: unknown }>) { + if (typeof m?.field === "string") fields.add(m.field); + if (typeof m?.query === "string") terms.add(m.query); } const fieldsStr = Array.from(fields).join("/"); const termsStr = Array.from(terms).slice(0, 3).join(", "); @@ -209,24 +233,32 @@ export function summarizeExplanation(explanation: any): string { } // Semantic score - if (typeof explanation.semanticScore === "number" && explanation.semanticScore > 0) { - parts.push(`Semantic: ${(explanation.semanticScore * 100).toFixed(1)}%`); + if (typeof exp.semanticScore === "number" && exp.semanticScore > 0) { + parts.push(`Semantic: ${(exp.semanticScore * 100).toFixed(1)}%`); } // Folder boost - if (explanation.folderBoost && typeof explanation.folderBoost.boostFactor === "number") { - const fb = explanation.folderBoost; + if ( + exp.folderBoost && + typeof (exp.folderBoost as { boostFactor?: number }).boostFactor === "number" + ) { + const fb = exp.folderBoost as { boostFactor: number; folder?: string }; const folder = fb.folder || "root"; parts.push(`Folder +${fb.boostFactor.toFixed(2)} (${folder})`); } // Graph connections (query-aware boost) - if (explanation.graphConnections && typeof explanation.graphConnections === "object") { - const gc = explanation.graphConnections; + if (exp.graphConnections && typeof exp.graphConnections === "object") { + const gc = exp.graphConnections as { + backlinks?: number; + coCitations?: number; + sharedTags?: number; + score?: number; + }; const bits: string[] = []; - if (gc.backlinks > 0) bits.push(`${gc.backlinks} backlinks`); - if (gc.coCitations > 0) bits.push(`${gc.coCitations} co-cites`); - if (gc.sharedTags > 0) bits.push(`${gc.sharedTags} tags`); + if ((gc.backlinks ?? 0) > 0) bits.push(`${gc.backlinks} backlinks`); + if ((gc.coCitations ?? 0) > 0) bits.push(`${gc.coCitations} co-cites`); + if ((gc.sharedTags ?? 0) > 0) bits.push(`${gc.sharedTags} tags`); if (typeof gc.score === "number") { parts.push(`Graph ${gc.score.toFixed(1)}${bits.length ? ` (${bits.join(", ")})` : ""}`); } else if (bits.length) { @@ -236,21 +268,21 @@ export function summarizeExplanation(explanation: any): string { // Legacy graph boost if ( - explanation.graphBoost && - typeof explanation.graphBoost.boostFactor === "number" && - !explanation.graphConnections + exp.graphBoost && + typeof (exp.graphBoost as { boostFactor?: number }).boostFactor === "number" && + !exp.graphConnections ) { - const gb = explanation.graphBoost; + const gb = exp.graphBoost as { boostFactor: number; connections?: number }; parts.push(`Graph +${gb.boostFactor.toFixed(2)} (${gb.connections} connections)`); } // Score adjustment if ( - typeof explanation.baseScore === "number" && - typeof explanation.finalScore === "number" && - explanation.baseScore !== explanation.finalScore + typeof exp.baseScore === "number" && + typeof exp.finalScore === "number" && + exp.baseScore !== exp.finalScore ) { - parts.push(`Score: ${explanation.baseScore.toFixed(4)}→${explanation.finalScore.toFixed(4)}`); + parts.push(`Score: ${exp.baseScore.toFixed(4)}→${exp.finalScore.toFixed(4)}`); } } catch { // Ignore explanation parsing errors, leave parts as-is @@ -278,8 +310,8 @@ export function summarizeExplanation(explanation: any): string { * @returns Formatted XML string with `` and `` sections */ export function formatSplitSearchResultsForLLM( - filterDocs: any[], - searchDocs: any[], + filterDocs: SearchDoc[], + searchDocs: SearchDoc[], startId = 1 ): string { let currentId = startId; @@ -288,7 +320,7 @@ export function formatSplitSearchResultsForLLM( // Format filter results if (filterDocs.length > 0) { const filterXml = filterDocs - .map((doc: any) => { + .map((doc) => { const id = doc.__sourceId || currentId++; const title = doc.title || "Untitled"; const path = doc.path || ""; @@ -313,7 +345,7 @@ ${doc.content || ""} // Format search results if (searchDocs.length > 0) { const searchXml = searchDocs - .map((doc: any) => { + .map((doc) => { const id = doc.__sourceId || currentId++; const title = doc.title || "Untitled"; const path = doc.path || ""; @@ -394,12 +426,12 @@ export function formatMetadataOnlyDocuments(docs: unknown, snippetLength = 300): return ""; } - const fileElements = docs - .map((doc: any) => { + const fileElements = (docs as SearchDoc[]) + .map((doc) => { const title = doc.title || "Untitled"; const path = doc.path || ""; const modified = toIsoString(doc.mtime); - const content = sanitizeContentForCitations((doc.content as string) || ""); + const content = sanitizeContentForCitations(doc.content || ""); const snippet = content.slice(0, snippetLength); const pathEl = path ? `\n${path}` : ""; @@ -413,7 +445,7 @@ export function formatMetadataOnlyDocuments(docs: unknown, snippetLength = 300): return `\n${fileElements}\n`; } -export function logSearchResultsDebugTable(searchResults: any[]): void { +export function logSearchResultsDebugTable(searchResults: SearchDoc[]): void { if (!Array.isArray(searchResults) || searchResults.length === 0) { logInfo("Search Results: (none)"); return; @@ -428,7 +460,7 @@ export function logSearchResultsDebugTable(searchResults: any[]): void { }; let includedCount = 0; - const rows: Row[] = searchResults.map((doc: any) => { + const rows: Row[] = searchResults.map((doc) => { const mtime = toIsoString(doc.mtime); const scoreNum = typeof doc.rerank_score === "number" ? doc.rerank_score : doc.score || 0; const score = (Number.isFinite(scoreNum) ? scoreNum : 0).toFixed(4); diff --git a/src/LLMProviders/chainRunner/utils/toolExecution.ts b/src/LLMProviders/chainRunner/utils/toolExecution.ts index ca900c01..917c1ef5 100644 --- a/src/LLMProviders/chainRunner/utils/toolExecution.ts +++ b/src/LLMProviders/chainRunner/utils/toolExecution.ts @@ -1,3 +1,4 @@ +import { StructuredTool } from "@langchain/core/tools"; import { logError, logInfo, logWarn } from "@/logger"; import { checkIsPlusUser, isSelfHostModeValid } from "@/plusUtils"; import { getSettings } from "@/settings/model"; @@ -30,7 +31,7 @@ export interface ToolExecutionResult { */ export async function executeSequentialToolCall( toolCall: ToolCall, - availableTools: any[], + availableTools: Pick[], originalUserMessage?: string ): Promise { const DEFAULT_TOOL_TIMEOUT = 120000; // 120 seconds timeout per tool @@ -49,7 +50,7 @@ export async function executeSequentialToolCall( const tool = availableTools.find((t) => t.name === toolCall.name); if (!tool) { - const availableToolNames = availableTools.map((t): string => t.name as string).join(", "); + const availableToolNames = availableTools.map((t): string => t.name).join(", "); return { toolName: toolCall.name, result: `Error: Tool '${toolCall.name}' not found. Available tools: ${availableToolNames}. Make sure you have the tool enabled in the Agent settings.`, @@ -213,7 +214,10 @@ export function getToolEmoji(toolName: string): string { /** * Get user confirmation message for tool call */ -export function getToolConfirmtionMessage(toolName: string, toolArgs?: any): string | null { +export function getToolConfirmtionMessage( + toolName: string, + toolArgs?: Record +): string | null { if (toolName == "writeFile" || toolName == "editFile") { return "Accept / reject in the Preview"; } @@ -283,11 +287,11 @@ export function logToolResult(toolName: string, result: ToolExecutionResult): vo * If path is not available, falls back to title */ export function deduplicateSources( - sources: { title: string; path: string; score: number; explanation?: any }[] -): { title: string; path: string; score: number; explanation?: any }[] { + sources: { title: string; path: string; score: number; explanation?: unknown }[] +): { title: string; path: string; score: number; explanation?: unknown }[] { const uniqueSources = new Map< string, - { title: string; path: string; score: number; explanation?: any } + { title: string; path: string; score: number; explanation?: unknown } >(); for (const source of sources) { diff --git a/src/LLMProviders/chainRunner/utils/toolPromptDebugger.ts b/src/LLMProviders/chainRunner/utils/toolPromptDebugger.ts index f44183f7..5edc1719 100644 --- a/src/LLMProviders/chainRunner/utils/toolPromptDebugger.ts +++ b/src/LLMProviders/chainRunner/utils/toolPromptDebugger.ts @@ -6,7 +6,7 @@ import { processRawChatHistory, processedMessagesToTextOnly } from "./chatHistor */ export interface BuildPromptDebugSectionsOptions { systemSections: PromptSection[]; - rawHistory?: any[]; + rawHistory?: unknown[]; adapterName: string; originalUserMessage: string; enhancedUserMessage: string; diff --git a/src/LLMProviders/chatModelManager.ts b/src/LLMProviders/chatModelManager.ts index 1c070fb8..48c2b5c3 100644 --- a/src/LLMProviders/chatModelManager.ts +++ b/src/LLMProviders/chatModelManager.ts @@ -38,35 +38,29 @@ import { ChatXAI } from "@langchain/xai"; import { MissingApiKeyError, MissingPlusLicenseError } from "@/error"; import { Notice } from "obsidian"; import { ChatOpenRouter } from "./ChatOpenRouter"; -import { ChatLMStudio, type ChatLMStudioInput } from "./ChatLMStudio"; +import { ChatLMStudio } from "./ChatLMStudio"; import { BedrockChatModel, type BedrockChatModelFields } from "./BedrockChatModel"; import { GitHubCopilotChatModel } from "@/LLMProviders/githubCopilot/GitHubCopilotChatModel"; -import { - GitHubCopilotResponsesModel, - type GitHubCopilotResponsesModelParams, -} from "@/LLMProviders/githubCopilot/GitHubCopilotResponsesModel"; +import { GitHubCopilotResponsesModel } from "@/LLMProviders/githubCopilot/GitHubCopilotResponsesModel"; // Patch BaseLanguageModel.prototype.getNumTokens once at module load to prevent // tiktoken CDN fetches. LangChain's default getNumTokens() downloads a ~3MB BPE // vocabulary from tiktoken.pages.dev, which blocks all LLM calls when the CDN is // unreachable. This char/4 estimation is the same fallback LangChain uses internally // before tiktoken loads. Actual token usage comes from API response metadata. +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- patching a private prototype method requires any cast (BaseLanguageModel.prototype as any).getNumTokens = async ( content: string | Array<{ type: string; text?: string }> ) => { const text = typeof content === "string" ? content - : content - .map((item: any): string => - typeof item === "string" ? item : ((item.text as string) ?? "") - ) - .join(""); + : content.map((item: { type: string; text?: string }): string => item.text ?? "").join(""); return Math.ceil(text.length / 4); }; type ChatConstructorType = { - new (config: any): any; + new (config: Record): BaseChatModel; }; const CHAT_PROVIDER_CONSTRUCTORS = { @@ -253,7 +247,7 @@ export default class ChatModelManager { }, }), }, - [ChatModelProviders.AZURE_OPENAI]: await (async (): Promise => { + [ChatModelProviders.AZURE_OPENAI]: await (async (): Promise> => { const azureUrl = normalizeAzureUrl(customModel.baseUrl); return { modelName: customModel.baseUrl @@ -495,7 +489,7 @@ export default class ChatModelManager { maxTokens: number, _temperature: number | undefined, customModel?: CustomModel - ): any { + ): Record { const settings = getSettings(); const modelInfo = getModelInfo(modelName); const resolvedTemperature = this.getTemperatureForModel( @@ -504,7 +498,7 @@ export default class ChatModelManager { settings ); - const config: any = { + const config: Record = { maxTokens, temperature: resolvedTemperature, }; @@ -597,7 +591,7 @@ export default class ChatModelManager { * This prevents passing undefined values to providers that don't support them */ private getProviderSpecificParams(provider: ChatModelProviders, customModel: CustomModel) { - const params: Record = {}; + const params: Record = {}; // Add topP only if defined if (customModel.topP !== undefined) { @@ -693,8 +687,9 @@ export default class ChatModelManager { } getProviderConstructor(model: CustomModel): ChatConstructorType { - const constructor: ChatConstructorType = - CHAT_PROVIDER_CONSTRUCTORS[model.provider as ChatModelProviders]; + const constructor: ChatConstructorType = CHAT_PROVIDER_CONSTRUCTORS[ + model.provider as ChatModelProviders + ] as unknown as ChatConstructorType; if (!constructor) { console.warn(`Unknown provider: ${model.provider} for model: ${model.name}`); throw new Error(`Unknown provider: ${model.provider} for model: ${model.name}`); @@ -828,7 +823,7 @@ export default class ChatModelManager { const modelInfo = getModelInfo(model.name); // For GPT-5 models, automatically use Responses API for proper verbosity support - const constructorConfig: any = { ...modelConfig }; + const constructorConfig: Record = { ...modelConfig }; const useCopilotResponses = shouldUseGitHubCopilotResponsesApi(model); if ( modelInfo.isGPT5 && @@ -850,20 +845,18 @@ export default class ChatModelManager { (model.provider as ChatModelProviders) === ChatModelProviders.LM_STUDIO && model.useResponsesApi !== false ) { - const lmStudioInstance = new ChatLMStudio(constructorConfig as ChatLMStudioInput); + const lmStudioInstance = new ChatLMStudio(constructorConfig); logInfo(`[ChatModelManager] Using Responses API for LM Studio model: ${model.name}`); return lmStudioInstance; } if (useCopilotResponses) { - return new GitHubCopilotResponsesModel( - constructorConfig as GitHubCopilotResponsesModelParams - ); + return new GitHubCopilotResponsesModel(constructorConfig); } const newModelInstance = new selectedModel.AIConstructor(constructorConfig); - return newModelInstance as BaseChatModel; + return newModelInstance; } validateChatModel(chatModel: BaseChatModel): boolean { @@ -917,7 +910,7 @@ export default class ChatModelManager { const pingMaxTokens = modelInfo.isThinkingEnabled ? 4096 : 30; const tokenConfig = { maxTokens: pingMaxTokens }; - const constructorConfig: any = { + const constructorConfig: Record = { ...pingConfig, ...tokenConfig, }; @@ -940,11 +933,9 @@ export default class ChatModelManager { const testModel = (model.provider as ChatModelProviders) === ChatModelProviders.LM_STUDIO && model.useResponsesApi !== false - ? new ChatLMStudio(constructorConfig as ChatLMStudioInput) + ? new ChatLMStudio(constructorConfig) : useCopilotResponses - ? new GitHubCopilotResponsesModel( - constructorConfig as GitHubCopilotResponsesModelParams - ) + ? new GitHubCopilotResponsesModel(constructorConfig) : new (this.getProviderConstructor(modelToTest))(constructorConfig); await testModel.invoke([{ role: "user", content: "hello" }], { timeout: 8000, diff --git a/src/LLMProviders/embeddingManager.ts b/src/LLMProviders/embeddingManager.ts index 96c49fe3..6e52e796 100644 --- a/src/LLMProviders/embeddingManager.ts +++ b/src/LLMProviders/embeddingManager.ts @@ -14,7 +14,7 @@ import { BrevilabsClient } from "./brevilabsClient"; import { CustomJinaEmbeddings } from "./CustomJinaEmbeddings"; import { CustomOpenAIEmbeddings } from "./CustomOpenAIEmbeddings"; -type EmbeddingConstructorType = new (config: any) => Embeddings; +type EmbeddingConstructorType = new (config: Record) => Embeddings; const EMBEDDING_PROVIDER_CONSTRUCTORS = { [EmbeddingModelProviders.COPILOT_PLUS]: CustomOpenAIEmbeddings, @@ -180,7 +180,7 @@ export default class EmbeddingManager { } } - private async getEmbeddingConfig(customModel: CustomModel): Promise { + private async getEmbeddingConfig(customModel: CustomModel): Promise> { const settings = getSettings(); const modelName = customModel.name; diff --git a/src/LLMProviders/githubCopilot/GitHubCopilotChatModel.ts b/src/LLMProviders/githubCopilot/GitHubCopilotChatModel.ts index 92668eae..e9024050 100644 --- a/src/LLMProviders/githubCopilot/GitHubCopilotChatModel.ts +++ b/src/LLMProviders/githubCopilot/GitHubCopilotChatModel.ts @@ -148,11 +148,11 @@ export class GitHubCopilotChatModel extends ChatOpenAICompletions { * @returns A normalized LangChain message chunk. */ protected override _convertCompletionsDeltaToBaseMessageChunk( - delta: Record, - rawResponse: any, + delta: Record, + rawResponse: unknown, // Reason: Parent expects OpenAI's ChatCompletionRole type, but we accept any string // to avoid coupling to the exact OpenAI SDK type. Cast is safe because we pass through. - defaultRole?: any + defaultRole?: string ): BaseMessageChunk { // Reason: Copilot API omits delta.role when proxying Claude models. // The parent converter uses `delta.role ?? defaultRole` to determine message type. diff --git a/src/LLMProviders/memoryManager.ts b/src/LLMProviders/memoryManager.ts index 29919122..2790938c 100644 --- a/src/LLMProviders/memoryManager.ts +++ b/src/LLMProviders/memoryManager.ts @@ -48,7 +48,7 @@ export default class MemoryManager { await this.memory.clear(); } - async loadMemoryVariables(): Promise { + async loadMemoryVariables(): Promise> { const variables = await this.memory.loadMemoryVariables({}); if (this.debug) logInfo("Loaded memory variables:", variables); return variables; @@ -59,7 +59,10 @@ export default class MemoryManager { * The output (assistant response) is compacted to reduce memory bloat from * accumulated tool results (localSearch, readNote, etc.). */ - async saveContext(input: any, output: any): Promise { + async saveContext( + input: Record, + output: Record | string + ): Promise { // Compact the output to prevent memory bloat from tool results const compactedOutput = typeof output === "string" @@ -73,7 +76,7 @@ export default class MemoryManager { logInfo("Saving to memory - Input:", input, "Output (compacted):", compactedOutput); } await this.memory.saveContext( - input as Parameters[0], + input, compactedOutput as Parameters[1] ); } diff --git a/src/LLMProviders/selfHostServices.test.ts b/src/LLMProviders/selfHostServices.test.ts index 12a549e6..7f3c73a4 100644 --- a/src/LLMProviders/selfHostServices.test.ts +++ b/src/LLMProviders/selfHostServices.test.ts @@ -4,7 +4,7 @@ import { hasSelfHostSearchKey, selfHostWebSearch } from "./selfHostServices"; const mockGetSettings = jest.fn(); jest.mock("@/settings/model", () => ({ - getSettings: (): any => mockGetSettings(), + getSettings: (): unknown => mockGetSettings(), })); jest.mock("@/encryptionService", () => ({ diff --git a/src/aiParams.ts b/src/aiParams.ts index 4662ae79..46910b4e 100644 --- a/src/aiParams.ts +++ b/src/aiParams.ts @@ -6,6 +6,7 @@ import { ModelCapability, ReasoningEffort, Verbosity } from "@/constants"; import { settingsAtom, settingsStore } from "@/settings/model"; import { SelectedTextContext } from "@/types/message"; import { atom, useAtom } from "jotai"; +import { TFile } from "obsidian"; const userModelKeyAtom = atom(null); const modelKeyAtom = atom( @@ -128,7 +129,7 @@ export interface ModelConfig { export interface SetChainOptions { prompt?: ChatPromptTemplate; chatModel?: BaseChatModel; - noteFile?: any; + noteFile?: TFile; abortController?: AbortController; refreshIndex?: boolean; } diff --git a/src/cache/fileCache.ts b/src/cache/fileCache.ts index b89ef8c1..6dd2517b 100644 --- a/src/cache/fileCache.ts +++ b/src/cache/fileCache.ts @@ -8,7 +8,7 @@ export interface FileCacheEntry { } export class FileCache { - private static instance: FileCache; + private static instance: FileCache; private cacheDir: string; private memoryCache: Map> = new Map(); diff --git a/src/chainFactory.ts b/src/chainFactory.ts index 83a1fb7d..4d4c3bba 100644 --- a/src/chainFactory.ts +++ b/src/chainFactory.ts @@ -38,7 +38,7 @@ export interface ConversationalRetrievalChainParams { }; } -export interface Document> { +export interface Document> { // Structure of Document, possibly including pageContent, metadata, etc. pageContent: string; metadata: T; diff --git a/src/commands/CustomCommandSettingsModal.tsx b/src/commands/CustomCommandSettingsModal.tsx index f731c0e5..fdde77b7 100644 --- a/src/commands/CustomCommandSettingsModal.tsx +++ b/src/commands/CustomCommandSettingsModal.tsx @@ -41,7 +41,7 @@ function CustomCommandSettingsModalContent({ const [command, setCommand] = useState(initialCommand); const [errors, setErrors] = useState({}); - const handleUpdate = (field: keyof CustomCommand, value: any) => { + const handleUpdate = (field: keyof CustomCommand, value: CustomCommand[keyof CustomCommand]) => { setCommand((prev) => ({ ...prev, [field]: value, diff --git a/src/commands/index.ts b/src/commands/index.ts index d0f361f0..ea8b0f6e 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -428,28 +428,30 @@ export function registerCommands( } // Map hits to chunks (getDocsByPath returns {document, score} format) - const chunks: any[] = hits.map((hit: { document: any }): any => hit.document); + const chunks: Record[] = hits.map( + (hit) => hit.document as unknown as Record + ); const content = [ `# Embedding Debug: ${activeFile.basename}`, "", `**Path:** ${activeFile.path}`, `**Chunks:** ${chunks.length}`, - `**Embedding Model:** ${chunks[0]?.embeddingModel || "unknown"}`, + `**Embedding Model:** ${(chunks[0]?.embeddingModel as string | undefined) || "unknown"}`, "", - ...chunks.flatMap((chunk: any, index: number) => { - const embedding = chunk.embedding || []; + ...chunks.flatMap((chunk: Record, index: number) => { + const embedding = (chunk.embedding as number[] | undefined) || []; const preview = embedding .slice(0, 10) .map((v: number) => v.toFixed(6)) .join(", "); return [ `## Chunk ${index + 1}`, - `- **ID:** ${chunk.id}`, - `- **Content Preview:** "${(chunk.content || "").substring(0, 200)}..."`, + `- **ID:** ${chunk.id as string}`, + `- **Content Preview:** "${((chunk.content as string | undefined) || "").substring(0, 200)}..."`, `- **Vector Length:** ${embedding.length}`, `- **Vector Preview:** [${preview}${embedding.length > 10 ? ", ..." : ""}]`, - `- **Tags:** ${(chunk.tags || []).join(", ") || "none"}`, - `- **Characters:** ${chunk.nchars || 0}`, + `- **Tags:** ${((chunk.tags as string[] | undefined) || []).join(", ") || "none"}`, + `- **Characters:** ${(chunk.nchars as number | undefined) || 0}`, "", ]; }), diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 13e96ac9..ace3a170 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -278,7 +278,10 @@ const ChatInternal: React.FC void; - onSelect: (category: AtMentionCategory, data: any) => void; + onSelect: (category: AtMentionCategory, data: TFile | string | TFolder | WebTabContext) => void; isCopilotPlus?: boolean; currentActiveFile?: TFile | null; } diff --git a/src/components/chat-components/ChatContextMenu.tsx b/src/components/chat-components/ChatContextMenu.tsx index 1863b34f..f111512a 100644 --- a/src/components/chat-components/ChatContextMenu.tsx +++ b/src/components/chat-components/ChatContextMenu.tsx @@ -1,5 +1,5 @@ import { AlertCircle, CheckCircle, CircleDashed, FileText, Loader2, X } from "lucide-react"; -import { Platform, TFile } from "obsidian"; +import { Platform, TFile, TFolder } from "obsidian"; import React, { useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; @@ -32,10 +32,10 @@ interface ChatContextMenuProps { contextFolders: string[]; contextWebTabs: WebTabContext[]; selectedTextContexts?: SelectedTextContext[]; - onRemoveContext: (category: string, data: any) => void; + onRemoveContext: (category: string, data: string) => void; showProgressCard: () => void; showIndexingCard?: () => void; - onTypeaheadSelect: (category: string, data: any) => void; + onTypeaheadSelect: (category: string, data: TFile | string | TFolder | WebTabContext) => void; lexicalEditorRef?: React.RefObject<{ focus: () => void }>; } @@ -44,7 +44,7 @@ function ContextSelection({ onRemoveContext, }: { selectedText: SelectedTextContext; - onRemoveContext: (category: string, data: any) => void; + onRemoveContext: (category: string, data: string) => void; }) { // Handle web selected text if (isWebSelectedTextContext(selectedText)) { @@ -123,7 +123,10 @@ export const ChatContextMenu: React.FC = ({ }; // Simple wrapper that adds focus management to the ContextControl handler - const handleTypeaheadSelect = (category: string, data: any) => { + const handleTypeaheadSelect = ( + category: string, + data: TFile | string | TFolder | WebTabContext + ) => { // Delegate to ContextControl handler onTypeaheadSelect(category, data); diff --git a/src/components/chat-components/ChatControls.tsx b/src/components/chat-components/ChatControls.tsx index 910ed5d3..c01ae8e0 100644 --- a/src/components/chat-components/ChatControls.tsx +++ b/src/components/chat-components/ChatControls.tsx @@ -38,6 +38,20 @@ import { import { TokenCounter } from "./TokenCounter"; import { ChatSettingsPopover } from "@/components/chat-components/ChatSettingsPopover"; +/** Minimal type for the internal Obsidian app with plugin access. */ +interface CopilotPluginInternal { + projectManager?: { + getProjectContext: (id: string) => Promise; + getCurrentChainManager: () => { createChainWithNewModel: () => Promise }; + }; +} + +interface ObsidianAppWithPlugins { + plugins: { + getPlugin: (id: string) => CopilotPluginInternal | undefined; + }; +} + export async function refreshVaultIndex() { try { const { getSettings } = await import("@/settings/model"); @@ -109,7 +123,7 @@ export async function reloadCurrentProject(app: App) { // Then, trigger the full load and processing logic via ProjectManager. // getProjectContext will call loadProjectContext if markdownNeedsReload is true (which it is now). // loadProjectContext will handle markdown, web, youtube, and other file types (including API calls for new ones). - const plugin = (app as any).plugins.getPlugin("copilot"); + const plugin = (app as unknown as ObsidianAppWithPlugins).plugins.getPlugin("copilot"); if (plugin && plugin.projectManager) { await plugin.projectManager.getProjectContext(currentProject.id); // Reason: chain/model config must be refreshed after context reload so the next @@ -159,7 +173,7 @@ export async function forceRebuildCurrentProjectContext(app: App) { // Step 2: Trigger a full reload from scratch. // getProjectContext will call loadProjectContext as the cache is now empty. // loadProjectContext will handle markdown, web, youtube, and all other file types. - const plugin = (app as any).plugins.getPlugin("copilot"); + const plugin = (app as unknown as ObsidianAppWithPlugins).plugins.getPlugin("copilot"); if (plugin && plugin.projectManager) { await plugin.projectManager.getProjectContext(currentProject.id); new Notice( diff --git a/src/components/chat-components/ChatInput.tsx b/src/components/chat-components/ChatInput.tsx index 0f70211c..ede6349d 100644 --- a/src/components/chat-components/ChatInput.tsx +++ b/src/components/chat-components/ChatInput.tsx @@ -22,9 +22,9 @@ import { useSettingsValue } from "@/settings/model"; import { SelectedTextContext, WebTabContext } from "@/types/message"; import { isAllowedFileForNoteContext } from "@/utils"; import { CornerDownLeft, Image, Loader2, StopCircle, X } from "lucide-react"; -import { App, Notice, TFile } from "obsidian"; +import { App, Notice, TFile, TFolder } from "obsidian"; import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { $getSelection, $isRangeSelection } from "lexical"; +import { $getSelection, $isRangeSelection, LexicalEditor as LexicalEditorType } from "lexical"; import { ContextControl } from "./ContextControl"; import { $removePillsByPath } from "./pills/NotePillNode"; import { $removeActiveNotePills } from "./pills/ActiveNotePillNode"; @@ -113,7 +113,7 @@ const ChatInput: React.FC = ({ const [contextFolders, setContextFolders] = useState(initialContext?.folders || []); const [contextWebTabs, setContextWebTabs] = useState([]); const containerRef = useRef(null); - const lexicalEditorRef = useRef(null); + const lexicalEditorRef = useRef(null); const [currentModelKey, setCurrentModelKey] = useModelKey(); const [currentChain] = useChainType(); const [isProjectLoading] = useProjectLoading(); @@ -153,7 +153,7 @@ const ChatInput: React.FC = ({ title: pill.getTitle(), faviconUrl: pill.getFaviconUrl(), })); - }) as WebTabContext[]; + }); }; // Toggle states for vault, web search, composer, and autonomous agent @@ -375,7 +375,7 @@ const ChatInput: React.FC = ({ }; // Unified handler for adding to context (from popover @ mention) - const handleAddToContext = (category: string, data: any) => { + const handleAddToContext = (category: string, data: TFile | string | TFolder | WebTabContext) => { switch (category) { case "activeNote": // Set active note context flag (no pill needed - context badge shows it) @@ -428,8 +428,13 @@ const ChatInput: React.FC = ({ break; case "webTabs": // Badge-only behavior (like notes): add to contextWebTabs state, no pill insertion - if (data && typeof data.url === "string") { - const normalized = normalizeWebTabContext(data as WebTabContext); + if ( + data && + typeof data === "object" && + "url" in data && + typeof (data as { url: unknown }).url === "string" + ) { + const normalized = normalizeWebTabContext(data); if (!normalized) break; // If selecting the active web tab, toggle the active badge instead @@ -462,7 +467,7 @@ const ChatInput: React.FC = ({ }; // Unified handler for removing from context (from context menu badges) - const handleRemoveFromContext = (category: string, data: any) => { + const handleRemoveFromContext = (category: string, data: string) => { switch (category) { case "activeNote": // Remove active note pill from editor and turn off flag @@ -638,7 +643,7 @@ const ChatInput: React.FC = ({ }; }, [app.workspace]); - const onEditorReady = useCallback((editor: any) => { + const onEditorReady = useCallback((editor: LexicalEditorType) => { lexicalEditorRef.current = editor; }, []); diff --git a/src/components/chat-components/ChatSettingsPopover.tsx b/src/components/chat-components/ChatSettingsPopover.tsx index f1110cfe..380f6f38 100644 --- a/src/components/chat-components/ChatSettingsPopover.tsx +++ b/src/components/chat-components/ChatSettingsPopover.tsx @@ -130,7 +130,7 @@ export function ChatSettingsPopover() { * Update model parameters (immediately update UI, delayed save) */ const handleParamChange = useCallback( - (field: keyof CustomModel, value: any) => { + (field: keyof CustomModel, value: CustomModel[keyof CustomModel]) => { if (!localModel) return; const updatedModel = { ...localModel, [field]: value }; diff --git a/src/components/chat-components/ChatSingleMessage.test.tsx b/src/components/chat-components/ChatSingleMessage.test.tsx index d9096d5d..39d10d26 100644 --- a/src/components/chat-components/ChatSingleMessage.test.tsx +++ b/src/components/chat-components/ChatSingleMessage.test.tsx @@ -97,7 +97,7 @@ describe("think block rendering — closing tags are not consumed by indented co }); beforeAll(() => { - (window as any).activeDocument = window.document; + (window as unknown as Record).activeDocument = window.document; }); /** @@ -279,7 +279,7 @@ describe("ChatSingleMessage", () => { }); beforeAll(() => { - (window as any).activeDocument = window.document; + (window as unknown as Record).activeDocument = window.document; }); it("normalizes rendered footnotes for assistant messages", async () => { diff --git a/src/components/chat-components/ContextControl.tsx b/src/components/chat-components/ContextControl.tsx index be8731bb..9bc47417 100644 --- a/src/components/chat-components/ContextControl.tsx +++ b/src/components/chat-components/ContextControl.tsx @@ -1,7 +1,7 @@ import React from "react"; import { SelectedTextContext, WebTabContext } from "@/types/message"; -import { TFile } from "obsidian"; +import { TFile, TFolder } from "obsidian"; import { ChatContextMenu } from "./ChatContextMenu"; interface ChatControlsProps { @@ -16,11 +16,11 @@ interface ChatControlsProps { selectedTextContexts?: SelectedTextContext[]; showProgressCard: () => void; showIndexingCard?: () => void; - lexicalEditorRef?: React.RefObject; + lexicalEditorRef?: React.RefObject<{ focus: () => void }>; // Unified handlers - onAddToContext: (category: string, data: any) => void; - onRemoveFromContext: (category: string, data: any) => void; + onAddToContext: (category: string, data: TFile | string | TFolder | WebTabContext) => void; + onRemoveFromContext: (category: string, data: string) => void; } export const ContextControl: React.FC = ({ @@ -39,12 +39,15 @@ export const ContextControl: React.FC = ({ onAddToContext, onRemoveFromContext, }) => { - const handleRemoveContext = (category: string, data: any) => { + const handleRemoveContext = (category: string, data: string) => { // Delegate to unified handler onRemoveFromContext(category, data); }; - const handleTypeaheadSelect = (category: string, data: any) => { + const handleTypeaheadSelect = ( + category: string, + data: TFile | string | TFolder | WebTabContext + ) => { // Delegate to unified handler onAddToContext(category, data); }; diff --git a/src/components/chat-components/LexicalEditor.tsx b/src/components/chat-components/LexicalEditor.tsx index c50c69bf..b47b32c2 100644 --- a/src/components/chat-components/LexicalEditor.tsx +++ b/src/components/chat-components/LexicalEditor.tsx @@ -59,7 +59,7 @@ interface LexicalEditorProps { onWebTabsRemoved?: (removedWebTabs: WebTabContext[]) => void; onActiveWebTabAdded?: () => void; onActiveWebTabRemoved?: () => void; - onEditorReady?: (editor: any) => void; + onEditorReady?: (editor: LexicalEditorType) => void; onImagePaste?: (files: File[]) => void; onTagSelected?: () => void; isCopilotPlus?: boolean; diff --git a/src/components/chat-components/pills/FolderPillNode.tsx b/src/components/chat-components/pills/FolderPillNode.tsx index d9a8c1b7..384e8768 100644 --- a/src/components/chat-components/pills/FolderPillNode.tsx +++ b/src/components/chat-components/pills/FolderPillNode.tsx @@ -121,7 +121,7 @@ export function $createFolderPillNode(folderPath: string): FolderPillNode { return new FolderPillNode(folderPath); } -export function $isFolderPillNode(node: any): node is FolderPillNode { +export function $isFolderPillNode(node: LexicalNode): node is FolderPillNode { return node instanceof FolderPillNode; } diff --git a/src/components/chat-components/pills/ToolPillNode.tsx b/src/components/chat-components/pills/ToolPillNode.tsx index e67aa617..281da74e 100644 --- a/src/components/chat-components/pills/ToolPillNode.tsx +++ b/src/components/chat-components/pills/ToolPillNode.tsx @@ -75,7 +75,7 @@ export function $createToolPillNode(toolName: string): ToolPillNode { return new ToolPillNode(toolName); } -export function $isToolPillNode(node: any): node is ToolPillNode { +export function $isToolPillNode(node: LexicalNode): node is ToolPillNode { return node instanceof ToolPillNode; } diff --git a/src/components/chat-components/pills/URLPillNode.tsx b/src/components/chat-components/pills/URLPillNode.tsx index a8788e56..87e3756f 100644 --- a/src/components/chat-components/pills/URLPillNode.tsx +++ b/src/components/chat-components/pills/URLPillNode.tsx @@ -187,6 +187,6 @@ export function $removePillsByURL(url: string): void { } } -export function $isURLPillNode(node: any): node is URLPillNode { +export function $isURLPillNode(node: LexicalNode): node is URLPillNode { return node instanceof URLPillNode; } diff --git a/src/components/chat-components/plugins/FocusPlugin.tsx b/src/components/chat-components/plugins/FocusPlugin.tsx index 085b806e..3fae9c4e 100644 --- a/src/components/chat-components/plugins/FocusPlugin.tsx +++ b/src/components/chat-components/plugins/FocusPlugin.tsx @@ -1,5 +1,6 @@ import React from "react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; +import type { LexicalEditor } from "lexical"; /** * Props for the FocusPlugin component @@ -8,7 +9,7 @@ interface FocusPluginProps { /** Callback that receives a function to programmatically focus the editor */ onFocus: (focusFn: () => void) => void; /** Optional callback that receives the editor instance when ready */ - onEditorReady?: (editor: any) => void; + onEditorReady?: (editor: LexicalEditor) => void; } /** diff --git a/src/components/chat-components/plugins/FolderPillSyncPlugin.tsx b/src/components/chat-components/plugins/FolderPillSyncPlugin.tsx index 23970b1d..ba26fe4a 100644 --- a/src/components/chat-components/plugins/FolderPillSyncPlugin.tsx +++ b/src/components/chat-components/plugins/FolderPillSyncPlugin.tsx @@ -1,6 +1,7 @@ import React from "react"; import { $isFolderPillNode, FolderPillNode } from "../pills/FolderPillNode"; import { GenericPillSyncPlugin, PillSyncConfig } from "./GenericPillSyncPlugin"; +import type { LexicalNode } from "lexical"; /** * Props for the FolderPillSyncPlugin component @@ -17,7 +18,7 @@ interface FolderPillSyncPluginProps { */ const folderPillConfig: PillSyncConfig = { isPillNode: $isFolderPillNode, - extractData: (node: any) => (node as FolderPillNode).getFolderPath(), + extractData: (node: LexicalNode) => (node as FolderPillNode).getFolderPath(), }; /** diff --git a/src/components/chat-components/plugins/GenericPillSyncPlugin.tsx b/src/components/chat-components/plugins/GenericPillSyncPlugin.tsx index ae1720f1..20e30442 100644 --- a/src/components/chat-components/plugins/GenericPillSyncPlugin.tsx +++ b/src/components/chat-components/plugins/GenericPillSyncPlugin.tsx @@ -7,9 +7,9 @@ import { $getRoot, LexicalNode } from "lexical"; */ export interface PillSyncConfig { /** Function to check if a node is of this pill type */ - isPillNode: (node: any) => boolean; + isPillNode: (node: LexicalNode) => boolean; /** Function to extract data from the pill node */ - extractData: (node: any) => T; + extractData: (node: LexicalNode) => T; /** Function to create a unique identity key for deduplication and removal detection */ getKey?: (item: T) => string; /** diff --git a/src/components/chat-components/plugins/NotePillSyncPlugin.tsx b/src/components/chat-components/plugins/NotePillSyncPlugin.tsx index 1d5d250c..03871350 100644 --- a/src/components/chat-components/plugins/NotePillSyncPlugin.tsx +++ b/src/components/chat-components/plugins/NotePillSyncPlugin.tsx @@ -22,10 +22,13 @@ type NoteData = { path: string; basename: string }; */ const notePillConfig: PillSyncConfig = { isPillNode: $isNotePillNode, - extractData: (node: { getNotePath: () => string; getNoteTitle: () => string }) => ({ - path: node.getNotePath(), - basename: node.getNoteTitle(), - }), + extractData: (node) => { + const noteNode = node as unknown as { getNotePath: () => string; getNoteTitle: () => string }; + return { + path: noteNode.getNotePath(), + basename: noteNode.getNoteTitle(), + }; + }, getKey: (note: NoteData) => note.path, // Use path as unique key }; diff --git a/src/components/chat-components/plugins/PillDeletionPlugin.tsx b/src/components/chat-components/plugins/PillDeletionPlugin.tsx index 2219ad3e..ed6ceb13 100644 --- a/src/components/chat-components/plugins/PillDeletionPlugin.tsx +++ b/src/components/chat-components/plugins/PillDeletionPlugin.tsx @@ -7,6 +7,7 @@ import { COMMAND_PRIORITY_CRITICAL, $isElementNode, DecoratorNode, + LexicalNode, } from "lexical"; /** @@ -19,7 +20,7 @@ export interface IPillNode { /** * Check if a node is a pill-like node (any DecoratorNode that implements IPillNode) */ -function $isPillNode(node: any): node is DecoratorNode & IPillNode { +function $isPillNode(node: LexicalNode): node is DecoratorNode & IPillNode { // Check if it's a DecoratorNode (all pills extend DecoratorNode) if (!(node instanceof DecoratorNode)) { return false; @@ -82,7 +83,7 @@ export function PillDeletionPlugin(): null { // Currently disabled since typeahead adds spaces after pills if (isBackward && anchor.offset === 0) { const previousSibling = anchorNode.getPreviousSibling(); - if ($isPillNode(previousSibling)) { + if (previousSibling && $isPillNode(previousSibling)) { previousSibling.remove(); handled = true; return; diff --git a/src/components/chat-components/plugins/ToolPillSyncPlugin.tsx b/src/components/chat-components/plugins/ToolPillSyncPlugin.tsx index a88ff549..d2acac61 100644 --- a/src/components/chat-components/plugins/ToolPillSyncPlugin.tsx +++ b/src/components/chat-components/plugins/ToolPillSyncPlugin.tsx @@ -1,6 +1,7 @@ import React from "react"; import { $isToolPillNode, ToolPillNode } from "../pills/ToolPillNode"; import { GenericPillSyncPlugin, PillSyncConfig } from "./GenericPillSyncPlugin"; +import type { LexicalNode } from "lexical"; /** * Props for the ToolPillSyncPlugin component @@ -17,7 +18,7 @@ interface ToolPillSyncPluginProps { */ const toolPillConfig: PillSyncConfig = { isPillNode: $isToolPillNode, - extractData: (node: any) => (node as ToolPillNode).getToolName(), + extractData: (node: LexicalNode) => (node as ToolPillNode).getToolName(), }; /** diff --git a/src/components/chat-components/plugins/URLPillSyncPlugin.tsx b/src/components/chat-components/plugins/URLPillSyncPlugin.tsx index 6adfbdbe..6ffb4de0 100644 --- a/src/components/chat-components/plugins/URLPillSyncPlugin.tsx +++ b/src/components/chat-components/plugins/URLPillSyncPlugin.tsx @@ -1,6 +1,7 @@ import React from "react"; import { $isURLPillNode, URLPillNode } from "../pills/URLPillNode"; import { GenericPillSyncPlugin, PillSyncConfig } from "./GenericPillSyncPlugin"; +import type { LexicalNode } from "lexical"; /** * Props for the URLPillSyncPlugin component @@ -17,7 +18,7 @@ interface URLPillSyncPluginProps { */ const urlPillConfig: PillSyncConfig = { isPillNode: $isURLPillNode, - extractData: (node: any) => (node as URLPillNode).getURL(), + extractData: (node: LexicalNode) => (node as URLPillNode).getURL(), }; /** diff --git a/src/components/chat-components/utils/lexicalTextUtils.test.ts b/src/components/chat-components/utils/lexicalTextUtils.test.ts index bed8fe69..f48f4130 100644 --- a/src/components/chat-components/utils/lexicalTextUtils.test.ts +++ b/src/components/chat-components/utils/lexicalTextUtils.test.ts @@ -70,7 +70,7 @@ describe("parseTextForPills", () => { }); // Mock TFile constructor to set properties - MockTFile.mockImplementation(function (this: any) { + MockTFile.mockImplementation(function (this: Record) { this.basename = "Valid Note"; this.path = "Valid Note.md"; }); @@ -279,7 +279,7 @@ describe("parseTextForPills", () => { return null; }); - MockTFile.mockImplementation(function (this: any) { + MockTFile.mockImplementation(function (this: Record) { this.basename = "Test Note"; this.path = "Test Note.md"; }); diff --git a/src/components/modals/SourcesModal.tsx b/src/components/modals/SourcesModal.tsx index 888e2012..9139858e 100644 --- a/src/components/modals/SourcesModal.tsx +++ b/src/components/modals/SourcesModal.tsx @@ -3,11 +3,11 @@ import { logError } from "@/logger"; import { App, Modal, Setting, TFile } from "obsidian"; export class SourcesModal extends Modal { - sources: { title: string; path: string; score: number; explanation?: any }[]; + sources: { title: string; path: string; score: number; explanation?: unknown }[]; constructor( app: App, - sources: { title: string; path: string; score: number; explanation?: any }[] + sources: { title: string; path: string; score: number; explanation?: unknown }[] ) { super(app); this.sources = sources; @@ -24,7 +24,7 @@ export class SourcesModal extends Modal { private createSourceList( container: HTMLElement, - sources: { title: string; path: string; score: number; explanation?: any }[] + sources: { title: string; path: string; score: number; explanation?: unknown }[] ) { const list = container.createEl("ul"); list.addClass("tw-list-none", "tw-p-0"); @@ -58,7 +58,14 @@ export class SourcesModal extends Modal { const filePath = source.path || source.title; const file = this.app.vault.getAbstractFileByPath(filePath); if (file instanceof TFile) { - const dragManager = (this.app as any).dragManager; + const dragManager = ( + this.app as unknown as { + dragManager?: { + dragLink: (e: DragEvent, text: string) => unknown; + onDragStart: (e: DragEvent, data: unknown) => void; + }; + } + ).dragManager; if (!dragManager) return; const linkText = this.app.metadataCache.fileToLinktext(file, ""); const dragData = dragManager.dragLink(e, linkText); @@ -99,7 +106,7 @@ export class SourcesModal extends Modal { }); } - private addExplanation(container: HTMLElement, explanation: any): HTMLElement { + private addExplanation(container: HTMLElement, explanation: unknown): HTMLElement { const explanationDiv = container.createDiv({ cls: "search-explanation" }); explanationDiv.addClass( "tw-ml-[2.5em]", @@ -111,11 +118,13 @@ export class SourcesModal extends Modal { "tw-border-l-border" ); + // Cast to a typed record for safe property access + const exp = explanation as Record; const details: string[] = []; // Add lexical matches - if (explanation.lexicalMatches && explanation.lexicalMatches.length > 0) { - const lexicalMatches = explanation.lexicalMatches as { field: string; query: string }[]; + if (exp.lexicalMatches && (exp.lexicalMatches as unknown[]).length > 0) { + const lexicalMatches = exp.lexicalMatches as { field: string; query: string }[]; const fields = new Set(lexicalMatches.map((m) => m.field)); const queries = new Set(lexicalMatches.map((m) => m.query)); details.push( @@ -124,21 +133,27 @@ export class SourcesModal extends Modal { } // Add semantic score - if (explanation.semanticScore !== undefined && explanation.semanticScore > 0) { - details.push(`Semantic: ${(explanation.semanticScore * 100).toFixed(1)}% similarity`); + if (exp.semanticScore !== undefined && (exp.semanticScore as number) > 0) { + details.push(`Semantic: ${((exp.semanticScore as number) * 100).toFixed(1)}% similarity`); } // Add folder boost - if (explanation.folderBoost) { + if (exp.folderBoost) { + const fb = exp.folderBoost as { boostFactor: number; documentCount: number; folder?: string }; details.push( - `Folder boost: ${explanation.folderBoost.boostFactor.toFixed(2)}x (${explanation.folderBoost.documentCount} docs in ${explanation.folderBoost.folder || "root"})` + `Folder boost: ${fb.boostFactor.toFixed(2)}x (${fb.documentCount} docs in ${fb.folder || "root"})` ); } // Add graph connections (new query-aware boost) - if (explanation.graphConnections) { - const gc = explanation.graphConnections; - const connectionParts = []; + if (exp.graphConnections) { + const gc = exp.graphConnections as { + backlinks: number; + coCitations: number; + sharedTags: number; + score: number; + }; + const connectionParts: string[] = []; if (gc.backlinks > 0) connectionParts.push(`${gc.backlinks} backlinks`); if (gc.coCitations > 0) connectionParts.push(`${gc.coCitations} co-citations`); if (gc.sharedTags > 0) connectionParts.push(`${gc.sharedTags} shared tags`); @@ -151,16 +166,15 @@ export class SourcesModal extends Modal { } // Add old graph boost (if still present for backwards compatibility) - if (explanation.graphBoost && !explanation.graphConnections) { - details.push( - `Graph boost: ${explanation.graphBoost.boostFactor.toFixed(2)}x (${explanation.graphBoost.connections} connections)` - ); + if (exp.graphBoost && !exp.graphConnections) { + const gb = exp.graphBoost as { boostFactor: number; connections: number }; + details.push(`Graph boost: ${gb.boostFactor.toFixed(2)}x (${gb.connections} connections)`); } // Add base vs final score if boosted - if (explanation.baseScore !== explanation.finalScore) { + if (exp.baseScore !== exp.finalScore) { details.push( - `Score: ${explanation.baseScore.toFixed(4)} → ${explanation.finalScore.toFixed(4)}` + `Score: ${(exp.baseScore as number).toFixed(4)} → ${(exp.finalScore as number).toFixed(4)}` ); } diff --git a/src/components/modals/project/AddProjectModal.tsx b/src/components/modals/project/AddProjectModal.tsx index 0d09a276..94e94dc2 100644 --- a/src/components/modals/project/AddProjectModal.tsx +++ b/src/components/modals/project/AddProjectModal.tsx @@ -117,7 +117,7 @@ function AddProjectModalContent({ const handleInputChange = ( field: string, - value: string | number | string[] | Record + value: string | number | string[] | Record ) => { setFormData((prev) => { if (typeof value === "string") { @@ -126,7 +126,7 @@ function AddProjectModalContent({ } } if (Array.isArray(value) && value.every((item) => typeof item === "string")) { - value = (value as string[]).map((item) => item.trim()).filter(Boolean); + value = value.map((item) => item.trim()).filter(Boolean); } if (field.includes(".")) { diff --git a/src/components/modals/project/context-manage-modal.tsx b/src/components/modals/project/context-manage-modal.tsx index e310e0d2..b39cb609 100644 --- a/src/components/modals/project/context-manage-modal.tsx +++ b/src/components/modals/project/context-manage-modal.tsx @@ -64,7 +64,7 @@ type ActiveSection = "tags" | "folders" | "files" | "extensions" | "ignoreFiles" type ActiveItem = string | null; interface SectionHeaderProps { - IconComponent: React.ComponentType; + IconComponent: React.ComponentType<{ className?: string }>; title: string; iconColorClassName: string; onAddClick: () => void; @@ -112,7 +112,7 @@ interface SectionItem { interface SectionListProps { title: string; - IconComponent: React.ComponentType; + IconComponent: React.ComponentType<{ className?: string }>; iconColorClassName: string; items: SectionItem[]; activeItem: string | null; diff --git a/src/components/ui/ModelParametersEditor.tsx b/src/components/ui/ModelParametersEditor.tsx index a4629164..712f6b63 100644 --- a/src/components/ui/ModelParametersEditor.tsx +++ b/src/components/ui/ModelParametersEditor.tsx @@ -31,7 +31,7 @@ const PARAM_RANGES = { interface ModelParametersEditorProps { model: CustomModel; settings: CopilotSettings; - onChange: (field: keyof CustomModel, value: any) => void; + onChange: (field: keyof CustomModel, value: CustomModel[keyof CustomModel]) => void; onReset?: (field: keyof CustomModel) => void; showTokenLimit?: boolean; // Whether to show Token limit, defaults to true } diff --git a/src/components/ui/mobile-card.tsx b/src/components/ui/mobile-card.tsx index 653b1274..d60430fe 100644 --- a/src/components/ui/mobile-card.tsx +++ b/src/components/ui/mobile-card.tsx @@ -12,7 +12,7 @@ import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { ChevronDown, ChevronRight, GripVertical, MoreVertical } from "lucide-react"; -export interface MobileCardDropdownAction { +export interface MobileCardDropdownAction { icon: React.ReactNode; label: string; onClick: (item: T) => void | Promise; diff --git a/src/context/ChatHistoryCompactor.test.ts b/src/context/ChatHistoryCompactor.test.ts index 404fd460..bea66a32 100644 --- a/src/context/ChatHistoryCompactor.test.ts +++ b/src/context/ChatHistoryCompactor.test.ts @@ -65,7 +65,7 @@ Here's a summary.`; const result = compactAssistantOutput(output, { verbatimThreshold: 1000 }); expect(Array.isArray(result)).toBe(true); - const resultArray = result as any[]; + const resultArray = result as Array<{ text: string; type: string }>; expect(resultArray[0].text.length).toBeLessThan(textItem.text.length); expect(resultArray[1]).toEqual(imageItem); // Image unchanged }); diff --git a/src/context/L2ContextCompactor.test.ts b/src/context/L2ContextCompactor.test.ts index f744ad78..d48d3803 100644 --- a/src/context/L2ContextCompactor.test.ts +++ b/src/context/L2ContextCompactor.test.ts @@ -631,7 +631,7 @@ Here's what I found in the note.`; const result = compactChatHistoryContent(content, { verbatimThreshold: 1000 }); expect(Array.isArray(result)).toBe(true); - const resultArray = result as any[]; + const resultArray = result as Array<{ type: string; text: string }>; expect(resultArray[0].type).toBe("text"); expect(resultArray[0].text.length).toBeLessThan(originalText.length); expect(resultArray[1]).toEqual(content[1]); // Image unchanged diff --git a/src/contextProcessor.dataview.test.ts b/src/contextProcessor.dataview.test.ts index 5f3d9d1f..d95a5760 100644 --- a/src/contextProcessor.dataview.test.ts +++ b/src/contextProcessor.dataview.test.ts @@ -11,6 +11,17 @@ jest.mock("@/chainFactory", () => ({ import { ContextProcessor } from "@/contextProcessor"; import { DATAVIEW_BLOCK_TAG } from "@/constants"; +// Typed helper to access plugins on the mocked global app +function getPlugins(): Record { + return (window.app as unknown as { plugins: { plugins: Record } }).plugins + .plugins; +} + +function setPlugins(plugins: Record): void { + (window.app as unknown as { plugins: { plugins: Record } }).plugins.plugins = + plugins; +} + // Mock the global app object for Dataview plugin access window.app = { plugins: { @@ -25,7 +36,7 @@ describe("ContextProcessor - Dataview Integration", () => { beforeEach(() => { contextProcessor = ContextProcessor.getInstance(); // Reset plugins for each test - (window.app as any).plugins.plugins = {}; + setPlugins({}); // Save and mock console.error to suppress expected error messages originalConsoleError = console.error; console.error = jest.fn(); @@ -44,7 +55,7 @@ describe("ContextProcessor - Dataview Integration", () => { }); it("should return content unchanged when Dataview API is not available", async () => { - (window.app as any).plugins.plugins.dataview = {}; + getPlugins().dataview = {}; const content = "```dataview\nLIST\n```"; const result = await contextProcessor.processDataviewBlocks(content, "test.md"); expect(result).toBe(content); @@ -54,7 +65,7 @@ describe("ContextProcessor - Dataview Integration", () => { describe("processDataviewBlocks - Regex Pattern Matching", () => { beforeEach(() => { // Mock successful Dataview plugin with API - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockResolvedValue({ successful: true, @@ -105,7 +116,7 @@ describe("ContextProcessor - Dataview Integration", () => { describe("processDataviewBlocks - Multiple Blocks", () => { beforeEach(() => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest .fn() @@ -172,7 +183,7 @@ LIST describe("processDataviewBlocks - Query Execution", () => { it("should include both original query and executed results", async () => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockResolvedValue({ successful: true, @@ -194,7 +205,7 @@ LIST }); it("should handle query timeout gracefully", async () => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockImplementation( () => @@ -213,7 +224,7 @@ LIST }, 10000); // Increase test timeout to 10s it("should handle query execution errors", async () => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockResolvedValue({ successful: false, @@ -229,7 +240,7 @@ LIST }); it("should handle dataviewjs with unsupported message", async () => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn(), }, @@ -244,7 +255,7 @@ LIST describe("processDataviewBlocks - Result Formatting", () => { it("should format LIST results correctly", async () => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockResolvedValue({ successful: true, @@ -265,7 +276,7 @@ LIST }); it("should format TABLE results correctly", async () => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockResolvedValue({ successful: true, @@ -291,7 +302,7 @@ LIST }); it("should format TASK results correctly", async () => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockResolvedValue({ successful: true, @@ -314,7 +325,7 @@ LIST }); it("should handle empty results", async () => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockResolvedValue({ successful: true, @@ -333,7 +344,7 @@ LIST }); it("should handle null values gracefully", async () => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockResolvedValue({ successful: true, @@ -354,7 +365,7 @@ LIST }); it("should handle arrays in values", async () => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockResolvedValue({ successful: true, @@ -375,7 +386,7 @@ LIST describe("processDataviewBlocks - Edge Cases", () => { beforeEach(() => { - (window.app as any).plugins.plugins.dataview = { + getPlugins().dataview = { api: { query: jest.fn().mockResolvedValue({ successful: true, diff --git a/src/contextProcessor.embeds.test.ts b/src/contextProcessor.embeds.test.ts index 4ee9387d..f215c244 100644 --- a/src/contextProcessor.embeds.test.ts +++ b/src/contextProcessor.embeds.test.ts @@ -21,7 +21,7 @@ const createMockFile = (path: string): TFile => describe("ContextProcessor - Embedded Notes", () => { let contextProcessor: ContextProcessor; let vault: Vault; - let fileParserManager: any; + let fileParserManager: unknown; let fileCaches: FileCacheMap; let fileContents: FileContentMap; let fileIndex: Map; @@ -41,7 +41,7 @@ describe("ContextProcessor - Embedded Notes", () => { getFileCache: jest.fn((file: TFile) => fileCaches[file.path] ?? {}), }; - (window as any).app = { + (window as unknown as Record).app = { metadataCache: metadataCacheMock, }; @@ -51,7 +51,7 @@ describe("ContextProcessor - Embedded Notes", () => { }, } as unknown as Vault; - (vault as any).getAbstractFileByPath = jest.fn(); + (vault as Vault & Record).getAbstractFileByPath = jest.fn(); fileParserManager = { supportsExtension: jest.fn( diff --git a/src/contextProcessor.ts b/src/contextProcessor.ts index 2fc873a3..6a375411 100644 --- a/src/contextProcessor.ts +++ b/src/contextProcessor.ts @@ -21,6 +21,31 @@ import { YOUTUBE_VIDEO_CONTEXT_TAG, } from "./constants"; +/** Minimal typing for the Dataview plugin API (third-party plugin, no official types). */ +interface DataviewApi { + query( + query: string, + sourcePath: string + ): Promise<{ successful: boolean; error?: string; value: DataviewResult }>; +} + +interface DataviewResult { + type: string; + values: DataviewRow[]; + headers?: string[]; +} + +type DataviewRow = unknown[] | DataviewTaskItem | DataviewLinkLike; + +interface DataviewTaskItem { + completed: boolean; + text?: string; +} + +interface DataviewLinkLike { + path: string; +} + interface EmbeddedLinkTarget { path: string | null; heading?: string; @@ -80,7 +105,9 @@ export class ContextProcessor { */ async processDataviewBlocks(content: string, sourcePath: string): Promise { // Check if Dataview plugin is available - const dataviewPlugin = (app as any).plugins?.plugins?.dataview; + const dataviewPlugin = ( + app as unknown as { plugins?: { plugins?: { dataview?: { api?: DataviewApi } } } } + ).plugins?.plugins?.dataview; if (!dataviewPlugin) { return content; // Dataview not installed, return content as-is } @@ -132,7 +159,7 @@ export class ContextProcessor { * Execute a Dataview query and format the results */ private async executeDataviewQuery( - dataviewApi: any, + dataviewApi: DataviewApi, query: string, queryType: string, sourcePath: string @@ -146,7 +173,7 @@ export class ContextProcessor { const result = await dataviewApi.query(query, sourcePath); if (!result.successful) { - throw new Error((result.error as string) || "Query failed"); + throw new Error(result.error ?? "Query failed"); } // Format results based on type @@ -156,29 +183,29 @@ export class ContextProcessor { /** * Format Dataview query results into readable text */ - private formatDataviewResult(result: any): string { + private formatDataviewResult(result: DataviewResult): string { if (!result) { return "No results"; } // Handle different result types if (result.type === "list") { - return this.formatDataviewList(result.values as any[]); + return this.formatDataviewList(result.values); } else if (result.type === "table") { - return this.formatDataviewTable(result.headers as string[], result.values as any[][]); + return this.formatDataviewTable(result.headers ?? [], result.values as unknown[][]); } else if (result.type === "task") { - return this.formatDataviewTasks(result.values as any[]); + return this.formatDataviewTasks(result.values as DataviewTaskItem[]); } else if (Array.isArray(result)) { - return result.map((item) => this.formatDataviewValue(item)).join("\n"); + return (result as unknown[]).map((item) => this.formatDataviewValue(item)).join("\n"); } - return String(result); + return JSON.stringify(result); } /** * Format Dataview list results */ - private formatDataviewList(values: any[]): string { + private formatDataviewList(values: DataviewRow[]): string { if (!values || values.length === 0) { return "No results"; } @@ -188,7 +215,7 @@ export class ContextProcessor { /** * Format Dataview table results */ - private formatDataviewTable(headers: string[], rows: any[][]): string { + private formatDataviewTable(headers: string[], rows: unknown[][]): string { if (!rows || rows.length === 0) { return "No results"; } @@ -207,14 +234,14 @@ export class ContextProcessor { /** * Format Dataview task results */ - private formatDataviewTasks(tasks: any[]): string { + private formatDataviewTasks(tasks: DataviewTaskItem[]): string { if (!tasks || tasks.length === 0) { return "No results"; } return tasks .map((task) => { const checkbox = task.completed ? "[x]" : "[ ]"; - return `- ${checkbox} ${this.formatDataviewValue(task.text || task)}`; + return `- ${checkbox} ${this.formatDataviewValue(task.text ?? task)}`; }) .join("\n"); } @@ -222,22 +249,27 @@ export class ContextProcessor { /** * Format individual Dataview values */ - private formatDataviewValue(value: any): string { + private formatDataviewValue(value: unknown): string { if (value === null || value === undefined) { return ""; } // Handle links - if (value && typeof value === "object" && value.path) { - return `[[${value.path}]]`; + if (value && typeof value === "object" && "path" in value) { + return `[[${(value as DataviewLinkLike).path}]]`; } // Handle arrays if (Array.isArray(value)) { - return value.map((v) => this.formatDataviewValue(v)).join(", "); + return (value as unknown[]).map((v) => this.formatDataviewValue(v)).join(", "); } - return String(value); + if (typeof value === "object") { + return JSON.stringify(value); + } + + // value is a primitive (string, number, boolean, bigint) at this point + return `${value as string | number | boolean | bigint}`; } /** diff --git a/src/core/ChatManager.ts b/src/core/ChatManager.ts index 6039056a..0703ed87 100644 --- a/src/core/ChatManager.ts +++ b/src/core/ChatManager.ts @@ -18,6 +18,7 @@ import { ContextManager } from "./ContextManager"; import { MessageRepository } from "./MessageRepository"; import { ChatPersistenceManager } from "./ChatPersistenceManager"; import { ACTIVE_WEB_TAB_MARKER, USER_SENDER } from "@/constants"; +import { MessageContent } from "@/imageProcessing/imageProcessor"; import { TFile, Vault } from "obsidian"; import { getWebViewerService } from "@/services/webViewerService/webViewerServiceSingleton"; import { @@ -416,7 +417,7 @@ export class ChatManager { chainType: ChainType, includeActiveNote: boolean = false, includeActiveWebTab: boolean = false, - content?: any[], + content?: MessageContent[], updateLoadingMessage?: (message: string) => void ): Promise { try { diff --git a/src/core/ChatPersistenceManager.ts b/src/core/ChatPersistenceManager.ts index 4567cd52..f8319600 100644 --- a/src/core/ChatPersistenceManager.ts +++ b/src/core/ChatPersistenceManager.ts @@ -6,7 +6,7 @@ import { logError, logInfo, logWarn } from "@/logger"; import { sanitizeVaultPathSegment } from "@/projects/projectUtils"; import { filterChatHistoryFiles, readChatPathProjectId } from "@/utils/chatHistoryUtils"; import { getSettings } from "@/settings/model"; -import { ChatMessage } from "@/types/message"; +import { ChatMessage, MessageContext } from "@/types/message"; import { ensureFolderExists, extractTextFromChunk, @@ -419,7 +419,7 @@ export class ChatPersistenceManager { const contentLines = fullContent.split("\n"); let messageText = fullContent; let timestamp = "Unknown time"; - let contextInfo: any = undefined; + let contextInfo: MessageContext | undefined = undefined; // Check for context and timestamp lines let endIndex = contentLines.length; @@ -492,8 +492,8 @@ export class ChatPersistenceManager { /** * Parse context string back into context object */ - private parseContextString(contextStr: string): any { - const context: any = { + private parseContextString(contextStr: string): MessageContext | undefined { + const context: MessageContext = { notes: [], urls: [], tags: [], @@ -581,9 +581,9 @@ export class ChatPersistenceManager { if ( context.notes.length > 0 || context.urls.length > 0 || - context.tags.length > 0 || - context.folders.length > 0 || - context.webTabs.length > 0 + (context.tags?.length ?? 0) > 0 || + (context.folders?.length ?? 0) > 0 || + (context.webTabs?.length ?? 0) > 0 ) { return context; } diff --git a/src/core/MessageRepository.test.ts b/src/core/MessageRepository.test.ts index dc31d657..4d3b578e 100644 --- a/src/core/MessageRepository.test.ts +++ b/src/core/MessageRepository.test.ts @@ -99,7 +99,7 @@ describe("MessageRepository", () => { messageRepo.addMessage("Hello", "Hello", "user"); // Make message invisible by directly accessing internal array - const internalMessages = (messageRepo as any).messages as StoredMessage[]; + const internalMessages = (messageRepo as unknown as { messages: StoredMessage[] }).messages; internalMessages[0].isVisible = false; const messages = messageRepo.getDisplayMessages(); diff --git a/src/core/MessageRepository.ts b/src/core/MessageRepository.ts index 1f857f17..8a3a3f5b 100644 --- a/src/core/MessageRepository.ts +++ b/src/core/MessageRepository.ts @@ -1,4 +1,5 @@ import { PromptContextEnvelope } from "@/context/PromptContextTypes"; +import { MessageContent } from "@/imageProcessing/imageProcessor"; import { formatDateTime } from "@/utils"; import { ChatMessage, MessageContext, NewChatMessage, StoredMessage } from "@/types/message"; import { logInfo } from "@/logger"; @@ -34,14 +35,14 @@ export class MessageRepository { processedText: string, sender: string, context?: MessageContext, - content?: any[] + content?: MessageContent[] ): string; addMessage( messageOrDisplayText: NewChatMessage | string, processedText?: string, sender?: string, context?: MessageContext, - content?: any[] + content?: MessageContent[] ): string { // If first parameter is a ChatMessage object if (typeof messageOrDisplayText === "object") { diff --git a/src/errorFormat.ts b/src/errorFormat.ts index 11b188ed..fe17815d 100644 --- a/src/errorFormat.ts +++ b/src/errorFormat.ts @@ -2,7 +2,14 @@ export function err2String(err: unknown, stack = false): string { try { if (err instanceof Error) { const cause = (err as { cause?: unknown }).cause; - const causeMsg = cause instanceof Error ? cause.message : cause ? String(cause as any) : ""; + const causeMsg = + cause instanceof Error + ? cause.message + : cause != null + ? typeof cause === "string" + ? cause + : (JSON.stringify(cause) ?? "") + : ""; const stackStr = stack && err.stack ? err.stack : ""; const parts = [err.message]; if (causeMsg) parts.push(`more message: ${causeMsg}`); diff --git a/src/hooks/use-streaming-chat-session.ts b/src/hooks/use-streaming-chat-session.ts index 3cecf10b..0bb28427 100644 --- a/src/hooks/use-streaming-chat-session.ts +++ b/src/hooks/use-streaming-chat-session.ts @@ -311,7 +311,7 @@ export function useStreamingChatSession( const stream = await chainWithSignal.stream({ input: prompt }); for await (const chunk of stream) { - thinkStreamer.processChunk(chunk); + thinkStreamer.processChunk(chunk as Parameters[0]); if (abortController.signal.aborted) break; } } catch (error) { diff --git a/src/lib/plugins/colorOpacityPlugin.ts b/src/lib/plugins/colorOpacityPlugin.ts index 04789f42..715e33a0 100644 --- a/src/lib/plugins/colorOpacityPlugin.ts +++ b/src/lib/plugins/colorOpacityPlugin.ts @@ -1,4 +1,5 @@ import plugin from "tailwindcss/plugin"; +import type { CSSRuleObject } from "tailwindcss/types/config"; // Types interface ColorValue { @@ -25,7 +26,8 @@ const getPropertyPrefix = (property: ColorProperty): string => { // Utility Generator const generateUtility = - (e: any) => (property: ColorProperty, name: string, color: string, opacity: number) => { + (e: (className: string) => string) => + (property: ColorProperty, name: string, color: string, opacity: number) => { const prefix = getPropertyPrefix(property); const className = `${prefix}-${name}/${opacity}`; @@ -37,24 +39,26 @@ const generateUtility = }; // Color Processing -const generateAllUtilities = (e: any) => (color: string, name: string, opacity: number) => { - const properties: ColorProperty[] = ["background-color", "border-color", "color"]; - const utilities = properties.map((property) => - generateUtility(e)(property, name, color, opacity) - ); +const generateAllUtilities = + (e: (className: string) => string) => (color: string, name: string, opacity: number) => { + const properties: ColorProperty[] = ["background-color", "border-color", "color"]; + const utilities = properties.map((property) => + generateUtility(e)(property, name, color, opacity) + ); - return Object.assign({}, ...utilities) as Record; -}; + return Object.assign({}, ...utilities) as CSSRuleObject; + }; const generateOpacityClasses = - (e: any, opacityUtilities: Record) => (color: string, name: string) => { + (e: (className: string) => string, opacityUtilities: CSSRuleObject) => + (color: string, name: string) => { Array.from({ length: 10 }, (_, i) => (i + 1) * 10).forEach((opacity) => { Object.assign(opacityUtilities, generateAllUtilities(e)(color, name, opacity)); }); }; const processColorObject = - (e: any, opacityUtilities: Record) => + (e: (className: string) => string, opacityUtilities: CSSRuleObject) => (colorValue: string | ColorValue, baseName: string, parentPath: string[] = []) => { const currentPath = [...parentPath, baseName]; @@ -88,7 +92,7 @@ const processColorObject = */ export const colorOpacityPlugin = plugin((api) => { const { theme, e } = api; - const opacityUtilities: Record = {}; + const opacityUtilities: CSSRuleObject = {}; // 处理所有颜色相关的主题配置 const processThemeColors = (themeKey: string, prefix?: string) => { diff --git a/src/main.ts b/src/main.ts index 4540ec5e..bebe6e6e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -912,7 +912,11 @@ export default class CopilotPlugin extends Plugin { await this.handleNewChat(); } - async customSearchDB(query: string, salientTerms: string[], textWeight: number): Promise { + async customSearchDB( + query: string, + salientTerms: string[], + textWeight: number + ): Promise<{ content: string; metadata: Record }[]> { const settings = getSettings(); // Run FilterRetriever for guaranteed title/tag matches diff --git a/src/memory/UserMemoryManager.test.ts b/src/memory/UserMemoryManager.test.ts index 9e12c953..1749a782 100644 --- a/src/memory/UserMemoryManager.test.ts +++ b/src/memory/UserMemoryManager.test.ts @@ -17,7 +17,7 @@ import { UserMemoryManager } from "./UserMemoryManager"; import { App, TFile, Vault } from "obsidian"; import { ChatMessage } from "@/types/message"; import { logError, logWarn } from "@/logger"; -import { getSettings } from "@/settings/model"; +import { CopilotSettings, getSettings } from "@/settings/model"; import { ensureFolderExists } from "@/utils"; import { BaseChatModel } from "@langchain/core/language_models/chat_models"; import { AIMessageChunk } from "@langchain/core/messages"; @@ -48,7 +48,7 @@ describe("UserMemoryManager", () => { let mockApp: jest.Mocked; let mockVault: jest.Mocked; let mockChatModel: jest.Mocked; - let mockSettings: any; + let mockSettings: Partial; beforeEach(() => { jest.clearAllMocks(); diff --git a/src/miyo/MiyoServiceDiscovery.ts b/src/miyo/MiyoServiceDiscovery.ts index 433d110d..d638c236 100644 --- a/src/miyo/MiyoServiceDiscovery.ts +++ b/src/miyo/MiyoServiceDiscovery.ts @@ -12,7 +12,7 @@ export interface MiyoServiceConfig { pid: number; } -type NodeRequire = (id: string) => any; +type NodeRequire = (id: string) => unknown; type ServiceConfigReadResult = MiyoServiceConfig | "missing" | null; /** diff --git a/src/plusUtils.ts b/src/plusUtils.ts index 826e5fd9..a2f64ba6 100644 --- a/src/plusUtils.ts +++ b/src/plusUtils.ts @@ -134,7 +134,9 @@ export function useIsPlusUser(): boolean | undefined { * Check if the user is a Plus user. * When self-host mode is valid, this returns true to allow offline usage. */ -export async function checkIsPlusUser(context?: Record): Promise { +export async function checkIsPlusUser( + context?: Record +): Promise { // Self-host mode with valid plan validation bypasses license check if (isSelfHostModeValid()) { return true; diff --git a/src/search/chunkedStorage.ts b/src/search/chunkedStorage.ts index 94958dd5..197894be 100644 --- a/src/search/chunkedStorage.ts +++ b/src/search/chunkedStorage.ts @@ -11,7 +11,7 @@ const LEGACY_INDEX_SUFFIX = ".json"; export interface ChunkMetadata { numPartitions: number; vectorLength: number; - schema: any; + schema: Record; lastModified: number; documentPartitions: Record; } @@ -54,10 +54,10 @@ export class ChunkedStorage { } private distributeDocumentsToPartitions( - documents: any[], + documents: Record[], numPartitions: number - ): Map { - const partitions = new Map(); + ): Map[]> { + const partitions = new Map[]>(); const documentPartitions: Record = {}; for (let i = 0; i < numPartitions; i++) { @@ -106,6 +106,7 @@ export class ChunkedStorage { } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is required here as it controls Orama's type inference async saveDatabase(db: Orama): Promise { try { const rawData: RawData = save(db); @@ -125,10 +126,10 @@ export class ChunkedStorage { } // NOTE: Orama RawData docs can be either an array or an object - const docsData = (rawData as any).docs?.docs; - const rawDocs: any[] = Array.isArray(docsData) - ? docsData - : Object.values((docsData as Record) || {}); + const docsData = (rawData as unknown as { docs?: { docs?: unknown } }).docs?.docs; + const rawDocs: Record[] = Array.isArray(docsData) + ? (docsData as Record[]) + : (Object.values((docsData as Record) || {}) as Record[]); if (getSettings().debug) { logInfo(`Starting save with ${rawDocs.length ?? 0} total documents`); @@ -161,7 +162,7 @@ export class ChunkedStorage { schema: db.schema, lastModified: Date.now(), documentPartitions: Object.fromEntries( - rawDocs.map((doc: any) => [ + rawDocs.map((doc) => [ doc.id, this.assignDocumentToPartition(String(doc.id), numPartitions), ]) @@ -174,7 +175,7 @@ export class ChunkedStorage { ...rawData, docs: { docs: {}, count: 0 }, index: { - ...(rawData as any).index, + ...(rawData as unknown as { index: Record }).index, vectorIndexes: undefined, }, }; @@ -186,13 +187,24 @@ export class ChunkedStorage { index: { vectorIndexes: { embedding: { - size: (rawData as any).index.vectorIndexes.embedding.size, + size: ( + rawData as unknown as { + index: { + vectorIndexes: { + embedding: { size: number; vectors: Record }; + }; + }; + } + ).index.vectorIndexes.embedding.size, vectors: Object.fromEntries( Object.entries( - (rawData as any).index.vectorIndexes.embedding.vectors as Record< - string, - unknown - > + ( + rawData as unknown as { + index: { + vectorIndexes: { embedding: { vectors: Record } }; + }; + } + ).index.vectorIndexes.embedding.vectors ).filter(([id]) => docs.some((doc) => doc.id === id)) ), }, @@ -234,6 +246,7 @@ export class ChunkedStorage { } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is required here as it controls Orama's type inference async loadDatabase(): Promise> { try { const legacyPath = this.getLegacyPath(); @@ -241,7 +254,7 @@ export class ChunkedStorage { // Try loading legacy format first if (await this.app.vault.adapter.exists(legacyPath)) { const legacyData = JSON.parse(await this.app.vault.adapter.read(legacyPath)) as RawData & { - schema?: any; + schema?: Record; }; if (!legacyData?.schema) { throw new CustomError("Invalid legacy database format"); @@ -294,14 +307,14 @@ export class ChunkedStorage { } // Create new docs object based on internalDocumentIDStore order - const orderedDocs: Record = {}; + const orderedDocs: Record = {}; let nextDocId = 1; for (const internalId of mergedData.internalDocumentIDStore.internalIdToId) { // Find document in any chunk const doc = allChunks .flatMap((chunk) => Object.values(chunk.docs.docs as Record)) - .find((doc: any) => doc.id === internalId); + .find((doc) => (doc as Record).id === internalId); if (doc) { orderedDocs[nextDocId.toString()] = doc; diff --git a/src/search/dbOperations.ts b/src/search/dbOperations.ts index 6caec67e..0dbd2a86 100644 --- a/src/search/dbOperations.ts +++ b/src/search/dbOperations.ts @@ -28,10 +28,11 @@ export interface OramaDocument { tags: string[]; extension: string; nchars: number; - metadata: Record; + metadata: Record; } export class DBOperations { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type private oramaDb: Orama | undefined; private chunkedStorage: ChunkedStorage | undefined; private isInitialized = false; @@ -84,6 +85,7 @@ export class DBOperations { this.isInitialized = true; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type async initializeDB(embeddingInstance: Embeddings | undefined): Promise | undefined> { try { if (!this.isInitialized) { @@ -209,6 +211,7 @@ export class DBOperations { } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type public getDb(): Orama | undefined { if (!this.oramaDb) { console.warn("Database not initialized. Some features may be limited."); @@ -267,6 +270,7 @@ export class DBOperations { this.hasUnsavedChanges = true; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type private async createNewDb(embeddingInstance: Embeddings | undefined): Promise> { if (!embeddingInstance) { throw new CustomError("Embedding instance not found."); @@ -298,17 +302,19 @@ export class DBOperations { return db; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type public static async getDocsByPath(db: Orama, path: string) { if (!db) throw new Error("DB not initialized"); if (!path) return; // Use getAllDocuments + JS filter for reliable path matching (handles Unicode/Chinese correctly) const allDocs = await DBOperations.getAllDocuments(db); - const filtered = allDocs.filter((doc: any) => doc.path === path); + const filtered = allDocs.filter((doc) => (doc as { path?: string }).path === path); // Return in same format as before (hits with document and score) - return filtered.map((doc: any) => ({ document: doc, score: 1 })); + return filtered.map((doc) => ({ document: doc, score: 1 })); } public static async getDocsByEmbedding( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type db: Orama, embedding: number[], options: { @@ -326,6 +332,7 @@ export class DBOperations { return result.hits; } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type public static async getLatestFileMtime(db: Orama | undefined): Promise { if (!db) throw new Error("DB not initialized"); @@ -396,14 +403,14 @@ export class DBOperations { try { await insert(db, docToSave as Parameters[1]); logInfo( - `${existingDoc.hits.length > 0 ? "Updated" : "Inserted"} document ${docToSave.id} in partition ${partition}` + `${existingDoc.hits.length > 0 ? "Updated" : "Inserted"} document ${String(docToSave.id)} in partition ${partition}` ); this.markUnsavedChanges(); return docToSave; } catch (insertErr) { logError( - `Failed to ${existingDoc.hits.length > 0 ? "update" : "insert"} document ${docToSave.id}:`, + `Failed to ${existingDoc.hits.length > 0 ? "update" : "insert"} document ${String(docToSave.id)}:`, insertErr ); // If we removed an existing document but failed to insert the new one, @@ -418,7 +425,7 @@ export class DBOperations { return undefined; } } catch (err) { - logError(`Error upserting document ${docToSave.id}:`, err); + logError(`Error upserting document ${String(docToSave.id)}:`, err); return undefined; } }); @@ -529,13 +536,14 @@ export class DBOperations { return false; } - public static async getAllDocuments(db: Orama): Promise { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type + public static async getAllDocuments(db: Orama): Promise { const result = await search(db, { term: "", limit: 100000, includeVectors: true, }); - return result.hits.map((hit) => hit.document); + return result.hits.map((hit) => hit.document as unknown as OramaDocument); } public async garbageCollect(): Promise { @@ -674,28 +682,17 @@ export class DBOperations { }); } - async getDocsJsonByPaths(paths: string[]): Promise> { + async getDocsJsonByPaths(paths: string[]): Promise> { if (!this.oramaDb) { throw new CustomError("Semantic index database not found."); } - const result: Record = {}; + const result: Record = {}; for (const path of paths) { const docs = await DBOperations.getDocsByPath(this.oramaDb, path); if (docs && docs.length > 0) { - result[path] = docs.map((hit) => ({ - id: hit.document.id, - title: hit.document.title, - path: hit.document.path, - content: hit.document.content, - metadata: hit.document.metadata, - embedding: hit.document.embedding, - embeddingModel: hit.document.embeddingModel, - tags: hit.document.tags, - extension: hit.document.extension, - nchars: hit.document.nchars, - })); + result[path] = docs.map((hit) => hit.document); } } diff --git a/src/search/findRelevantNotes.ts b/src/search/findRelevantNotes.ts index 16147465..39018aa0 100644 --- a/src/search/findRelevantNotes.ts +++ b/src/search/findRelevantNotes.ts @@ -35,6 +35,7 @@ function shouldUseMiyoForRelevantNotes(): boolean { * @param currentFilePath - The current file path. * @returns A map of the highest score hits for each note. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- InternalTypedDocument is required as Orama controls this type function getHighestScoreHits(hits: Result>[], currentFilePath: string) { const hitMap = new Map(); for (const hit of hits) { @@ -103,6 +104,7 @@ async function calculateSimilarityScoreFromOrama({ filePath, currentNoteEmbeddings, }: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type db: Orama; filePath: string; currentNoteEmbeddings: number[][]; diff --git a/src/search/hybridRetriever.ts b/src/search/hybridRetriever.ts index 40a975ec..47e34a8a 100644 --- a/src/search/hybridRetriever.ts +++ b/src/search/hybridRetriever.ts @@ -162,7 +162,7 @@ export class HybridRetriever extends BaseRetriever { const db = await VectorStoreManager.getInstance().getDb(); - const searchParams: any = { + const searchParams: Record = { similarity: this.options.minSimilarityScore, limit: this.options.maxK, includeVectors: true, @@ -239,7 +239,6 @@ export class HybridRetriever extends BaseRetriever { mtime: { between: [startTime, endTime] }, }; - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const timeIntervalResults = await search(db, searchParams); // Convert timeIntervalResults to Document objects @@ -277,7 +276,6 @@ export class HybridRetriever extends BaseRetriever { logInfo("Orama search params:\n", searchParams); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const searchResults = await search(db, searchParams); // Add null check and validation for search results diff --git a/src/search/indexBackend/OramaIndexBackend.ts b/src/search/indexBackend/OramaIndexBackend.ts index ad2046ee..c6f1608e 100644 --- a/src/search/indexBackend/OramaIndexBackend.ts +++ b/src/search/indexBackend/OramaIndexBackend.ts @@ -114,7 +114,7 @@ export class OramaIndexBackend implements SemanticIndexBackend { if (!hits) { return []; } - return hits.map((hit) => hit.document as SemanticIndexDocument); + return hits.map((hit) => hit.document); } /** @@ -197,6 +197,7 @@ export class OramaIndexBackend implements SemanticIndexBackend { /** * Return the underlying Orama database instance when available. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type public getDb(): Orama | undefined { return this.dbOps.getDb(); } diff --git a/src/search/indexOperations.ts b/src/search/indexOperations.ts index ddae0996..211b0bae 100644 --- a/src/search/indexOperations.ts +++ b/src/search/indexOperations.ts @@ -188,7 +188,7 @@ export class IndexOperations { // Skip documents with invalid embeddings if (!embedding || !Array.isArray(embedding) || embedding.length === 0) { logError( - `Invalid embedding for document ${chunk.fileInfo.path}: ${JSON.stringify(embedding)}` + `Invalid embedding for document ${String(chunk.fileInfo.path)}: ${JSON.stringify(embedding)}` ); this.indexBackend.markFileMissingEmbeddings(chunk.fileInfo.path as string); continue; @@ -196,28 +196,28 @@ export class IndexOperations { docsToUpsert.push({ doc: { - ...(chunk.fileInfo as SemanticIndexDocument), + ...(chunk.fileInfo as unknown as SemanticIndexDocument), id: this.getDocHash(chunk.content), content: chunk.content, embedding, created_at: Date.now(), nchars: chunk.content.length, }, - filePath: chunk.fileInfo.path, + filePath: chunk.fileInfo.path as string, }); } } else { for (const chunk of batch) { docsToUpsert.push({ doc: { - ...(chunk.fileInfo as SemanticIndexDocument), + ...(chunk.fileInfo as unknown as SemanticIndexDocument), id: this.getDocHash(chunk.content), content: chunk.content, embedding: [], created_at: Date.now(), nchars: chunk.content.length, }, - filePath: chunk.fileInfo.path, + filePath: chunk.fileInfo.path as string, }); } } @@ -262,7 +262,7 @@ export class IndexOperations { } } catch (err) { this.handleError(err, { - filePath: batch?.[0]?.fileInfo?.path, + filePath: batch?.[0]?.fileInfo?.path as string | undefined, errors, batch, }); @@ -320,10 +320,14 @@ export class IndexOperations { Array<{ content: string; chunkId: string; - fileInfo: any; + fileInfo: Record; }> > { - const allChunks: Array<{ content: string; chunkId: string; fileInfo: any }> = []; + const allChunks: Array<{ + content: string; + chunkId: string; + fileInfo: Record; + }> = []; // Process files in batches to bypass ChunkManager's 1000-file limit // The limit exists to protect memory during search queries, but indexing needs all files @@ -486,7 +490,7 @@ export class IndexOperations { } } - private isStringLengthError(error: any): boolean { + private isStringLengthError(error: unknown): boolean { if (!error) return false; // Check if it's a direct RangeError @@ -495,17 +499,22 @@ export class IndexOperations { } // Check the error message at any depth - const message: string = (error.message || error.toString()) as string; + const message: string = + error instanceof Error + ? error.message + : typeof error === "string" + ? error + : (JSON.stringify(error) ?? ""); const lowerMessage = message.toLowerCase(); return lowerMessage.includes("string length") || lowerMessage.includes("rangeerror"); } private handleError( - error: any, + error: unknown, context?: { filePath?: string; errors?: string[]; - batch?: Array<{ content: string; fileInfo: any }>; + batch?: Array<{ content: string; fileInfo: Record }>; } ): void { const filePath = context?.filePath; @@ -524,8 +533,8 @@ export class IndexOperations { hasFileInfo: !!context.batch[0].fileInfo, } : "No chunks in batch", - errorType: error?.constructor?.name, - errorMessage: error?.message, + errorType: error instanceof Error ? error.constructor?.name : typeof error, + errorMessage: error instanceof Error ? error.message : String(error), }); } else { console.error(`Error indexing file ${filePath}:`, error); @@ -548,8 +557,8 @@ export class IndexOperations { // as they spam the user on every file event when the DB is not yet loaded. } - private isRateLimitError(err: any): boolean { - return (err?.message?.includes?.("rate limit") as boolean) || false; + private isRateLimitError(err: unknown): boolean { + return (err instanceof Error && err.message.toLowerCase().includes("rate limit")) || false; } private finalizeIndexing(errors: string[]): void { @@ -613,7 +622,7 @@ export class IndexOperations { for (let i = 0; i < chunks.length; i++) { const chunk = chunks[i]; await this.indexBackend.upsert({ - ...(chunk.fileInfo as SemanticIndexDocument), + ...(chunk.fileInfo as unknown as SemanticIndexDocument), id: this.getDocHash(chunk.content), content: chunk.content, embedding: embeddings[i], @@ -624,7 +633,7 @@ export class IndexOperations { } else { for (const chunk of chunks) { await this.indexBackend.upsert({ - ...(chunk.fileInfo as SemanticIndexDocument), + ...(chunk.fileInfo as unknown as SemanticIndexDocument), id: this.getDocHash(chunk.content), content: chunk.content, embedding: [], diff --git a/src/search/v3/MergedSemanticRetriever.ts b/src/search/v3/MergedSemanticRetriever.ts index 94ba39f8..28f36b49 100644 --- a/src/search/v3/MergedSemanticRetriever.ts +++ b/src/search/v3/MergedSemanticRetriever.ts @@ -170,7 +170,7 @@ export class MergedSemanticRetriever extends BaseRetriever { * @returns Decorated Document instance */ private decorateDocument(doc: Document, source: SourceKind): Document { - const metadata: Record = { + const metadata: Record = { ...(doc.metadata ?? {}), source, }; @@ -203,7 +203,7 @@ export class MergedSemanticRetriever extends BaseRetriever { * @param metadata - Document metadata bag * @returns Numeric score or zero when unavailable */ - private extractBaseScore(metadata: Record): number { + private extractBaseScore(metadata: Record): number { const candidates = [metadata?.rerank_score, metadata?.score]; for (const value of candidates) { if (typeof value === "number" && !Number.isNaN(value)) { @@ -227,7 +227,7 @@ export class MergedSemanticRetriever extends BaseRetriever { * @param metadata - Document metadata bag * @returns True if tag matches were present in the explanation */ - private hasTagMatch(metadata: Record): boolean { + private hasTagMatch(metadata: Record): boolean { const explanation = metadata?.explanation as { lexicalMatches?: unknown } | undefined; if (!explanation) { return false; diff --git a/src/search/v3/QueryExpander.ts b/src/search/v3/QueryExpander.ts index 62c1a810..3ee973f5 100644 --- a/src/search/v3/QueryExpander.ts +++ b/src/search/v3/QueryExpander.ts @@ -123,7 +123,7 @@ Format: this.config.timeout, "Query expansion" ); - } catch (error: any) { + } catch (error: unknown) { if (error instanceof TimeoutError) { logInfo(`QueryExpander: Timeout reached for "${query}"`); return this.fallbackExpansion(query); @@ -189,12 +189,18 @@ Format: * @param response - The LLM response object or string * @returns The extracted text content or null if empty */ - private extractContent(response: any): string | null { + private extractContent(response: unknown): string | null { // Elegant extraction with nullish coalescing const typed = response as { content?: unknown; text?: unknown } | null | undefined; + const extracted = typed?.content ?? typed?.text ?? ""; return typeof response === "string" ? response - : String((typed?.content ?? typed?.text ?? "") as any).trim() || null; + : (typeof extracted === "string" + ? extracted + : typeof extracted === "number" + ? String(extracted) + : "" + ).trim() || null; } /** diff --git a/src/search/v3/SearchCore.test.ts b/src/search/v3/SearchCore.test.ts index 728fe50a..dccfea8e 100644 --- a/src/search/v3/SearchCore.test.ts +++ b/src/search/v3/SearchCore.test.ts @@ -73,7 +73,7 @@ jest.mock("./chunks", () => { import { SearchCore, selectDiverseTopK } from "./SearchCore"; describe("SearchCore retrieve", () => { - let mockApp: any; + let mockApp: unknown; beforeEach(() => { buildFromCandidatesMock.mockReset(); diff --git a/src/search/v3/TieredLexicalRetriever.ts b/src/search/v3/TieredLexicalRetriever.ts index c0e5550c..941d622a 100644 --- a/src/search/v3/TieredLexicalRetriever.ts +++ b/src/search/v3/TieredLexicalRetriever.ts @@ -137,7 +137,7 @@ export class TieredLexicalRetriever extends BaseRetriever { * Supports both chunk IDs (note_path#chunk_index) and note IDs. */ private async convertToDocuments( - searchResults: Array<{ id: string; score: number; engine?: string; explanation?: any }> + searchResults: Array<{ id: string; score: number; engine?: string; explanation?: unknown }> ): Promise { const documents: Document[] = []; @@ -155,7 +155,10 @@ export class TieredLexicalRetriever extends BaseRetriever { // Get chunk content (not full note content) // Prefer async getter to auto-regenerate on cache miss; fall back to sync for test mocks let chunkContent = ""; - const cm: any = this.chunkManager as any; + const cm = this.chunkManager as unknown as { + getChunkText?: (id: string) => Promise; + getChunkTextSync?: (id: string) => string | undefined; + }; if (typeof cm.getChunkText === "function") { chunkContent = await cm.getChunkText(result.id); } else if (typeof cm.getChunkTextSync === "function") { diff --git a/src/search/v3/chunks.test.ts b/src/search/v3/chunks.test.ts index 39d7dd82..0d4b40ee 100644 --- a/src/search/v3/chunks.test.ts +++ b/src/search/v3/chunks.test.ts @@ -12,7 +12,7 @@ jest.mock("obsidian", () => { } // Add instanceof compatibility - static [Symbol.hasInstance](instance: any): boolean { + static [Symbol.hasInstance](instance: unknown): boolean { return Boolean( instance && typeof instance === "object" && "path" in instance && "basename" in instance ); @@ -43,9 +43,19 @@ type ChunkManagerInternal = { const asChunkInternal = (m: ChunkManager): ChunkManagerInternal => m as unknown as ChunkManagerInternal; +interface MockApp { + vault: { + getAbstractFileByPath: jest.Mock; + cachedRead: jest.Mock; + }; + metadataCache: { + getFileCache: jest.Mock; + }; +} + describe("ChunkManager", () => { let chunkManager: ChunkManager; - let mockApp: any; + let mockApp: MockApp; // Cache mock files to ensure consistent mtime across calls let mockFileCache: Map; @@ -150,7 +160,7 @@ describe("ChunkManager", () => { }, }; - chunkManager = new ChunkManager(mockApp as App); + chunkManager = new ChunkManager(mockApp as unknown as App); }); afterEach(() => { @@ -498,7 +508,7 @@ describe("ChunkManager", () => { describe("cache behavior", () => { it("should evict cache when memory limit is exceeded", async () => { // Mock a manager with very small cache limit - const smallCacheManager = new ChunkManager(mockApp as App); + const smallCacheManager = new ChunkManager(mockApp as unknown as App); asChunkInternal(smallCacheManager).maxCacheBytes = 1000; // 1KB limit // Add multiple large documents @@ -724,7 +734,7 @@ describe("ChunkManager", () => { ]; for (const testCase of testCases) { - const manager = new ChunkManager(mockApp as App); + const manager = new ChunkManager(mockApp as unknown as App); const generatedId = asChunkInternal(manager).generateChunkId("test.md", testCase.index); expect(generatedId).toBe(`test.md#${testCase.expected}`); } diff --git a/src/search/v3/engines/FullTextEngine.test.ts b/src/search/v3/engines/FullTextEngine.test.ts index f645f356..64f79c81 100644 --- a/src/search/v3/engines/FullTextEngine.test.ts +++ b/src/search/v3/engines/FullTextEngine.test.ts @@ -232,9 +232,21 @@ type FullTextEngineInternal = { const asInternal = (e: FullTextEngine): FullTextEngineInternal => e as unknown as FullTextEngineInternal; +interface MockApp { + vault: { + getAbstractFileByPath: jest.Mock; + cachedRead: jest.Mock; + }; + metadataCache: { + getFileCache: jest.Mock; + resolvedLinks: Record>; + getBacklinksForFile: jest.Mock; + }; +} + describe("FullTextEngine", () => { let engine: FullTextEngine; - let mockApp: any; + let mockApp: MockApp; beforeEach(() => { // Mock metadata cache @@ -350,7 +362,7 @@ describe("FullTextEngine", () => { }, }; - engine = new FullTextEngine(mockApp as App); + engine = new FullTextEngine(mockApp as unknown as App); }); describe("tokenizeMixed", () => { diff --git a/src/search/vectorStoreManager.ts b/src/search/vectorStoreManager.ts index 3fea3766..6946c7c3 100644 --- a/src/search/vectorStoreManager.ts +++ b/src/search/vectorStoreManager.ts @@ -276,6 +276,7 @@ export default class VectorStoreManager { this.indexBackend.onunload(); } + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama is the correct API type public async getDb(): Promise> { await this.waitForInitialization(); const db = this.oramaBackend.getDb(); diff --git a/src/settings/providerModels.ts b/src/settings/providerModels.ts index 1b1f373e..cbe5a516 100644 --- a/src/settings/providerModels.ts +++ b/src/settings/providerModels.ts @@ -228,7 +228,7 @@ export interface GroqModel { owned_by: string; active?: boolean; context_window?: number; - public_apps?: any; + public_apps?: unknown; } // XAI response model definition @@ -515,30 +515,44 @@ export const providerAdapters: ProviderModelAdapters = { * Default model adapter - handles unknown provider or format model data * Attempts to detect common data structure patterns and extract relevant information */ +/** Coerce an unknown model field value to a string, falling back to a default. */ +const toStr = (val: unknown, fallback: string): string => + typeof val === "string" && val.length > 0 + ? val + : typeof val === "number" + ? String(val) + : fallback; + export const getDefaultModelAdapter = (provider: SettingKeyProviders) => { - return (data: any): StandardModel[] => { + return (data: Record): StandardModel[] => { // Try to detect common data structure patterns if (data.data && Array.isArray(data.data)) { - return (data.data as any[]).map( - (model: any): StandardModel => ({ - id: model.id || model.name || String(Math.random()), - name: model.name || model.id || model.display_name || "Unknown Model", + return (data.data as Record[]).map( + (model): StandardModel => ({ + id: toStr(model.id, "") || toStr(model.name, String(Math.random())), + name: + toStr(model.name, "") || + toStr(model.id, "") || + toStr(model.display_name, "Unknown Model"), provider: provider, }) ); } else if (data.models && Array.isArray(data.models)) { - return (data.models as any[]).map( - (model: any): StandardModel => ({ - id: model.id || model.name || String(Math.random()), - name: model.name || model.displayName || model.id || "Unknown Model", + return (data.models as Record[]).map( + (model): StandardModel => ({ + id: toStr(model.id, "") || toStr(model.name, String(Math.random())), + name: + toStr(model.name, "") || + toStr(model.displayName, "") || + toStr(model.id, "Unknown Model"), provider: provider, }) ); } else if (Array.isArray(data)) { - return data.map( - (model: any): StandardModel => ({ - id: model.id || model.name || String(Math.random()), - name: model.name || model.id || "Unknown Model", + return (data as Record[]).map( + (model): StandardModel => ({ + id: toStr(model.id, "") || toStr(model.name, String(Math.random())), + name: toStr(model.name, "") || toStr(model.id, "Unknown Model"), provider: provider, }) ); diff --git a/src/settings/v2/components/ModelEditDialog.tsx b/src/settings/v2/components/ModelEditDialog.tsx index 96977b7a..1a983fe5 100644 --- a/src/settings/v2/components/ModelEditDialog.tsx +++ b/src/settings/v2/components/ModelEditDialog.tsx @@ -66,7 +66,7 @@ export const ModelEditModalContent: React.FC = ({ // Function to update local state immediately const handleLocalUpdate = useCallback( - (field: keyof CustomModel, value: any) => { + (field: keyof CustomModel, value: CustomModel[keyof CustomModel]) => { setLocalModel((prevModel) => { const updatedModel = { ...prevModel, diff --git a/src/state/ChatUIState.ts b/src/state/ChatUIState.ts index 6419d1b2..7ddba06c 100644 --- a/src/state/ChatUIState.ts +++ b/src/state/ChatUIState.ts @@ -1,6 +1,7 @@ import { ChainType } from "@/chainType"; import { logInfo } from "@/logger"; import { ChatManager } from "@/core/ChatManager"; +import { MessageContent } from "@/imageProcessing/imageProcessor"; import { ChatMessage, MessageContext } from "@/types/message"; import { TFile } from "obsidian"; @@ -63,7 +64,7 @@ export class ChatUIState { chainType: ChainType, includeActiveNote: boolean = false, includeActiveWebTab: boolean = false, - content?: any[], + content?: MessageContent[], updateLoadingMessage?: (message: string) => void ): Promise { const messageId = await this.chatManager.sendMessage( diff --git a/src/system-prompts/systemPromptManager.test.ts b/src/system-prompts/systemPromptManager.test.ts index f5c086dd..03d7326d 100644 --- a/src/system-prompts/systemPromptManager.test.ts +++ b/src/system-prompts/systemPromptManager.test.ts @@ -52,7 +52,7 @@ describe("SystemPromptManager", () => { beforeEach(() => { // Reset the singleton instance before each test - (SystemPromptManager as any).instance = undefined; + (SystemPromptManager as unknown as Record).instance = undefined; // Clear all mocks jest.clearAllMocks(); @@ -92,7 +92,7 @@ describe("SystemPromptManager", () => { }); it("throws error if vault not provided on first call", () => { - (SystemPromptManager as any).instance = undefined; + (SystemPromptManager as unknown as Record).instance = undefined; expect(() => SystemPromptManager.getInstance()).toThrow( "Vault is required for first initialization" ); diff --git a/src/system-prompts/systemPromptRegister.test.ts b/src/system-prompts/systemPromptRegister.test.ts index dad3151c..08bd3577 100644 --- a/src/system-prompts/systemPromptRegister.test.ts +++ b/src/system-prompts/systemPromptRegister.test.ts @@ -69,7 +69,7 @@ describe("SystemPromptRegister", () => { let mockVault: Vault; let register: SystemPromptRegister; - let vaultEventHandlers: Record unknown>; + let vaultEventHandlers: Record unknown>; beforeEach(() => { jest.clearAllMocks(); @@ -79,7 +79,7 @@ describe("SystemPromptRegister", () => { mockPlugin = {} as Plugin; mockVault = { - on: jest.fn((event: string, handler: (...args: any[]) => unknown) => { + on: jest.fn((event: string, handler: (...args: unknown[]) => unknown) => { vaultEventHandlers[event] = handler; }), off: jest.fn(), diff --git a/src/tools/FileParserManager.ts b/src/tools/FileParserManager.ts index fb741c4b..4a5865a7 100644 --- a/src/tools/FileParserManager.ts +++ b/src/tools/FileParserManager.ts @@ -384,18 +384,19 @@ export class Docs4LLMParser implements FileParser { content = markdownParts.join("\n\n"); } else if (typeof docs4llmResponse.response === "object") { // Handle single object response (backward compatibility) - if (docs4llmResponse.response.md) { - content = docs4llmResponse.response.md; - } else if (docs4llmResponse.response.text) { - content = docs4llmResponse.response.text; - } else if (docs4llmResponse.response.content) { - content = docs4llmResponse.response.content; + const resp = docs4llmResponse.response as Record; + if (resp.md) { + content = resp.md as string; + } else if (resp.text) { + content = resp.text as string; + } else if (resp.content) { + content = resp.content as string; } else { // If no markdown/text/content field, stringify the entire response content = JSON.stringify(docs4llmResponse.response, null, 2); } } else { - content = String(docs4llmResponse.response); + content = JSON.stringify(docs4llmResponse.response); } // Cache the converted content @@ -421,7 +422,7 @@ export class Docs4LLMParser implements FileParser { } } - private showRateLimitNotice(error: any): void { + private showRateLimitNotice(error: unknown): void { const now = Date.now(); // Only show one rate limit notice per minute to avoid spam diff --git a/src/tools/FileTreeTools.test.ts b/src/tools/FileTreeTools.test.ts index a6d88b26..610e346b 100644 --- a/src/tools/FileTreeTools.test.ts +++ b/src/tools/FileTreeTools.test.ts @@ -1,6 +1,7 @@ import * as searchUtils from "@/search/searchUtils"; +import { mockTFile, mockTFolder } from "@/__tests__/mockObsidian"; import { ToolManager } from "@/tools/toolManager"; -import { TFolder } from "obsidian"; +import { TFile, TFolder } from "obsidian"; import { buildFileTree, createGetFileTreeTool } from "./FileTreeTools"; // Mock the searchUtils functions @@ -9,82 +10,66 @@ jest.mock("@/search/searchUtils", () => ({ shouldIndexFile: jest.fn(), })); -// Mock TFile class -class MockTFile { - vault: any; - stat: { ctime: number; mtime: number; size: number }; - basename: string; - extension: string; - name: string; - parent: TFolder; - path: string; - - constructor(path: string, parent: TFolder) { - this.path = path; - this.name = path.split("/").pop() || ""; - this.basename = this.name.split(".")[0]; - this.extension = this.name.split(".")[1] || ""; - this.parent = parent; - this.vault = {}; - this.stat = { - ctime: Date.now(), - mtime: Date.now(), - size: 0, - }; - } +/** + * Build a TFolder with the given path, parent, and children. + */ +function makeFolder(path: string, parent: TFolder | null, children: (TFile | TFolder)[]): TFolder { + const folder = mockTFolder({ path, name: path.split("/").pop() ?? "", parent, children }); + return folder; } -// Mock TFolder class -class MockTFolder { - vault: any; - name: string; - path: string; - parent: TFolder | null; - children: Array; - - constructor(path: string, parent: TFolder | null) { - this.path = path; - this.name = path.split("/").pop() || ""; - this.parent = parent; - this.vault = {}; - this.children = []; - } - - isRoot(): boolean { - return this.parent === null; - } +/** + * Build a TFile with the given path and parent. + */ +function makeFile(path: string, parent: TFolder): TFile { + const name = path.split("/").pop() ?? ""; + const basename = name.includes(".") ? name.split(".")[0] : name; + const extension = name.includes(".") ? (name.split(".")[1] ?? "") : ""; + return mockTFile({ + path, + name, + basename, + extension, + parent, + stat: { ctime: Date.now(), mtime: Date.now(), size: 0 }, + }); } describe("FileTreeTools", () => { - let root: MockTFolder; + let root: TFolder; beforeEach(() => { - root = new MockTFolder("", null); + // We need to build the tree bottom-up, then attach children. + // Use Object.assign after creation to set children (mockTFolder returns a writable object). + root = mockTFolder({ path: "", name: "", parent: null, children: [] }); - // Create a mock file structure - const docs = new MockTFolder("docs", root); - const projects = new MockTFolder("docs/projects", docs); - const notes = new MockTFolder("docs/notes", docs); + const docs = makeFolder("docs", root, []); + const projects = makeFolder("docs/projects", docs, []); + const notes = makeFolder("docs/notes", docs, []); - docs.children = [projects, notes, new MockTFile("docs/readme.md", docs)]; - - projects.children = [ - new MockTFile("docs/projects/project1.md", projects), - new MockTFile("docs/projects/project2.md", projects), - new MockTFile("docs/projects/data.json", projects), + (projects as TFolder & { children: (TFile | TFolder)[] }).children = [ + makeFile("docs/projects/project1.md", projects), + makeFile("docs/projects/project2.md", projects), + makeFile("docs/projects/data.json", projects), ]; - notes.children = [ - new MockTFile("docs/notes/note1.md", notes), - new MockTFile("docs/notes/note2.md", notes), - new MockTFile("docs/notes/image.png", notes), + (notes as TFolder & { children: (TFile | TFolder)[] }).children = [ + makeFile("docs/notes/note1.md", notes), + makeFile("docs/notes/note2.md", notes), + makeFile("docs/notes/image.png", notes), ]; - root.children = [ + (docs as TFolder & { children: (TFile | TFolder)[] }).children = [ + projects, + notes, + makeFile("docs/readme.md", docs), + ]; + + (root as TFolder & { children: (TFile | TFolder)[] }).children = [ docs, - new MockTFile("readme.md", root), - new MockTFile("config.json", root), - new MockTFile("text", root), + makeFile("readme.md", root), + makeFile("config.json", root), + makeFile("text", root), ]; // Reset mocks before each test diff --git a/src/tools/FileTreeTools.ts b/src/tools/FileTreeTools.ts index ff1ee1fb..76bc9122 100644 --- a/src/tools/FileTreeTools.ts +++ b/src/tools/FileTreeTools.ts @@ -9,12 +9,12 @@ interface FileTreeNode { extensionCounts?: Record; } -function isTFolder(item: any): item is TFolder { - return "children" in item && "path" in item; +function isTFolder(item: unknown): item is TFolder { + return typeof item === "object" && item !== null && "children" in item && "path" in item; } -function isTFile(item: any): item is TFile { - return "path" in item && !("children" in item); +function isTFile(item: unknown): item is TFile { + return typeof item === "object" && item !== null && "path" in item && !("children" in item); } function getFileExtension(filename: string): string { diff --git a/src/tools/SearchTools.ts b/src/tools/SearchTools.ts index fda332af..ea7f2dcb 100644 --- a/src/tools/SearchTools.ts +++ b/src/tools/SearchTools.ts @@ -209,8 +209,8 @@ async function performLexicalSearch({ source: doc.metadata.source, mtime: doc.metadata.mtime ?? null, ctime: doc.metadata.ctime ?? null, - chunkId: (doc.metadata as any).chunkId ?? null, - isChunk: (doc.metadata as any).isChunk ?? false, + chunkId: (doc.metadata as Record).chunkId ?? null, + isChunk: (doc.metadata as Record).isChunk ?? false, explanation: doc.metadata.explanation ?? null, isFilterResult: isFilter, matchType: isFilter ? doc.metadata.source || "filter" : (undefined as string | undefined), @@ -316,8 +316,8 @@ const semanticSearchTool = createLangChainTool({ source: doc.metadata.source, mtime: doc.metadata.mtime ?? null, ctime: doc.metadata.ctime ?? null, - chunkId: (doc.metadata as any).chunkId ?? null, - isChunk: (doc.metadata as any).isChunk ?? false, + chunkId: (doc.metadata as Record).chunkId ?? null, + isChunk: (doc.metadata as Record).isChunk ?? false, explanation: doc.metadata.explanation ?? null, }; }); @@ -433,8 +433,8 @@ async function performMiyoSearch({ source: doc.metadata.source, mtime: doc.metadata.mtime ?? null, ctime: doc.metadata.ctime ?? null, - chunkId: (doc.metadata as any).chunkId ?? null, - isChunk: (doc.metadata as any).isChunk ?? false, + chunkId: (doc.metadata as Record).chunkId ?? null, + isChunk: (doc.metadata as Record).isChunk ?? false, explanation: doc.metadata.explanation ?? null, isFilterResult: isFilter, matchType: isFilter ? doc.metadata.source || "filter" : (undefined as string | undefined), @@ -523,10 +523,10 @@ const indexTool = createLangChainTool({ `Semantic search index has been refreshed with ${count} documents.`, documentCount: count, }; - } catch (error: any) { + } catch (error: unknown) { return { success: false, - message: `Failed to index with semantic search: ${error.message}`, + message: `Failed to index with semantic search: ${error instanceof Error ? error.message : String(error)}`, }; } } else { diff --git a/src/tools/ToolResultFormatter.ts b/src/tools/ToolResultFormatter.ts index 2bfd8e3d..6b69e576 100644 --- a/src/tools/ToolResultFormatter.ts +++ b/src/tools/ToolResultFormatter.ts @@ -40,20 +40,21 @@ function clampReadNoteMessage(message: string): string { return `${trimmed.slice(0, READ_NOTE_SUMMARY_MAX_LENGTH)}…`; } -function summarizeReadNotePayload(payload: any): string | null { +function summarizeReadNotePayload(payload: unknown): string | null { if (!payload || typeof payload !== "object") { return null; } - const status = typeof payload.status === "string" ? (payload.status as string) : null; + const p = payload as Record; + const status = typeof p.status === "string" ? p.status : null; const message = - typeof payload.message === "string" && (payload.message as string).trim().length > 0 - ? clampReadNoteMessage(payload.message as string) + typeof p.message === "string" && p.message.trim().length > 0 + ? clampReadNoteMessage(p.message) : null; - const notePath = typeof payload.notePath === "string" ? (payload.notePath as string) : ""; + const notePath = typeof p.notePath === "string" ? p.notePath : ""; const noteTitle = - typeof payload.noteTitle === "string" && (payload.noteTitle as string).trim().length > 0 - ? (payload.noteTitle as string).trim() + typeof p.noteTitle === "string" && p.noteTitle.trim().length > 0 + ? p.noteTitle.trim() : deriveReadNoteDisplayName(notePath); const displayName = noteTitle || deriveReadNoteDisplayName(notePath); @@ -64,7 +65,7 @@ function summarizeReadNotePayload(payload: any): string | null { return message ?? `⚠️ Note "${displayName}" not found`; } if (status === "not_unique") { - const candidateCount = Array.isArray(payload.candidates) ? payload.candidates.length : 0; + const candidateCount = Array.isArray(p.candidates) ? p.candidates.length : 0; if (message) { return message; } @@ -80,13 +81,9 @@ function summarizeReadNotePayload(payload: any): string | null { return message; } const totalChunks = - typeof payload.totalChunks === "number" && Number.isFinite(payload.totalChunks) - ? payload.totalChunks - : null; + typeof p.totalChunks === "number" && Number.isFinite(p.totalChunks) ? p.totalChunks : null; const requested = - typeof payload.chunkIndex === "number" && Number.isFinite(payload.chunkIndex) - ? payload.chunkIndex - : null; + typeof p.chunkIndex === "number" && Number.isFinite(p.chunkIndex) ? p.chunkIndex : null; if (requested !== null && totalChunks !== null) { const maxIndex = Math.max(totalChunks - 1, 0); return `⚠️ Chunk ${requested} exceeds available range (max index ${maxIndex})`; @@ -95,14 +92,10 @@ function summarizeReadNotePayload(payload: any): string | null { } const chunkIndex = - typeof payload.chunkIndex === "number" && Number.isFinite(payload.chunkIndex) - ? payload.chunkIndex - : 0; + typeof p.chunkIndex === "number" && Number.isFinite(p.chunkIndex) ? p.chunkIndex : 0; const totalChunks = - typeof payload.totalChunks === "number" && Number.isFinite(payload.totalChunks) - ? payload.totalChunks - : null; - const hasMore = Boolean(payload.hasMore); + typeof p.totalChunks === "number" && Number.isFinite(p.totalChunks) ? p.totalChunks : null; + const hasMore = Boolean(p.hasMore); const parts: string[] = [`✅ Read "${displayName || "note"}"`]; if (totalChunks && totalChunks > 0) { @@ -135,7 +128,7 @@ export class ToolResultFormatter { } // Try to parse as JSON for all tools now that they return JSON - let parsedResult: any; + let parsedResult: unknown; try { parsedResult = JSON.parse(normalized); } catch { @@ -172,17 +165,19 @@ export class ToolResultFormatter { * @param documents Array of parsed local search documents * @returns Display-friendly summary string */ - static formatLocalSearchDocuments(documents: any[]): string { + static formatLocalSearchDocuments(documents: unknown[]): string { if (!Array.isArray(documents) || documents.length === 0) { return "📚 Found 0 relevant notes\n\nNo matching notes found."; } const total = documents.length; const topResults = documents.slice(0, 10); - const hasScoringData = topResults.some( - (item) => + const hasScoringData = topResults.some((doc) => { + const item = doc as Record; + return ( typeof item?.rerank_score === "number" || typeof item?.score === "number" || item?.source - ); + ); + }); const formattedItems = topResults .map((item, index) => @@ -197,7 +192,7 @@ export class ToolResultFormatter { return `📚 Found ${total} relevant notes\n\nTop results:\n\n${formattedItems}${footer}`; } - private static formatLocalSearch(result: any): string { + private static formatLocalSearch(result: unknown): string { // Handle XML-wrapped results from chain runners if (typeof result === "string") { // Check if it's XML-wrapped content @@ -215,7 +210,7 @@ export class ToolResultFormatter { } // Robustly extract document information regardless of tag ordering - const documents: any[] = []; + const documents: unknown[] = []; const blockRegex = /([\s\S]*?)<\/document>/g; let blockMatch; while ((blockMatch = blockRegex.exec(xmlContent)) !== null) { @@ -251,20 +246,26 @@ export class ToolResultFormatter { return this.formatLocalSearchDocuments(searchResults); } - private static parseSearchResults(result: any): any[] { + private static parseSearchResults(result: unknown): unknown[] { // Only support the new structured format or pre-formatted XML flow if (typeof result === "object" && result !== null) { - if (result.type === "local_search" && Array.isArray(result.documents)) { - return result.documents as any[]; + const r = result as Record; + if (r.type === "local_search" && Array.isArray(r.documents)) { + return r.documents as unknown[]; } return []; } if (typeof result === "string") { // Allow parsing of structured JSON string try { - const parsed = JSON.parse(result); - if (parsed && parsed.type === "local_search" && Array.isArray(parsed.documents)) { - return parsed.documents as any[]; + const parsed = JSON.parse(result) as unknown; + if ( + parsed && + typeof parsed === "object" && + (parsed as Record).type === "local_search" && + Array.isArray((parsed as Record).documents) + ) { + return (parsed as Record).documents as unknown[]; } } catch { // ignore JSON parse errors; fall through to empty array @@ -274,58 +275,76 @@ export class ToolResultFormatter { return []; } - private static formatSearchItem(item: any, index: number): string { - const filename = item.path?.split("/").pop()?.replace(/\.md$/, "") || item.title || "Untitled"; - const score = item.rerank_score || item.score || 0; + private static formatSearchItem(item: unknown, index: number): string { + const it = item as Record; + const pathStr = typeof it.path === "string" ? it.path : ""; + const titleStr = typeof it.title === "string" ? it.title : ""; + const filename = pathStr + ? pathStr.split("/").pop()?.replace(/\.md$/, "") || titleStr || "Untitled" + : titleStr || "Untitled"; + const score = (it.rerank_score as number) || (it.score as number) || 0; const scoreDisplay = typeof score === "number" ? score.toFixed(4) : score; // For time-filtered results, show as "Recency" instead of "Relevance" - const scoreLabel = item.source === "time-filtered" ? "Recency" : "Relevance"; + const scoreLabel = it.source === "time-filtered" ? "Recency" : "Relevance"; const lines = [`${index + 1}. ${filename}`]; // For time-filtered queries, show actual modified time instead of a recency score - if (item.source === "time-filtered") { - if (item.mtime) { + if (it.source === "time-filtered") { + if (it.mtime) { try { - const d = new Date(item.mtime as string | number | Date); - const iso = isNaN(d.getTime()) ? String(item.mtime) : d.toISOString(); - lines.push(` 🕒 Modified: ${iso}${item.includeInContext ? " ✓" : ""}`); + const mtimeVal = it.mtime as string | number | Date; + const d = new Date(mtimeVal); + const iso = isNaN(d.getTime()) + ? typeof mtimeVal === "string" || typeof mtimeVal === "number" + ? String(mtimeVal) + : "" + : d.toISOString(); + lines.push(` 🕒 Modified: ${iso}${it.includeInContext ? " ✓" : ""}`); } catch { - lines.push(` 🕒 Modified: ${String(item.mtime)}${item.includeInContext ? " ✓" : ""}`); + const mtimeStr = + typeof it.mtime === "string" || typeof it.mtime === "number" ? String(it.mtime) : ""; + lines.push(` 🕒 Modified: ${mtimeStr}${it.includeInContext ? " ✓" : ""}`); } } - } else if (item.source === "title-match") { + } else if (it.source === "title-match") { // For title matches, avoid misleading numeric scores; mark as a title match - lines.push(` 🔖 Title match${item.includeInContext ? " ✓" : ""}`); + lines.push(` 🔖 Title match${it.includeInContext ? " ✓" : ""}`); } else { // Default: show relevance-like score line - lines.push(` 📊 ${scoreLabel}: ${scoreDisplay}${item.includeInContext ? " ✓" : ""}`); + lines.push(` 📊 ${scoreLabel}: ${scoreDisplay}${it.includeInContext ? " ✓" : ""}`); } - const snippet = this.extractContentSnippet(item.content as string); + const snippet = this.extractContentSnippet(it.content as string); if (snippet) { - lines.push(` 💬 "${snippet}${item.content?.length > 150 ? "..." : ""}"`); + const contentLength = typeof it.content === "string" ? it.content.length : 0; + lines.push(` 💬 "${snippet}${contentLength > 150 ? "..." : ""}"`); } - if (item.path && !item.path.endsWith(`/${filename}.md`)) { - lines.push(` 📁 ${item.path}`); + if (pathStr && !pathStr.endsWith(`/${filename}.md`)) { + lines.push(` 📁 ${pathStr}`); } return lines.join("\n"); } - private static formatBasicSearchItem(item: any, index: number): string { - const title = item.title || item.path || `Result ${index + 1}`; + private static formatBasicSearchItem(item: unknown, index: number): string { + const it = item as Record; + const title = (it.title as string) || (it.path as string) || `Result ${index + 1}`; const lines = [`${index + 1}. ${title}`]; - const modified = item.mtime || item.modified || item.modified_at || item.updated_at; + const modified = it.mtime || it.modified || it.modified_at || it.updated_at; if (modified) { - lines.push(` 🕒 Modified: ${String(modified)}`); + const modifiedStr = + typeof modified === "string" || typeof modified === "number" ? String(modified) : ""; + if (modifiedStr) { + lines.push(` 🕒 Modified: ${modifiedStr}`); + } } - if (item.path && item.path !== title) { - lines.push(` 📁 ${item.path}`); + if (typeof it.path === "string" && it.path && it.path !== title) { + lines.push(` 📁 ${it.path}`); } return lines.join("\n"); @@ -341,31 +360,35 @@ export class ToolResultFormatter { return cleanContent.substring(0, maxLength).replace(/\s+/g, " ").trim(); } - private static formatWebSearch(result: any): string { + private static formatWebSearch(result: unknown): string { // Handle new JSON array format from webSearch tool - if (Array.isArray(result) && result.length > 0 && result[0].type === "web_search") { + const firstItem = + Array.isArray(result) && result.length > 0 ? (result[0] as Record) : null; + if (firstItem && firstItem.type === "web_search") { const output: string[] = ["🌐 Web Search Results"]; - const item = result[0]; // Add the main content - if (item.content) { + if (firstItem.content) { output.push(""); - output.push(item.content as string); + output.push(typeof firstItem.content === "string" ? firstItem.content : ""); } // Add citations if present - if (item.citations && item.citations.length > 0) { + const citations = Array.isArray(firstItem.citations) ? firstItem.citations : []; + if (citations.length > 0) { output.push(""); output.push("Sources:"); - item.citations.forEach((url: string, index: number) => { - output.push(`[${index + 1}] ${url}`); + citations.forEach((url: unknown, index: number) => { + output.push(`[${index + 1}] ${String(url)}`); }); } // Add instruction for the model - if (item.instruction) { + if (firstItem.instruction) { output.push(""); - output.push(`Note: ${item.instruction}`); + output.push( + `Note: ${typeof firstItem.instruction === "string" ? firstItem.instruction : ""}` + ); } return output.join("\n"); @@ -415,12 +438,12 @@ export class ToolResultFormatter { return output.join("\n"); } - return result as string; + return typeof result === "string" ? result : ""; } - private static formatYoutubeTranscription(result: any): string { + private static formatYoutubeTranscription(result: unknown): string { // Handle both string and object results - let parsed: any; + let parsed: unknown; if (typeof result === "string") { try { @@ -432,28 +455,42 @@ export class ToolResultFormatter { } else if (typeof result === "object") { parsed = result; } else { - return String(result); + return typeof result === "number" || typeof result === "boolean" ? String(result) : ""; + } + + // Narrow parsed to a record for member access + const p = + typeof parsed === "object" && parsed !== null ? (parsed as Record) : null; + if (!p) { + return typeof result === "object" ? JSON.stringify(result, null, 2) : ""; } // Handle error case - if (parsed.success === false) { - return `📺 YouTube Transcription Failed\n\n${parsed.message}`; + if (p.success === false) { + return `📺 YouTube Transcription Failed\n\n${typeof p.message === "string" ? p.message : ""}`; } // Handle new multi-URL format - if (parsed.results && Array.isArray(parsed.results)) { + if (p.results && Array.isArray(p.results)) { + const totalUrls = typeof p.total_urls === "number" ? p.total_urls : p.results.length; const output: string[] = [ - `📺 YouTube Transcripts (${parsed.total_urls} video${parsed.total_urls > 1 ? "s" : ""})`, + `📺 YouTube Transcripts (${totalUrls} video${totalUrls > 1 ? "s" : ""})`, ]; output.push(""); - for (const videoResult of parsed.results) { - if (videoResult.success) { - output.push(`📹 Video: ${videoResult.url}`); + for (const videoResult of p.results) { + const vr = + typeof videoResult === "object" && videoResult !== null + ? (videoResult as Record) + : null; + if (!vr) continue; + if (vr.success) { + output.push(`📹 Video: ${typeof vr.url === "string" ? vr.url : ""}`); output.push(""); // Format transcript - const lines = videoResult.transcript.split("\n"); + const transcript = typeof vr.transcript === "string" ? vr.transcript : ""; + const lines = transcript.split("\n"); let formattedLines = 0; for (const line of lines) { @@ -477,13 +514,15 @@ export class ToolResultFormatter { } } - if (videoResult.elapsed_time_ms) { + if (vr.elapsed_time_ms) { output.push(""); - output.push(`Processing time: ${(videoResult.elapsed_time_ms / 1000).toFixed(1)}s`); + output.push( + `Processing time: ${(typeof vr.elapsed_time_ms === "number" ? vr.elapsed_time_ms / 1000 : 0).toFixed(1)}s` + ); } } else { - output.push(`❌ Failed to transcribe: ${videoResult.url}`); - output.push(` ${videoResult.message}`); + output.push(`❌ Failed to transcribe: ${typeof vr.url === "string" ? vr.url : ""}`); + output.push(` ${typeof vr.message === "string" ? vr.message : ""}`); } output.push(""); @@ -495,12 +534,13 @@ export class ToolResultFormatter { } // Handle old single-video format - if (parsed.transcript) { + if (p.transcript) { const output: string[] = ["📺 YouTube Transcript"]; output.push(""); // Split transcript into manageable chunks - const lines = parsed.transcript.split("\n"); + const transcript = typeof p.transcript === "string" ? p.transcript : ""; + const lines = transcript.split("\n"); let formattedLines = 0; for (const line of lines) { @@ -524,22 +564,32 @@ export class ToolResultFormatter { } } - if (parsed.elapsed_time_ms) { + if (p.elapsed_time_ms) { output.push(""); - output.push(`Processing time: ${(parsed.elapsed_time_ms / 1000).toFixed(1)}s`); + output.push( + `Processing time: ${(typeof p.elapsed_time_ms === "number" ? p.elapsed_time_ms / 1000 : 0).toFixed(1)}s` + ); } return output.join("\n"); } // If we can't format it, return as string - return typeof result === "object" ? JSON.stringify(result, null, 2) : String(result); + return typeof result === "object" ? JSON.stringify(result, null, 2) : ""; } - private static formatWriteToFile(result: any): string { + private static formatWriteToFile(result: unknown): string { // Extract result status from object or use string directly - const status = typeof result === "object" ? result.result : result; - const statusStr = String(status).toLowerCase(); + const r = + typeof result === "object" && result !== null ? (result as Record) : null; + const status = r ? r.result : result; + const statusStr = ( + typeof status === "string" + ? status + : typeof status === "number" || typeof status === "boolean" + ? String(status) + : "" + ).toLowerCase(); if (statusStr.includes("accepted")) { return "✅ File change: accepted"; @@ -548,17 +598,29 @@ export class ToolResultFormatter { } // Return message if available, otherwise the raw result - return typeof result === "object" && result.message - ? (result.message as string) - : String(status); + return r && typeof r.message === "string" + ? r.message + : typeof status === "string" + ? status + : ""; } - private static formatReplaceInFile(result: any): string { - const status = typeof result === "object" ? String(result.result ?? "") : String(result); - const diff = typeof result === "object" ? result.diff : undefined; + private static formatReplaceInFile(result: unknown): string { + const r = + typeof result === "object" && result !== null ? (result as Record) : null; + const rResult = r ? r.result : undefined; + const status = r + ? typeof rResult === "string" || typeof rResult === "number" || typeof rResult === "boolean" + ? String(rResult) + : "" + : typeof result === "string" + ? result + : ""; + const diff = r ? r.diff : undefined; + const diffStr = typeof diff === "string" ? diff : typeof diff === "number" ? String(diff) : ""; - if (status.toLowerCase().includes("accepted") && diff) { - return `✅ Edit accepted\n\`\`\`diff\n${diff}\n\`\`\``; + if (status.toLowerCase().includes("accepted") && diffStr) { + return `✅ Edit accepted\n\`\`\`diff\n${diffStr}\n\`\`\``; } else if (status.toLowerCase().includes("accepted")) { return "✅ Edit accepted"; } else if (status.toLowerCase().includes("rejected")) { @@ -566,11 +628,11 @@ export class ToolResultFormatter { } // Error / not-found strings pass through unchanged - return typeof result === "object" && result.message ? (result.message as string) : status; + return r && typeof r.message === "string" ? r.message : status; } - private static formatReadNote(result: any): string { - let payload: any = result; + private static formatReadNote(result: unknown): string { + let payload: unknown = result; if (typeof result === "string") { try { payload = JSON.parse(result); diff --git a/src/tools/allTools.validation.test.ts b/src/tools/allTools.validation.test.ts index bbea01af..a51a921e 100644 --- a/src/tools/allTools.validation.test.ts +++ b/src/tools/allTools.validation.test.ts @@ -186,8 +186,13 @@ describe("All Tools Validation", () => { // Check that descriptions are accessible const shape = wellDocumentedSchema.shape; - expect((shape.query as any)._def.description).toBe("The search query to execute"); - expect((shape.limit as any)._def.description).toBe("Maximum number of results to return"); + type ZodWithDef = { _def: { description: string } }; + expect((shape.query as unknown as ZodWithDef)._def.description).toBe( + "The search query to execute" + ); + expect((shape.limit as unknown as ZodWithDef)._def.description).toBe( + "Maximum number of results to return" + ); }); }); diff --git a/src/tools/toolManager.ts b/src/tools/toolManager.ts index 6aff7cb6..32f28019 100644 --- a/src/tools/toolManager.ts +++ b/src/tools/toolManager.ts @@ -20,7 +20,7 @@ export class ToolManager { * Call a tool with the given arguments. * Throws on error so caller can handle with proper context (args, tool name). */ - static async callTool(tool: any, args: any): Promise { + static async callTool(tool: unknown, args: unknown): Promise { if (!tool) { throw new Error("Tool is undefined"); } diff --git a/src/types.ts b/src/types.ts index aa2d9968..11f1bb8f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,7 +6,7 @@ declare module "obsidian" { // Note that this API is considered internal and may work differently in the // future. getBacklinksForFile(file: TFile): { - data: Map; + data: Map; } | null; } diff --git a/src/types/message.ts b/src/types/message.ts index 4439ea10..6a447399 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -144,10 +144,10 @@ export interface ChatMessage { isVisible: boolean; /** Sources cited in the response */ - sources?: { title: string; path: string; score: number; explanation?: any }[]; + sources?: { title: string; path: string; score: number; explanation?: unknown }[]; /** Rich content (images, etc.) */ - content?: any[]; + content?: unknown[]; /** Context attached to this message */ context?: MessageContext; @@ -212,7 +212,7 @@ export interface StoredMessage { isVisible: boolean; isErrorMessage?: boolean; - sources?: { title: string; path: string; score: number; explanation?: any }[]; - content?: any[]; + sources?: { title: string; path: string; score: number; explanation?: unknown }[]; + content?: unknown[]; responseMetadata?: ResponseMetadata; } diff --git a/src/utils.ts b/src/utils.ts index f7bae519..407a1bdb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -53,7 +53,7 @@ export function getDomainFromUrl(url: string): string { // Add custom error type at the top of the file interface APIError extends Error { - json?: any; + json?: unknown; } // Error message constants @@ -71,26 +71,29 @@ export interface ErrorDetail { reason?: string; } -export function extractErrorDetail(error: any): ErrorDetail { - const errorDetail: ErrorDetail = error?.detail || {}; +export function extractErrorDetail(error: unknown): ErrorDetail { + const err = error as Record | null | undefined; + const errorDetail: ErrorDetail = (err?.detail as ErrorDetail) || {}; return { status: errorDetail.status, - message: errorDetail.message || (error?.message as string | undefined), + message: errorDetail.message || (err?.message as string | undefined), reason: errorDetail.reason, }; } -export function isLicenseKeyError(error: any): boolean { +export function isLicenseKeyError(error: unknown): boolean { const errorDetail = extractErrorDetail(error); + const err = error as Record | null | undefined; + const message = err?.message as string | undefined; return Boolean( errorDetail.reason === "Invalid license key" || - error?.message === "Invalid license key" || - error?.message?.includes("status 403") || + message === "Invalid license key" || + message?.includes("status 403") || errorDetail.status === 403 ); } -export function getApiErrorMessage(error: any): string { +export function getApiErrorMessage(error: unknown): string { const errorDetail = extractErrorDetail(error); if (isLicenseKeyError(error)) { return ERROR_MESSAGES.INVALID_LICENSE_KEY_USER; @@ -275,11 +278,15 @@ export const stringToChainType = (chain: string): ChainType => { // TODO: These chain validation functions are deprecated // Remove after confirming chainManager no longer uses them export const isLLMChain = (chain: RunnableSequence): chain is RunnableSequence => { - return Boolean((chain as any).last?.modelName || (chain as any).last?.model); + const c = chain as unknown as Record; + const last = c.last as Record | undefined; + return Boolean(last?.modelName || last?.model); }; export const isRetrievalQAChain = (chain: BaseChain): chain is RetrievalQAChain => { - return (chain as any).last?.retriever !== undefined; + const c = chain as unknown as Record; + const last = c.last as Record | undefined; + return last?.retriever !== undefined; }; export const isSupportedChain = (chain: RunnableSequence): chain is RunnableSequence => { @@ -642,7 +649,7 @@ export function isNoteTitleUnique(title: string, vault: Vault): boolean { // Helper function to determine if we should show the full path for a file export function shouldShowPath(file: TFile): boolean { - return (file as any).needsPathDisplay === true; + return (file as unknown as Record).needsPathDisplay === true; } /** @@ -674,7 +681,7 @@ export function extractUniqueTitlesFromDocs(docs: Document[]): string[] { return Array.from(titlesSet); } -export function extractJsonFromCodeBlock(content: string): any { +export function extractJsonFromCodeBlock(content: string): unknown { const codeBlockMatch = content.match(/```(?:json)?\s*([\s\S]*?)\s*```/); const jsonContent = codeBlockMatch ? codeBlockMatch[1].trim() : content.trim(); return JSON.parse(jsonContent); @@ -938,10 +945,7 @@ function createReadableStreamFromString(input: string) { // err2String is now exported from '@/errorFormat' to avoid circular dependencies and duplication. -export function omit, K extends keyof T>( - obj: T, - keys: K[] -): Omit { +export function omit(obj: T, keys: K[]): Omit { const result = { ...obj }; keys.forEach((key) => { delete result[key]; @@ -1158,7 +1162,8 @@ export function isOSeriesModel(model: BaseChatModel | string): boolean { } // For BaseChatModel instances - const modelName: string = (model as any).modelName || (model as any).model || ""; + const m = model as unknown as Record; + const modelName: string = (m.modelName as string) || (m.model as string) || ""; return modelName.startsWith("o1") || modelName.startsWith("o3") || modelName.startsWith("o4"); } @@ -1168,7 +1173,8 @@ export function isGPT5Model(model: BaseChatModel | string): boolean { } // For BaseChatModel instances - const modelName: string = (model as any).modelName || (model as any).model || ""; + const m = model as unknown as Record; + const modelName: string = (m.modelName as string) || (m.model as string) || ""; return modelName.startsWith("gpt-5"); } @@ -1179,8 +1185,9 @@ export function isGPT5Model(model: BaseChatModel | string): boolean { * @returns True when the model name indicates a Codex model. */ export function isCodexModel(model: BaseChatModel | string): boolean { + const m = model as unknown as Record; const modelName: string = - typeof model === "string" ? model : (model as any).modelName || (model as any).model || ""; + typeof model === "string" ? model : (m.modelName as string) || (m.model as string) || ""; return modelName.toLowerCase().includes("codex"); } @@ -1215,10 +1222,9 @@ export interface ModelInfo { } export function getModelInfo(model: BaseChatModel | string): ModelInfo { + const m = model as unknown as Record; const modelName: string = - typeof model === "string" - ? model - : ((model as any).modelName as string) || ((model as any).model as string) || ""; + typeof model === "string" ? model : (m.modelName as string) || (m.model as string) || ""; const isOSeries = isOSeriesModel(modelName); const isGPT5 = isGPT5Model(modelName); @@ -1316,7 +1322,7 @@ export function checkModelApiKey( * Extracts text content from a message chunk that could be either a string * or an array of content objects (Claude 3.7 format) */ -export function extractTextFromChunk(content: any): string { +export function extractTextFromChunk(content: unknown): string { if (typeof content === "string") { return content; } @@ -1326,8 +1332,8 @@ export function extractTextFromChunk(content: any): string { .map((item) => item.text as string) .join(""); } - // For any other type, try to convert to string or return empty - return String(content || ""); + // For any other type, return empty string + return ""; } /** @@ -1337,7 +1343,7 @@ export function extractTextFromChunk(content: any): string { * @param text - The text or content array to remove think tags from * @returns The text with think tags removed */ -export function removeThinkTags(text: any): string { +export function removeThinkTags(text: unknown): string { // First convert any content format to plain text const plainText = extractTextFromChunk(text); // Remove complete think tags and their content @@ -1354,7 +1360,7 @@ export function removeThinkTags(text: any): string { * @param text - The text or content array to remove error tags from * @returns The text with error tags removed */ -export function removeErrorTags(text: any): string { +export function removeErrorTags(text: unknown): string { // First convert any content format to plain text const plainText = extractTextFromChunk(text); // Then remove error tags @@ -1542,7 +1548,9 @@ export async function openFileInWorkspace(file: TFile, focusIfOpen: boolean = tr leaf.view.getViewType() === "pdf" || leaf.view.getViewType() === "canvas" ) { - const viewFile = (leaf.view as any).file; + const viewFile = (leaf.view as unknown as Record).file as + | { path: string } + | undefined; if (viewFile && viewFile.path === file.path) { existingLeaf = leaf; } diff --git a/src/utils/debounce.ts b/src/utils/debounce.ts index a1ec295a..644605f1 100644 --- a/src/utils/debounce.ts +++ b/src/utils/debounce.ts @@ -2,6 +2,7 @@ * Returned by {@link debounce}. Calling it schedules the underlying function, * with `cancel` and `flush` controls modeled after `lodash.debounce`. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic constraint must accept arbitrary function shapes (lodash.debounce convention) export interface DebouncedFunction any> { (...args: Parameters): ReturnType | undefined; cancel(): void; @@ -18,6 +19,7 @@ export interface DebounceOptions { * Supports the `leading` / `trailing` options and `cancel` / `flush` methods * used by the `lodash.debounce` API the codebase relied on previously. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic constraint must accept arbitrary function shapes (lodash.debounce convention) export function debounce any>( func: T, wait: number, diff --git a/src/utils/rateLimitUtils.ts b/src/utils/rateLimitUtils.ts index 83a96a84..67cb90f1 100644 --- a/src/utils/rateLimitUtils.ts +++ b/src/utils/rateLimitUtils.ts @@ -11,15 +11,16 @@ * @param error The error object to check * @returns true if the error is identified as a rate limit error */ -export function isRateLimitError(error: any): boolean { +export function isRateLimitError(error: unknown): boolean { if (!error || typeof error !== "object") return false; - const errorMessage: string = error.message || error.toString(); + const err = error as Record; + const errorMessage: string = (err.message as string) || ""; return ( errorMessage.includes("Request rate limit exceeded") || errorMessage.includes("RATE_LIMIT_EXCEEDED") || errorMessage.includes("429") || - error.status === 429 + err.status === 429 ); } @@ -28,8 +29,9 @@ export function isRateLimitError(error: any): boolean { * @param error The rate limit error object * @returns The retry time string if found, or 'some time' as fallback */ -export function extractRetryTime(error: any): string { - const errorMessage: string = error?.message || error?.toString() || ""; +export function extractRetryTime(error: unknown): string { + const err = error as Record | null | undefined; + const errorMessage: string = (err?.message as string) || ""; const retryMatch = errorMessage.match(/Try again in ([\d\w\s]+)/); return retryMatch ? retryMatch[1] : "some time"; }