diff --git a/src/adapters/base-adapter.ts b/src/adapters/base-adapter.ts index b3a740e..9d0ae65 100644 --- a/src/adapters/base-adapter.ts +++ b/src/adapters/base-adapter.ts @@ -1,5 +1,4 @@ import { spawn, ChildProcess } from "child_process"; -import path from "path"; import { logger } from "../shared/logger"; import { IDEAdapter, ModelInfo, TrackedFiles } from "../shared/types"; @@ -60,7 +59,8 @@ const FILE_READ_PATTERNS = [ /(?:read(?:ing)?|analyz(?:e|ed|ing)|inspect(?:ed|ing)?|review(?:ed|ing)?|look(?:ed|ing)?\s+at|open(?:ed|ing)?)\s+[`'"]?([^\s`'"]+\.\w{1,10})[`'"]?/gi, ]; -const FILE_PATH_PATTERN = /(?:^|\s)([a-zA-Z]?:?(?:\/|\\)?(?:[\w.\-]+(?:\/|\\))+[\w.\-]+\.\w{1,10})(?:\s|$|[,;:)`'"])/g; +const FILE_PATH_PATTERN = + /(?:^|\s)([a-zA-Z]?:?(?:\/|\\)?(?:[\w.-]+(?:\/|\\))+[\w.-]+\.\w{1,10})(?:\s|$|[,;:)`'"])/g; export abstract class BaseAdapter implements IDEAdapter { protected connected: boolean = false; @@ -85,7 +85,7 @@ export abstract class BaseAdapter implements IDEAdapter { return formatted; } - protected exitCodeIndicatesSuccess(code: number | null, output: string): boolean { + protected exitCodeIndicatesSuccess(code: number | null, _output: string): boolean { return code === 0; } @@ -96,9 +96,12 @@ export abstract class BaseAdapter implements IDEAdapter { try { const { exec } = require("child_process"); await new Promise((resolve, reject) => { - exec(`${config.cliCommand} --version`, (error: any, stdout: string) => { - if (error) reject(error); - else resolve(stdout); + exec(`${config.cliCommand} --version`, (error: Error | null, stdout: string) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } }); }); logger.debug(`${config.displayName} installed`); @@ -288,10 +291,17 @@ export abstract class BaseAdapter implements IDEAdapter { try { const { exec } = require("child_process"); await new Promise((resolve, reject) => { - exec(`${config.cliCommand} --version`, { timeout: 5000 }, (error: any, stdout: string) => { - if (error) reject(error); - else resolve(stdout); - }); + exec( + `${config.cliCommand} --version`, + { timeout: 5000 }, + (error: Error | null, stdout: string) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }, + ); }); return true; } catch (error) { @@ -327,7 +337,9 @@ export abstract class BaseAdapter implements IDEAdapter { let match; while ((match = pattern.exec(cleaned)) !== null) { const fp = this.normalizeFilePath(match[1]); - if (fp) this.modifiedFiles.add(fp); + if (fp) { + this.modifiedFiles.add(fp); + } } } @@ -336,7 +348,9 @@ export abstract class BaseAdapter implements IDEAdapter { let match; while ((match = pattern.exec(cleaned)) !== null) { const fp = this.normalizeFilePath(match[1]); - if (fp) this.readFiles.add(fp); + if (fp) { + this.readFiles.add(fp); + } } } @@ -352,18 +366,60 @@ export abstract class BaseAdapter implements IDEAdapter { private normalizeFilePath(raw: string): string | null { const trimmed = raw.trim().replace(/['"`,;:)]+$/, ""); - if (!trimmed || trimmed.length < 3) return null; + if (!trimmed || trimmed.length < 3) { + return null; + } const ext = trimmed.split(".").pop() || ""; const codeExtensions = [ - "ts", "tsx", "js", "jsx", "py", "rs", "go", "java", "c", "cpp", "h", "hpp", - "cs", "rb", "php", "swift", "kt", "scala", "vue", "svelte", "html", "css", - "scss", "less", "json", "yaml", "yml", "toml", "xml", "md", "txt", "sql", - "sh", "bash", "zsh", "ps1", "bat", "cmd", "dockerfile", "makefile", + "ts", + "tsx", + "js", + "jsx", + "py", + "rs", + "go", + "java", + "c", + "cpp", + "h", + "hpp", + "cs", + "rb", + "php", + "swift", + "kt", + "scala", + "vue", + "svelte", + "html", + "css", + "scss", + "less", + "json", + "yaml", + "yml", + "toml", + "xml", + "md", + "txt", + "sql", + "sh", + "bash", + "zsh", + "ps1", + "bat", + "cmd", + "dockerfile", + "makefile", ]; - if (!codeExtensions.includes(ext.toLowerCase())) return null; + if (!codeExtensions.includes(ext.toLowerCase())) { + return null; + } - if (trimmed.includes("node_modules") || trimmed.includes(".git/")) return null; + if (trimmed.includes("node_modules") || trimmed.includes(".git/")) { + return null; + } return trimmed; } @@ -395,9 +451,15 @@ export abstract class BaseAdapter implements IDEAdapter { for (const line of lines) { const trimmed = line.trim(); - if (!trimmed) continue; - if (isCliNoiseLine(trimmed)) continue; - if (isDiffLine(trimmed)) continue; + if (!trimmed) { + continue; + } + if (isCliNoiseLine(trimmed)) { + continue; + } + if (isDiffLine(trimmed)) { + continue; + } kept.push(line); } @@ -411,7 +473,9 @@ export abstract class BaseAdapter implements IDEAdapter { for (const line of lines) { const trimmed = line.trim(); - if (isCliNoiseLine(trimmed)) continue; + if (isCliNoiseLine(trimmed)) { + continue; + } kept.push(line); } @@ -470,6 +534,7 @@ export abstract class BaseAdapter implements IDEAdapter { } export function stripAnsi(text: string): string { + // eslint-disable-next-line no-control-regex return text.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\[[0-9;]*[A-Za-z]/g, ""); } @@ -492,7 +557,9 @@ export function isDiffLine(trimmed: string): boolean { } export function isCliNoiseLine(trimmed: string): boolean { - if (!trimmed) return true; + if (!trimmed) { + return true; + } return CLI_NOISE_PATTERNS.some((p) => p.test(trimmed)); } diff --git a/src/adapters/ollama-claude-code.ts b/src/adapters/ollama-claude-code.ts index a22baf1..b187ffb 100644 --- a/src/adapters/ollama-claude-code.ts +++ b/src/adapters/ollama-claude-code.ts @@ -52,17 +52,17 @@ export class OllamaClaudeCodeAdapter extends BaseAdapter { throw new Error("Ollama not responding"); } - const data: any = await response.json(); + const data = (await response.json()) as { models?: Array<{ name: string }> }; const models = data.models || []; if (models.length === 0) { throw new Error("No models found. Please pull a model first."); } - const modelExists = models.some((m: any) => m.name === this.ollamaModel); + const modelExists = models.some((m) => m.name === this.ollamaModel); if (!modelExists) { logger.debug(`Model ${this.ollamaModel} not found`); - logger.debug(` Available models: ${models.map((m: any) => m.name).join(", ")}`); + logger.debug(` Available models: ${models.map((m) => m.name).join(", ")}`); throw new Error(`Model ${this.ollamaModel} not available`); } @@ -97,7 +97,7 @@ Session: ${this.currentSessionId || "None"}`; try { const response = await fetch("http://localhost:11434/api/tags"); - const data: any = await response.json(); + const data = (await response.json()) as { models?: Array<{ name: string }> }; const models = data.models || []; return `${this.getStatusText()} @@ -112,10 +112,17 @@ Available models: ${models.length}`; const config = this.getConfig(); const { exec } = require("child_process"); await new Promise((resolve, reject) => { - exec(`${config.cliCommand} --version`, { timeout: 5000 }, (error: any, stdout: string) => { - if (error) reject(error); - else resolve(stdout); - }); + exec( + `${config.cliCommand} --version`, + { timeout: 5000 }, + (error: Error | null, stdout: string) => { + if (error) { + reject(error); + } else { + resolve(stdout); + } + }, + ); }); const response = await fetch("http://localhost:11434/api/tags", { @@ -125,9 +132,9 @@ Available models: ${models.length}`; return false; } - const data: any = await response.json(); + const data = (await response.json()) as { models?: Array<{ name: string }> }; const models = data.models || []; - return models.some((m: any) => m.name === this.ollamaModel); + return models.some((m) => m.name === this.ollamaModel); } catch (error) { logger.debug(`Ollama health check failed: ${error}`); return false; @@ -160,9 +167,9 @@ Available models: ${models.length}`; try { const response = await fetch("http://localhost:11434/api/tags"); if (response.ok) { - const data: any = await response.json(); + const data = (await response.json()) as { models?: Array<{ name: string }> }; const models = data.models || []; - this.cachedModels = models.map((m: any) => ({ + this.cachedModels = models.map((m) => ({ id: m.name, name: m.name, })); diff --git a/src/adapters/openai-codex.ts b/src/adapters/openai-codex.ts index b976458..f6268a0 100644 --- a/src/adapters/openai-codex.ts +++ b/src/adapters/openai-codex.ts @@ -25,7 +25,15 @@ export class CodexAdapter extends BaseAdapter { } protected buildArgs(fullInstruction: string): string[] { - return ["exec", "--full-auto", "-m", this.codexModel, "--cd", this.projectPath, fullInstruction]; + return [ + "exec", + "--full-auto", + "-m", + this.codexModel, + "--cd", + this.projectPath, + fullInstruction, + ]; } protected exitCodeIndicatesSuccess(code: number | null, output: string): boolean { diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts index 8aec5e3..c2edfbf 100644 --- a/src/cli/commands/auth.ts +++ b/src/cli/commands/auth.ts @@ -5,7 +5,6 @@ import makeWASocket, { useMultiFileAuthState, fetchLatestBaileysVersion, makeCacheableSignalKeyStore, - DisconnectReason, } from "@whiskeysockets/baileys"; import chalk from "chalk"; import qrcode from "qrcode-terminal"; @@ -21,8 +20,9 @@ const CONFIG_DIR = path.join(os.homedir(), ".txtcode"); const CONFIG_FILE = path.join(CONFIG_DIR, "config.json"); const WA_AUTH_DIR = path.join(CONFIG_DIR, ".wacli_auth"); +type BaileysLogger = NonNullable[0]["logger"]>; const noop = () => {}; -const silentLogger = { +const silentLogger: BaileysLogger = { level: "silent" as const, fatal: noop, error: noop, @@ -31,7 +31,7 @@ const silentLogger = { debug: noop, trace: noop, child: () => silentLogger, -} as any; +} as BaileysLogger; // Validate API key function validateApiKeyFormat(apiKey: string): { valid: boolean; error?: string } { @@ -44,15 +44,30 @@ function validateApiKeyFormat(apiKey: string): { valid: boolean; error?: string return { valid: true }; } -async function authenticateWhatsApp(): Promise { - return new Promise(async (resolve, reject) => { - let sock: any = null; +function authenticateWhatsApp(): Promise { + let resolvePromise!: () => void; + let rejectPromise!: (err: Error) => void; + + const promise = new Promise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + + const closeSock = (s: unknown) => { + try { + (s as { ws?: { close: () => void } })?.ws?.close(); + } catch { + // Ignore + } + }; + + (async () => { + let sock: ReturnType | null = null; let pairingComplete = false; let connectionTimeout: NodeJS.Timeout; let connectionAttempted = false; try { - // Check if already authenticated and offer to clear if (fs.existsSync(WA_AUTH_DIR)) { const files = fs.readdirSync(WA_AUTH_DIR); if (files.length > 0) { @@ -60,16 +75,14 @@ async function authenticateWhatsApp(): Promise { console.log(chalk.gray("Clearing old session to start fresh...")); console.log(); - // Clear old session files to avoid 405 errors try { fs.rmSync(WA_AUTH_DIR, { recursive: true, force: true }); fs.mkdirSync(WA_AUTH_DIR, { recursive: true }); - } catch (e) { + } catch { console.log(chalk.yellow("Warning: Could not clear old session")); } } } else { - // Create directory if it doesn't exist fs.mkdirSync(WA_AUTH_DIR, { recursive: true }); } @@ -77,26 +90,19 @@ async function authenticateWhatsApp(): Promise { console.log(); const { state, saveCreds } = await useMultiFileAuthState(WA_AUTH_DIR); - - // Fetch latest Baileys version (like openclaw does) const { version } = await fetchLatestBaileysVersion(); - // Set a longer timeout for initial connection (60 seconds) connectionTimeout = setTimeout(() => { if (!pairingComplete && sock) { - try { - sock.ws?.close(); - } catch (e) { - // Ignore - } + closeSock(sock); if (!connectionAttempted) { - reject( + rejectPromise( new Error( "Connection timeout - No response from WhatsApp servers. Please check your internet connection.", ), ); } else { - reject(new Error("QR code generation timeout - Please try again.")); + rejectPromise(new Error("QR code generation timeout - Please try again.")); } } }, 60000); @@ -118,7 +124,7 @@ async function authenticateWhatsApp(): Promise { sock.ev.on("creds.update", saveCreds); - sock.ev.on("connection.update", async (update: any) => { + sock.ev.on("connection.update", async (update) => { connectionAttempted = true; const { connection, qr, lastDisconnect } = update; @@ -133,15 +139,10 @@ async function authenticateWhatsApp(): Promise { console.log(chalk.gray("Open WhatsApp → Settings → Linked Devices → Link a Device")); console.log(); - // Set new timeout for QR scanning (2 minutes) connectionTimeout = setTimeout(() => { if (!pairingComplete) { - try { - sock.ws?.close(); - } catch (e) { - // Ignore - } - reject(new Error("QR code scan timeout - Please try again")); + closeSock(sock); + rejectPromise(new Error("QR code scan timeout - Please try again")); } }, 120000); } @@ -151,39 +152,29 @@ async function authenticateWhatsApp(): Promise { pairingComplete = true; console.log(chalk.green("\n[OK] WhatsApp authenticated successfully!")); - // Close socket and resolve setTimeout(() => { - try { - sock.ws?.close(); - } catch (e) { - // Ignore - } - resolve(); + closeSock(sock); + resolvePromise(); }, 500); } if (connection === "close" && !pairingComplete) { clearTimeout(connectionTimeout); - const statusCode = lastDisconnect?.error?.output?.statusCode; + const statusCode = (lastDisconnect?.error as { output?: { statusCode?: number } })?.output + ?.statusCode; const errorMessage = lastDisconnect?.error?.message || "Unknown error"; - // If connection closed before showing QR, it's a connection failure if (!hasShownQR) { - try { - sock.ws?.close(); - } catch (e) { - // Ignore - } + closeSock(sock); - // Special handling for 405 errors if (statusCode === 405) { - reject( + rejectPromise( new Error( `WhatsApp connection failed (Error 405). This usually means:\n • WhatsApp updated their protocol and the library needs updating\n • Try updating: npm install @whiskeysockets/baileys@latest\n • Or use Telegram/Discord instead (more reliable)`, ), ); } else { - reject( + rejectPromise( new Error( `Failed to connect to WhatsApp servers (${statusCode || "no status"}). ${errorMessage}. Please check your internet connection and try again.`, ), @@ -192,19 +183,13 @@ async function authenticateWhatsApp(): Promise { return; } - // 515 means pairing successful but needs restart - OpenClaw pattern if (statusCode === 515 && hasShownQR) { console.log( chalk.cyan("\n[INFO] WhatsApp pairing complete, restarting connection...\n"), ); - try { - sock.ws?.close(); - } catch (e) { - // Ignore - } + closeSock(sock); - // Restart connection without QR const { state: newState, saveCreds: newSaveCreds } = await useMultiFileAuthState(WA_AUTH_DIR); const retrySock = makeWASocket({ @@ -215,53 +200,37 @@ async function authenticateWhatsApp(): Promise { retrySock.ev.on("creds.update", newSaveCreds); - retrySock.ev.on("connection.update", async (retryUpdate: any) => { + retrySock.ev.on("connection.update", (retryUpdate) => { if (retryUpdate.connection === "open") { pairingComplete = true; console.log(chalk.green("[OK] WhatsApp linked successfully!\n")); - // Close and resolve setTimeout(() => { - try { - retrySock.ws?.close(); - } catch (e) { - // Ignore - } - resolve(); + closeSock(retrySock); + resolvePromise(); }, 500); } if (retryUpdate.connection === "close") { - try { - retrySock.ws?.close(); - } catch (e) { - // Ignore - } - reject(new Error("WhatsApp authentication failed after restart")); + closeSock(retrySock); + rejectPromise(new Error("WhatsApp authentication failed after restart")); } }); } else if (hasShownQR && statusCode !== 515) { - // Other errors after showing QR - try { - sock.ws?.close(); - } catch (e) { - // Ignore - } - reject(new Error(`WhatsApp authentication failed (code: ${statusCode})`)); + closeSock(sock); + rejectPromise(new Error(`WhatsApp authentication failed (code: ${statusCode})`)); } } }); } catch (error) { if (sock) { - try { - sock.ws?.close(); - } catch (e) { - // Ignore - } + closeSock(sock); } - reject(error); + rejectPromise(error instanceof Error ? error : new Error(String(error))); } - }); + })(); + + return promise; } export async function authCommand() { @@ -275,12 +244,15 @@ export async function authCommand() { // Check for existing configuration const existingConfig = loadConfig(); - if (existingConfig && existingConfig.providers) { + const existingProviders = existingConfig?.providers as + | Record + | undefined; + if (existingConfig && existingProviders) { console.log(chalk.yellow("⚠️ Existing configuration detected!")); console.log(); console.log(chalk.gray("Currently configured providers:")); - Object.keys(existingConfig.providers).forEach((provider) => { - const providerConfig = existingConfig.providers[provider]; + Object.keys(existingProviders).forEach((provider) => { + const providerConfig = existingProviders[provider]; console.log(chalk.white(`• ${provider} (${providerConfig.model})`)); }); console.log(); @@ -418,6 +390,7 @@ export async function authCommand() { } else { throw new Error( "HuggingFace model discovery failed. Please run 'txtcode auth' again with a valid API key.", + { cause: error }, ); } } @@ -450,15 +423,18 @@ export async function authCommand() { } else { throw new Error( "OpenRouter model discovery failed. Please run 'txtcode auth' again with a valid API key.", + { cause: error }, ); } } } else { const providerModels = modelsCatalog.providers[providerValue]; - modelChoices = providerModels.models.map((model: any) => ({ - name: model.recommended ? `${model.name} - Recommended` : model.name, - value: model.id, - })); + modelChoices = providerModels.models.map( + (model: { id: string; name: string; recommended?: boolean }) => ({ + name: model.recommended ? `${model.name} - Recommended` : model.name, + value: model.id, + }), + ); } // Add "Enter custom model name" option at the top @@ -701,10 +677,9 @@ export async function authCommand() { if (discordToken) { await setBotToken("discord", discordToken); } - } catch (error) { + } catch { console.log(chalk.red("\n[ERROR] Failed to store credentials in keychain")); console.log(chalk.yellow("Falling back to encrypted file storage...\n")); - // Continue with file storage as fallback } // Build providers object dynamically @@ -737,8 +712,8 @@ export async function authCommand() { try { fs.chmodSync(CONFIG_DIR, 0o700); fs.chmodSync(CONFIG_FILE, 0o600); - } catch (error) { - // Windows doesn't support chmod, will use icacls in security-check + } catch { + // Windows doesn't support chmod } console.log(chalk.green("\nAuthentication successful!")); @@ -764,15 +739,15 @@ export async function authCommand() { } } -export function loadConfig(): any { +export function loadConfig(): Record | null { if (!fs.existsSync(CONFIG_FILE)) { return null; } try { const data = fs.readFileSync(CONFIG_FILE, "utf-8"); - return JSON.parse(data); - } catch (error) { + return JSON.parse(data) as Record; + } catch { return null; } } diff --git a/src/cli/commands/config.ts b/src/cli/commands/config.ts index 2d6d44e..8ece1de 100644 --- a/src/cli/commands/config.ts +++ b/src/cli/commands/config.ts @@ -67,7 +67,7 @@ export async function configCommand() { } } -async function configurePlatform(config: any) { +async function configurePlatform(config: Record) { console.log(); centerLog(chalk.cyan("📱 Messaging Platform Configuration")); console.log(); @@ -104,7 +104,7 @@ async function configurePlatform(config: any) { console.log(); } -async function configureIDE(config: any) { +async function configureIDE(config: Record) { console.log(); centerLog(chalk.cyan("🤖 IDE Configuration")); console.log(); @@ -148,7 +148,7 @@ async function configureIDE(config: any) { console.log(); } -async function configureAI(config: any) { +async function configureAI(config: Record) { console.log(); centerLog(chalk.cyan("🧠 AI Provider Configuration")); console.log(); @@ -157,8 +157,7 @@ async function configureAI(config: any) { centerLog(chalk.gray("To reconfigure all providers, run authentication again.")); console.log(); - // Get list of already configured providers - const configuredProviders = config.providers || {}; + const configuredProviders = (config.providers || {}) as Record; const providerList = Object.keys(configuredProviders); if (providerList.length === 0) { @@ -170,7 +169,6 @@ async function configureAI(config: any) { return; } - // Show configured providers const providerChoices = providerList.map((providerId) => ({ name: `${providerId} (${configuredProviders[providerId].model})`, value: providerId, @@ -181,7 +179,6 @@ async function configureAI(config: any) { choices: providerChoices, }); - // Update top-level fields to match selected provider config.aiProvider = selectedProvider; config.aiModel = configuredProviders[selectedProvider].model; config.updatedAt = new Date().toISOString(); @@ -198,7 +195,7 @@ async function configureAI(config: any) { console.log(); } -async function configureProject(config: any) { +async function configureProject(config: Record) { console.log(); centerLog(chalk.cyan("📁 Project Path Configuration")); console.log(); @@ -207,7 +204,7 @@ async function configureProject(config: any) { message: "Enter your project path:", }); - config.projectPath = projectPath || config.projectPath || process.cwd(); + config.projectPath = projectPath || (config.projectPath as string) || process.cwd(); saveConfig(config); console.log(); @@ -215,43 +212,44 @@ async function configureProject(config: any) { console.log(); } -function viewConfig(config: any) { +function viewConfig(config: Record) { console.log(); centerLog(chalk.cyan("Current Configuration")); console.log(); - centerLog(chalk.white("Platform: ") + chalk.yellow(config.platform)); - centerLog(chalk.white("IDE Type: ") + chalk.yellow(config.ideType)); - centerLog(chalk.white("AI Provider: ") + chalk.yellow(config.aiProvider)); + centerLog(chalk.white("Platform: ") + chalk.yellow(String(config.platform))); + centerLog(chalk.white("IDE Type: ") + chalk.yellow(String(config.ideType))); + centerLog(chalk.white("AI Provider: ") + chalk.yellow(String(config.aiProvider))); if (config.projectPath) { - centerLog(chalk.white("Project Path: ") + chalk.yellow(config.projectPath)); + centerLog(chalk.white("Project Path: ") + chalk.yellow(String(config.projectPath))); } if (config.ollamaModel) { - centerLog(chalk.white("Ollama Model: ") + chalk.yellow(config.ollamaModel)); + centerLog(chalk.white("Ollama Model: ") + chalk.yellow(String(config.ollamaModel))); } if (config.claudeModel) { - centerLog(chalk.white("Claude Model: ") + chalk.yellow(config.claudeModel)); + centerLog(chalk.white("Claude Model: ") + chalk.yellow(String(config.claudeModel))); } if (config.geminiModel) { - centerLog(chalk.white("Gemini Model: ") + chalk.yellow(config.geminiModel)); + centerLog(chalk.white("Gemini Model: ") + chalk.yellow(String(config.geminiModel))); } if (config.authorizedUser) { - centerLog(chalk.white("Authorized User: ") + chalk.yellow(config.authorizedUser)); + centerLog(chalk.white("Authorized User: ") + chalk.yellow(String(config.authorizedUser))); } centerLog( - chalk.white("Configured At: ") + chalk.yellow(new Date(config.configuredAt).toLocaleString()), + chalk.white("Configured At: ") + + chalk.yellow(new Date(String(config.configuredAt)).toLocaleString()), ); console.log(); centerLog(chalk.gray(`Config file: ${CONFIG_FILE}`)); console.log(); } -function saveConfig(config: any) { +function saveConfig(config: Record) { config.updatedAt = new Date().toISOString(); fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); } diff --git a/src/cli/commands/help.ts b/src/cli/commands/help.ts index 6d0acfb..b807c59 100644 --- a/src/cli/commands/help.ts +++ b/src/cli/commands/help.ts @@ -31,20 +31,18 @@ export function showHelpScreen(): void { console.log(chalk.bold(" Modes")); console.log(); - console.log(` ${chalk.yellow("Chat")} ${chalk.gray("(default)")} Messages are processed by the primary LLM`); - console.log(` ${chalk.yellow("Code")} ${chalk.gray("(/code)")} Messages are sent directly to the coding CLI`); - console.log(); - - console.log(chalk.bold(" Tips")); - console.log(); - console.log( - chalk.gray(" • In code mode, sending a new message auto-cancels the previous one"), - ); console.log( - chalk.gray(" • Use /cli-model to switch between models without restarting"), + ` ${chalk.yellow("Chat")} ${chalk.gray("(default)")} Messages are processed by the primary LLM`, ); console.log( - chalk.gray(" • Use /switch to change both the LLM provider and coding adapter"), + ` ${chalk.yellow("Code")} ${chalk.gray("(/code)")} Messages are sent directly to the coding CLI`, ); console.log(); + + console.log(chalk.bold(" Tips")); + console.log(); + console.log(chalk.gray(" • In code mode, sending a new message auto-cancels the previous one")); + console.log(chalk.gray(" • Use /cli-model to switch between models without restarting")); + console.log(chalk.gray(" • Use /switch to change both the LLM provider and coding adapter")); + console.log(); } diff --git a/src/cli/commands/reset.ts b/src/cli/commands/reset.ts index b6a8bc7..96af825 100644 --- a/src/cli/commands/reset.ts +++ b/src/cli/commands/reset.ts @@ -23,7 +23,7 @@ export async function resetCommand() { try { const configData = fs.readFileSync(CONFIG_FILE, "utf-8"); config = JSON.parse(configData); - } catch (parseError) { + } catch { console.log(); centerLog(chalk.red("❌ Config file is corrupted.")); console.log(); @@ -41,12 +41,12 @@ export async function resetCommand() { console.log(); centerLog(chalk.cyan("The next person to message will become the authorized user.")); console.log(); - } catch (writeError) { + } catch { console.log(); centerLog(chalk.red("❌ Failed to save config file.")); console.log(); } - } catch (error) { + } catch { console.log(); centerLog(chalk.red("❌ Failed to reset. Unexpected error.")); console.log(); @@ -67,7 +67,7 @@ export async function logoutCommand() { centerLog(chalk.yellow("⚠️ No WhatsApp session found.")); console.log(); } - } catch (error) { + } catch { console.log(); centerLog(chalk.red("❌ Failed to delete session.")); console.log(); @@ -98,7 +98,7 @@ export async function hardResetCommand() { centerLog(chalk.green("✓ Deleted configuration directory")); deletedItems++; } - } catch (error) { + } catch { console.log(); centerLog(chalk.red("✗ Failed to delete configuration directory")); } diff --git a/src/cli/commands/start.ts b/src/cli/commands/start.ts index 2e0f75c..69a6e01 100644 --- a/src/cli/commands/start.ts +++ b/src/cli/commands/start.ts @@ -4,14 +4,15 @@ import { DiscordBot } from "../../platforms/discord"; import { TelegramBot } from "../../platforms/telegram"; import { WhatsAppBot } from "../../platforms/whatsapp"; import { logger } from "../../shared/logger"; +import type { Config } from "../../shared/types"; import { getApiKey, getBotToken } from "../../utils/keychain"; import { centerLog } from "../tui"; import { loadConfig } from "./auth"; -export async function startCommand(options: { daemon?: boolean }) { - const config = loadConfig(); +export async function startCommand(_options: { daemon?: boolean }) { + const rawConfig = loadConfig(); - if (!config) { + if (!rawConfig) { console.log(); centerLog(chalk.yellow("⚠️ TxtCode is not configured yet.")); console.log(); @@ -20,11 +21,12 @@ export async function startCommand(options: { daemon?: boolean }) { process.exit(1); } + const config = rawConfig as unknown as Config; + logger.info(chalk.blue.bold("\nStarting TxtCode Agent\n")); logger.info(chalk.cyan(`Platform: ${config.platform}`)); logger.info(chalk.cyan(`IDE: ${config.ideType}\n`)); - // Retrieve API key from keychain const apiKey = await getApiKey(config.aiProvider); if (!apiKey) { console.log(); @@ -35,7 +37,6 @@ export async function startCommand(options: { daemon?: boolean }) { process.exit(1); } - // Retrieve bot tokens from keychain if needed let telegramToken = ""; let discordToken = ""; @@ -65,7 +66,7 @@ export async function startCommand(options: { daemon?: boolean }) { process.env.TELEGRAM_BOT_TOKEN = telegramToken; process.env.DISCORD_BOT_TOKEN = discordToken; process.env.IDE_TYPE = config.ideType; - process.env.IDE_PORT = config.idePort; + process.env.IDE_PORT = String(config.idePort); process.env.AI_API_KEY = apiKey; process.env.AI_PROVIDER = config.aiProvider; process.env.AI_MODEL = config.aiModel; diff --git a/src/cli/tui/components/centered-confirm.ts b/src/cli/tui/components/centered-confirm.ts index 40274d1..50204c0 100644 --- a/src/cli/tui/components/centered-confirm.ts +++ b/src/cli/tui/components/centered-confirm.ts @@ -26,7 +26,7 @@ export async function showCenteredConfirm(options: CenteredConfirmOptions): Prom process.stdin.pause(); }; - const onKeypress = (str: string, key: any) => { + const onKeypress = (str: string, key: { name: string; ctrl?: boolean }) => { if (key.ctrl && key.name === "c") { cleanup(); process.exit(0); diff --git a/src/cli/tui/components/centered-input.ts b/src/cli/tui/components/centered-input.ts index 07518c8..cbd6bbf 100644 --- a/src/cli/tui/components/centered-input.ts +++ b/src/cli/tui/components/centered-input.ts @@ -27,7 +27,7 @@ export async function showCenteredInput(options: CenteredInputOptions): Promise< process.stdin.pause(); }; - const onKeypress = (str: string, key: any) => { + const onKeypress = (str: string, key: { name: string; ctrl?: boolean; meta?: boolean }) => { if (key.ctrl && key.name === "c") { cleanup(); process.exit(0); diff --git a/src/cli/tui/components/centered-list.ts b/src/cli/tui/components/centered-list.ts index 5ea5fb0..2c47983 100644 --- a/src/cli/tui/components/centered-list.ts +++ b/src/cli/tui/components/centered-list.ts @@ -93,7 +93,7 @@ export async function showCenteredList(options: CenteredListOptions): Promise { + const onKeypress = (_str: string, key: { name: string; ctrl?: boolean }) => { const pageItems = getCurrentPageItems(); const localIdx = toLocalIndex(selectedIndex); diff --git a/src/cli/tui/components/centered-text.ts b/src/cli/tui/components/centered-text.ts index 6882af6..0aeb179 100644 --- a/src/cli/tui/components/centered-text.ts +++ b/src/cli/tui/components/centered-text.ts @@ -12,6 +12,7 @@ export function getTerminalHeight(): number { export function centerText(text: string, width?: number): string { const terminalWidth = width || getTerminalWidth(); + // eslint-disable-next-line no-control-regex const plainText = text.replace(/\x1b\[[0-9;]*m/g, ""); const padding = Math.max(0, Math.floor((terminalWidth - plainText.length) / 2)); return " ".repeat(padding) + text; diff --git a/src/cli/tui/components/menu.ts b/src/cli/tui/components/menu.ts index 824e5f5..cf0caa4 100644 --- a/src/cli/tui/components/menu.ts +++ b/src/cli/tui/components/menu.ts @@ -22,6 +22,7 @@ export async function showMenu(options: MenuOptions): Promise { const longestChoice = Math.max( ...options.items.map((item) => { + // eslint-disable-next-line no-control-regex const plainText = item.name.replace(/\x1b\[[0-9;]*m/g, ""); return plainText.length; }), @@ -84,7 +85,7 @@ export async function showMenu(options: MenuOptions): Promise { process.stdin.setRawMode(true); } - const onKeypress = (_str: string, key: any) => { + const onKeypress = (_str: string, key: { name: string; ctrl?: boolean }) => { if (key.name === "up") { selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : options.items.length - 1; render(false); diff --git a/src/core/agent.ts b/src/core/agent.ts index 1dcbdc1..57e1bcd 100644 --- a/src/core/agent.ts +++ b/src/core/agent.ts @@ -1,14 +1,17 @@ import { logger } from "../shared/logger"; import { Message } from "../shared/types"; import { getApiKey } from "../utils/keychain"; -import { Router, AVAILABLE_ADAPTERS } from "./router"; +import { Router } from "./router"; export class AgentCore { private router: Router; private authorizedUser: string | null; private configPath: string; private userModes: Map = new Map(); - private pendingSwitch: Map = new Map(); + private pendingSwitch: Map< + string, + "main" | "adapter" | "provider" | "cli-model" | "cli-model-custom" + > = new Map(); constructor() { this.router = new Router(); @@ -29,7 +32,7 @@ export class AgentCore { } } - private loadConfigSafely(): any | null { + private loadConfigSafely(): Record | null { try { const fs = require("fs"); if (!fs.existsSync(this.configPath)) { @@ -72,7 +75,9 @@ export class AgentCore { ) { return false; } - if (this.pendingSwitch.has(userId)) return false; + if (this.pendingSwitch.has(userId)) { + return false; + } return this.userModes.get(userId) === "code"; } @@ -211,11 +216,9 @@ Reply with 1 or 2:`; const currentProvider = this.router.getProviderName(); const currentModel = this.router.getCurrentModel(); - // Get configured providers and validate them - const configuredProviders = config.providers || {}; + const configuredProviders = (config.providers || {}) as Record; const allProviders = Object.keys(configuredProviders); - // Filter out providers with invalid configurations const validProviders = allProviders.filter((provider) => { const providerConfig = configuredProviders[provider]; return providerConfig && providerConfig.model; @@ -225,7 +228,6 @@ Reply with 1 or 2:`; return `[ERROR] No valid providers configured. Please run 'txtcode auth' to configure providers.`; } - // Warn if some providers were filtered out if (validProviders.length < allProviders.length) { const invalidCount = allProviders.length - validProviders.length; logger.debug(`${invalidCount} provider(s) have invalid configuration and were excluded`); @@ -319,10 +321,9 @@ Reply with 1 or 2:`; return `[ERROR] Failed to load configuration. Config file may be corrupted.\n\nPlease run 'txtcode auth' to reconfigure.`; } - const configuredProviders = config.providers || {}; + const configuredProviders = (config.providers || {}) as Record; const allProviders = Object.keys(configuredProviders); - // Filter out providers with invalid configurations (same as showProviderList) const validProviders = allProviders.filter((provider) => { const providerConfig = configuredProviders[provider]; return providerConfig && providerConfig.model; @@ -342,7 +343,6 @@ Reply with 1 or 2:`; const providerConfig = configuredProviders[selectedProvider]; - // Validate provider configuration (redundant check for safety) if (!providerConfig || !providerConfig.model) { return `[ERROR] Provider configuration for ${selectedProvider} is invalid.\n\nPlease run 'txtcode auth' to reconfigure.`; } @@ -375,8 +375,8 @@ Provider: ${selectedProvider} Model: ${providerConfig.model} Your chat messages will now use ${selectedProvider}.`; - } catch (error: any) { - return `[ERROR] Failed to switch provider: ${error.message}`; + } catch (error: unknown) { + return `[ERROR] Failed to switch provider: ${error instanceof Error ? error.message : String(error)}`; } } @@ -476,12 +476,14 @@ Your chat messages will now use ${selectedProvider}.`; private persistAdapterModel(adapterName: string, modelId: string): void { try { const config = this.loadConfigSafely(); - if (!config) return; + if (!config) { + return; + } if (!config.adapterModels) { - config.adapterModels = {}; + config.adapterModels = {} as Record; } - config.adapterModels[adapterName] = modelId; + (config.adapterModels as Record)[adapterName] = modelId; config.updatedAt = new Date().toISOString(); const fs = require("fs"); diff --git a/src/core/router.ts b/src/core/router.ts index 656bf81..f89cd75 100644 --- a/src/core/router.ts +++ b/src/core/router.ts @@ -272,7 +272,9 @@ export class Router { const path = require("path"); const os = require("os"); const configPath = path.join(os.homedir(), ".txtcode", "config.json"); - if (!fs.existsSync(configPath)) return; + if (!fs.existsSync(configPath)) { + return; + } const config = JSON.parse(fs.readFileSync(configPath, "utf-8")); const savedModel = config.adapterModels?.[adapterName]; if (savedModel) { diff --git a/src/platforms/discord.ts b/src/platforms/discord.ts index d035aa1..5eee837 100644 --- a/src/platforms/discord.ts +++ b/src/platforms/discord.ts @@ -1,4 +1,12 @@ -import { Client, GatewayIntentBits, Message, Partials } from "discord.js"; +import { + Client, + type DMChannel, + GatewayIntentBits, + Message, + type NewsChannel, + Partials, + type TextChannel, +} from "discord.js"; import { AgentCore } from "../core/agent"; import { BlockReplyPipeline } from "../shared/block-reply-pipeline"; import { logger } from "../shared/logger"; @@ -36,9 +44,13 @@ export class DiscordBot { private cleanupRequest(userId: string) { const active = this.activeRequests.get(userId); - if (!active) return; + if (!active) { + return; + } active.aborted = true; - if (active.heartbeatInterval) clearInterval(active.heartbeatInterval); + if (active.heartbeatInterval) { + clearInterval(active.heartbeatInterval); + } this.activeRequests.delete(userId); } @@ -49,12 +61,18 @@ export class DiscordBot { }); this.client.on("messageCreate", async (message: Message) => { - if (message.author.bot) return; - if (message.guild && !message.mentions.has(this.client.user!)) return; + if (message.author.bot) { + return; + } + if (message.guild && !message.mentions.has(this.client.user!)) { + return; + } const from = message.author.id; const text = message.content.replace(`<@${this.client.user?.id}>`, "").trim(); - if (!text) return; + if (!text) { + return; + } logger.debug(`Incoming message from ${message.author.tag}: ${text}`); @@ -73,7 +91,7 @@ export class DiscordBot { const response = await this.agent.processMessage({ from, text, timestamp: new Date() }); try { await this.sendLongReply(message, response); - } catch (error: any) { + } catch (error) { logger.error("Failed to send Discord message", error); } return; @@ -120,9 +138,13 @@ export class DiscordBot { }, typingSignaler, onChunk: async (chunk: StreamChunk) => { - if (active.aborted || !active.progressMessage) return; + if (active.aborted || !active.progressMessage) { + return; + } const preview = truncate(chunk.text, MAX_DISCORD_LENGTH - 50); - if (preview === lastEditText) return; + if (preview === lastEditText) { + return; + } try { await active.progressMessage.edit(preview); lastEditText = preview; @@ -133,10 +155,14 @@ export class DiscordBot { }); active.heartbeatInterval = setInterval(async () => { - if (active.aborted || !active.progressMessage) return; + if (active.aborted || !active.progressMessage) { + return; + } const elapsed = Math.floor((Date.now() - taskStartTime) / 1000); const msg = `Still working... (${elapsed}s)`; - if (msg === lastEditText) return; + if (msg === lastEditText) { + return; + } try { await active.progressMessage.edit(msg); lastEditText = msg; @@ -149,13 +175,19 @@ export class DiscordBot { const response = await this.agent.processMessage( { from, text, timestamp: new Date() }, async (chunk: string) => { - if (!active.aborted) await pipeline.processText(chunk); + if (!active.aborted) { + await pipeline.processText(chunk); + } }, ); - if (active.aborted) return; + if (active.aborted) { + return; + } - if (active.heartbeatInterval) clearInterval(active.heartbeatInterval); + if (active.heartbeatInterval) { + clearInterval(active.heartbeatInterval); + } this.activeRequests.delete(from); await pipeline.flush(); @@ -182,14 +214,20 @@ export class DiscordBot { } } } catch (error) { - if (active.aborted) return; + if (active.aborted) { + return; + } - if (active.heartbeatInterval) clearInterval(active.heartbeatInterval); + if (active.heartbeatInterval) { + clearInterval(active.heartbeatInterval); + } this.activeRequests.delete(from); await typingSignaler.stopTyping(); const isAbort = error instanceof Error && error.message.includes("aborted"); - if (isAbort) return; + if (isAbort) { + return; + } const errMsg = `Error: ${error instanceof Error ? error.message : "Unknown error"}`; if (active.progressMessage) { @@ -217,7 +255,7 @@ export class DiscordBot { if (i === 0) { await message.reply(parts[i]); } else { - await (message.channel as any).send(parts[i]); + await (message.channel as TextChannel | DMChannel | NewsChannel).send(parts[i]); } } } @@ -235,7 +273,9 @@ export class DiscordBot { } function truncate(text: string, max: number): string { - if (text.length <= max) return text; + if (text.length <= max) { + return text; + } return text.slice(0, max - 20) + "\n\n... (truncated)"; } @@ -248,7 +288,9 @@ function splitMessage(text: string, max: number): string[] { break; } let breakAt = remaining.lastIndexOf("\n", max); - if (breakAt < max / 2) breakAt = max; + if (breakAt < max / 2) { + breakAt = max; + } parts.push(remaining.slice(0, breakAt)); remaining = remaining.slice(breakAt); } diff --git a/src/platforms/telegram.ts b/src/platforms/telegram.ts index 2fd26f6..467db22 100644 --- a/src/platforms/telegram.ts +++ b/src/platforms/telegram.ts @@ -33,9 +33,13 @@ export class TelegramBot { private cleanupRequest(userId: string) { const active = this.activeRequests.get(userId); - if (!active) return; + if (!active) { + return; + } active.aborted = true; - if (active.heartbeatInterval) clearInterval(active.heartbeatInterval); + if (active.heartbeatInterval) { + clearInterval(active.heartbeatInterval); + } this.activeRequests.delete(userId); } @@ -126,9 +130,13 @@ export class TelegramBot { }, typingSignaler, onChunk: async (chunk: StreamChunk) => { - if (active.aborted || !active.progressMessageId) return; + if (active.aborted || !active.progressMessageId) { + return; + } const preview = truncate(chunk.text, MAX_TELEGRAM_LENGTH - 50); - if (preview === lastEditText) return; + if (preview === lastEditText) { + return; + } try { await ctx.telegram.editMessageText( ctx.chat.id, @@ -144,11 +152,17 @@ export class TelegramBot { }); active.heartbeatInterval = setInterval(async () => { - if (active.aborted) return; + if (active.aborted) { + return; + } const elapsed = Math.floor((Date.now() - taskStartTime) / 1000); - if (!active.progressMessageId) return; + if (!active.progressMessageId) { + return; + } const msg = `Still working... (${elapsed}s)`; - if (msg === lastEditText) return; + if (msg === lastEditText) { + return; + } try { await ctx.telegram.editMessageText(ctx.chat.id, active.progressMessageId, undefined, msg); lastEditText = msg; @@ -161,13 +175,19 @@ export class TelegramBot { const response = await this.agent.processMessage( { from, text, timestamp: new Date() }, async (chunk: string) => { - if (!active.aborted) await pipeline.processText(chunk); + if (!active.aborted) { + await pipeline.processText(chunk); + } }, ); - if (active.aborted) return; + if (active.aborted) { + return; + } - if (active.heartbeatInterval) clearInterval(active.heartbeatInterval); + if (active.heartbeatInterval) { + clearInterval(active.heartbeatInterval); + } this.activeRequests.delete(from); await pipeline.flush(); @@ -201,14 +221,20 @@ export class TelegramBot { } } } catch (error) { - if (active.aborted) return; + if (active.aborted) { + return; + } - if (active.heartbeatInterval) clearInterval(active.heartbeatInterval); + if (active.heartbeatInterval) { + clearInterval(active.heartbeatInterval); + } this.activeRequests.delete(from); await typingSignaler.stopTyping(); const isAbort = error instanceof Error && error.message.includes("aborted"); - if (isAbort) return; + if (isAbort) { + return; + } const errMsg = `Error: ${error instanceof Error ? error.message : "Unknown error"}`; if (active.progressMessageId) { @@ -227,7 +253,10 @@ export class TelegramBot { }); } - private async sendLongMessage(ctx: any, text: string): Promise { + private async sendLongMessage( + ctx: { reply: (text: string) => Promise }, + text: string, + ): Promise { if (text.length <= MAX_TELEGRAM_LENGTH) { await ctx.reply(text); return; @@ -249,7 +278,9 @@ export class TelegramBot { } function truncate(text: string, max: number): string { - if (text.length <= max) return text; + if (text.length <= max) { + return text; + } return text.slice(0, max - 20) + "\n\n... (truncated)"; } @@ -262,7 +293,9 @@ function splitMessage(text: string, max: number): string[] { break; } let breakAt = remaining.lastIndexOf("\n", max); - if (breakAt < max / 2) breakAt = max; + if (breakAt < max / 2) { + breakAt = max; + } parts.push(remaining.slice(0, breakAt)); remaining = remaining.slice(breakAt); } diff --git a/src/platforms/whatsapp.ts b/src/platforms/whatsapp.ts index 52ec893..269e968 100644 --- a/src/platforms/whatsapp.ts +++ b/src/platforms/whatsapp.ts @@ -3,8 +3,10 @@ import os from "os"; import path from "path"; import { Boom } from "@hapi/boom"; import makeWASocket, { + type ConnectionState, DisconnectReason, useMultiFileAuthState, + type WASocket, WAMessage, } from "@whiskeysockets/baileys"; import { AgentCore } from "../core/agent"; @@ -24,7 +26,7 @@ const silentLogger = { debug: noop, trace: noop, child: () => silentLogger, -} as any; +} as unknown as Parameters[0]["logger"]; interface ActiveRequest { heartbeatInterval: NodeJS.Timeout | null; @@ -33,7 +35,7 @@ interface ActiveRequest { export class WhatsAppBot { private agent: AgentCore; - private sock: any; + private sock!: WASocket; private lastProcessedTimestamp: number = 0; private activeRequests: Map = new Map(); @@ -43,9 +45,13 @@ export class WhatsAppBot { private cleanupRequest(userId: string) { const active = this.activeRequests.get(userId); - if (!active) return; + if (!active) { + return; + } active.aborted = true; - if (active.heartbeatInterval) clearInterval(active.heartbeatInterval); + if (active.heartbeatInterval) { + clearInterval(active.heartbeatInterval); + } this.activeRequests.delete(userId); } @@ -65,7 +71,7 @@ export class WhatsAppBot { logger: silentLogger, }); - this.sock.ev.on("connection.update", async (update: any) => { + this.sock.ev.on("connection.update", async (update: Partial) => { const { connection, lastDisconnect } = update; if (connection === "open") { @@ -94,20 +100,31 @@ export class WhatsAppBot { async ({ messages, type }: { messages: WAMessage[]; type: string }) => { for (const msg of messages) { try { - if (type !== "notify") continue; - if (!msg.message || msg.key.remoteJid === "status@broadcast") continue; + if (type !== "notify") { + continue; + } + if (!msg.message || msg.key.remoteJid === "status@broadcast") { + continue; + } const from = msg.key.remoteJid || ""; const isFromMe = msg.key.fromMe || false; const messageTimestamp = (msg.messageTimestamp as number) || 0; - if (!isFromMe) continue; - if (from.endsWith("@g.us")) continue; + if (!isFromMe) { + continue; + } + if (from.endsWith("@g.us")) { + continue; + } - const text = - msg.message.conversation || msg.message.extendedTextMessage?.text || ""; - if (!text) continue; - if (messageTimestamp <= this.lastProcessedTimestamp) continue; + const text = msg.message.conversation || msg.message.extendedTextMessage?.text || ""; + if (!text) { + continue; + } + if (messageTimestamp <= this.lastProcessedTimestamp) { + continue; + } this.lastProcessedTimestamp = messageTimestamp; logger.debug(`Incoming message: ${text}`); @@ -142,7 +159,9 @@ export class WhatsAppBot { const typingSignaler = new WhatsAppTypingSignaler(this.sock, from); active.heartbeatInterval = setInterval(async () => { - if (active.aborted) return; + if (active.aborted) { + return; + } await typingSignaler.signalTyping(); }, 3000); @@ -154,26 +173,38 @@ export class WhatsAppBot { timestamp: new Date(messageTimestamp * 1000), }, async (_chunk: string) => { - if (!active.aborted) await typingSignaler.signalTyping(); + if (!active.aborted) { + await typingSignaler.signalTyping(); + } }, ); - if (active.aborted) continue; + if (active.aborted) { + continue; + } - if (active.heartbeatInterval) clearInterval(active.heartbeatInterval); + if (active.heartbeatInterval) { + clearInterval(active.heartbeatInterval); + } this.activeRequests.delete(from); await typingSignaler.stopTyping(); await this.sendLongMessage(from, response, msg); } catch (error) { - if (active.aborted) continue; + if (active.aborted) { + continue; + } - if (active.heartbeatInterval) clearInterval(active.heartbeatInterval); + if (active.heartbeatInterval) { + clearInterval(active.heartbeatInterval); + } this.activeRequests.delete(from); await typingSignaler.stopTyping(); const isAbort = error instanceof Error && error.message.includes("aborted"); - if (isAbort) continue; + if (isAbort) { + continue; + } const errMsg = `Error: ${error instanceof Error ? error.message : "Unknown error"}`; await this.sock.sendMessage(from, { text: errMsg }, { quoted: msg }); @@ -186,11 +217,7 @@ export class WhatsAppBot { ); } - private async sendLongMessage( - jid: string, - text: string, - quotedMsg: WAMessage, - ): Promise { + private async sendLongMessage(jid: string, text: string, quotedMsg: WAMessage): Promise { if (text.length <= MAX_WA_LENGTH) { await this.sock.sendMessage(jid, { text }, { quoted: quotedMsg }); return; @@ -215,7 +242,9 @@ function splitMessage(text: string, max: number): string[] { break; } let breakAt = remaining.lastIndexOf("\n", max); - if (breakAt < max / 2) breakAt = max; + if (breakAt < max / 2) { + breakAt = max; + } parts.push(remaining.slice(0, breakAt)); remaining = remaining.slice(breakAt); } diff --git a/src/providers/anthropic.ts b/src/providers/anthropic.ts index c908a53..9966a3b 100644 --- a/src/providers/anthropic.ts +++ b/src/providers/anthropic.ts @@ -1,6 +1,14 @@ import fs from "fs"; import path from "path"; import Anthropic from "@anthropic-ai/sdk"; +import type { + ContentBlock, + MessageParam, + TextBlock, + ToolResultBlockParam, + ToolUnion, + ToolUseBlock, +} from "@anthropic-ai/sdk/resources/messages/messages"; import { ToolRegistry } from "../tools/registry"; const MAX_ITERATIONS = 10; @@ -23,9 +31,11 @@ export async function processWithAnthropic( try { const anthropic = new Anthropic({ apiKey }); - const tools = toolRegistry ? toolRegistry.getDefinitionsForProvider("anthropic") : undefined; + const tools = toolRegistry + ? (toolRegistry.getDefinitionsForProvider("anthropic") as unknown as ToolUnion[]) + : undefined; - const messages: any[] = [{ role: "user", content: instruction }]; + const messages: MessageParam[] = [{ role: "user", content: instruction }]; for (let i = 0; i < MAX_ITERATIONS; i++) { const response = await anthropic.messages.create({ @@ -37,10 +47,12 @@ export async function processWithAnthropic( }); const textParts = response.content - .filter((block: any) => block.type === "text") - .map((block: any) => block.text); + .filter((block: ContentBlock): block is TextBlock => block.type === "text") + .map((block: TextBlock) => block.text); - const toolCalls = response.content.filter((block: any) => block.type === "tool_use"); + const toolCalls = response.content.filter( + (block: ContentBlock): block is ToolUseBlock => block.type === "tool_use", + ); if (toolCalls.length === 0 || !toolRegistry) { return textParts.join("\n") || "No response from Claude"; @@ -48,9 +60,8 @@ export async function processWithAnthropic( messages.push({ role: "assistant", content: response.content }); - const toolResults: any[] = []; - for (const call of toolCalls) { - const toolUse = call as any; + const toolResults: ToolResultBlockParam[] = []; + for (const toolUse of toolCalls) { const result = await toolRegistry.execute( toolUse.name, toolUse.input as Record, @@ -66,7 +77,7 @@ export async function processWithAnthropic( } return "Reached maximum tool iterations."; - } catch (error) { + } catch (error: unknown) { throw new Error( `Anthropic API error: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error }, diff --git a/src/providers/gemini.ts b/src/providers/gemini.ts index 007bfee..986231f 100644 --- a/src/providers/gemini.ts +++ b/src/providers/gemini.ts @@ -1,6 +1,10 @@ import fs from "fs"; import path from "path"; -import { GoogleGenerativeAI } from "@google/generative-ai"; +import { + type FunctionResponsePart, + type Tool as GeminiTool, + GoogleGenerativeAI, +} from "@google/generative-ai"; import { ToolRegistry } from "../tools/registry"; const MAX_ITERATIONS = 10; @@ -23,7 +27,9 @@ export async function processWithGemini( try { const genAI = new GoogleGenerativeAI(apiKey); - const tools = toolRegistry ? toolRegistry.getDefinitionsForProvider("gemini") : undefined; + const tools = toolRegistry + ? (toolRegistry.getDefinitionsForProvider("gemini") as unknown as GeminiTool[]) + : undefined; const genModel = genAI.getGenerativeModel({ model, @@ -42,7 +48,7 @@ export async function processWithGemini( return response.text(); } - const toolResults: any[] = []; + const toolResults: FunctionResponsePart[] = []; for (const call of calls) { const execResult = await toolRegistry.execute( call.name, @@ -60,7 +66,7 @@ export async function processWithGemini( } return "Reached maximum tool iterations."; - } catch (error) { + } catch (error: unknown) { throw new Error( `Gemini API error: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error }, diff --git a/src/providers/huggingface.ts b/src/providers/huggingface.ts index e1437de..95989b7 100644 --- a/src/providers/huggingface.ts +++ b/src/providers/huggingface.ts @@ -1,6 +1,10 @@ import fs from "fs"; import path from "path"; import OpenAI from "openai"; +import type { + ChatCompletionMessageParam, + ChatCompletionTool, +} from "openai/resources/chat/completions/completions"; import { ToolRegistry } from "../tools/registry"; const MAX_ITERATIONS = 10; @@ -28,9 +32,11 @@ export async function processWithHuggingFace( baseURL: HUGGINGFACE_BASE_URL, }); - const tools = toolRegistry ? toolRegistry.getDefinitionsForProvider("openai") : undefined; + const tools = toolRegistry + ? (toolRegistry.getDefinitionsForProvider("openai") as unknown as ChatCompletionTool[]) + : undefined; - const messages: any[] = [ + const messages: ChatCompletionMessageParam[] = [ { role: "system", content: loadSystemPrompt() }, { role: "user", content: instruction }, ]; @@ -52,8 +58,10 @@ export async function processWithHuggingFace( messages.push(assistantMsg); - for (const call of assistantMsg.tool_calls) { - const toolCall = call as any; + for (const toolCall of assistantMsg.tool_calls) { + if (toolCall.type !== "function") { + continue; + } const args = JSON.parse(toolCall.function.arguments); const result = await toolRegistry.execute(toolCall.function.name, args); messages.push({ @@ -65,7 +73,7 @@ export async function processWithHuggingFace( } return "Reached maximum tool iterations."; - } catch (error) { + } catch (error: unknown) { throw new Error( `HuggingFace API error: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error }, diff --git a/src/providers/minimax.ts b/src/providers/minimax.ts index fe9db3d..aa0326c 100644 --- a/src/providers/minimax.ts +++ b/src/providers/minimax.ts @@ -1,6 +1,14 @@ import fs from "fs"; import path from "path"; import Anthropic from "@anthropic-ai/sdk"; +import type { + ContentBlock, + MessageParam, + TextBlock, + ToolResultBlockParam, + ToolUnion, + ToolUseBlock, +} from "@anthropic-ai/sdk/resources/messages/messages"; import { ToolRegistry } from "../tools/registry"; const MAX_ITERATIONS = 10; @@ -28,9 +36,11 @@ export async function processWithMiniMax( baseURL: MINIMAX_BASE_URL, }); - const tools = toolRegistry ? toolRegistry.getDefinitionsForProvider("anthropic") : undefined; + const tools = toolRegistry + ? (toolRegistry.getDefinitionsForProvider("anthropic") as unknown as ToolUnion[]) + : undefined; - const messages: any[] = [{ role: "user", content: instruction }]; + const messages: MessageParam[] = [{ role: "user", content: instruction }]; for (let i = 0; i < MAX_ITERATIONS; i++) { const response = await client.messages.create({ @@ -42,10 +52,12 @@ export async function processWithMiniMax( }); const textParts = response.content - .filter((block: any) => block.type === "text") - .map((block: any) => block.text); + .filter((block: ContentBlock): block is TextBlock => block.type === "text") + .map((block: TextBlock) => block.text); - const toolCalls = response.content.filter((block: any) => block.type === "tool_use"); + const toolCalls = response.content.filter( + (block: ContentBlock): block is ToolUseBlock => block.type === "tool_use", + ); if (toolCalls.length === 0 || !toolRegistry) { return textParts.join("\n") || "No response from MiniMax"; @@ -53,9 +65,8 @@ export async function processWithMiniMax( messages.push({ role: "assistant", content: response.content }); - const toolResults: any[] = []; - for (const call of toolCalls) { - const toolUse = call as any; + const toolResults: ToolResultBlockParam[] = []; + for (const toolUse of toolCalls) { const result = await toolRegistry.execute( toolUse.name, toolUse.input as Record, @@ -71,7 +82,7 @@ export async function processWithMiniMax( } return "Reached maximum tool iterations."; - } catch (error) { + } catch (error: unknown) { throw new Error( `MiniMax API error: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error }, diff --git a/src/providers/mistral.ts b/src/providers/mistral.ts index 3c47e2e..d88fe1f 100644 --- a/src/providers/mistral.ts +++ b/src/providers/mistral.ts @@ -1,6 +1,10 @@ import fs from "fs"; import path from "path"; import OpenAI from "openai"; +import type { + ChatCompletionMessageParam, + ChatCompletionTool, +} from "openai/resources/chat/completions/completions"; import { ToolRegistry } from "../tools/registry"; const MAX_ITERATIONS = 10; @@ -26,9 +30,11 @@ export async function processWithMistral( baseURL: "https://api.mistral.ai/v1", }); - const tools = toolRegistry ? toolRegistry.getDefinitionsForProvider("openai") : undefined; + const tools = toolRegistry + ? (toolRegistry.getDefinitionsForProvider("openai") as unknown as ChatCompletionTool[]) + : undefined; - const messages: any[] = [ + const messages: ChatCompletionMessageParam[] = [ { role: "system", content: loadSystemPrompt() }, { role: "user", content: instruction }, ]; @@ -50,8 +56,10 @@ export async function processWithMistral( messages.push(assistantMsg); - for (const call of assistantMsg.tool_calls) { - const toolCall = call as any; + for (const toolCall of assistantMsg.tool_calls) { + if (toolCall.type !== "function") { + continue; + } const args = JSON.parse(toolCall.function.arguments); const result = await toolRegistry.execute(toolCall.function.name, args); messages.push({ @@ -63,7 +71,7 @@ export async function processWithMistral( } return "Reached maximum tool iterations."; - } catch (error) { + } catch (error: unknown) { throw new Error( `Mistral AI API error: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error }, diff --git a/src/providers/moonshot.ts b/src/providers/moonshot.ts index f34ee52..a1b40b6 100644 --- a/src/providers/moonshot.ts +++ b/src/providers/moonshot.ts @@ -1,6 +1,10 @@ import fs from "fs"; import path from "path"; import OpenAI from "openai"; +import type { + ChatCompletionMessageParam, + ChatCompletionTool, +} from "openai/resources/chat/completions/completions"; import { ToolRegistry } from "../tools/registry"; const MAX_ITERATIONS = 10; @@ -26,9 +30,11 @@ export async function processWithMoonshot( baseURL: "https://api.moonshot.cn/v1", }); - const tools = toolRegistry ? toolRegistry.getDefinitionsForProvider("openai") : undefined; + const tools = toolRegistry + ? (toolRegistry.getDefinitionsForProvider("openai") as unknown as ChatCompletionTool[]) + : undefined; - const messages: any[] = [ + const messages: ChatCompletionMessageParam[] = [ { role: "system", content: loadSystemPrompt() }, { role: "user", content: instruction }, ]; @@ -50,8 +56,10 @@ export async function processWithMoonshot( messages.push(assistantMsg); - for (const call of assistantMsg.tool_calls) { - const toolCall = call as any; + for (const toolCall of assistantMsg.tool_calls) { + if (toolCall.type !== "function") { + continue; + } const args = JSON.parse(toolCall.function.arguments); const result = await toolRegistry.execute(toolCall.function.name, args); messages.push({ @@ -63,7 +71,7 @@ export async function processWithMoonshot( } return "Reached maximum tool iterations."; - } catch (error) { + } catch (error: unknown) { throw new Error( `Moonshot AI API error: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error }, diff --git a/src/providers/openai.ts b/src/providers/openai.ts index b158f39..e979952 100644 --- a/src/providers/openai.ts +++ b/src/providers/openai.ts @@ -1,6 +1,10 @@ import fs from "fs"; import path from "path"; import OpenAI from "openai"; +import type { + ChatCompletionMessageParam, + ChatCompletionTool, +} from "openai/resources/chat/completions/completions"; import { ToolRegistry } from "../tools/registry"; const MAX_ITERATIONS = 10; @@ -23,9 +27,11 @@ export async function processWithOpenAI( try { const openai = new OpenAI({ apiKey }); - const tools = toolRegistry ? toolRegistry.getDefinitionsForProvider("openai") : undefined; + const tools = toolRegistry + ? (toolRegistry.getDefinitionsForProvider("openai") as unknown as ChatCompletionTool[]) + : undefined; - const messages: any[] = [ + const messages: ChatCompletionMessageParam[] = [ { role: "system", content: loadSystemPrompt() }, { role: "user", content: instruction }, ]; @@ -47,8 +53,10 @@ export async function processWithOpenAI( messages.push(assistantMsg); - for (const call of assistantMsg.tool_calls) { - const toolCall = call as any; + for (const toolCall of assistantMsg.tool_calls) { + if (toolCall.type !== "function") { + continue; + } const args = JSON.parse(toolCall.function.arguments); const result = await toolRegistry.execute(toolCall.function.name, args); messages.push({ @@ -60,7 +68,7 @@ export async function processWithOpenAI( } return "Reached maximum tool iterations."; - } catch (error) { + } catch (error: unknown) { throw new Error( `OpenAI API error: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error }, diff --git a/src/providers/openrouter.ts b/src/providers/openrouter.ts index 0f3638b..d4e9a7e 100644 --- a/src/providers/openrouter.ts +++ b/src/providers/openrouter.ts @@ -1,6 +1,10 @@ import fs from "fs"; import path from "path"; import OpenAI from "openai"; +import type { + ChatCompletionMessageParam, + ChatCompletionTool, +} from "openai/resources/chat/completions/completions"; import { ToolRegistry } from "../tools/registry"; const MAX_ITERATIONS = 10; @@ -30,9 +34,11 @@ export async function processWithOpenRouter( }, }); - const tools = toolRegistry ? toolRegistry.getDefinitionsForProvider("openai") : undefined; + const tools = toolRegistry + ? (toolRegistry.getDefinitionsForProvider("openai") as unknown as ChatCompletionTool[]) + : undefined; - const messages: any[] = [ + const messages: ChatCompletionMessageParam[] = [ { role: "system", content: loadSystemPrompt() }, { role: "user", content: instruction }, ]; @@ -54,8 +60,10 @@ export async function processWithOpenRouter( messages.push(assistantMsg); - for (const call of assistantMsg.tool_calls) { - const toolCall = call as any; + for (const toolCall of assistantMsg.tool_calls) { + if (toolCall.type !== "function") { + continue; + } const args = JSON.parse(toolCall.function.arguments); const result = await toolRegistry.execute(toolCall.function.name, args); messages.push({ @@ -67,7 +75,7 @@ export async function processWithOpenRouter( } return "Reached maximum tool iterations."; - } catch (error) { + } catch (error: unknown) { throw new Error( `OpenRouter API error: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error }, diff --git a/src/providers/xai.ts b/src/providers/xai.ts index a1ab3d3..9faf52b 100644 --- a/src/providers/xai.ts +++ b/src/providers/xai.ts @@ -1,6 +1,10 @@ import fs from "fs"; import path from "path"; import OpenAI from "openai"; +import type { + ChatCompletionMessageParam, + ChatCompletionTool, +} from "openai/resources/chat/completions/completions"; import { ToolRegistry } from "../tools/registry"; const MAX_ITERATIONS = 10; @@ -26,9 +30,11 @@ export async function processWithXAI( baseURL: "https://api.x.ai/v1", }); - const tools = toolRegistry ? toolRegistry.getDefinitionsForProvider("openai") : undefined; + const tools = toolRegistry + ? (toolRegistry.getDefinitionsForProvider("openai") as unknown as ChatCompletionTool[]) + : undefined; - const messages: any[] = [ + const messages: ChatCompletionMessageParam[] = [ { role: "system", content: loadSystemPrompt() }, { role: "user", content: instruction }, ]; @@ -50,8 +56,10 @@ export async function processWithXAI( messages.push(assistantMsg); - for (const call of assistantMsg.tool_calls) { - const toolCall = call as any; + for (const toolCall of assistantMsg.tool_calls) { + if (toolCall.type !== "function") { + continue; + } const args = JSON.parse(toolCall.function.arguments); const result = await toolRegistry.execute(toolCall.function.name, args); messages.push({ @@ -63,7 +71,7 @@ export async function processWithXAI( } return "Reached maximum tool iterations."; - } catch (error) { + } catch (error: unknown) { throw new Error(`xAI API error: ${error instanceof Error ? error.message : "Unknown error"}`, { cause: error, }); diff --git a/src/shared/block-chunker.ts b/src/shared/block-chunker.ts index cb7a4f5..55e9f8e 100644 --- a/src/shared/block-chunker.ts +++ b/src/shared/block-chunker.ts @@ -3,7 +3,7 @@ * Inspired by OpenClaw's block reply pipeline */ -import type { BlockChunkingConfig, ChunkBreakPreference } from "./streaming-types"; +import type { BlockChunkingConfig } from "./streaming-types"; export class BlockChunker { private buffer: string = ""; diff --git a/src/shared/block-reply-pipeline.ts b/src/shared/block-reply-pipeline.ts index c6b9c0d..2b4d53a 100644 --- a/src/shared/block-reply-pipeline.ts +++ b/src/shared/block-reply-pipeline.ts @@ -21,7 +21,9 @@ export class BlockReplyPipeline { async processText(text: string): Promise { const normalized = normalizeStreamOutput(text); - if (normalized.skip) return; + if (normalized.skip) { + return; + } this.accumulatedText += normalized.text; @@ -29,7 +31,7 @@ export class BlockReplyPipeline { const chunks = this.chunker.addText(normalized.text); - for (const chunkText of chunks) { + for (const _chunkText of chunks) { await this.sendChunk(false); } } @@ -55,7 +57,7 @@ export class BlockReplyPipeline { try { await this.config.onChunk(chunk); - } catch (error) { + } catch { // Streaming should be resilient } } diff --git a/src/shared/logger.ts b/src/shared/logger.ts index d2ba321..354cb30 100644 --- a/src/shared/logger.ts +++ b/src/shared/logger.ts @@ -77,15 +77,18 @@ class Logger { info(msg: string): void { console.log(msg); + // eslint-disable-next-line no-control-regex this.writeToFile("INFO", msg.replace(/\x1b\[[0-9;]*m/g, "")); } debug(msg: string): void { + // eslint-disable-next-line no-control-regex this.writeToFile("DEBUG", msg.replace(/\x1b\[[0-9;]*m/g, "")); } error(msg: string, err?: unknown): void { const errStr = err instanceof Error ? `: ${err.message}` : err ? `: ${String(err)}` : ""; + // eslint-disable-next-line no-control-regex this.writeToFile("ERROR", msg.replace(/\x1b\[[0-9;]*m/g, "") + errStr); } diff --git a/src/shared/stream-normalizer.ts b/src/shared/stream-normalizer.ts index a9303c8..4846b72 100644 --- a/src/shared/stream-normalizer.ts +++ b/src/shared/stream-normalizer.ts @@ -5,7 +5,9 @@ import type { NormalizedStreamOutput } from "./streaming-types"; +// eslint-disable-next-line no-control-regex const ANSI_REGEX = /\x1b\[[0-9;]*m/g; +// eslint-disable-next-line no-control-regex const CONTROL_CHARS_REGEX = /[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]/g; const HEARTBEAT_TOKEN = "HEARTBEAT_OK"; diff --git a/src/shared/typing-signaler.ts b/src/shared/typing-signaler.ts index 34c1247..c49f1b7 100644 --- a/src/shared/typing-signaler.ts +++ b/src/shared/typing-signaler.ts @@ -38,11 +38,11 @@ export class NoOpTypingSignaler implements TypingSignaler { * Discord typing signaler */ export class DiscordTypingSignaler implements TypingSignaler { - private channel: any; + private channel: unknown; private lastTypingSignal: number = 0; private typingInterval: NodeJS.Timeout | null = null; - constructor(channel: any) { + constructor(channel: unknown) { this.channel = channel; } @@ -51,8 +51,9 @@ export class DiscordTypingSignaler implements TypingSignaler { // Discord typing indicator lasts 10 seconds, refresh every 8 seconds if (now - this.lastTypingSignal > 8000) { try { - if ("sendTyping" in this.channel) { - await this.channel.sendTyping(); + const ch = this.channel as Record; + if ("sendTyping" in ch && typeof ch.sendTyping === "function") { + await ch.sendTyping(); this.lastTypingSignal = now; } } catch { @@ -77,10 +78,10 @@ export class DiscordTypingSignaler implements TypingSignaler { * Telegram typing signaler */ export class TelegramTypingSignaler implements TypingSignaler { - private ctx: any; + private ctx: unknown; private lastTypingSignal: number = 0; - constructor(ctx: any) { + constructor(ctx: unknown) { this.ctx = ctx; } @@ -89,7 +90,11 @@ export class TelegramTypingSignaler implements TypingSignaler { // Telegram typing indicator lasts 5 seconds, refresh every 4 seconds if (now - this.lastTypingSignal > 4000) { try { - await this.ctx.telegram.sendChatAction(this.ctx.chat.id, "typing"); + const ctx = this.ctx as { + telegram: { sendChatAction: (chatId: unknown, action: string) => Promise }; + chat: { id: unknown }; + }; + await ctx.telegram.sendChatAction(ctx.chat.id, "typing"); this.lastTypingSignal = now; } catch { // Ignore typing errors @@ -110,11 +115,11 @@ export class TelegramTypingSignaler implements TypingSignaler { * WhatsApp typing signaler */ export class WhatsAppTypingSignaler implements TypingSignaler { - private sock: any; + private sock: unknown; private jid: string; private lastTypingSignal: number = 0; - constructor(sock: any, jid: string) { + constructor(sock: unknown, jid: string) { this.sock = sock; this.jid = jid; } @@ -124,7 +129,10 @@ export class WhatsAppTypingSignaler implements TypingSignaler { // WhatsApp typing indicator, refresh every 3 seconds if (now - this.lastTypingSignal > 3000) { try { - await this.sock.sendPresenceUpdate("composing", this.jid); + const sock = this.sock as { + sendPresenceUpdate: (status: string, jid: string) => Promise; + }; + await sock.sendPresenceUpdate("composing", this.jid); this.lastTypingSignal = now; } catch { // Ignore typing errors @@ -138,7 +146,10 @@ export class WhatsAppTypingSignaler implements TypingSignaler { async stopTyping(): Promise { try { - await this.sock.sendPresenceUpdate("paused", this.jid); + const sock = this.sock as { + sendPresenceUpdate: (status: string, jid: string) => Promise; + }; + await sock.sendPresenceUpdate("paused", this.jid); } catch { // Ignore errors } diff --git a/src/tools/cron.ts b/src/tools/cron.ts index 751286c..9cc7ba6 100644 --- a/src/tools/cron.ts +++ b/src/tools/cron.ts @@ -13,7 +13,7 @@ function runCommand( resolve({ stdout: stdout?.toString() ?? "", stderr: stderr?.toString() ?? "", - code: err ? (err as any).code ?? 1 : 0, + code: err ? ((err as { code?: number }).code ?? 1) : 0, }); }); }); @@ -28,7 +28,7 @@ function runShell( resolve({ stdout: stdout?.toString() ?? "", stderr: stderr?.toString() ?? "", - code: err ? (err as any).code ?? 1 : 0, + code: err ? ((err as { code?: number }).code ?? 1) : 0, }); }); }); @@ -55,11 +55,13 @@ export class CronTool implements Tool { }, name: { type: "string", - description: "Task name (required for get, add, remove). On Unix, used as a comment identifier in crontab.", + description: + "Task name (required for get, add, remove). On Unix, used as a comment identifier in crontab.", }, schedule: { type: "string", - description: "Schedule expression. Unix: cron expression (e.g. '0 2 * * *'). Windows: schtasks schedule (e.g. '/sc daily /st 02:00').", + description: + "Schedule expression. Unix: cron expression (e.g. '0 2 * * *'). Windows: schtasks schedule (e.g. '/sc daily /st 02:00').", }, command: { type: "string", @@ -87,15 +89,17 @@ export class CronTool implements Tool { case "list": return isWindows ? this.listWindows() : this.listUnix(); case "get": - return isWindows - ? this.getWindows(args.name as string) - : this.getUnix(args.name as string); + return isWindows ? this.getWindows(args.name as string) : this.getUnix(args.name as string); case "add": return this.addTask(args, isWindows); case "remove": return this.removeTask(args, isWindows); default: - return { toolCallId: "", output: `Unknown action: ${action}. Use: list, get, add, remove.`, isError: true }; + return { + toolCallId: "", + output: `Unknown action: ${action}. Use: list, get, add, remove.`, + isError: true, + }; } } @@ -104,9 +108,17 @@ export class CronTool implements Tool { if (result.code !== 0) { if (result.stderr.includes("no crontab")) { - return { toolCallId: "", output: "No crontab configured for current user.", isError: false }; + return { + toolCallId: "", + output: "No crontab configured for current user.", + isError: false, + }; } - return { toolCallId: "", output: `Failed to list crontab: ${result.stderr.trim()}`, isError: true }; + return { + toolCallId: "", + output: `Failed to list crontab: ${result.stderr.trim()}`, + isError: true, + }; } const lines = result.stdout.trim().split("\n"); @@ -114,7 +126,9 @@ export class CronTool implements Tool { for (const line of lines) { const trimmed = line.trim(); - if (!trimmed) continue; + if (!trimmed) { + continue; + } if (trimmed.startsWith("#")) { jobs.push(trimmed); continue; @@ -126,14 +140,23 @@ export class CronTool implements Tool { return { toolCallId: "", output: "Crontab is empty.", isError: false }; } - return { toolCallId: "", output: `Crontab entries:\n\n${jobs.join("\n")}`, isError: false, metadata: { count: jobs.length } }; + return { + toolCallId: "", + output: `Crontab entries:\n\n${jobs.join("\n")}`, + isError: false, + metadata: { count: jobs.length }, + }; } private async listWindows(): Promise { const result = await runCommand("schtasks.exe", ["/query", "/fo", "TABLE", "/nh"]); if (result.code !== 0) { - return { toolCallId: "", output: `Failed to list scheduled tasks: ${result.stderr.trim()}`, isError: true }; + return { + toolCallId: "", + output: `Failed to list scheduled tasks: ${result.stderr.trim()}`, + isError: true, + }; } const output = result.stdout.trim(); @@ -151,7 +174,11 @@ export class CronTool implements Tool { const result = await runCommand("crontab", ["-l"]); if (result.code !== 0) { - return { toolCallId: "", output: `Failed to read crontab: ${result.stderr.trim()}`, isError: true }; + return { + toolCallId: "", + output: `Failed to read crontab: ${result.stderr.trim()}`, + isError: true, + }; } const lines = result.stdout.split("\n"); @@ -167,7 +194,11 @@ export class CronTool implements Tool { return { toolCallId: "", output: `No crontab entry matching "${name}".`, isError: false }; } - return { toolCallId: "", output: `Entries matching "${name}":\n\n${matching.join("\n")}`, isError: false }; + return { + toolCallId: "", + output: `Entries matching "${name}":\n\n${matching.join("\n")}`, + isError: false, + }; } private async getWindows(name: string | undefined): Promise { @@ -178,7 +209,11 @@ export class CronTool implements Tool { const result = await runCommand("schtasks.exe", ["/query", "/tn", name, "/v", "/fo", "LIST"]); if (result.code !== 0) { - return { toolCallId: "", output: `Task "${name}" not found: ${result.stderr.trim()}`, isError: true }; + return { + toolCallId: "", + output: `Task "${name}" not found: ${result.stderr.trim()}`, + isError: true, + }; } return { toolCallId: "", output: result.stdout.trim(), isError: false }; @@ -190,9 +225,15 @@ export class CronTool implements Tool { const command = (args.command as string)?.trim(); const confirm = args.confirm === true; - if (!name) return { toolCallId: "", output: "Error: name is required for add.", isError: true }; - if (!schedule) return { toolCallId: "", output: "Error: schedule is required for add.", isError: true }; - if (!command) return { toolCallId: "", output: "Error: command is required for add.", isError: true }; + if (!name) { + return { toolCallId: "", output: "Error: name is required for add.", isError: true }; + } + if (!schedule) { + return { toolCallId: "", output: "Error: schedule is required for add.", isError: true }; + } + if (!command) { + return { toolCallId: "", output: "Error: command is required for add.", isError: true }; + } if (!confirm) { return { toolCallId: "", @@ -209,7 +250,11 @@ export class CronTool implements Tool { const result = await runCommand("schtasks.exe", schtasksArgs); if (result.code !== 0) { - return { toolCallId: "", output: `Failed to create task: ${(result.stdout + result.stderr).trim()}`, isError: true }; + return { + toolCallId: "", + output: `Failed to create task: ${(result.stdout + result.stderr).trim()}`, + isError: true, + }; } return { toolCallId: "", output: `Task "${name}" created successfully.`, isError: false }; } @@ -222,7 +267,11 @@ export class CronTool implements Tool { const result = await runShell(`echo '${newCrontab.replace(/'/g, "'\\''")}' | crontab -`); if (result.code !== 0) { - return { toolCallId: "", output: `Failed to add cron job: ${result.stderr.trim()}`, isError: true }; + return { + toolCallId: "", + output: `Failed to add cron job: ${result.stderr.trim()}`, + isError: true, + }; } return { toolCallId: "", output: `Cron job "${name}" added:\n ${cronLine}`, isError: false }; @@ -232,15 +281,25 @@ export class CronTool implements Tool { const name = (args.name as string)?.trim(); const confirm = args.confirm === true; - if (!name) return { toolCallId: "", output: "Error: name is required for remove.", isError: true }; + if (!name) { + return { toolCallId: "", output: "Error: name is required for remove.", isError: true }; + } if (!confirm) { - return { toolCallId: "", output: `This will remove the scheduled task "${name}". Set confirm=true to proceed.`, isError: false }; + return { + toolCallId: "", + output: `This will remove the scheduled task "${name}". Set confirm=true to proceed.`, + isError: false, + }; } if (isWindows) { const result = await runCommand("schtasks.exe", ["/delete", "/tn", name, "/f"]); if (result.code !== 0) { - return { toolCallId: "", output: `Failed to remove task: ${(result.stdout + result.stderr).trim()}`, isError: true }; + return { + toolCallId: "", + output: `Failed to remove task: ${(result.stdout + result.stderr).trim()}`, + isError: true, + }; } return { toolCallId: "", output: `Task "${name}" removed.`, isError: false }; } @@ -248,7 +307,11 @@ export class CronTool implements Tool { // Unix: remove matching lines from crontab const existing = await runCommand("crontab", ["-l"]); if (existing.code !== 0) { - return { toolCallId: "", output: `No crontab to modify: ${existing.stderr.trim()}`, isError: true }; + return { + toolCallId: "", + output: `No crontab to modify: ${existing.stderr.trim()}`, + isError: true, + }; } const lines = existing.stdout.split("\n"); @@ -261,7 +324,11 @@ export class CronTool implements Tool { const newCrontab = filtered.join("\n"); const result = await runShell(`echo '${newCrontab.replace(/'/g, "'\\''")}' | crontab -`); if (result.code !== 0) { - return { toolCallId: "", output: `Failed to update crontab: ${result.stderr.trim()}`, isError: true }; + return { + toolCallId: "", + output: `Failed to update crontab: ${result.stderr.trim()}`, + isError: true, + }; } return { toolCallId: "", output: `Cron job "${name}" removed.`, isError: false }; diff --git a/src/tools/env.ts b/src/tools/env.ts index e0dbff2..edc9ab5 100644 --- a/src/tools/env.ts +++ b/src/tools/env.ts @@ -3,9 +3,21 @@ import path from "path"; import { Tool, ToolDefinition, ToolResult } from "./types"; const SECRET_PATTERNS = [ - /secret/i, /key/i, /token/i, /password/i, /passwd/i, /credential/i, - /auth/i, /private/i, /api_key/i, /apikey/i, /access/i, /jwt/i, - /encrypt/i, /signing/i, /certificate/i, + /secret/i, + /key/i, + /token/i, + /password/i, + /passwd/i, + /credential/i, + /auth/i, + /private/i, + /api_key/i, + /apikey/i, + /access/i, + /jwt/i, + /encrypt/i, + /signing/i, + /certificate/i, ]; function isSensitive(name: string): boolean { @@ -13,9 +25,13 @@ function isSensitive(name: string): boolean { } function maskValue(name: string, value: string, unmask: boolean): string { - if (unmask) return value; + if (unmask) { + return value; + } if (isSensitive(name)) { - if (value.length <= 4) return "****"; + if (value.length <= 4) { + return "****"; + } return value.substring(0, 2) + "****" + value.substring(value.length - 2); } return value; @@ -48,7 +64,8 @@ export class EnvTool implements Tool { }, name: { type: "string", - description: "Variable name (for action=get) or comma-separated names (for action=check).", + description: + "Variable name (for action=get) or comma-separated names (for action=check).", }, file: { type: "string", @@ -56,7 +73,8 @@ export class EnvTool implements Tool { }, filter: { type: "string", - description: "Filter env vars by prefix or substring (for action=list). e.g. 'NODE', 'DATABASE'.", + description: + "Filter env vars by prefix or substring (for action=list). e.g. 'NODE', 'DATABASE'.", }, unmask: { type: "boolean", @@ -86,7 +104,11 @@ export class EnvTool implements Tool { case "check": return this.actionCheck(args.name as string | undefined); default: - return { toolCallId: "", output: `Unknown action: ${action}. Use: list, get, dotenv, check.`, isError: true }; + return { + toolCallId: "", + output: `Unknown action: ${action}. Use: list, get, dotenv, check.`, + isError: true, + }; } } @@ -102,7 +124,11 @@ export class EnvTool implements Tool { entries.sort(([a], [b]) => a.localeCompare(b)); if (entries.length === 0) { - return { toolCallId: "", output: filter ? `No env vars matching "${filter}".` : "No environment variables found.", isError: false }; + return { + toolCallId: "", + output: filter ? `No env vars matching "${filter}".` : "No environment variables found.", + isError: false, + }; } const lines = entries.map(([k, v]) => `${k}=${maskValue(k, v, unmask)}`); @@ -126,7 +152,12 @@ export class EnvTool implements Tool { const value = process.env[name]; if (value === undefined) { - return { toolCallId: "", output: `${name} is not set.`, isError: false, metadata: { exists: false } }; + return { + toolCallId: "", + output: `${name} is not set.`, + isError: false, + metadata: { exists: false }, + }; } return { @@ -146,7 +177,9 @@ export class EnvTool implements Tool { const found: string[] = []; for (const alt of alternatives) { const altPath = path.join(this.defaultCwd, alt); - if (fs.existsSync(altPath)) found.push(alt); + if (fs.existsSync(altPath)) { + found.push(alt); + } } let msg = `File not found: ${envFile}`; @@ -160,7 +193,11 @@ export class EnvTool implements Tool { try { content = fs.readFileSync(envFile, "utf-8"); } catch (err) { - return { toolCallId: "", output: `Cannot read ${envFile}: ${err instanceof Error ? err.message : "permission denied"}`, isError: true }; + return { + toolCallId: "", + output: `Cannot read ${envFile}: ${err instanceof Error ? err.message : "permission denied"}`, + isError: true, + }; } const lines = content.split("\n"); @@ -180,7 +217,10 @@ export class EnvTool implements Tool { } const key = trimmed.substring(0, eqIndex).trim(); - const val = trimmed.substring(eqIndex + 1).trim().replace(/^["']|["']$/g, ""); + const val = trimmed + .substring(eqIndex + 1) + .trim() + .replace(/^["']|["']$/g, ""); result.push(`${key}=${maskValue(key, val, unmask)}`); } @@ -193,10 +233,17 @@ export class EnvTool implements Tool { private actionCheck(names: string | undefined): ToolResult { if (!names) { - return { toolCallId: "", output: "Error: name is required for action=check (comma-separated).", isError: true }; + return { + toolCallId: "", + output: "Error: name is required for action=check (comma-separated).", + isError: true, + }; } - const varNames = names.split(",").map((n) => n.trim()).filter(Boolean); + const varNames = names + .split(",") + .map((n) => n.trim()) + .filter(Boolean); const results: string[] = []; let allSet = true; diff --git a/src/tools/git.ts b/src/tools/git.ts index ebcca64..acf4499 100644 --- a/src/tools/git.ts +++ b/src/tools/git.ts @@ -16,13 +16,18 @@ function runGit( signal?: AbortSignal, ): Promise<{ stdout: string; stderr: string; code: number | null }> { return new Promise((resolve) => { - const proc = execFile("git", args, { cwd, maxBuffer: 1024 * 1024, timeout: 30_000 }, (err, stdout, stderr) => { - resolve({ - stdout: stdout?.toString() ?? "", - stderr: stderr?.toString() ?? "", - code: err ? (err as any).code ?? 1 : 0, - }); - }); + const proc = execFile( + "git", + args, + { cwd, maxBuffer: 1024 * 1024, timeout: 30_000 }, + (err, stdout, stderr) => { + resolve({ + stdout: stdout?.toString() ?? "", + stderr: stderr?.toString() ?? "", + code: err ? ((err as { code?: number }).code ?? 1) : 0, + }); + }, + ); if (signal) { const handler = () => { @@ -63,11 +68,32 @@ export class GitTool implements Tool { action: { type: "string", description: "Git action to perform.", - enum: ["status", "diff", "log", "branch", "commit", "stash", "checkout", "add", "reset", "remote", "show", "blame", "tag", "pull", "push", "fetch", "merge", "rebase", "cherry-pick"], + enum: [ + "status", + "diff", + "log", + "branch", + "commit", + "stash", + "checkout", + "add", + "reset", + "remote", + "show", + "blame", + "tag", + "pull", + "push", + "fetch", + "merge", + "rebase", + "cherry-pick", + ], }, args: { type: "string", - description: "Additional arguments (e.g. branch name, file path, --oneline, -n 10). Passed directly to git.", + description: + "Additional arguments (e.g. branch name, file path, --oneline, -n 10). Passed directly to git.", }, message: { type: "string", @@ -79,7 +105,8 @@ export class GitTool implements Tool { }, force: { type: "boolean", - description: "Allow destructive operations like push --force or reset --hard. Default false.", + description: + "Allow destructive operations like push --force or reset --hard. Default false.", }, }, required: ["action"], @@ -124,38 +151,57 @@ export class GitTool implements Tool { switch (action) { case "status": gitArgs = ["status", "--short", "--branch"]; - if (extraArgs) gitArgs.push(...extraArgs.split(/\s+/)); + if (extraArgs) { + gitArgs.push(...extraArgs.split(/\s+/)); + } break; case "diff": gitArgs = ["diff"]; - if (extraArgs) gitArgs.push(...extraArgs.split(/\s+/)); - else gitArgs.push("--stat"); + if (extraArgs) { + gitArgs.push(...extraArgs.split(/\s+/)); + } else { + gitArgs.push("--stat"); + } break; case "log": gitArgs = ["log", "--oneline"]; - if (extraArgs) gitArgs.push(...extraArgs.split(/\s+/)); - else gitArgs.push("-20"); + if (extraArgs) { + gitArgs.push(...extraArgs.split(/\s+/)); + } else { + gitArgs.push("-20"); + } break; case "commit": if (!message) { - return { toolCallId: "", output: "Error: commit requires a message parameter.", isError: true }; + return { + toolCallId: "", + output: "Error: commit requires a message parameter.", + isError: true, + }; } gitArgs = ["commit", "-m", message]; - if (extraArgs) gitArgs.push(...extraArgs.split(/\s+/)); + if (extraArgs) { + gitArgs.push(...extraArgs.split(/\s+/)); + } break; case "branch": gitArgs = ["branch"]; - if (extraArgs) gitArgs.push(...extraArgs.split(/\s+/)); - else gitArgs.push("-a"); + if (extraArgs) { + gitArgs.push(...extraArgs.split(/\s+/)); + } else { + gitArgs.push("-a"); + } break; default: gitArgs = [action]; - if (extraArgs) gitArgs.push(...extraArgs.split(/\s+/)); + if (extraArgs) { + gitArgs.push(...extraArgs.split(/\s+/)); + } break; } diff --git a/src/tools/http.ts b/src/tools/http.ts index 91072b2..6746767 100644 --- a/src/tools/http.ts +++ b/src/tools/http.ts @@ -3,11 +3,11 @@ import { Tool, ToolDefinition, ToolResult } from "./types"; const DEFAULT_TIMEOUT_MS = 30_000; const MAX_BODY_SIZE = 50_000; -const BLOCKED_HOSTS = [ +const BLOCKED_HOSTS = new Set([ "169.254.169.254", // AWS/GCP/Azure metadata "metadata.google.internal", "metadata.internal", -]; +]); export class HttpTool implements Tool { name = "http"; @@ -34,11 +34,13 @@ export class HttpTool implements Tool { }, headers: { type: "object", - description: "Request headers as key-value pairs (e.g. {\"Authorization\": \"Bearer ...\", \"Content-Type\": \"application/json\"}).", + description: + 'Request headers as key-value pairs (e.g. {"Authorization": "Bearer ...", "Content-Type": "application/json"}).', }, body: { type: "string", - description: "Request body (for POST/PUT/PATCH). Send as string — use JSON.stringify for JSON payloads.", + description: + "Request body (for POST/PUT/PATCH). Send as string — use JSON.stringify for JSON payloads.", }, timeout: { type: "number", @@ -67,8 +69,12 @@ export class HttpTool implements Tool { return { toolCallId: "", output: `Error: invalid URL: ${url}`, isError: true }; } - if (BLOCKED_HOSTS.includes(parsedUrl.hostname)) { - return { toolCallId: "", output: `Blocked: requests to ${parsedUrl.hostname} are not allowed (cloud metadata security).`, isError: true }; + if (BLOCKED_HOSTS.has(parsedUrl.hostname)) { + return { + toolCallId: "", + output: `Blocked: requests to ${parsedUrl.hostname} are not allowed (cloud metadata security).`, + isError: true, + }; } const method = ((args.method as string) || "GET").toUpperCase(); @@ -100,19 +106,30 @@ export class HttpTool implements Tool { const elapsed = Date.now() - startTime; const responseHeaders: Record = {}; - const importantHeaders = ["content-type", "content-length", "server", "x-request-id", "location", "set-cookie", "cache-control"]; + const importantHeaders = [ + "content-type", + "content-length", + "server", + "x-request-id", + "location", + "set-cookie", + "cache-control", + ]; for (const key of importantHeaders) { const val = response.headers.get(key); - if (val) responseHeaders[key] = val; + if (val) { + responseHeaders[key] = val; + } } let responseBody = ""; if (method !== "HEAD") { try { const text = await response.text(); - responseBody = text.length > MAX_BODY_SIZE - ? text.substring(0, MAX_BODY_SIZE) + "\n\n(body truncated)" - : text; + responseBody = + text.length > MAX_BODY_SIZE + ? text.substring(0, MAX_BODY_SIZE) + "\n\n(body truncated)" + : text; } catch { responseBody = "(could not read response body)"; } @@ -126,7 +143,9 @@ export class HttpTool implements Tool { `${response.status} ${response.statusText} (${elapsed}ms)`, headerLines ? `\nHeaders:\n${headerLines}` : "", responseBody ? `\nBody:\n${responseBody}` : "", - ].filter(Boolean).join("\n"); + ] + .filter(Boolean) + .join("\n"); return { toolCallId: "", diff --git a/src/tools/network.ts b/src/tools/network.ts index 4190e9c..bc2a768 100644 --- a/src/tools/network.ts +++ b/src/tools/network.ts @@ -15,7 +15,7 @@ function runCommand( resolve({ stdout: stdout?.toString() ?? "", stderr: stderr?.toString() ?? "", - code: err ? (err as any).code ?? 1 : 0, + code: err ? ((err as { code?: number }).code ?? 1) : 0, }); }); }); @@ -78,7 +78,11 @@ export class NetworkTool implements Tool { case "ports": return this.actionPorts(port); default: - return { toolCallId: "", output: `Unknown action: ${action}. Use: ping, dns, reachable, ports.`, isError: true }; + return { + toolCallId: "", + output: `Unknown action: ${action}. Use: ping, dns, reachable, ports.`, + isError: true, + }; } } @@ -157,7 +161,11 @@ export class NetworkTool implements Tool { isError: results.length <= 1, }; } catch (err) { - return { toolCallId: "", output: `DNS lookup failed for ${hostname}: ${err instanceof Error ? err.message : String(err)}`, isError: true }; + return { + toolCallId: "", + output: `DNS lookup failed for ${hostname}: ${err instanceof Error ? err.message : String(err)}`, + isError: true, + }; } } @@ -192,9 +200,19 @@ export class NetworkTool implements Tool { } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes("abort")) { - return { toolCallId: "", output: `${target} is not reachable (timed out after ${REACHABLE_TIMEOUT}ms).`, isError: false, metadata: { reachable: false } }; + return { + toolCallId: "", + output: `${target} is not reachable (timed out after ${REACHABLE_TIMEOUT}ms).`, + isError: false, + metadata: { reachable: false }, + }; } - return { toolCallId: "", output: `${target} is not reachable: ${message}`, isError: false, metadata: { reachable: false } }; + return { + toolCallId: "", + output: `${target} is not reachable: ${message}`, + isError: false, + metadata: { reachable: false }, + }; } finally { clearTimeout(timeoutId); } @@ -209,15 +227,19 @@ export class NetworkTool implements Tool { if (isWindows) { if (port) { result = await runCommand("powershell.exe", [ - "-NoProfile", "-NonInteractive", "-Command", + "-NoProfile", + "-NonInteractive", + "-Command", `Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | ` + - `Select-Object LocalAddress, LocalPort, OwningProcess | Format-Table -AutoSize`, + `Select-Object LocalAddress, LocalPort, OwningProcess | Format-Table -AutoSize`, ]); } else { result = await runCommand("powershell.exe", [ - "-NoProfile", "-NonInteractive", "-Command", + "-NoProfile", + "-NonInteractive", + "-Command", `Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | ` + - `Select-Object LocalAddress, LocalPort, OwningProcess | Sort-Object LocalPort | Format-Table -AutoSize`, + `Select-Object LocalAddress, LocalPort, OwningProcess | Sort-Object LocalPort | Format-Table -AutoSize`, ]); } } else if (isMac) { @@ -241,7 +263,11 @@ export class NetworkTool implements Tool { if (port) { return { toolCallId: "", output: `No process listening on port ${port}.`, isError: false }; } - return { toolCallId: "", output: output || "Could not retrieve listening ports.", isError: result.code !== 0 }; + return { + toolCallId: "", + output: output || "Could not retrieve listening ports.", + isError: result.code !== 0, + }; } return { diff --git a/src/tools/process.ts b/src/tools/process.ts index 72ce332..3f34006 100644 --- a/src/tools/process.ts +++ b/src/tools/process.ts @@ -157,7 +157,7 @@ export class ProcessTool implements Tool { const running = getSession(sessionId); if (running) { - const truncNote = running.truncated ? "\n(output was truncated)" : ""; + const _truncNote = running.truncated ? "\n(output was truncated)" : ""; return { toolCallId: "", output: running.aggregated || "(no output yet)", diff --git a/src/tools/registry.ts b/src/tools/registry.ts index b68c1dc..e659a03 100644 --- a/src/tools/registry.ts +++ b/src/tools/registry.ts @@ -11,7 +11,7 @@ export class ToolRegistry { return Array.from(this.tools.values()).map((t) => t.getDefinition()); } - getDefinitionsForProvider(provider: string): any[] { + getDefinitionsForProvider(provider: string): (Record | ToolDefinition)[] { const defs = this.getDefinitions(); switch (provider) { @@ -68,7 +68,11 @@ export class ToolRegistry { async executeAll(calls: ToolCall[], signal?: AbortSignal): Promise { const promises = calls.map(async (call) => { if (signal?.aborted) { - return { toolCallId: call.id, output: "Tool execution aborted", isError: true } as ToolResult; + return { + toolCallId: call.id, + output: "Tool execution aborted", + isError: true, + } as ToolResult; } const result = await this.execute(call.name, call.arguments, signal); diff --git a/src/tools/search.ts b/src/tools/search.ts index 124d254..4388614 100644 --- a/src/tools/search.ts +++ b/src/tools/search.ts @@ -3,9 +3,25 @@ import path from "path"; import { Tool, ToolDefinition, ToolResult } from "./types"; const SKIP_DIRS = new Set([ - "node_modules", ".git", "dist", "build", ".next", ".nuxt", "__pycache__", - ".cache", ".venv", "venv", "vendor", "target", ".gradle", ".idea", ".vs", - "coverage", ".nyc_output", ".turbo", ".parcel-cache", + "node_modules", + ".git", + "dist", + "build", + ".next", + ".nuxt", + "__pycache__", + ".cache", + ".venv", + "venv", + "vendor", + "target", + ".gradle", + ".idea", + ".vs", + "coverage", + ".nyc_output", + ".turbo", + ".parcel-cache", ]); const MAX_RESULTS = 200; @@ -18,7 +34,9 @@ function walkDir( depth: number = 0, maxDepth: number = 20, ): void { - if (depth > maxDepth) return; + if (depth > maxDepth) { + return; + } let entries: fs.Dirent[]; try { @@ -28,8 +46,12 @@ function walkDir( } for (const entry of entries) { - if (SKIP_DIRS.has(entry.name)) continue; - if (entry.name.startsWith(".") && entry.name !== ".env" && depth > 0) continue; + if (SKIP_DIRS.has(entry.name)) { + continue; + } + if (entry.name.startsWith(".") && entry.name !== ".env" && depth > 0) { + continue; + } const fullPath = path.join(dir, entry.name); @@ -39,7 +61,9 @@ function walkDir( try { const stat = fs.statSync(fullPath); const shouldStop = callback(fullPath, stat); - if (shouldStop) return; + if (shouldStop) { + return; + } } catch {} } } @@ -80,7 +104,8 @@ export class SearchTool implements Tool { }, pattern: { type: "string", - description: "Search pattern. For grep: regex or text. For glob: file pattern (e.g. *.ts, *.py). For find: file extension (e.g. ts, py).", + description: + "Search pattern. For grep: regex or text. For glob: file pattern (e.g. *.ts, *.py). For find: file extension (e.g. ts, py).", }, path: { type: "string", @@ -96,7 +121,8 @@ export class SearchTool implements Tool { }, include: { type: "string", - description: "Only search files matching this glob pattern (e.g. *.ts, *.py). For grep action.", + description: + "Only search files matching this glob pattern (e.g. *.ts, *.py). For grep action.", }, }, required: ["action", "pattern"], @@ -113,7 +139,8 @@ export class SearchTool implements Tool { const pattern = args.pattern as string; const searchPath = (args.path as string)?.trim() || this.defaultCwd; const caseSensitive = args.case_sensitive === true; - const maxResults = typeof args.max_results === "number" ? Math.min(args.max_results, MAX_RESULTS) : MAX_RESULTS; + const maxResults = + typeof args.max_results === "number" ? Math.min(args.max_results, MAX_RESULTS) : MAX_RESULTS; const include = (args.include as string)?.trim() || ""; if (!pattern) { @@ -132,7 +159,11 @@ export class SearchTool implements Tool { case "find": return this.actionFind(searchPath, pattern, maxResults); default: - return { toolCallId: "", output: `Unknown action: ${action}. Use: grep, glob, find.`, isError: true }; + return { + toolCallId: "", + output: `Unknown action: ${action}. Use: grep, glob, find.`, + isError: true, + }; } } @@ -147,15 +178,22 @@ export class SearchTool implements Tool { try { regex = new RegExp(pattern, caseSensitive ? "g" : "gi"); } catch { - regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), caseSensitive ? "g" : "gi"); + regex = new RegExp( + pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), + caseSensitive ? "g" : "gi", + ); } const matches: string[] = []; let fileCount = 0; walkDir(searchPath, (filePath, stat) => { - if (stat.size > MAX_FILE_SIZE) return false; - if (include && !matchGlob(path.basename(filePath), include)) return false; + if (stat.size > MAX_FILE_SIZE) { + return false; + } + if (include && !matchGlob(path.basename(filePath), include)) { + return false; + } let content: string; try { @@ -165,7 +203,9 @@ export class SearchTool implements Tool { } // Skip binary files - if (content.includes("\0")) return false; + if (content.includes("\0")) { + return false; + } const lines = content.split("\n"); const relativePath = path.relative(searchPath, filePath); @@ -178,12 +218,15 @@ export class SearchTool implements Tool { fileHasMatch = true; fileCount++; } - const lineContent = lines[i].length > MAX_MATCH_CONTEXT - ? lines[i].substring(0, MAX_MATCH_CONTEXT) + "..." - : lines[i]; + const lineContent = + lines[i].length > MAX_MATCH_CONTEXT + ? lines[i].substring(0, MAX_MATCH_CONTEXT) + "..." + : lines[i]; matches.push(`${relativePath}:${i + 1}: ${lineContent.trim()}`); - if (matches.length >= maxResults) return true; + if (matches.length >= maxResults) { + return true; + } } } return false; @@ -209,7 +252,9 @@ export class SearchTool implements Tool { const filename = path.basename(filePath); if (matchGlob(filename, pattern)) { results.push(path.relative(searchPath, filePath)); - if (results.length >= maxResults) return true; + if (results.length >= maxResults) { + return true; + } } return false; }); @@ -237,7 +282,9 @@ export class SearchTool implements Tool { size: formatSize(stat.size), modified: stat.mtime.toISOString().split("T")[0], }); - if (results.length >= maxResults) return true; + if (results.length >= maxResults) { + return true; + } } return false; }); @@ -257,7 +304,11 @@ export class SearchTool implements Tool { } function formatSize(bytes: number): string { - if (bytes < 1024) return `${bytes}B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`; + if (bytes < 1024) { + return `${bytes}B`; + } + if (bytes < 1024 * 1024) { + return `${(bytes / 1024).toFixed(1)}KB`; + } return `${(bytes / (1024 * 1024)).toFixed(1)}MB`; } diff --git a/src/tools/sysinfo.ts b/src/tools/sysinfo.ts index f99505c..6cfd415 100644 --- a/src/tools/sysinfo.ts +++ b/src/tools/sysinfo.ts @@ -1,14 +1,10 @@ -import os from "os"; import { execFile } from "child_process"; +import os from "os"; import { Tool, ToolDefinition, ToolResult } from "./types"; const CMD_TIMEOUT = 10_000; -function runCommand( - cmd: string, - args: string[], - timeout: number = CMD_TIMEOUT, -): Promise { +function runCommand(cmd: string, args: string[], timeout: number = CMD_TIMEOUT): Promise { return new Promise((resolve) => { execFile(cmd, args, { timeout, maxBuffer: 512 * 1024 }, (err, stdout) => { resolve(stdout?.toString().trim() ?? ""); @@ -18,7 +14,9 @@ function runCommand( function formatBytes(bytes: number): string { const gb = bytes / (1024 * 1024 * 1024); - if (gb >= 1) return `${gb.toFixed(1)} GB`; + if (gb >= 1) { + return `${gb.toFixed(1)} GB`; + } const mb = bytes / (1024 * 1024); return `${mb.toFixed(0)} MB`; } @@ -29,8 +27,12 @@ function formatUptime(seconds: number): string { const minutes = Math.floor((seconds % 3600) / 60); const parts: string[] = []; - if (days > 0) parts.push(`${days}d`); - if (hours > 0) parts.push(`${hours}h`); + if (days > 0) { + parts.push(`${days}d`); + } + if (hours > 0) { + parts.push(`${hours}h`); + } parts.push(`${minutes}m`); return parts.join(" "); } @@ -85,7 +87,11 @@ export class SysinfoTool implements Tool { case "processes": return this.actionProcesses(count); default: - return { toolCallId: "", output: `Unknown action: ${action}. Use: overview, cpu, memory, disk, uptime, processes.`, isError: true }; + return { + toolCallId: "", + output: `Unknown action: ${action}. Use: overview, cpu, memory, disk, uptime, processes.`, + isError: true, + }; } } @@ -185,21 +191,38 @@ export class SysinfoTool implements Tool { if (isWindows) { const output = await runCommand("powershell.exe", [ - "-NoProfile", "-NonInteractive", "-Command", + "-NoProfile", + "-NonInteractive", + "-Command", `Get-PSDrive -PSProvider FileSystem | Select-Object Name, ` + - `@{N='Used(GB)';E={[math]::Round($_.Used/1GB,1)}}, ` + - `@{N='Free(GB)';E={[math]::Round($_.Free/1GB,1)}}, ` + - `@{N='Total(GB)';E={[math]::Round(($_.Used+$_.Free)/1GB,1)}}, ` + - `Root | Format-Table -AutoSize`, + `@{N='Used(GB)';E={[math]::Round($_.Used/1GB,1)}}, ` + + `@{N='Free(GB)';E={[math]::Round($_.Free/1GB,1)}}, ` + + `@{N='Total(GB)';E={[math]::Round(($_.Used+$_.Free)/1GB,1)}}, ` + + `Root | Format-Table -AutoSize`, ]); - return { toolCallId: "", output: output || "Could not retrieve disk info.", isError: !output }; + return { + toolCallId: "", + output: output || "Could not retrieve disk info.", + isError: !output, + }; } - const output = await runCommand("df", ["-h", "--type=ext4", "--type=xfs", "--type=btrfs", "--type=apfs", "--type=hfs"]); + const output = await runCommand("df", [ + "-h", + "--type=ext4", + "--type=xfs", + "--type=btrfs", + "--type=apfs", + "--type=hfs", + ]); if (!output) { // Fallback without type filter (macOS df doesn't support --type) const fallback = await runCommand("df", ["-h"]); - return { toolCallId: "", output: fallback || "Could not retrieve disk info.", isError: !fallback }; + return { + toolCallId: "", + output: fallback || "Could not retrieve disk info.", + isError: !fallback, + }; } return { toolCallId: "", output, isError: false }; @@ -209,10 +232,7 @@ export class SysinfoTool implements Tool { const uptime = os.uptime(); const bootTime = new Date(Date.now() - uptime * 1000); - const lines = [ - `Uptime: ${formatUptime(uptime)}`, - `Boot time: ${bootTime.toISOString()}`, - ]; + const lines = [`Uptime: ${formatUptime(uptime)}`, `Boot time: ${bootTime.toISOString()}`]; return { toolCallId: "", output: lines.join("\n"), isError: false }; } @@ -222,23 +242,25 @@ export class SysinfoTool implements Tool { if (isWindows) { const output = await runCommand("powershell.exe", [ - "-NoProfile", "-NonInteractive", "-Command", + "-NoProfile", + "-NonInteractive", + "-Command", `Get-Process | Sort-Object CPU -Descending | Select-Object -First ${count} ` + - `Id, ProcessName, ` + - `@{N='CPU(s)';E={[math]::Round($_.CPU,1)}}, ` + - `@{N='Mem(MB)';E={[math]::Round($_.WorkingSet64/1MB,1)}} ` + - `| Format-Table -AutoSize`, + `Id, ProcessName, ` + + `@{N='CPU(s)';E={[math]::Round($_.CPU,1)}}, ` + + `@{N='Mem(MB)';E={[math]::Round($_.WorkingSet64/1MB,1)}} ` + + `| Format-Table -AutoSize`, ]); return { toolCallId: "", - output: output ? `Top ${count} processes by CPU:\n\n${output}` : "Could not retrieve process list.", + output: output + ? `Top ${count} processes by CPU:\n\n${output}` + : "Could not retrieve process list.", isError: !output, }; } - const output = await runCommand("ps", [ - "aux", "--sort=-%cpu", - ]); + const output = await runCommand("ps", ["aux", "--sort=-%cpu"]); if (!output) { return { toolCallId: "", output: "Could not retrieve process list.", isError: true }; diff --git a/src/tools/terminal.ts b/src/tools/terminal.ts index 1265d6e..66af272 100644 --- a/src/tools/terminal.ts +++ b/src/tools/terminal.ts @@ -4,8 +4,6 @@ import { appendOutput, markExited, markBackgrounded, - drainSession, - tail, ProcessSession, } from "./process-registry"; import { Tool, ToolDefinition, ToolResult } from "./types"; @@ -40,7 +38,7 @@ function resolveShell(): { shell: string; buildArgs: (cmd: string) => string[] } const isWindows = process.platform === "win32"; if (isWindows) { - const pwsh = process.env.COMSPEC?.toLowerCase().includes("powershell") + const _pwsh = process.env.COMSPEC?.toLowerCase().includes("powershell") ? process.env.COMSPEC : null; @@ -56,8 +54,10 @@ function resolveShell(): { shell: string; buildArgs: (cmd: string) => string[] } buildArgs: (cmd: string) => [ "-NoProfile", "-NonInteractive", - "-ExecutionPolicy", "Bypass", - "-Command", cmd, + "-ExecutionPolicy", + "Bypass", + "-Command", + cmd, ], }; } catch {} diff --git a/src/utils/keychain.ts b/src/utils/keychain.ts index 0337abf..146f47f 100644 --- a/src/utils/keychain.ts +++ b/src/utils/keychain.ts @@ -9,7 +9,7 @@ export async function setApiKey(provider: string, apiKey: string): Promise try { await keytar.setPassword(SERVICE_NAME, `${provider}-api-key`, apiKey); } catch (error) { - throw new Error(`Failed to store API key in keychain: ${error}`); + throw new Error(`Failed to store API key in keychain: ${error}`, { cause: error }); } } @@ -20,7 +20,7 @@ export async function getApiKey(provider: string): Promise { try { return await keytar.getPassword(SERVICE_NAME, `${provider}-api-key`); } catch (error) { - throw new Error(`Failed to retrieve API key from keychain: ${error}`); + throw new Error(`Failed to retrieve API key from keychain: ${error}`, { cause: error }); } } @@ -31,7 +31,7 @@ export async function deleteApiKey(provider: string): Promise { try { return await keytar.deletePassword(SERVICE_NAME, `${provider}-api-key`); } catch (error) { - throw new Error(`Failed to delete API key from keychain: ${error}`); + throw new Error(`Failed to delete API key from keychain: ${error}`, { cause: error }); } } @@ -42,7 +42,7 @@ export async function setBotToken(platform: string, token: string): Promise { try { return await keytar.getPassword(SERVICE_NAME, `${platform}-bot-token`); } catch (error) { - throw new Error(`Failed to retrieve bot token from keychain: ${error}`); + throw new Error(`Failed to retrieve bot token from keychain: ${error}`, { cause: error }); } } @@ -64,7 +64,7 @@ export async function deleteBotToken(platform: string): Promise { try { return await keytar.deletePassword(SERVICE_NAME, `${platform}-bot-token`); } catch (error) { - throw new Error(`Failed to delete bot token from keychain: ${error}`); + throw new Error(`Failed to delete bot token from keychain: ${error}`, { cause: error }); } } @@ -77,7 +77,7 @@ export async function isKeychainAvailable(): Promise { await keytar.setPassword(SERVICE_NAME, "test-key", "test-value"); await keytar.deletePassword(SERVICE_NAME, "test-key"); return true; - } catch (error) { + } catch { return false; } } @@ -92,6 +92,6 @@ export async function clearAllCredentials(): Promise { await keytar.deletePassword(SERVICE_NAME, cred.account); } } catch (error) { - throw new Error(`Failed to clear credentials: ${error}`); + throw new Error(`Failed to clear credentials: ${error}`, { cause: error }); } } diff --git a/src/utils/model-discovery-util.ts b/src/utils/model-discovery-util.ts index e75f0f9..dddc4c8 100644 --- a/src/utils/model-discovery-util.ts +++ b/src/utils/model-discovery-util.ts @@ -196,6 +196,7 @@ export async function discoverHuggingFaceModels(apiKey: string): Promise config !== null, readFileSync: (_p: string) => { - if (!config) throw new Error("ENOENT"); + if (!config) { + throw new Error("ENOENT"); + } return JSON.stringify(config); }, writeFileSync: (p: string, data: string) => { diff --git a/test/unit/adapters.test.ts b/test/unit/adapters.test.ts index 1d57c70..11498ab 100644 --- a/test/unit/adapters.test.ts +++ b/test/unit/adapters.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; +import type { AdapterConfig } from "../../src/adapters/base-adapter"; import { ClaudeCodeAdapter } from "../../src/adapters/claude-code"; import { CursorAdapter } from "../../src/adapters/cursor-cli"; import { GeminiCodeAdapter } from "../../src/adapters/gemini-cli"; @@ -6,6 +7,11 @@ import { KiroAdapter } from "../../src/adapters/kiro-cli"; import { CodexAdapter } from "../../src/adapters/openai-codex"; import { OpenCodeAdapter } from "../../src/adapters/opencode"; +interface AdapterInternals { + buildArgs(instruction: string): string[]; + getConfig(): AdapterConfig; +} + // Suppress logger file I/O during tests vi.mock("../../src/shared/logger", () => ({ logger: { @@ -46,7 +52,7 @@ describe("ClaudeCodeAdapter", () => { }); it("buildArgs includes --model flag", () => { - const args = (adapter as any).buildArgs("fix the bug"); + const args = (adapter as unknown as AdapterInternals).buildArgs("fix the bug"); expect(args).toContain("--model"); expect(args).toContain("sonnet"); expect(args).toContain("fix the bug"); @@ -55,12 +61,12 @@ describe("ClaudeCodeAdapter", () => { it("buildArgs uses updated model after setModel", () => { adapter.setModel("opus"); - const args = (adapter as any).buildArgs("do something"); + const args = (adapter as unknown as AdapterInternals).buildArgs("do something"); expect(args).toContain("opus"); }); it("getConfig returns correct CLI command", () => { - const config = (adapter as any).getConfig(); + const config = (adapter as unknown as AdapterInternals).getConfig(); expect(config.cliCommand).toBe("claude"); expect(config.displayName).toContain("Claude Code"); }); @@ -84,7 +90,7 @@ describe("CursorAdapter", () => { }); it("buildArgs omits --model when Auto", () => { - const args = (adapter as any).buildArgs("write tests"); + const args = (adapter as unknown as AdapterInternals).buildArgs("write tests"); expect(args).not.toContain("--model"); expect(args).toContain("--headless"); expect(args).toContain("--prompt"); @@ -92,13 +98,13 @@ describe("CursorAdapter", () => { it("buildArgs includes --model when non-Auto", () => { adapter.setModel("GPT-5.3 Codex"); - const args = (adapter as any).buildArgs("refactor"); + const args = (adapter as unknown as AdapterInternals).buildArgs("refactor"); expect(args).toContain("--model"); expect(args).toContain("GPT-5.3 Codex"); }); it("getConfig returns cursor CLI command", () => { - const config = (adapter as any).getConfig(); + const config = (adapter as unknown as AdapterInternals).getConfig(); expect(config.cliCommand).toBe("cursor"); }); }); @@ -127,7 +133,7 @@ describe("GeminiCodeAdapter", () => { }); it("buildArgs omits --model when model is empty", () => { - const args = (adapter as any).buildArgs("explain code"); + const args = (adapter as unknown as AdapterInternals).buildArgs("explain code"); expect(args).not.toContain("--model"); expect(args).toContain("--approval-mode"); expect(args).toContain("yolo"); @@ -136,7 +142,7 @@ describe("GeminiCodeAdapter", () => { it("buildArgs includes --model when model is set", () => { adapter.setModel("gemini-2.5-pro"); - const args = (adapter as any).buildArgs("explain code"); + const args = (adapter as unknown as AdapterInternals).buildArgs("explain code"); expect(args).toContain("--model"); expect(args).toContain("gemini-2.5-pro"); }); @@ -160,7 +166,7 @@ describe("KiroAdapter", () => { }); it("buildArgs omits --model when auto", () => { - const args = (adapter as any).buildArgs("build feature"); + const args = (adapter as unknown as AdapterInternals).buildArgs("build feature"); expect(args).toContain("chat"); expect(args).toContain("--no-interactive"); expect(args).not.toContain("--model"); @@ -169,7 +175,7 @@ describe("KiroAdapter", () => { it("buildArgs includes --model when non-auto", () => { adapter.setModel("claude-sonnet-4"); - const args = (adapter as any).buildArgs("build feature"); + const args = (adapter as unknown as AdapterInternals).buildArgs("build feature"); expect(args).toContain("--model"); expect(args).toContain("claude-sonnet-4"); }); @@ -193,7 +199,7 @@ describe("CodexAdapter", () => { }); it("buildArgs includes -m flag with model", () => { - const args = (adapter as any).buildArgs("create api"); + const args = (adapter as unknown as AdapterInternals).buildArgs("create api"); expect(args).toContain("exec"); expect(args).toContain("--full-auto"); expect(args).toContain("-m"); @@ -202,7 +208,7 @@ describe("CodexAdapter", () => { it("buildArgs reflects model change", () => { adapter.setModel("gpt-5.2-codex"); - const args = (adapter as any).buildArgs("refactor"); + const args = (adapter as unknown as AdapterInternals).buildArgs("refactor"); expect(args).toContain("gpt-5.2-codex"); }); }); @@ -225,7 +231,7 @@ describe("OpenCodeAdapter", () => { }); it("buildArgs omits --model when default (empty)", () => { - const args = (adapter as any).buildArgs("test"); + const args = (adapter as unknown as AdapterInternals).buildArgs("test"); expect(args).toContain("--non-interactive"); expect(args).toContain("--message"); expect(args).not.toContain("--model"); @@ -233,7 +239,7 @@ describe("OpenCodeAdapter", () => { it("buildArgs includes --model after setModel", () => { adapter.setModel("openai/gpt-5.2"); - const args = (adapter as any).buildArgs("test"); + const args = (adapter as unknown as AdapterInternals).buildArgs("test"); expect(args).toContain("--model"); expect(args).toContain("openai/gpt-5.2"); }); diff --git a/test/unit/agent-commands.test.ts b/test/unit/agent-commands.test.ts index cc7829b..c502333 100644 --- a/test/unit/agent-commands.test.ts +++ b/test/unit/agent-commands.test.ts @@ -43,9 +43,11 @@ vi.mock("../../src/utils/keychain", () => ({ vi.mock("child_process", () => ({ spawn: vi.fn(), - exec: vi.fn((_cmd: string, ...args: any[]) => { + exec: vi.fn((_cmd: string, ...args: unknown[]) => { const cb = args[args.length - 1]; - if (typeof cb === "function") cb(null, "1.0.0"); + if (typeof cb === "function") { + cb(null, "1.0.0"); + } }), execSync: vi.fn(), })); @@ -102,7 +104,7 @@ describe("AgentCore commands", () => { agent = new AgentCore(); // Directly set authorized user since require("fs") inside AgentCore // may not resolve the vi.mock in all environments - (agent as any).authorizedUser = "user1"; + (agent as unknown as { authorizedUser: string }).authorizedUser = "user1"; }); describe("/code command", () => { diff --git a/test/unit/router.test.ts b/test/unit/router.test.ts index a1bde10..aa6e92e 100644 --- a/test/unit/router.test.ts +++ b/test/unit/router.test.ts @@ -8,9 +8,11 @@ vi.mock("../../src/shared/logger", () => ({ // Mock child_process vi.mock("child_process", () => ({ spawn: vi.fn(), - exec: vi.fn((_cmd: string, ...args: any[]) => { + exec: vi.fn((_cmd: string, ...args: unknown[]) => { const cb = args[args.length - 1]; - if (typeof cb === "function") cb(null, "1.0.0"); + if (typeof cb === "function") { + cb(null, "1.0.0"); + } }), execSync: vi.fn(), })); @@ -71,9 +73,9 @@ vi.mock("fs", () => ({ })); describe("Router", () => { - let Router: any; - let AVAILABLE_ADAPTERS: any; - let router: any; + let Router: typeof import("../../src/core/router").Router; + let AVAILABLE_ADAPTERS: typeof import("../../src/core/router").AVAILABLE_ADAPTERS; + let router: InstanceType; beforeEach(async () => { process.env.IDE_TYPE = "claude-code"; @@ -143,7 +145,7 @@ describe("Router", () => { describe("AVAILABLE_ADAPTERS", () => { it("has expected adapter entries", () => { - const ids = AVAILABLE_ADAPTERS.map((a: any) => a.id); + const ids = AVAILABLE_ADAPTERS.map((a: { id: string }) => a.id); expect(ids).toContain("claude-code"); expect(ids).toContain("cursor"); expect(ids).toContain("gemini-code");