chore(eslint): enable no-explicit-any; fix ~395 violations (#2452)

* 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<any> API surfaces where typed alternatives poison Orama's
internal inference to `never`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

* chore(eslint): fix remaining no-explicit-any and unnecessary-type-assertion errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Zero Liu 2026-05-14 02:08:45 -07:00 committed by GitHub
parent e1385d95f1
commit 6ca2dc01ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
107 changed files with 1322 additions and 853 deletions

View file

@ -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" }],

View file

@ -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<void> {
const chainContext = {
memoryManager,
userMemoryManager,
} as any;
} as unknown as ChainManager;
const adapter = ModelAdapterFactory.createAdapter({
modelName: "gpt-4",

View file

@ -88,6 +88,6 @@ export class Modal {
}
}
export function parseYaml(_: string): any {
export function parseYaml(_: string): unknown {
return {};
}

View file

@ -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", () => {

View file

@ -133,12 +133,18 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
/**
* 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<string, unknown>;
}> {
return tools.map((tool) => {
let inputSchema: any = { type: "object", properties: {} };
let inputSchema: Record<string, unknown> = { 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<string, unknown>;
}
return {
name: tool.name,
@ -151,19 +157,26 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
/**
* 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<string, unknown>; type: "tool_call" }>
| undefined {
const dataObj = data as Record<string, unknown> | 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<string, unknown>[]).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<string, unknown>,
type: "tool_call" as const,
})) as Array<{ id: string; name: string; args: Record<string, unknown>; type: "tool_call" }>;
}));
}
async _generate(
@ -188,7 +201,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
throw new Error(`Amazon Bedrock request failed with status ${response.status}: ${errorText}`);
}
const data = await response.json();
const data = (await response.json()) as Record<string, unknown>;
const text = this.extractText(data);
const toolCalls = this.extractToolCalls(data);
@ -306,13 +319,14 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
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<string, unknown>;
// Handle AWS EventStream format where bytes field is at top level
let eventToProcess = outerEvent;
@ -404,7 +418,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
}
}
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<BedrockChatModelCallOptions>
* @returns Claude-style content array or null if not classifiable
*/
private buildContentItemsFromDelta(
event: any
event: Record<string, unknown>
): 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<string, unknown> | undefined;
const contentBlockDeltaCamel = event.contentBlockDelta as Record<string, unknown> | 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<string, unknown>;
const deltaType = delta.type;
@ -461,27 +478,29 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
* Returns tool_call_chunks format that LangChain can concatenate.
*/
private extractToolCallChunk(
event: any
event: Record<string, unknown>
): { 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<string, unknown> | 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<string, unknown> | 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<BedrockChatModelCallOptions>
}
private async processStreamEvent(
event: any,
event: Record<string, unknown>,
runManager: CallbackManagerForLLMRun | undefined,
currentUsage?: Record<string, unknown>,
currentStopReason?: string
@ -506,23 +525,25 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
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<string, unknown> | 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<string, unknown>;
const chunkMetadata = this.buildChunkMetadata(innerEvent as Record<string, unknown>);
const chunkMetadata = this.buildChunkMetadata(innerEvent);
// Try to build structured content (Claude-style arrays)
const contentItems = this.buildContentItemsFromDelta(innerEvent as Record<string, unknown>);
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<string, unknown>);
const toolCallChunk = this.extractToolCallChunk(innerEvent);
if (toolCallChunk) {
const messageChunk = new AIMessageChunk({
content: "",
@ -595,9 +616,10 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
} 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<string, unknown> | 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<BedrockChatModelCallOptions>
}
}
} else {
const chunkMetadata = this.buildChunkMetadata(event as Record<string, unknown>);
const chunkMetadata = this.buildChunkMetadata(event);
// Try to build structured content (Claude-style arrays)
const contentItems = this.buildContentItemsFromDelta(event as Record<string, unknown>);
const contentItems = this.buildContentItemsFromDelta(event);
// Check for tool call chunks first
const toolCallChunk = this.extractToolCallChunk(event as Record<string, unknown>);
const toolCallChunk = this.extractToolCallChunk(event);
if (toolCallChunk) {
const messageChunk = new AIMessageChunk({
content: "",
@ -942,18 +964,28 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
return metadata;
}
private extractStreamText(event: any): string | null {
private extractStreamText(event: Record<string, unknown>): string | null {
if (!event || typeof event !== "object") {
return null;
}
const evDelta = event.delta as Record<string, unknown> | undefined;
const evContentBlockDelta = event.content_block_delta as Record<string, unknown> | undefined;
const evContentBlockDeltaCamel = event.contentBlockDelta as Record<string, unknown> | undefined;
const evContentBlockDeltaInner = evContentBlockDelta?.delta as
| Record<string, unknown>
| undefined;
const evContentBlockDeltaCamelInner = evContentBlockDeltaCamel?.delta as
| Record<string, unknown>
| undefined;
// Check for thinking/reasoning fields first (fallback for non-streaming or debugging)
// This ensures thinking content is never silently dropped
const thinkingCandidates: Array<unknown> = [
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<BedrockChatModelCallOptions>
}
}
const evMessage = event.message as Record<string, unknown> | undefined;
const evMessageStop = event.messageStop as Record<string, unknown> | undefined;
const evMessageStopSnake = event.message_stop as Record<string, unknown> | undefined;
const evMessageStopMsg = evMessageStop?.message as Record<string, unknown> | undefined;
const evMessageStopSnakeMsg = evMessageStopSnake?.message as
| Record<string, unknown>
| undefined;
const nestedCandidates: Array<unknown> = [
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<BedrockChatModelCallOptions>
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<string, unknown>;
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<string, unknown> | null | undefined;
return typeof subObj?.text === "string" ? subObj.text : "";
})
.join("");
}
}
@ -1071,7 +1115,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
return null;
}
private extractUsage(event: any): Record<string, unknown> | undefined {
private extractUsage(event: Record<string, unknown>): Record<string, unknown> | undefined {
if (!event || typeof event !== "object") {
return undefined;
}
@ -1093,29 +1137,31 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
}
if (event.messageStop && typeof event.messageStop === "object") {
return this.extractUsage(event.messageStop);
return this.extractUsage(event.messageStop as Record<string, unknown>);
}
if (event.message_stop && typeof event.message_stop === "object") {
return this.extractUsage(event.message_stop);
return this.extractUsage(event.message_stop as Record<string, unknown>);
}
return undefined;
}
private extractStopReason(event: any): string | undefined {
private extractStopReason(event: Record<string, unknown>): string | undefined {
if (!event || typeof event !== "object") {
return undefined;
}
const evMessageStop = event.messageStop as Record<string, unknown> | undefined;
const evMessageStopSnake = event.message_stop as Record<string, unknown> | 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<BedrockChatModelCallOptions>
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<string, unknown> | 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<string, unknown>;
const claudeImage = this.convertImageContent(imageUrlBlock.url as string);
if (claudeImage) {
contentBlocks.push(claudeImage);
}
@ -1423,7 +1473,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
*/
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<BedrockChatModelCallOptions>
}
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<BedrockChatModelCallOptions>
return "";
}
private extractText(data: any): string {
private extractText(data: Record<string, unknown>): 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<string, unknown>;
if (typeof itemObj.text === "string") return itemObj.text;
if (itemObj.text && typeof itemObj.text === "object" && "text" in itemObj.text) {
return ((itemObj.text as Record<string, unknown>).text as string | undefined) ?? "";
}
}
return "";
@ -1516,11 +1567,11 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
}
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 "";

View file

@ -11,14 +11,14 @@ import { ChatOpenAI } from "@langchain/openai";
export interface ChatLMStudioInput {
modelName?: string;
apiKey?: string;
configuration?: any;
configuration?: Record<string, unknown>;
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<string, unknown> | undefined),
text: { format: { type: "text" } },
},
});

View file

@ -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<string, string>;
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
[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<string, unknown> {
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<void> }
): AsyncGenerator<ChatGenerationChunk> {
const params = this.invocationParams(options);
const openaiMessages = this.toOpenRouterMessages(messages);
const stream = (await this.openaiClient.chat.completions.create({
...(params as Record<string, unknown>),
...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<string, unknown>,
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<string, unknown>;
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<string, any>;
delta: Record<string, unknown>;
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<string, Record<string, unknown>>;
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<string, unknown>)?.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 {

View file

@ -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<string, unknown>;
constructor(config: any) {
super(config as OpenAIEmbeddingsParams);
constructor(config: Record<string, unknown>) {
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<string, string>) || {}),
},
body: JSON.stringify(requestBody),
});

View file

@ -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<T>(
endpoint: string,
body: any,
body: Record<string, unknown>,
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<string, unknown>).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<string, any>
context?: Record<string, unknown>
): Promise<{ isValid: boolean | undefined; plan?: string }> {
// Build the request body with proper structure
const requestBody: Record<string, any> = {
const requestBody: Record<string, unknown> = {
license_key: await getDecryptedKey(getSettings().plusLicenseKey),
};

View file

@ -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<AgentRunContext> {
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<typeof thinkStreamer.processChunk>[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,

View file

@ -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;

View file

@ -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<string, unknown>;
};
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<AIMessageChunk> = await withSuppressedTokenWarnings(
() => boundModel.stream(planningMessages) as Promise<AsyncIterable<AIMessageChunk>>
const stream: AsyncIterable<AIMessageChunk> = await withSuppressedTokenWarnings(() =>
(
boundModel as { stream: (msgs: unknown) => Promise<AsyncIterable<AIMessageChunk>> }
).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<ToolCallWithExecutor[]> {
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<string, unknown>
)) {
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<number>((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</${output.tool}>`;
})
.join("\n\n");

View file

@ -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<any[]> {
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<typeof streamer.processChunk>[0]);
}
} catch (error: unknown) {
// Check if the error is due to abort signal

View file

@ -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>${title}</title>\n<path>${path}</path>\n<content>\n${sanitizeContentForCitations(doc.pageContent as string)}\n</content>\n</${RETRIEVED_DOCUMENT_TAG}>`;
return `<${RETRIEVED_DOCUMENT_TAG}>\n<title>${title}</title>\n<path>${path}</path>\n<content>\n${sanitizeContentForCitations(doc.pageContent ?? "")}\n</content>\n</${RETRIEVED_DOCUMENT_TAG}>`;
})
.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<typeof streamer.processChunk>[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 {

View file

@ -10,7 +10,7 @@ const MockedToolManager = ToolManager as jest.Mocked<typeof ToolManager>;
const MockedToolResultFormatter = ToolResultFormatter as jest.Mocked<typeof ToolResultFormatter>;
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<any[]> {
const outputContents: any[] = [];
async function processChunks(chunks: { content: string | null }[]): Promise<unknown[]> {
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

View file

@ -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<any, void, unknown> {
async *processChunk(
chunk: Record<string, unknown>
): AsyncGenerator<Record<string, unknown>, 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

View file

@ -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<string, unknown>;
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<typeof this.handleDeepseekChunk>[0]);
} else if (isThinkingChunk) {
// OpenRouter format with delta.reasoning or reasoning_details
this.handleOpenRouterChunk(chunk);
this.handleOpenRouterChunk(chunk as Parameters<typeof this.handleOpenRouterChunk>[0]);
} else {
// Default case: regular content or other formats
this.handleDeepseekChunk(chunk);
this.handleDeepseekChunk(chunk as Parameters<typeof this.handleDeepseekChunk>[0]);
}
// Handle text-level think tags (e.g., from nvidia/nemotron models)

View file

@ -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([

View file

@ -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<string, unknown>;
// 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<string, unknown>): "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<string, unknown>) => Promise<{ history?: unknown[] }>;
},
messages: Array<{ role: string; content: string | unknown[] }>
): Promise<ProcessedMessage[]> {
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) {

View file

@ -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<string, unknown>;
}): 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<string, unknown>;
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),
};
}

View file

@ -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");

View file

@ -9,7 +9,7 @@ const createAdapter = () => ({
basePrompt: string,
toolDescriptions: string,
toolNames?: string[],
toolMetadata?: any[]
toolMetadata?: unknown[]
): PromptSection[] => [
{
id: "system",

View file

@ -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("");

View file

@ -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<string, unknown>;
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<string>();
const terms = new Set<string>();
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 `<filterResults>` and `<searchResults>` 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>${path}</path>` : "";
@ -413,7 +445,7 @@ export function formatMetadataOnlyDocuments(docs: unknown, snippetLength = 300):
return `<additionalMatches count="${docs.length}" note="These results contain titles and metadata only. To read the full content of a note, call the readNote tool with its path.">\n${fileElements}\n</additionalMatches>`;
}
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);

View file

@ -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<StructuredTool, "name" | "invoke">[],
originalUserMessage?: string
): Promise<ToolExecutionResult> {
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, unknown>
): 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) {

View file

@ -6,7 +6,7 @@ import { processRawChatHistory, processedMessagesToTextOnly } from "./chatHistor
*/
export interface BuildPromptDebugSectionsOptions {
systemSections: PromptSection[];
rawHistory?: any[];
rawHistory?: unknown[];
adapterName: string;
originalUserMessage: string;
enhancedUserMessage: string;

View file

@ -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<string, unknown>): BaseChatModel;
};
const CHAT_PROVIDER_CONSTRUCTORS = {
@ -253,7 +247,7 @@ export default class ChatModelManager {
},
}),
},
[ChatModelProviders.AZURE_OPENAI]: await (async (): Promise<any> => {
[ChatModelProviders.AZURE_OPENAI]: await (async (): Promise<Record<string, unknown>> => {
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<string, unknown> {
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<string, unknown> = {
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<string, any> = {};
const params: Record<string, unknown> = {};
// 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<string, unknown> = { ...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<string, unknown> = {
...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,

View file

@ -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<string, unknown>) => Embeddings;
const EMBEDDING_PROVIDER_CONSTRUCTORS = {
[EmbeddingModelProviders.COPILOT_PLUS]: CustomOpenAIEmbeddings,
@ -180,7 +180,7 @@ export default class EmbeddingManager {
}
}
private async getEmbeddingConfig(customModel: CustomModel): Promise<any> {
private async getEmbeddingConfig(customModel: CustomModel): Promise<Record<string, unknown>> {
const settings = getSettings();
const modelName = customModel.name;

View file

@ -148,11 +148,11 @@ export class GitHubCopilotChatModel extends ChatOpenAICompletions {
* @returns A normalized LangChain message chunk.
*/
protected override _convertCompletionsDeltaToBaseMessageChunk(
delta: Record<string, any>,
rawResponse: any,
delta: Record<string, unknown>,
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.

View file

@ -48,7 +48,7 @@ export default class MemoryManager {
await this.memory.clear();
}
async loadMemoryVariables(): Promise<any> {
async loadMemoryVariables(): Promise<Record<string, unknown>> {
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<void> {
async saveContext(
input: Record<string, unknown>,
output: Record<string, unknown> | string
): Promise<void> {
// 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<typeof this.memory.saveContext>[0],
input,
compactedOutput as Parameters<typeof this.memory.saveContext>[1]
);
}

View file

@ -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", () => ({

View file

@ -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<string | null>(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;
}

View file

@ -8,7 +8,7 @@ export interface FileCacheEntry<T> {
}
export class FileCache<T> {
private static instance: FileCache<any>;
private static instance: FileCache<unknown>;
private cacheDir: string;
private memoryCache: Map<string, FileCacheEntry<T>> = new Map();

View file

@ -38,7 +38,7 @@ export interface ConversationalRetrievalChainParams {
};
}
export interface Document<T = Record<string, any>> {
export interface Document<T = Record<string, unknown>> {
// Structure of Document, possibly including pageContent, metadata, etc.
pageContent: string;
metadata: T;

View file

@ -41,7 +41,7 @@ function CustomCommandSettingsModalContent({
const [command, setCommand] = useState(initialCommand);
const [errors, setErrors] = useState<FormErrors>({});
const handleUpdate = (field: keyof CustomCommand, value: any) => {
const handleUpdate = (field: keyof CustomCommand, value: CustomCommand[keyof CustomCommand]) => {
setCommand((prev) => ({
...prev,
[field]: value,

View file

@ -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<string, unknown>[] = hits.map(
(hit) => hit.document as unknown as Record<string, unknown>
);
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<string, unknown>, 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}`,
"",
];
}),

View file

@ -278,7 +278,10 @@ const ChatInternal: React.FC<ChatProps & { chatInput: ReturnType<typeof useChatI
try {
// Create message content array
const content: any[] = [];
type MessageContentItem =
| { type: "text"; text: string }
| { type: "image_url"; image_url: { url: string } };
const content: MessageContentItem[] = [];
// Add text content if present
if (inputMessage) {

View file

@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from "react";
import { TFile } from "obsidian";
import { TFile, TFolder } from "obsidian";
import { TypeaheadMenuPopover } from "./TypeaheadMenuPopover";
import {
useAtMentionCategories,
@ -8,11 +8,12 @@ import {
CategoryOption,
} from "./hooks/useAtMentionCategories";
import { useAtMentionSearch } from "./hooks/useAtMentionSearch";
import type { WebTabContext } from "@/types/message";
interface AtMentionTypeaheadProps {
isOpen: boolean;
onClose: () => void;
onSelect: (category: AtMentionCategory, data: any) => void;
onSelect: (category: AtMentionCategory, data: TFile | string | TFolder | WebTabContext) => void;
isCopilotPlus?: boolean;
currentActiveFile?: TFile | null;
}

View file

@ -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<ChatContextMenuProps> = ({
};
// 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);

View file

@ -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<unknown>;
getCurrentChainManager: () => { createChainWithNewModel: () => Promise<void> };
};
}
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(

View file

@ -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<ChatInputProps> = ({
const [contextFolders, setContextFolders] = useState<string[]>(initialContext?.folders || []);
const [contextWebTabs, setContextWebTabs] = useState<WebTabContext[]>([]);
const containerRef = useRef<HTMLDivElement>(null);
const lexicalEditorRef = useRef<any>(null);
const lexicalEditorRef = useRef<LexicalEditorType | null>(null);
const [currentModelKey, setCurrentModelKey] = useModelKey();
const [currentChain] = useChainType();
const [isProjectLoading] = useProjectLoading();
@ -153,7 +153,7 @@ const ChatInput: React.FC<ChatInputProps> = ({
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<ChatInputProps> = ({
};
// 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<ChatInputProps> = ({
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<ChatInputProps> = ({
};
// 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<ChatInputProps> = ({
};
}, [app.workspace]);
const onEditorReady = useCallback((editor: any) => {
const onEditorReady = useCallback((editor: LexicalEditorType) => {
lexicalEditorRef.current = editor;
}, []);

View file

@ -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 };

View file

@ -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<string, unknown>).activeDocument = window.document;
});
/**
@ -279,7 +279,7 @@ describe("ChatSingleMessage", () => {
});
beforeAll(() => {
(window as any).activeDocument = window.document;
(window as unknown as Record<string, unknown>).activeDocument = window.document;
});
it("normalizes rendered footnotes for assistant messages", async () => {

View file

@ -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<any>;
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<ChatControlsProps> = ({
@ -39,12 +39,15 @@ export const ContextControl: React.FC<ChatControlsProps> = ({
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);
};

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}
/**

View file

@ -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<string> = {
isPillNode: $isFolderPillNode,
extractData: (node: any) => (node as FolderPillNode).getFolderPath(),
extractData: (node: LexicalNode) => (node as FolderPillNode).getFolderPath(),
};
/**

View file

@ -7,9 +7,9 @@ import { $getRoot, LexicalNode } from "lexical";
*/
export interface PillSyncConfig<T> {
/** 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;
/**

View file

@ -22,10 +22,13 @@ type NoteData = { path: string; basename: string };
*/
const notePillConfig: PillSyncConfig<NoteData> = {
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
};

View file

@ -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<any> & IPillNode {
function $isPillNode(node: LexicalNode): node is DecoratorNode<React.ReactNode> & 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;

View file

@ -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<string> = {
isPillNode: $isToolPillNode,
extractData: (node: any) => (node as ToolPillNode).getToolName(),
extractData: (node: LexicalNode) => (node as ToolPillNode).getToolName(),
};
/**

View file

@ -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<string> = {
isPillNode: $isURLPillNode,
extractData: (node: any) => (node as URLPillNode).getURL(),
extractData: (node: LexicalNode) => (node as URLPillNode).getURL(),
};
/**

View file

@ -70,7 +70,7 @@ describe("parseTextForPills", () => {
});
// Mock TFile constructor to set properties
MockTFile.mockImplementation(function (this: any) {
MockTFile.mockImplementation(function (this: Record<string, unknown>) {
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<string, unknown>) {
this.basename = "Test Note";
this.path = "Test Note.md";
});

View file

@ -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<string, unknown>;
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)}`
);
}

View file

@ -117,7 +117,7 @@ function AddProjectModalContent({
const handleInputChange = (
field: string,
value: string | number | string[] | Record<string, any>
value: string | number | string[] | Record<string, unknown>
) => {
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(".")) {

View file

@ -64,7 +64,7 @@ type ActiveSection = "tags" | "folders" | "files" | "extensions" | "ignoreFiles"
type ActiveItem = string | null;
interface SectionHeaderProps {
IconComponent: React.ComponentType<any>;
IconComponent: React.ComponentType<{ className?: string }>;
title: string;
iconColorClassName: string;
onAddClick: () => void;
@ -112,7 +112,7 @@ interface SectionItem {
interface SectionListProps {
title: string;
IconComponent: React.ComponentType<any>;
IconComponent: React.ComponentType<{ className?: string }>;
iconColorClassName: string;
items: SectionItem[];
activeItem: string | null;

View file

@ -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
}

View file

@ -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<T = any> {
export interface MobileCardDropdownAction<T = object> {
icon: React.ReactNode;
label: string;
onClick: (item: T) => void | Promise<void>;

View file

@ -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
});

View file

@ -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

View file

@ -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<string, unknown> {
return (window.app as unknown as { plugins: { plugins: Record<string, unknown> } }).plugins
.plugins;
}
function setPlugins(plugins: Record<string, unknown>): void {
(window.app as unknown as { plugins: { plugins: Record<string, unknown> } }).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,

View file

@ -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<string, TFile>;
@ -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<string, unknown>).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<string, unknown>).getAbstractFileByPath = jest.fn();
fileParserManager = {
supportsExtension: jest.fn(

View file

@ -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<string> {
// 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}`;
}
/**

View file

@ -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<string> {
try {

View file

@ -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;
}

View file

@ -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();

View file

@ -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") {

View file

@ -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}`);

View file

@ -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<typeof thinkStreamer.processChunk>[0]);
if (abortController.signal.aborted) break;
}
} catch (error) {

View file

@ -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<string, unknown>;
};
return Object.assign({}, ...utilities) as CSSRuleObject;
};
const generateOpacityClasses =
(e: any, opacityUtilities: Record<string, any>) => (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<string, any>) =>
(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<string, any> = {};
const opacityUtilities: CSSRuleObject = {};
// 处理所有颜色相关的主题配置
const processThemeColors = (themeKey: string, prefix?: string) => {

View file

@ -912,7 +912,11 @@ export default class CopilotPlugin extends Plugin {
await this.handleNewChat();
}
async customSearchDB(query: string, salientTerms: string[], textWeight: number): Promise<any[]> {
async customSearchDB(
query: string,
salientTerms: string[],
textWeight: number
): Promise<{ content: string; metadata: Record<string, unknown> }[]> {
const settings = getSettings();
// Run FilterRetriever for guaranteed title/tag matches

View file

@ -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<App>;
let mockVault: jest.Mocked<Vault>;
let mockChatModel: jest.Mocked<BaseChatModel>;
let mockSettings: any;
let mockSettings: Partial<CopilotSettings>;
beforeEach(() => {
jest.clearAllMocks();

View file

@ -12,7 +12,7 @@ export interface MiyoServiceConfig {
pid: number;
}
type NodeRequire = (id: string) => any;
type NodeRequire = (id: string) => unknown;
type ServiceConfigReadResult = MiyoServiceConfig | "missing" | null;
/**

View file

@ -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<string, any>): Promise<boolean | undefined> {
export async function checkIsPlusUser(
context?: Record<string, unknown>
): Promise<boolean | undefined> {
// Self-host mode with valid plan validation bypasses license check
if (isSelfHostModeValid()) {
return true;

View file

@ -11,7 +11,7 @@ const LEGACY_INDEX_SUFFIX = ".json";
export interface ChunkMetadata {
numPartitions: number;
vectorLength: number;
schema: any;
schema: Record<string, string>;
lastModified: number;
documentPartitions: Record<string, number>;
}
@ -54,10 +54,10 @@ export class ChunkedStorage {
}
private distributeDocumentsToPartitions(
documents: any[],
documents: Record<string, unknown>[],
numPartitions: number
): Map<number, any[]> {
const partitions = new Map<number, any[]>();
): Map<number, Record<string, unknown>[]> {
const partitions = new Map<number, Record<string, unknown>[]>();
const documentPartitions: Record<string, number> = {};
for (let i = 0; i < numPartitions; i++) {
@ -106,6 +106,7 @@ export class ChunkedStorage {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is required here as it controls Orama's type inference
async saveDatabase(db: Orama<any>): Promise<void> {
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<string, unknown>) || {});
const docsData = (rawData as unknown as { docs?: { docs?: unknown } }).docs?.docs;
const rawDocs: Record<string, unknown>[] = Array.isArray(docsData)
? (docsData as Record<string, unknown>[])
: (Object.values((docsData as Record<string, unknown>) || {}) as Record<string, unknown>[]);
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<string, unknown> }).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<string, unknown> };
};
};
}
).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<string, unknown> } };
};
}
).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<any> is required here as it controls Orama's type inference
async loadDatabase(): Promise<Orama<any>> {
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<string, string>;
};
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<string, any> = {};
const orderedDocs: Record<string, unknown> = {};
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<string, unknown>))
.find((doc: any) => doc.id === internalId);
.find((doc) => (doc as Record<string, unknown>).id === internalId);
if (doc) {
orderedDocs[nextDocId.toString()] = doc;

View file

@ -28,10 +28,11 @@ export interface OramaDocument {
tags: string[];
extension: string;
nchars: number;
metadata: Record<string, any>;
metadata: Record<string, unknown>;
}
export class DBOperations {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
private oramaDb: Orama<any> | 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<any> is the correct API type
async initializeDB(embeddingInstance: Embeddings | undefined): Promise<Orama<any> | undefined> {
try {
if (!this.isInitialized) {
@ -209,6 +211,7 @@ export class DBOperations {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
public getDb(): Orama<any> | 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<any> is the correct API type
private async createNewDb(embeddingInstance: Embeddings | undefined): Promise<Orama<any>> {
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<any> is the correct API type
public static async getDocsByPath(db: Orama<any>, 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<any> is the correct API type
db: Orama<any>,
embedding: number[],
options: {
@ -326,6 +332,7 @@ export class DBOperations {
return result.hits;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
public static async getLatestFileMtime(db: Orama<any> | undefined): Promise<number> {
if (!db) throw new Error("DB not initialized");
@ -396,14 +403,14 @@ export class DBOperations {
try {
await insert(db, docToSave as Parameters<typeof insert>[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<any>): Promise<any[]> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
public static async getAllDocuments(db: Orama<any>): Promise<OramaDocument[]> {
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<number> {
@ -674,28 +682,17 @@ export class DBOperations {
});
}
async getDocsJsonByPaths(paths: string[]): Promise<Record<string, any[]>> {
async getDocsJsonByPaths(paths: string[]): Promise<Record<string, OramaDocument[]>> {
if (!this.oramaDb) {
throw new CustomError("Semantic index database not found.");
}
const result: Record<string, any[]> = {};
const result: Record<string, OramaDocument[]> = {};
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);
}
}

View file

@ -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<any> is required as Orama controls this type
function getHighestScoreHits(hits: Result<InternalTypedDocument<any>>[], currentFilePath: string) {
const hitMap = new Map<string, number>();
for (const hit of hits) {
@ -103,6 +104,7 @@ async function calculateSimilarityScoreFromOrama({
filePath,
currentNoteEmbeddings,
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
db: Orama<any>;
filePath: string;
currentNoteEmbeddings: number[][];

View file

@ -162,7 +162,7 @@ export class HybridRetriever extends BaseRetriever {
const db = await VectorStoreManager.getInstance().getDb();
const searchParams: any = {
const searchParams: Record<string, unknown> = {
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

View file

@ -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<any> is the correct API type
public getDb(): Orama<any> | undefined {
return this.dbOps.getDb();
}

View file

@ -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<string, unknown>;
}>
> {
const allChunks: Array<{ content: string; chunkId: string; fileInfo: any }> = [];
const allChunks: Array<{
content: string;
chunkId: string;
fileInfo: Record<string, unknown>;
}> = [];
// 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<string, unknown> }>;
}
): 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: [],

View file

@ -170,7 +170,7 @@ export class MergedSemanticRetriever extends BaseRetriever {
* @returns Decorated Document instance
*/
private decorateDocument(doc: Document, source: SourceKind): Document {
const metadata: Record<string, any> = {
const metadata: Record<string, unknown> = {
...(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<string, any>): number {
private extractBaseScore(metadata: Record<string, unknown>): 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<string, any>): boolean {
private hasTagMatch(metadata: Record<string, unknown>): boolean {
const explanation = metadata?.explanation as { lexicalMatches?: unknown } | undefined;
if (!explanation) {
return false;

View file

@ -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;
}
/**

View file

@ -73,7 +73,7 @@ jest.mock("./chunks", () => {
import { SearchCore, selectDiverseTopK } from "./SearchCore";
describe("SearchCore retrieve", () => {
let mockApp: any;
let mockApp: unknown;
beforeEach(() => {
buildFromCandidatesMock.mockReset();

View file

@ -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<Document[]> {
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<string>;
getChunkTextSync?: (id: string) => string | undefined;
};
if (typeof cm.getChunkText === "function") {
chunkContent = await cm.getChunkText(result.id);
} else if (typeof cm.getChunkTextSync === "function") {

View file

@ -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<string, TFile>;
@ -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}`);
}

View file

@ -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<string, Record<string, number>>;
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", () => {

View file

@ -276,6 +276,7 @@ export default class VectorStoreManager {
this.indexBackend.onunload();
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Orama<any> is the correct API type
public async getDb(): Promise<Orama<any>> {
await this.waitForInitialization();
const db = this.oramaBackend.getDb();

View file

@ -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<string, unknown>): 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<string, unknown>[]).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<string, unknown>[]).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<string, unknown>[]).map(
(model): StandardModel => ({
id: toStr(model.id, "") || toStr(model.name, String(Math.random())),
name: toStr(model.name, "") || toStr(model.id, "Unknown Model"),
provider: provider,
})
);

View file

@ -66,7 +66,7 @@ export const ModelEditModalContent: React.FC<ModelEditModalContentProps> = ({
// 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,

View file

@ -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<string> {
const messageId = await this.chatManager.sendMessage(

View file

@ -52,7 +52,7 @@ describe("SystemPromptManager", () => {
beforeEach(() => {
// Reset the singleton instance before each test
(SystemPromptManager as any).instance = undefined;
(SystemPromptManager as unknown as Record<string, unknown>).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<string, unknown>).instance = undefined;
expect(() => SystemPromptManager.getInstance()).toThrow(
"Vault is required for first initialization"
);

View file

@ -69,7 +69,7 @@ describe("SystemPromptRegister", () => {
let mockVault: Vault;
let register: SystemPromptRegister;
let vaultEventHandlers: Record<string, (...args: any[]) => unknown>;
let vaultEventHandlers: Record<string, (...args: unknown[]) => 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(),

View file

@ -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<string, unknown>;
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

View file

@ -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<MockTFile | MockTFolder>;
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

View file

@ -9,12 +9,12 @@ interface FileTreeNode {
extensionCounts?: Record<string, number>;
}
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 {

View file

@ -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<string, unknown>).chunkId ?? null,
isChunk: (doc.metadata as Record<string, unknown>).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<string, unknown>).chunkId ?? null,
isChunk: (doc.metadata as Record<string, unknown>).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<string, unknown>).chunkId ?? null,
isChunk: (doc.metadata as Record<string, unknown>).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 {

View file

@ -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<string, unknown>;
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<string, unknown>;
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 = /<document>([\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<string, unknown>;
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<string, unknown>).type === "local_search" &&
Array.isArray((parsed as Record<string, unknown>).documents)
) {
return (parsed as Record<string, unknown>).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<string, unknown>;
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<string, unknown>;
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<string, unknown>) : 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<string, unknown>) : 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<string, unknown>)
: 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<string, unknown>) : 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<string, unknown>) : 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);

Some files were not shown because too many files have changed in this diff Show more