From e03234137df98a1b0f8f0666dd4e543cf620b299 Mon Sep 17 00:00:00 2001 From: Zero Liu Date: Fri, 15 May 2026 18:26:40 -0700 Subject: [PATCH] Enable unsafe member access linting (#2474) --- eslint.config.mjs | 58 +------------------ src/LLMProviders/ChatOpenRouter.ts | 20 ++++--- src/LLMProviders/CustomOpenAIEmbeddings.ts | 4 +- .../chainRunner/AutonomousAgentChainRunner.ts | 8 ++- .../chainRunner/CopilotPlusChainRunner.ts | 2 +- .../chainRunner/utils/citationUtils.ts | 2 +- src/LLMProviders/chatModelManager.ts | 8 +-- src/LLMProviders/selfHostServices.ts | 19 +++--- src/commands/customCommandManager.ts | 34 ++++++----- src/commands/customCommandUtils.ts | 2 +- src/encryptionService.ts | 3 +- src/projects/projectUtils.ts | 4 +- src/search/chunkedStorage.ts | 29 +++++----- src/search/hybridRetriever.ts | 8 +-- src/system-prompts/systemPromptUtils.ts | 4 +- src/tools/FileParserManager.ts | 9 +-- src/utils.ts | 45 ++++++++------ 17 files changed, 118 insertions(+), 141 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 61d6ed85..69aa77f4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -103,10 +103,8 @@ export default [ // are candidates to enable in small follow-up PRs. // --- Heavy: any-flow through Obsidian/LangChain APIs --- - // no-unsafe-member-access: enabled globally; tests and heavy source files - // are exempted via per-file overrides below (see "no-unsafe-member-access - // exemptions"). Remaining source files (≤5 violations each) were fixed in - // this PR. + // no-unsafe-member-access: enabled globally; tests are exempted via the + // test-file override below. "@typescript-eslint/no-unsafe-assignment": "off", // enabled for tests below; follow-up PR for production "@typescript-eslint/no-unsafe-call": "off", // 107 violations @@ -158,58 +156,6 @@ export default [ }, }, - // no-unsafe-member-access exemptions: heavy source files that flow `any` - // through Obsidian / LangChain / Bedrock APIs. Counts are current as of the - // PR that enabled the rule; pick these off one at a time in follow-up PRs. - { - files: [ - "src/LLMProviders/BedrockChatModel.ts", // 106 - "src/LLMProviders/ChatOpenRouter.ts", // 28 - "src/LLMProviders/CustomOpenAIEmbeddings.ts", // 16 - "src/LLMProviders/brevilabsClient.ts", // 7 - "src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts", // 13 - "src/LLMProviders/chainRunner/BaseChainRunner.ts", // 7 - "src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts", // 55 - "src/LLMProviders/chainRunner/VaultQAChainRunner.ts", // 9 - "src/LLMProviders/chainRunner/utils/ActionBlockStreamer.ts", // 8 - "src/LLMProviders/chainRunner/utils/ThinkBlockStreamer.ts", // 33 - "src/LLMProviders/chainRunner/utils/chatHistoryUtils.ts", // 17 - "src/LLMProviders/chainRunner/utils/citationUtils.ts", // 11 - "src/LLMProviders/chainRunner/utils/finishReasonDetector.ts", // 29 - "src/LLMProviders/chainRunner/utils/modelAdapter.ts", // 9 - "src/LLMProviders/chainRunner/utils/promptPayloadRecorder.ts", // 12 - "src/LLMProviders/chainRunner/utils/searchResultUtils.ts", // 81 - "src/LLMProviders/chainRunner/utils/toolExecution.ts", // 9 - "src/LLMProviders/chatModelManager.ts", // 9 - "src/LLMProviders/selfHostServices.ts", // 9 - "src/commands/customCommandManager.ts", // 10 - "src/commands/customCommandUtils.ts", // 10 - "src/commands/index.ts", // 14 - "src/components/chat-components/ChatControls.tsx", // 8 - "src/components/chat-components/ChatInput.tsx", // 14 - "src/components/modals/SourcesModal.tsx", // 33 - "src/contextProcessor.ts", // 17 - "src/core/ChatPersistenceManager.ts", // 10 - "src/encryptionService.ts", // 6 - "src/projects/projectUtils.ts", // 38 - "src/search/chunkedStorage.ts", // 28 - "src/search/dbOperations.ts", // 27 - "src/search/hybridRetriever.ts", // 11 - "src/search/indexOperations.ts", // 15 - "src/search/v3/TieredLexicalRetriever.ts", // 9 - "src/settings/providerModels.ts", // 20 - "src/system-prompts/systemPromptUtils.ts", // 9 - "src/tools/FileParserManager.ts", // 11 - "src/tools/SearchTools.ts", // 11 - "src/tools/ToolResultFormatter.ts", // 106 - "src/utils.ts", // 49 - "src/utils/rateLimitUtils.ts", // 10 - ], - rules: { - "@typescript-eslint/no-unsafe-member-access": "off", - }, - }, - // Tests have been cleaned of unsafe `any` assignments. Production code // (~499 violations) is a follow-up; keep tests enforced. { diff --git a/src/LLMProviders/ChatOpenRouter.ts b/src/LLMProviders/ChatOpenRouter.ts index c2f52193..3bf7a617 100644 --- a/src/LLMProviders/ChatOpenRouter.ts +++ b/src/LLMProviders/ChatOpenRouter.ts @@ -448,13 +448,19 @@ export class ChatOpenRouter extends ChatOpenAI { return undefined; } - return toolCalls.map((call) => ({ - name: call?.function?.name, - args: call?.function?.arguments, - id: call?.id, - index: call?.index, - type: "tool_call_chunk" as const, - })); + return toolCalls.map((rawCall) => { + const call = rawCall as + | { function?: { name?: string; arguments?: string }; id?: string; index?: number } + | null + | undefined; + return { + name: call?.function?.name, + args: call?.function?.arguments, + id: call?.id, + index: call?.index, + type: "tool_call_chunk" as const, + }; + }); } /** diff --git a/src/LLMProviders/CustomOpenAIEmbeddings.ts b/src/LLMProviders/CustomOpenAIEmbeddings.ts index cba18fb7..68e98f65 100644 --- a/src/LLMProviders/CustomOpenAIEmbeddings.ts +++ b/src/LLMProviders/CustomOpenAIEmbeddings.ts @@ -55,13 +55,13 @@ export class CustomOpenAIEmbeddings extends OpenAIEmbeddings { ); } - const responseData = await response.json(); + const responseData = (await response.json()) as { data?: Array<{ embedding?: unknown }> }; if (!responseData.data || !Array.isArray(responseData.data)) { throw new Error("Invalid API response format: missing or invalid data array"); } - return (responseData.data as Array<{ embedding?: unknown }>).map((item) => { + return responseData.data.map((item) => { if (!item.embedding || !Array.isArray(item.embedding)) { throw new Error("Invalid API response format: missing or invalid embedding array"); } diff --git a/src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts b/src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts index 965968b3..5927ef84 100644 --- a/src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts +++ b/src/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts @@ -1030,9 +1030,15 @@ export class AutonomousAgentChainRunner extends CopilotPlusChainRunner { }) ); - for await (const chunk of stream) { + for await (const rawChunk of stream) { if (abortController.signal.aborted) break; + const chunk = rawChunk as { + response_metadata?: { finish_reason?: string }; + tool_call_chunks?: unknown; + content?: unknown; + }; + // Check for MALFORMED_FUNCTION_CALL error - throw to trigger fallback const finishReason = chunk.response_metadata?.finish_reason; if (finishReason === "MALFORMED_FUNCTION_CALL") { diff --git a/src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts b/src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts index cce52281..02e45428 100644 --- a/src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts +++ b/src/LLMProviders/chainRunner/CopilotPlusChainRunner.ts @@ -1224,7 +1224,7 @@ Include your extracted terms as: [SALIENT_TERMS: term1, term2, term3]`; } try { - const parsed = JSON.parse(toolResult.result); + const parsed = JSON.parse(toolResult.result) as { type?: unknown; documents?: unknown }; const searchResults = parsed && typeof parsed === "object" && diff --git a/src/LLMProviders/chainRunner/utils/citationUtils.ts b/src/LLMProviders/chainRunner/utils/citationUtils.ts index 8bc03f36..97ba7e28 100644 --- a/src/LLMProviders/chainRunner/utils/citationUtils.ts +++ b/src/LLMProviders/chainRunner/utils/citationUtils.ts @@ -334,7 +334,7 @@ export function normalizeCitations(content: string, map: Map): s }); // Handle multiple citations: [^n, ^m] -> [n, m] - result = result.replace(/\[\^(\d+(?:\s*,\s*\^?\d+)*)\]/g, (match, citationList) => { + result = result.replace(/\[\^(\d+(?:\s*,\s*\^?\d+)*)\]/g, (match, citationList: string) => { // Split and process each number in the list const processedNumbers = citationList .split(",") diff --git a/src/LLMProviders/chatModelManager.ts b/src/LLMProviders/chatModelManager.ts index 6b7b940f..329cd6e4 100644 --- a/src/LLMProviders/chatModelManager.ts +++ b/src/LLMProviders/chatModelManager.ts @@ -55,10 +55,10 @@ const GOOGLE_SAFETY_SETTINGS_BLOCK_NONE: SafetySetting[] = [ // 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 }> -) => { + +( + BaseLanguageModel.prototype as { getNumTokens: (...args: unknown[]) => Promise } +).getNumTokens = async (content: string | Array<{ type: string; text?: string }>) => { const text = typeof content === "string" ? content diff --git a/src/LLMProviders/selfHostServices.ts b/src/LLMProviders/selfHostServices.ts index 2a4cfe4e..e0433c2d 100644 --- a/src/LLMProviders/selfHostServices.ts +++ b/src/LLMProviders/selfHostServices.ts @@ -60,7 +60,9 @@ async function firecrawlSearch(query: string, apiKey: string): Promise; + citations?: unknown; + }; const content = json?.choices?.[0]?.message?.content ?? ""; - const citations: string[] = Array.isArray(json?.citations) ? json.citations : []; + const citations: string[] = Array.isArray(json?.citations) ? (json.citations as string[]) : []; return { content, citations }; } @@ -154,7 +159,7 @@ export async function selfHostYoutube4llm(url: string): Promise { - frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu; - frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu; - frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order; - frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey; - frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs; - }); + await app.fileManager.processFrontMatter( + commandFile, + (frontmatter: Record) => { + frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu; + frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu; + frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order; + frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey; + frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs; + } + ); if (!mergedOptions.skipStoreUpdate) { updateCachedCommand(command, command.title); @@ -126,13 +129,16 @@ export class CustomCommandManager { if (commandFile instanceof TFile) { await app.vault.modify(commandFile, command.content); - await app.fileManager.processFrontMatter(commandFile, (frontmatter) => { - frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu; - frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu; - frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order; - frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey; - frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs; - }); + await app.fileManager.processFrontMatter( + commandFile, + (frontmatter: Record) => { + frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu; + frontmatter[COPILOT_COMMAND_SLASH_ENABLED] = command.showInSlashMenu; + frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ORDER] = command.order; + frontmatter[COPILOT_COMMAND_MODEL_KEY] = command.modelKey; + frontmatter[COPILOT_COMMAND_LAST_USED] = command.lastUsedMs; + } + ); } } finally { removePendingFileWrite(filePath); diff --git a/src/commands/customCommandUtils.ts b/src/commands/customCommandUtils.ts index 2519601f..1def76e0 100644 --- a/src/commands/customCommandUtils.ts +++ b/src/commands/customCommandUtils.ts @@ -492,7 +492,7 @@ export function getNextCustomCommandOrder(): number { export async function ensureCommandFrontmatter(file: TFile, command: CustomCommand) { try { addPendingFileWrite(file.path); - await app.fileManager.processFrontMatter(file, (frontmatter) => { + await app.fileManager.processFrontMatter(file, (frontmatter: Record) => { if (frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] == null) { frontmatter[COPILOT_COMMAND_CONTEXT_MENU_ENABLED] = command.showInContextMenu; } diff --git a/src/encryptionService.ts b/src/encryptionService.ts index bf10ba7a..7ca18847 100644 --- a/src/encryptionService.ts +++ b/src/encryptionService.ts @@ -41,7 +41,8 @@ function getSafeStorage(): SafeStorage | null { if (safeStorageInternal) return safeStorageInternal; try { // eslint-disable-next-line @typescript-eslint/no-require-imports - safeStorageInternal = require("electron")?.remote?.safeStorage as SafeStorage | null; + const electron = require("electron") as { remote?: { safeStorage?: SafeStorage } } | undefined; + safeStorageInternal = electron?.remote?.safeStorage ?? null; return safeStorageInternal; } catch { return null; diff --git a/src/projects/projectUtils.ts b/src/projects/projectUtils.ts index c779c88d..75e13539 100644 --- a/src/projects/projectUtils.ts +++ b/src/projects/projectUtils.ts @@ -62,7 +62,7 @@ export async function writeProjectFrontmatter( const webUrls = splitUrlsStringToArray(project.contextSource?.webUrls || ""); const youtubeUrls = splitUrlsStringToArray(project.contextSource?.youtubeUrls || ""); - await app.fileManager.processFrontMatter(file, (frontmatter) => { + await app.fileManager.processFrontMatter(file, (frontmatter: Record) => { // Reason: project.id is the stable logical identity, always set by createProject/migration. // Do NOT fallback to folderName — with name-based folders, folderName is derived from // project name, not id, so it cannot serve as an id substitute. @@ -423,7 +423,7 @@ export async function ensureProjectFrontmatter( try { if (!alreadyPending) addPendingFileWrite(file.path); - await app.fileManager.processFrontMatter(file, (frontmatter) => { + await app.fileManager.processFrontMatter(file, (frontmatter: Record) => { // Reason: do NOT fallback to record.folderName for id — with name-based folders, // folderName is derived from project name, not id. if (frontmatter[COPILOT_PROJECT_ID] == null && record.project.id) { diff --git a/src/search/chunkedStorage.ts b/src/search/chunkedStorage.ts index 197894be..6ba70d90 100644 --- a/src/search/chunkedStorage.ts +++ b/src/search/chunkedStorage.ts @@ -10,7 +10,6 @@ const LEGACY_INDEX_SUFFIX = ".json"; export interface ChunkMetadata { numPartitions: number; - vectorLength: number; schema: Record; lastModified: number; documentPartitions: Record; @@ -138,7 +137,6 @@ export class ChunkedStorage { if (!rawDocs || rawDocs.length === 0) { const metadata: ChunkMetadata = { numPartitions, - vectorLength: db.schema.embedding.match(/\d+/)[0], schema: db.schema, lastModified: Date.now(), documentPartitions: {}, @@ -158,7 +156,6 @@ export class ChunkedStorage { const metadata: ChunkMetadata = { numPartitions, - vectorLength: db.schema.embedding.match(/\d+/)[0], schema: db.schema, lastModified: Date.now(), documentPartitions: Object.fromEntries( @@ -242,7 +239,7 @@ export class ChunkedStorage { } } catch (error) { console.error(`Error saving database:`, error); - throw new CustomError(`Failed to save database: ${error.message}`); + throw new CustomError(`Failed to save database: ${(error as Error).message}`); } } @@ -285,14 +282,19 @@ export class ChunkedStorage { }); // Load and merge all partitions - let mergedData = null; - const allChunks = []; + type ChunkData = { + internalDocumentIDStore: { internalIdToId: string[] }; + docs: { docs: Record; count: number }; + index: { vectorIndexes: { embedding: { vectors: Record } } }; + }; + let mergedData: ChunkData | null = null; + const allChunks: ChunkData[] = []; // First, load all chunks for (let i = 0; i < metadata.numPartitions; i++) { const chunkPath = this.getChunkPath(i); if (await this.app.vault.adapter.exists(chunkPath)) { - const chunkData = JSON.parse(await this.app.vault.adapter.read(chunkPath)); + const chunkData = JSON.parse(await this.app.vault.adapter.read(chunkPath)) as ChunkData; allChunks.push(chunkData); // First chunk contains global data @@ -313,7 +315,7 @@ export class ChunkedStorage { for (const internalId of mergedData.internalDocumentIDStore.internalIdToId) { // Find document in any chunk const doc = allChunks - .flatMap((chunk) => Object.values(chunk.docs.docs as Record)) + .flatMap((chunk) => Object.values(chunk.docs.docs)) .find((doc) => (doc as Record).id === internalId); if (doc) { @@ -333,16 +335,13 @@ export class ChunkedStorage { // upsert cycles. These "ghost" IDs cause position mismatches after load, // where some user IDs point to wrong doc positions or undefined entries. mergedData.internalDocumentIDStore.internalIdToId = Object.values(orderedDocs).map( - (doc: { id: string }): string => doc.id + (doc): string => (doc as { id: string }).id ); // Merge vectors from all chunks mergedData.index.vectorIndexes.embedding.vectors = Object.assign( {}, - ...allChunks.map( - (chunk) => - (chunk.index?.vectorIndexes?.embedding?.vectors as Record) || {} - ) + ...allChunks.map((chunk) => chunk.index?.vectorIndexes?.embedding?.vectors || {}) ); // Load merged data into database @@ -350,7 +349,7 @@ export class ChunkedStorage { return newDb; } catch (error) { console.error(`Error loading database:`, error); - throw new CustomError(`Failed to load database: ${error.message}`); + throw new CustomError(`Failed to load database: ${(error as Error).message}`); } } @@ -373,7 +372,7 @@ export class ChunkedStorage { } } catch (error) { console.error(`Error clearing storage:`, error); - throw new CustomError(`Failed to clear storage: ${error.message}`); + throw new CustomError(`Failed to clear storage: ${(error as Error).message}`); } } diff --git a/src/search/hybridRetriever.ts b/src/search/hybridRetriever.ts index 47e34a8a..e272afc4 100644 --- a/src/search/hybridRetriever.ts +++ b/src/search/hybridRetriever.ts @@ -260,7 +260,7 @@ export class HybridRetriever extends BaseRetriever { created_at: hit.document.created_at, nchars: hit.document.nchars, // Expose chunkId explicitly for cross-engine deduplication - chunkId: hit.document.metadata?.chunkId, + chunkId: (hit.document.metadata as { chunkId?: string } | undefined)?.chunkId, }, }) ); @@ -268,8 +268,8 @@ export class HybridRetriever extends BaseRetriever { // Combine and deduplicate results const combinedResults = [...dailyNoteResultsWithContext, ...timeIntervalDocuments]; const uniqueResults = Array.from( - new Set(combinedResults.map((doc): string => doc.metadata.id as string)) - ).map((id) => combinedResults.find((doc) => doc.metadata.id === id)); + new Set(combinedResults.map((doc): string => (doc.metadata as { id: string }).id)) + ).map((id) => combinedResults.find((doc) => (doc.metadata as { id: string }).id === id)); return uniqueResults.filter((doc): doc is Document => doc !== undefined); } @@ -316,7 +316,7 @@ export class HybridRetriever extends BaseRetriever { created_at: hit.document.created_at, nchars: hit.document.nchars, // Expose chunkId explicitly for cross-engine deduplication - chunkId: hit.document.metadata?.chunkId, + chunkId: (hit.document.metadata as { chunkId?: string } | undefined)?.chunkId, }, }); }) diff --git a/src/system-prompts/systemPromptUtils.ts b/src/system-prompts/systemPromptUtils.ts index 08c8e6e0..22f9fa63 100644 --- a/src/system-prompts/systemPromptUtils.ts +++ b/src/system-prompts/systemPromptUtils.ts @@ -185,7 +185,7 @@ export async function ensurePromptFrontmatter(file: TFile, prompt: UserSystemPro if (!alreadyPending) { addPendingFileWrite(file.path); } - await app.fileManager.processFrontMatter(file, (frontmatter) => { + await app.fileManager.processFrontMatter(file, (frontmatter: Record) => { if (frontmatter[COPILOT_SYSTEM_PROMPT_CREATED] == null) { frontmatter[COPILOT_SYSTEM_PROMPT_CREATED] = createdMs; } @@ -247,7 +247,7 @@ export async function updatePromptDefaultFlag( if (!alreadyPending) { addPendingFileWrite(file.path); } - await app.fileManager.processFrontMatter(file, (frontmatter) => { + await app.fileManager.processFrontMatter(file, (frontmatter: Record) => { if (isDefault) { frontmatter[COPILOT_SYSTEM_PROMPT_DEFAULT] = true; } else { diff --git a/src/tools/FileParserManager.ts b/src/tools/FileParserManager.ts index 326faaa1..25d37c5f 100644 --- a/src/tools/FileParserManager.ts +++ b/src/tools/FileParserManager.ts @@ -371,13 +371,14 @@ export class Docs4LLMParser implements FileParser { } else if (Array.isArray(docs4llmResponse.response)) { // Handle array of documents from docs4llm const markdownParts: string[] = []; - for (const doc of docs4llmResponse.response) { - if (doc.content) { + for (const rawDoc of docs4llmResponse.response) { + const doc = rawDoc as { content?: { md?: string; text?: string } } | null | undefined; + if (doc?.content) { // Prioritize markdown content, then fallback to text content if (doc.content.md) { - markdownParts.push(doc.content.md as string); + markdownParts.push(doc.content.md); } else if (doc.content.text) { - markdownParts.push(doc.content.text as string); + markdownParts.push(doc.content.text); } } } diff --git a/src/utils.ts b/src/utils.ts index 78e91a88..5a360073 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -411,7 +411,7 @@ export interface ChatHistoryEntry { // TODO: Deprecated, use chatHistoryUtils.processRawChatHistory instead export function extractChatHistory(memoryVariables: MemoryVariables): ChatHistoryEntry[] { const chatHistory: ChatHistoryEntry[] = []; - const { history } = memoryVariables; + const history = memoryVariables.history as Array<{ content?: string }>; for (let i = 0; i < history.length; i += 2) { const userMessage = history[i]?.content || ""; @@ -694,12 +694,21 @@ export async function safeFetch( // Check if response is error status (only throw if throwOnHttpError is true) if (throwOnHttpError && response.status >= 400) { - let errorJson; + type ErrorJson = { + detail?: { reason?: string; message?: string } | string; + reason?: string; + message?: string; + }; + let errorJson: ErrorJson | null = null; try { - errorJson = typeof response.json === "string" ? JSON.parse(response.json) : response.json; + errorJson = ( + typeof response.json === "string" ? JSON.parse(response.json) : response.json + ) as ErrorJson; } catch { try { - errorJson = typeof response.text === "string" ? JSON.parse(response.text) : response.text; + errorJson = ( + typeof response.text === "string" ? JSON.parse(response.text) : response.text + ) as ErrorJson; } catch { errorJson = null; } @@ -710,15 +719,13 @@ export async function safeFetch( error.json = errorJson; // Handle nested error structure - if ( - errorJson?.detail?.reason === "Invalid license key" || - errorJson?.reason === "Invalid license key" - ) { + const detail = errorJson && typeof errorJson.detail === "object" ? errorJson.detail : undefined; + if (detail?.reason === "Invalid license key" || errorJson?.reason === "Invalid license key") { error.message = "Invalid license key"; - } else if (errorJson?.detail?.message || errorJson?.message) { - const message = errorJson?.detail?.message || errorJson?.message; - const reason = errorJson?.detail?.reason || errorJson?.reason; - error.message = reason ? `${message}: ${reason}` : message; + } else if (detail?.message || errorJson?.message) { + const message = detail?.message || errorJson?.message; + const reason = detail?.reason || errorJson?.reason; + error.message = reason ? `${message}: ${reason}` : (message ?? ""); } else if (errorJson?.detail) { error.message = JSON.stringify(errorJson.detail); } else if (errorJson) { @@ -990,7 +997,7 @@ export async function checkLatestVersion(): Promise<{ url: "https://api.github.com/repos/logancyang/obsidian-copilot/releases/latest", method: "GET", }); - const version = response.json.tag_name.replace("v", ""); + const version = (response.json as { tag_name: string }).tag_name.replace("v", ""); return { version, error: null }; } catch (error) { const errorMessage = error instanceof Error ? error.message : "Failed to check for updates"; @@ -1180,9 +1187,9 @@ export function extractTextFromChunk(content: unknown): string { return content; } if (Array.isArray(content)) { - return content + return (content as Array<{ type?: string; text?: string }>) .filter((item) => item.type === "text") - .map((item) => item.text as string) + .map((item) => item.text ?? "") .join(""); } // For any other type, return empty string @@ -1236,12 +1243,12 @@ export async function withSuppressedTokenWarnings(fn: () => Promise): Prom try { // Replace with filtered version - console.warn = function (...args) { + console.warn = function (...args: unknown[]) { // Ignore token counting warnings + const first = args[0]; if ( - args[0]?.includes && - (args[0].includes("Failed to calculate number of tokens") || - args[0].includes("Unknown model")) + typeof first === "string" && + (first.includes("Failed to calculate number of tokens") || first.includes("Unknown model")) ) { return; }