diff --git a/docs/plans/state.json b/docs/plans/state.json index b5ae85e..3c8ed8c 100644 --- a/docs/plans/state.json +++ b/docs/plans/state.json @@ -2677,10 +2677,10 @@ "test_status": "pnpm test:run src/channels/utils.test.ts src/channels/discord/adapter.test.ts src/channels/slack/adapter.test.ts src/channels/whatsapp/adapter.test.ts src/daemon/routing.test.ts src/gateway/handlers/handlers.test.ts src/frontends/tui/minimal.test.ts src/gateway/tailscale.test.ts src/automation/webhooks.test.ts src/automation/cron.test.ts src/automation/heartbeat.test.ts src/frontends/tui/minimal.login.test.ts src/daemon/clientFactory.test.ts src/gateway/handlers/services.test.ts src/models/local/llamacpp.test.ts src/models/local/ollama.test.ts src/tools/builtin/image-analyze.test.ts + pnpm lint passing (0 errors, 203 warnings)" }, "audit-followup-lint-warning-reduction-pass-2": { - "status": "in_progress", + "status": "completed", "date": "2026-02-16", "updated": "2026-02-16", - "summary": "Continued warning burn-down on current hotspots by removing non-null assertions and broad `any` usage, tightening adapter/client typing, and cleaning unused imports/params across orchestrator/model/channel/gateway/TUI/doctor files and selected tests. Warning count reduced from 203 to 115 (88 warnings burned down) while keeping lint at 0 errors.", + "summary": "Completed warning burn-down by removing non-null assertions and broad `any` usage, tightening adapter/client typing, and cleaning unused imports/params across orchestrator/model/channel/gateway/TUI/setup/skills/tools files and tests. Warning count reduced from 203 to 0 (203 warnings burned down) with lint fully clean.", "files_modified": [ "src/backends/native/agent.ts", "src/backends/native/orchestrator.test.ts", @@ -2699,7 +2699,7 @@ "src/models/router.test.ts", "src/skills/installer.test.ts" ], - "test_status": "pnpm exec eslint on edited files + pnpm lint passing (0 errors, 115 warnings)" + "test_status": "pnpm exec eslint on edited files + pnpm lint passing (0 errors, 0 warnings)" } }, "overall_progress": { diff --git a/src/agents/registry.test.ts b/src/agents/registry.test.ts index fc838db..5708186 100644 --- a/src/agents/registry.test.ts +++ b/src/agents/registry.test.ts @@ -52,12 +52,18 @@ describe('AgentConfigRegistry', () => { }); expect(registry.list()).toHaveLength(2); - const assistant = registry.get('assistant')!; + const assistant = registry.get('assistant'); + if (!assistant) { + throw new Error('Expected assistant config'); + } expect(assistant.systemPrompt).toBe('Be helpful.'); expect(assistant.modelTier).toBe('default'); expect(assistant.toolProfile).toBe('messaging'); - const coder = registry.get('coder')!; + const coder = registry.get('coder'); + if (!coder) { + throw new Error('Expected coder config'); + } expect(coder.sandbox).toBe(true); }); }); diff --git a/src/agents/router.test.ts b/src/agents/router.test.ts index 6f255c8..830214c 100644 --- a/src/agents/router.test.ts +++ b/src/agents/router.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect } from 'vitest'; import { AgentRouter } from './router.js'; -import type { RoutingConfig } from '../config/schema.js'; describe('AgentRouter', () => { describe('resolve()', () => { diff --git a/src/audit/export.ts b/src/audit/export.ts index 6c5e5fe..5a7717b 100644 --- a/src/audit/export.ts +++ b/src/audit/export.ts @@ -1,5 +1,4 @@ -import { createReadStream, promises as fs } from 'fs'; -import { dirname, basename } from 'path'; +import { promises as fs } from 'fs'; import type { AuditEvent, AuditQuery } from './types.js'; export async function queryAuditLogs(logPath: string, query: AuditQuery): Promise { diff --git a/src/audit/logger.ts b/src/audit/logger.ts index f3af376..8be6f02 100644 --- a/src/audit/logger.ts +++ b/src/audit/logger.ts @@ -1,4 +1,4 @@ -import { createWriteStream, existsSync, mkdirSync, promises as fs } from 'fs'; +import { createWriteStream, existsSync, mkdirSync } from 'fs'; import { dirname } from 'path'; import type { AuditEvent, @@ -53,14 +53,15 @@ export class AuditLogger { } private write(event: Omit): void { - if (!this.config.enabled || !this.writeStream) { + const writeStream = this.writeStream; + if (!this.config.enabled || !writeStream) { return; } this.rotator.checkRotation(); const fullEvent: AuditEvent = { ...event, timestamp: Date.now() }; - this.writeStream!.write(JSON.stringify(fullEvent) + '\n'); + writeStream.write(JSON.stringify(fullEvent) + '\n'); } private shouldLog(category: 'tools' | 'sessions' | 'automation', level: string): boolean { @@ -294,9 +295,10 @@ export class AuditLogger { // ── Lifecycle ─────────────────────────────────────────────── async close(): Promise { - if (this.writeStream) { + const writeStream = this.writeStream; + if (writeStream) { await new Promise((resolve) => { - this.writeStream!.end(() => resolve()); + writeStream.end(() => resolve()); }); this.writeStream = null; } diff --git a/src/automation/heartbeat.ts b/src/automation/heartbeat.ts index e3d0973..8084d64 100644 --- a/src/automation/heartbeat.ts +++ b/src/automation/heartbeat.ts @@ -1,7 +1,7 @@ import { statfsSync, accessSync, constants as fsConstants } from 'fs'; import { request } from 'http'; import type { HeartbeatConfig, HeartbeatCheck } from '../config/schema.js'; -import type { ChannelAdapter, ChannelStatus, OutboundMessage } from '../channels/types.js'; +import type { ChannelAdapter, OutboundMessage } from '../channels/types.js'; import { auditLogger } from '../audit/index.js'; /** Result of a single health check. */ diff --git a/src/backends/native/agent.test.ts b/src/backends/native/agent.test.ts index 4893e60..84f99d1 100644 --- a/src/backends/native/agent.test.ts +++ b/src/backends/native/agent.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { NativeAgent } from './agent.js'; -import type { ModelClient, ChatResponse } from '../../models/types.js'; +import type { ModelClient, ChatRequest, ChatResponse } from '../../models/types.js'; import { ToolRegistry, ToolExecutor } from '../../tools/index.js'; import { HookEngine } from '../../hooks/index.js'; -import type { Tool, ToolResult } from '../../tools/index.js'; +import type { Tool } from '../../tools/index.js'; describe('NativeAgent', () => { const createMockClient = (): ModelClient => ({ @@ -197,7 +197,7 @@ describe('NativeAgent tool loop', () => { it('nudges model after same tool called too many times with different args', async () => { let callCount = 0; const mockClient: ModelClient = { - chat: vi.fn().mockImplementation((req: any) => { + chat: vi.fn().mockImplementation((req: ChatRequest) => { callCount++; // After nudge message, model should respond with text const lastMsg = req.messages[req.messages.length - 1]; diff --git a/src/channels/discord/adapter.test.ts b/src/channels/discord/adapter.test.ts index c7f5935..d068bad 100644 --- a/src/channels/discord/adapter.test.ts +++ b/src/channels/discord/adapter.test.ts @@ -15,7 +15,10 @@ function createMockClient() { user: null as { id: string; tag: string } | null, on: vi.fn((event: string, handler: (...args: unknown[]) => void) => { if (!handlers.has(event)) {handlers.set(event, []);} - handlers.get(event)!.push(handler); + const eventHandlers = handlers.get(event); + if (eventHandlers) { + eventHandlers.push(handler); + } }), login: vi.fn(async (_token: string) => { // Set user info after login diff --git a/src/channels/matrix/adapter.test.ts b/src/channels/matrix/adapter.test.ts index e05a0c8..c57f941 100644 --- a/src/channels/matrix/adapter.test.ts +++ b/src/channels/matrix/adapter.test.ts @@ -87,7 +87,7 @@ describe('MatrixAdapter', () => { it('send delivers a message via PUT', async () => { let syncStarted = false; - mockFetch.mockImplementation(async (url: string, init?: any) => { + mockFetch.mockImplementation(async (url: string, init?: RequestInit) => { if (url.endsWith('/_matrix/client/v3/account/whoami')) { return jsonResponse({ user_id: '@flynn:example.org' }); } @@ -99,7 +99,7 @@ describe('MatrixAdapter', () => { return new Promise(() => {}); } if (init?.method === 'PUT' && url.includes('/send/m.room.message/')) { - const body = JSON.parse(init.body); + const body = JSON.parse(String(init?.body ?? '{}')); expect(body.msgtype).toBe('m.text'); expect(body.body).toBe('Hello there'); return jsonResponse({ event_id: '$sent1' }); diff --git a/src/channels/matrix/adapter.ts b/src/channels/matrix/adapter.ts index f07daf2..c3611cf 100644 --- a/src/channels/matrix/adapter.ts +++ b/src/channels/matrix/adapter.ts @@ -201,8 +201,10 @@ export class MatrixAdapter implements ChannelAdapter { return; } - const err = error as any; - if (err && typeof err === 'object' && err.name === 'AbortError') { + const errName = error && typeof error === 'object' && 'name' in error + ? String((error as { name?: unknown }).name) + : ''; + if (errName === 'AbortError') { return; } diff --git a/src/channels/pairing.test.ts b/src/channels/pairing.test.ts index 0c23875..aaa396f 100644 --- a/src/channels/pairing.test.ts +++ b/src/channels/pairing.test.ts @@ -121,7 +121,7 @@ describe('PairingManager', () => { it('listPendingCodes returns only non-expired codes', () => { vi.useFakeTimers(); - const code1 = manager.generateCode('first'); + manager.generateCode('first'); // Advance time so the first code is almost expired vi.advanceTimersByTime(200_000); diff --git a/src/channels/slack/adapter.ts b/src/channels/slack/adapter.ts index acf9cce..06eefd4 100644 --- a/src/channels/slack/adapter.ts +++ b/src/channels/slack/adapter.ts @@ -226,7 +226,11 @@ export class SlackAdapter implements ChannelAdapter { } try { - const result = await this.app!.client.users.info({ user: userId }); + const app = this.app; + if (!app) { + return userId; + } + const result = await app.client.users.info({ user: userId }); const name = result.user?.real_name || result.user?.name || userId; this.userNameCache.set(userId, { name, expiresAt: now + this.userNameCacheTtlMs }); if (this.userNameCache.size > this.userNameCacheMaxEntries) { diff --git a/src/channels/telegram/adapter.ts b/src/channels/telegram/adapter.ts index e419a24..f877339 100644 --- a/src/channels/telegram/adapter.ts +++ b/src/channels/telegram/adapter.ts @@ -2,7 +2,6 @@ import { Bot, InputFile } from 'grammy'; import type { HookEngine } from '../../hooks/index.js'; import type { - Attachment, InboundMessage, OutboundMessage, OutboundAttachment, @@ -438,7 +437,8 @@ export class TelegramAdapter implements ChannelAdapter { /** Send an outbound message, automatically chunking if it exceeds Telegram's limit. */ async send(peerId: string, message: OutboundMessage): Promise { - if (!this.bot) {throw new Error('Telegram adapter not connected');} + const bot = this.bot; + if (!bot) {throw new Error('Telegram adapter not connected');} const chatId = Number(peerId); const text = message.text ?? ''; @@ -459,7 +459,7 @@ export class TelegramAdapter implements ChannelAdapter { // is strict and can fail on unescaped characters. If Telegram rejects the // message, retry once without parse_mode so users still get the content. try { - await this.bot!.api.sendMessage(chatId, chunk, { parse_mode: 'Markdown' }); + await bot.api.sendMessage(chatId, chunk, { parse_mode: 'Markdown' }); } catch (error) { const description = error && typeof error === 'object' && 'description' in error ? String((error as { description?: unknown }).description) @@ -472,7 +472,7 @@ export class TelegramAdapter implements ChannelAdapter { throw error; } - await this.bot!.api.sendMessage(chatId, chunk); + await bot.api.sendMessage(chatId, chunk); } }; diff --git a/src/cli/sessions.test.ts b/src/cli/sessions.test.ts index 52a8b4a..c1cc943 100644 --- a/src/cli/sessions.test.ts +++ b/src/cli/sessions.test.ts @@ -31,10 +31,16 @@ describe('sessions command', () => { const telegramSession = sessions.find(s => s.id === 'telegram:123'); expect(telegramSession).toBeDefined(); - expect(telegramSession!.messageCount).toBe(2); + if (!telegramSession) { + throw new Error('Expected telegram session'); + } + expect(telegramSession.messageCount).toBe(2); const tuiSession = sessions.find(s => s.id === 'tui:local'); expect(tuiSession).toBeDefined(); - expect(tuiSession!.messageCount).toBe(1); + if (!tuiSession) { + throw new Error('Expected tui session'); + } + expect(tuiSession.messageCount).toBe(1); }); }); diff --git a/src/cli/setup/automation.ts b/src/cli/setup/automation.ts index 51fa53d..0621ec2 100644 --- a/src/cli/setup/automation.ts +++ b/src/cli/setup/automation.ts @@ -110,8 +110,8 @@ export async function setupAutomation(p: Prompter, builder: ConfigBuilder): Prom * Run OAuth auth flows for enabled Google services. * Called after config is saved so the auth commands can read it. */ -export async function runGoogleAuth(p: Prompter, config: Record): Promise { - const automation = config.automation as Record | undefined; +export async function runGoogleAuth(p: Prompter, config: Record): Promise { + const automation = config.automation as Record | undefined; if (!automation) {return;} const pending: { name: string; authCmd: string }[] = []; diff --git a/src/cli/setup/channels.test.ts b/src/cli/setup/channels.test.ts index be31b27..962a48b 100644 --- a/src/cli/setup/channels.test.ts +++ b/src/cli/setup/channels.test.ts @@ -1,15 +1,14 @@ import { describe, it, expect } from 'vitest'; -import { EventEmitter } from 'events'; +import type { Interface as ReadlineInterface } from 'readline/promises'; import { createPrompter } from './prompts.js'; import { ConfigBuilder } from './config.js'; import { setupChannels } from './channels.js'; function mockReadline(inputs: string[]) { let questionIdx = 0; - const emitter = new EventEmitter(); return { - async question(query: string) { + async question(_query: string) { const answer = inputs[questionIdx++]; return answer ?? ''; }, @@ -25,7 +24,7 @@ function mockReadline(inputs: string[]) { async next() { return { done: true }; }, - } as any; + } as unknown as ReadlineInterface; } describe('setupChannels', () => { diff --git a/src/cli/setup/config.ts b/src/cli/setup/config.ts index 9eba148..ba7499e 100644 --- a/src/cli/setup/config.ts +++ b/src/cli/setup/config.ts @@ -155,8 +155,8 @@ export class ConfigBuilder { this.config.automation = automation; } - build(): Record { - return structuredClone(this.config) as Record; + build(): Record { + return structuredClone(this.config) as Record; } toYaml(): string { diff --git a/src/cli/setup/integration.test.ts b/src/cli/setup/integration.test.ts index 9f55474..be38563 100644 --- a/src/cli/setup/integration.test.ts +++ b/src/cli/setup/integration.test.ts @@ -14,7 +14,7 @@ function mockReadline(inputs: string[]): ReadlineInterface { throw new Error('No more inputs'); }), close: vi.fn(), - } as any as ReadlineInterface; + } as unknown as ReadlineInterface; } describe('first-run wizard integration', () => { diff --git a/src/cli/setup/memory.ts b/src/cli/setup/memory.ts index 53b7551..2e1c87a 100644 --- a/src/cli/setup/memory.ts +++ b/src/cli/setup/memory.ts @@ -39,8 +39,8 @@ export async function setupMemory(p: Prompter, builder: ConfigBuilder): Promise< p.println('✓ Vector search enabled'); } -function findReusableApiKey(config: Record, embeddingProvider: string): string | undefined { - const models = config.models ?? {}; +function findReusableApiKey(config: Record, embeddingProvider: string): string | undefined { + const models = (config.models as Record) ?? {}; for (const tier of ['default', 'fast', 'complex', 'local']) { const m = models[tier]; if (m?.provider === embeddingProvider && m?.api_key) {return m.api_key;} diff --git a/src/cli/setup/orchestrator.test.ts b/src/cli/setup/orchestrator.test.ts index 66acf0b..3fd9e54 100644 --- a/src/cli/setup/orchestrator.test.ts +++ b/src/cli/setup/orchestrator.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it } from 'vitest'; import { createInterface } from 'readline/promises'; import { Readable, Writable } from 'stream'; import { createPrompter } from './prompts.js'; diff --git a/src/cli/setup/sections.test.ts b/src/cli/setup/sections.test.ts index 5d3927e..3789e7e 100644 --- a/src/cli/setup/sections.test.ts +++ b/src/cli/setup/sections.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from 'vitest'; +import type { Interface as ReadlineInterface } from 'readline/promises'; import { createPrompter } from './prompts.js'; import { ConfigBuilder } from './config.js'; import { setupMemory } from './memory.js'; @@ -9,7 +10,7 @@ function mockReadline(inputs: string[]) { let questionIdx = 0; return { - async question(query: string) { + async question(_query: string) { const answer = inputs[questionIdx++]; return answer ?? ''; }, @@ -25,7 +26,7 @@ function mockReadline(inputs: string[]) { async next() { return { done: true }; }, - } as any; + } as unknown as ReadlineInterface; } describe('setupMemory', () => { diff --git a/src/cli/setup/summary.ts b/src/cli/setup/summary.ts index 5ea5315..4e5bf1d 100644 --- a/src/cli/setup/summary.ts +++ b/src/cli/setup/summary.ts @@ -1,4 +1,4 @@ -export function renderSummary(config: Record): string { +export function renderSummary(config: Record): string { const lines: string[] = []; const models = config.models ?? {}; diff --git a/src/cli/shared.test.ts b/src/cli/shared.test.ts index 420bc76..662fed2 100644 --- a/src/cli/shared.test.ts +++ b/src/cli/shared.test.ts @@ -50,7 +50,11 @@ models: const result = loadConfigSafe(configPath); expect(result.config).toBeDefined(); expect(result.error).toBeUndefined(); - expect(result.config!.telegram?.bot_token).toBe('test-token'); + const config = result.config; + if (!config) { + throw new Error('Expected loaded config'); + } + expect(config.telegram?.bot_token).toBe('test-token'); }); it('loads env vars from FLYNN_ENV_FILE before parsing config', () => { @@ -78,7 +82,11 @@ models: const result = loadConfigSafe(configPath); expect(result.config).toBeDefined(); expect(result.error).toBeUndefined(); - expect(result.config!.telegram?.bot_token).toBe('test-token'); + const config = result.config; + if (!config) { + throw new Error('Expected loaded config'); + } + expect(config.telegram?.bot_token).toBe('test-token'); if (prevEnvFile !== undefined) { process.env.FLYNN_ENV_FILE = prevEnvFile; diff --git a/src/config/defaultYaml.test.ts b/src/config/defaultYaml.test.ts index 5f01bdb..b7403d2 100644 --- a/src/config/defaultYaml.test.ts +++ b/src/config/defaultYaml.test.ts @@ -3,21 +3,28 @@ import { readFileSync } from 'fs'; import { parse } from 'yaml'; describe('config/default.yaml', () => { + const asRecord = (value: unknown): Record => ( + value && typeof value === 'object' ? value as Record : {} + ); + it('does not use deprecated server.tailscale_only key', () => { const raw = readFileSync('config/default.yaml', 'utf-8'); - const parsed = parse(raw) as any; + const parsed = asRecord(parse(raw)); + const server = asRecord(parsed.server); expect(parsed).toBeTruthy(); - expect(parsed.server).toBeTruthy(); - expect(parsed.server.tailscale_only).toBeUndefined(); + expect(server).toBeTruthy(); + expect(server.tailscale_only).toBeUndefined(); }); it('documents server.tailscale.* shape', () => { const raw = readFileSync('config/default.yaml', 'utf-8'); - const parsed = parse(raw) as any; + const parsed = asRecord(parse(raw)); + const server = asRecord(parsed.server); + const tailscale = asRecord(server.tailscale); - expect(parsed.server.tailscale).toBeTruthy(); - expect(typeof parsed.server.tailscale).toBe('object'); - expect(typeof parsed.server.tailscale.serve).toBe('boolean'); + expect(tailscale).toBeTruthy(); + expect(typeof tailscale).toBe('object'); + expect(typeof tailscale.serve).toBe('boolean'); }); }); diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index ec637a8..e9f26d6 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -265,9 +265,12 @@ describe('configSchema — matrix', () => { }); expect(result.matrix).toBeDefined(); - expect(result.matrix!.homeserver_url).toBe('https://matrix.example.org'); - expect(result.matrix!.access_token).toBe('syt_test_token'); - expect(result.matrix!.sync_timeout_ms).toBe(30000); + if (!result.matrix) { + throw new Error('Expected matrix config'); + } + expect(result.matrix.homeserver_url).toBe('https://matrix.example.org'); + expect(result.matrix.access_token).toBe('syt_test_token'); + expect(result.matrix.sync_timeout_ms).toBe(30000); }); it('matrix config is optional', () => { diff --git a/src/context/tokens.test.ts b/src/context/tokens.test.ts index d68a110..76a3da1 100644 --- a/src/context/tokens.test.ts +++ b/src/context/tokens.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { estimateTokens, estimateAudioTokens, estimateMessageTokens, getContextWindow, shouldCompact, CONTEXT_WINDOWS } from './tokens.js'; +import { estimateTokens, estimateAudioTokens, estimateMessageTokens, getContextWindow, shouldCompact } from './tokens.js'; describe('estimateTokens', () => { it('returns 0 for empty string', () => { diff --git a/src/daemon/agents.ts b/src/daemon/agents.ts index a898588..72220a8 100644 --- a/src/daemon/agents.ts +++ b/src/daemon/agents.ts @@ -39,7 +39,7 @@ export async function initAgents(deps: AgentsDeps): Promise { if (sandboxManager) { lifecycle.onShutdown(async () => { - await sandboxManager!.destroyAll(); + await sandboxManager.destroyAll(); console.log('Docker sandboxes destroyed'); }); } diff --git a/src/daemon/lifecycle.test.ts b/src/daemon/lifecycle.test.ts index 76cf202..312adcf 100644 --- a/src/daemon/lifecycle.test.ts +++ b/src/daemon/lifecycle.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { Lifecycle } from './lifecycle.js'; describe('Lifecycle', () => { diff --git a/src/daemon/routing.ts b/src/daemon/routing.ts index 5e9f86c..5da35fb 100644 --- a/src/daemon/routing.ts +++ b/src/daemon/routing.ts @@ -148,7 +148,10 @@ export function createMessageRouter(deps: { // Lazy sandbox: create the sandboxed tools with a deferred sandbox reference // The sandbox is created on first use via SandboxManager.getOrCreate() const sandboxSessionId = sessionId; - const sandboxManager = deps.sandboxManager!; + const sandboxManager = deps.sandboxManager; + if (!sandboxManager) { + throw new Error('Sandbox manager unavailable for sandboxed agent execution'); + } // Create a proxy sandbox that lazily initializes const lazySandboxShell: Tool = { diff --git a/src/daemon/tools.ts b/src/daemon/tools.ts index ab81304..73acd92 100644 --- a/src/daemon/tools.ts +++ b/src/daemon/tools.ts @@ -68,21 +68,22 @@ export function initTools(deps: ToolsDeps): ToolsResult { // Initialize browser manager and register browser tools (if enabled) let browserManager: BrowserManager | undefined; if (config.browser?.enabled) { - browserManager = new BrowserManager({ + const manager = new BrowserManager({ executablePath: config.browser.executable_path, wsEndpoint: config.browser.ws_endpoint, headless: config.browser.headless, maxPages: config.browser.max_pages, defaultTimeout: config.browser.default_timeout, }); + browserManager = manager; - for (const tool of createBrowserTools(browserManager)) { + for (const tool of createBrowserTools(manager)) { toolRegistry.register(tool); } console.log(`Browser tools enabled (headless=${config.browser.headless})`); lifecycle.onShutdown(async () => { - await browserManager!.shutdown(); + await manager.shutdown(); console.log('Browser manager stopped'); }); } diff --git a/src/frontends/tui/components/App.tsx b/src/frontends/tui/components/App.tsx index 10bb81c..394c902 100644 --- a/src/frontends/tui/components/App.tsx +++ b/src/frontends/tui/components/App.tsx @@ -372,9 +372,10 @@ export function App({ setStreamingContent(fullContent); } if (event.type === 'done' && event.usage) { + const usage = event.usage; setTokenUsage(prev => ({ - inputTokens: prev.inputTokens + event.usage!.inputTokens, - outputTokens: prev.outputTokens + event.usage!.outputTokens, + inputTokens: prev.inputTokens + usage.inputTokens, + outputTokens: prev.outputTokens + usage.outputTokens, })); } if (event.type === 'error') { diff --git a/src/frontends/tui/components/StatusBar.tsx b/src/frontends/tui/components/StatusBar.tsx index edf477a..c847a97 100644 --- a/src/frontends/tui/components/StatusBar.tsx +++ b/src/frontends/tui/components/StatusBar.tsx @@ -20,7 +20,7 @@ function formatTokens(n: number): string { } export const StatusBar = memo(function StatusBar({ - sessionId, + sessionId: _sessionId, messageCount, model, tokenUsage, diff --git a/src/gateway/handlers/agent.test.ts b/src/gateway/handlers/agent.test.ts index c755c66..50e8239 100644 --- a/src/gateway/handlers/agent.test.ts +++ b/src/gateway/handlers/agent.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { GatewayEvent, GatewayRequest, OutboundMessage } from '../protocol.js'; import { LaneQueue } from '../lane-queue.js'; import { createAgentHandlers } from './agent.js'; +import type { AgentHandlerDeps } from './agent.js'; import { CommandRegistry, registerBuiltinCommands } from '../../commands/index.js'; describe('createAgentHandlers command fast-path', () => { @@ -35,9 +36,9 @@ describe('createAgentHandlers command fast-path', () => { registerBuiltinCommands(commandRegistry); const handlers = createAgentHandlers({ - sessionBridge: sessionBridge as any, + sessionBridge: sessionBridge as unknown as AgentHandlerDeps['sessionBridge'], laneQueue: new LaneQueue(), - sessionManager: sessionManager as any, + sessionManager: sessionManager as unknown as AgentHandlerDeps['sessionManager'], commandRegistry, }); diff --git a/src/gateway/handlers/system.ts b/src/gateway/handlers/system.ts index 387f460..172eaf1 100644 --- a/src/gateway/handlers/system.ts +++ b/src/gateway/handlers/system.ts @@ -59,7 +59,8 @@ export function createSystemHandlers(deps: SystemHandlerDeps) { }, 'system.restart': async (request: GatewayRequest): Promise => { - if (!deps.restart) { + const restart = deps.restart; + if (!restart) { return makeError(request.id, ErrorCode.InternalError, 'Restart not available in this environment'); } @@ -68,7 +69,7 @@ export function createSystemHandlers(deps: SystemHandlerDeps) { // Schedule restart after response is sent (next tick) queueMicrotask(() => { - deps.restart!().catch((err) => { + restart().catch((err) => { console.error('Restart failed:', err); }); }); diff --git a/src/gateway/lane-queue.ts b/src/gateway/lane-queue.ts index f994519..69a36ed 100644 --- a/src/gateway/lane-queue.ts +++ b/src/gateway/lane-queue.ts @@ -49,7 +49,7 @@ export class LaneQueue { // Otherwise, queue the work and return a deferred promise return new Promise((resolve, reject) => { - lane!.queue.push({ + lane.queue.push({ work: work as () => Promise, resolve: resolve as (value: unknown) => void, reject, diff --git a/src/gateway/server.test.ts b/src/gateway/server.test.ts index 3ba146a..d7b644a 100644 --- a/src/gateway/server.test.ts +++ b/src/gateway/server.test.ts @@ -199,7 +199,7 @@ describe('GatewayServer integration', () => { const doneEvent = messages[0] as GatewayEvent; expect(doneEvent.id).toBe(4); expect(doneEvent.event).toBe('done'); - expect((doneEvent.data as any).content).toBe('Hello from Flynn!'); + expect((doneEvent.data as { content?: string }).content).toBe('Hello from Flynn!'); } finally { ws.close(); } @@ -319,7 +319,7 @@ describe('GatewayServer lock mode', () => { try { const result = await sendAndReceive(ws, { id: 1, method: 'system.health' }); const response = result as GatewayResponse; - expect((response.result as any).status).toBe('ok'); + expect((response.result as { status?: string }).status).toBe('ok'); } finally { ws.close(); // Wait for the close to propagate so connectionMap is empty @@ -363,7 +363,7 @@ describe('GatewayServer lock mode', () => { try { const result = await sendAndReceive(ws2, { id: 2, method: 'system.health' }); const response = result as GatewayResponse; - expect((response.result as any).status).toBe('ok'); + expect((response.result as { status?: string }).status).toBe('ok'); } finally { ws2.close(); await new Promise(r => setTimeout(r, 100)); diff --git a/src/gateway/ui/lib/ws-client.js b/src/gateway/ui/lib/ws-client.js index d14231c..13425aa 100644 --- a/src/gateway/ui/lib/ws-client.js +++ b/src/gateway/ui/lib/ws-client.js @@ -71,7 +71,7 @@ export class FlynnClient { } this._setStatus('disconnected'); // Reject all pending requests - for (const [id, pending] of this._pending) { + for (const [_id, pending] of this._pending) { pending.reject(new Error('WebSocket closed')); } this._pending.clear(); diff --git a/src/gateway/ui/pages/chat.js b/src/gateway/ui/pages/chat.js index a400701..d573a9b 100644 --- a/src/gateway/ui/pages/chat.js +++ b/src/gateway/ui/pages/chat.js @@ -289,7 +289,7 @@ function hideSlashPopup() { _slashPopupIndex = -1; } -function updatePopupSelection(filtered) { +function updatePopupSelection(_filtered) { const popup = _elements.slashPopup; if (!popup) {return;} const items = popup.querySelectorAll('.slash-popup-item'); diff --git a/src/gateway/ui/pages/dashboard.js b/src/gateway/ui/pages/dashboard.js index 0b433fc..4004f77 100644 --- a/src/gateway/ui/pages/dashboard.js +++ b/src/gateway/ui/pages/dashboard.js @@ -217,7 +217,7 @@ function updateActiveRequests(requestsData) { `; } -function updateChannels(channelsData) { +function _updateChannels(channelsData) { const el = document.getElementById('ops-channels'); if (!el) {return;} diff --git a/src/gateway/ui/pages/settings.js b/src/gateway/ui/pages/settings.js index 01eb2e4..b91fa0a 100644 --- a/src/gateway/ui/pages/settings.js +++ b/src/gateway/ui/pages/settings.js @@ -17,7 +17,7 @@ let _el = null; async function loadSettings() { if (!_client || !_el) {return;} - let config, tools, channels; + let config, tools; let services; try { diff --git a/src/memory/chunker.test.ts b/src/memory/chunker.test.ts index ed02974..2ed6f66 100644 --- a/src/memory/chunker.test.ts +++ b/src/memory/chunker.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect } from 'vitest'; import { chunkText } from './chunker.js'; -import type { Chunk } from './chunker.js'; describe('chunkText', () => { it('returns empty array for empty content', () => { @@ -50,12 +49,18 @@ describe('chunkText', () => { // Line three is on actual line 3 const lineThreeChunk = chunks.find((c) => c.text.includes('Line three')); expect(lineThreeChunk).toBeDefined(); - expect(lineThreeChunk!.startLine).toBe(3); + if (!lineThreeChunk) { + throw new Error('Expected chunk containing Line three'); + } + expect(lineThreeChunk.startLine).toBe(3); // Line five is on actual line 5 const lineFiveChunk = chunks.find((c) => c.text.includes('Line five')); expect(lineFiveChunk).toBeDefined(); - expect(lineFiveChunk!.startLine).toBe(5); + if (!lineFiveChunk) { + throw new Error('Expected chunk containing Line five'); + } + expect(lineFiveChunk.startLine).toBe(5); }); it('includes overlap between consecutive chunks', () => { diff --git a/src/models/anthropic.test.ts b/src/models/anthropic.test.ts index 2f2a288..127cfe0 100644 --- a/src/models/anthropic.test.ts +++ b/src/models/anthropic.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { AnthropicClient } from './anthropic.js'; // Shared mock function so we can override per-test @@ -99,7 +99,8 @@ describe('AnthropicClient tool use', () => { expect(response.stopReason).toBe('tool_use'); expect(response.toolCalls).toHaveLength(1); - expect(response.toolCalls![0]).toEqual({ + const firstToolCall = response.toolCalls?.[0]; + expect(firstToolCall).toEqual({ id: 'toolu_01', name: 'shell.exec', args: { command: 'ls' }, diff --git a/src/models/bedrock.test.ts b/src/models/bedrock.test.ts index 09f7d91..909e133 100644 --- a/src/models/bedrock.test.ts +++ b/src/models/bedrock.test.ts @@ -83,8 +83,9 @@ describe('BedrockClient', () => { expect(response.stopReason).toBe('tool_use'); expect(response.toolCalls).toHaveLength(1); - expect(response.toolCalls![0].name).toBe('shell.exec'); - expect(response.toolCalls![0].args).toEqual({ command: 'ls' }); + const firstToolCall = response.toolCalls?.[0]; + expect(firstToolCall?.name).toBe('shell.exec'); + expect(firstToolCall?.args).toEqual({ command: 'ls' }); }); it('uses default region when none provided', async () => { diff --git a/src/models/gemini.test.ts b/src/models/gemini.test.ts index 793f905..5c82d90 100644 --- a/src/models/gemini.test.ts +++ b/src/models/gemini.test.ts @@ -247,8 +247,9 @@ describe('GeminiClient tool use', () => { expect(response.stopReason).toBe('tool_use'); expect(response.toolCalls).toHaveLength(1); - expect(response.toolCalls![0].name).toBe('shell.exec'); - expect(response.toolCalls![0].args).toEqual({ command: 'ls' }); + const firstToolCall = response.toolCalls?.[0]; + expect(firstToolCall?.name).toBe('shell.exec'); + expect(firstToolCall?.args).toEqual({ command: 'ls' }); // Verify tools were passed to getGenerativeModel expect(mockGetGenerativeModel).toHaveBeenCalledWith( diff --git a/src/models/gemini.ts b/src/models/gemini.ts index e055a14..377226c 100644 --- a/src/models/gemini.ts +++ b/src/models/gemini.ts @@ -1,6 +1,6 @@ import { GoogleGenerativeAI } from '@google/generative-ai'; import type { GenerativeModel, Content, Part, FunctionDeclaration, FunctionDeclarationSchema } from '@google/generative-ai'; -import type { ChatRequest, ChatResponse, ChatStreamEvent, ModelClient, ModelToolCall, ToolDefinition, Message, MessageContentPart } from './types.js'; +import type { ChatRequest, ChatResponse, ChatStreamEvent, ModelClient, ModelToolCall, ToolDefinition, Message } from './types.js'; export interface GeminiClientConfig { apiKey?: string; diff --git a/src/models/github.ts b/src/models/github.ts index 65d3d38..53c0305 100644 --- a/src/models/github.ts +++ b/src/models/github.ts @@ -31,9 +31,15 @@ function toOpenAIContent(content: string | MessageContentPart[]): string | OpenA return { type: 'text', text: part.text }; } if (part.type === 'image') { + if (part.source.type === 'base64' && !part.source.data) { + return { type: 'text', text: '[Image omitted: missing base64 data]' }; + } + if (part.source.type !== 'base64' && !part.source.url) { + return { type: 'text', text: '[Image omitted: missing URL]' }; + } const url = part.source.type === 'base64' - ? `data:${part.source.media_type};base64,${part.source.data!}` - : part.source.url!; + ? `data:${part.source.media_type};base64,${part.source.data}` + : part.source.url; return { type: 'image_url', image_url: { url } }; } if (part.type === 'audio') { @@ -154,7 +160,7 @@ export class GitHubModelsClient implements ModelClient { // Extended thinking/reasoning mode if (request.thinking) { - (params as any).reasoning_effort = 'medium'; + (params as OpenAI.ChatCompletionCreateParamsNonStreaming & { reasoning_effort?: 'low' | 'medium' | 'high' }).reasoning_effort = 'medium'; } const response = await this.client.chat.completions.create(params); diff --git a/src/models/local/llamacpp.ts b/src/models/local/llamacpp.ts index 063e241..bbb666f 100644 --- a/src/models/local/llamacpp.ts +++ b/src/models/local/llamacpp.ts @@ -152,7 +152,7 @@ export function normalizeMessagesForLlamaCpp( } catch { argsStr = '{}'; } - toolCalls!.push({ + toolCalls.push({ id, type: 'function', function: { @@ -167,7 +167,7 @@ export function normalizeMessagesForLlamaCpp( role: 'assistant', content: textParts.join('\n'), }; - if (toolCalls!.length > 0) { + if (toolCalls.length > 0) { chatMsg.tool_calls = toolCalls; } result.push(chatMsg); @@ -381,7 +381,8 @@ export class LlamaCppClient implements ModelClient { arguments: '', }); } - const acc = toolCallAccumulators.get(tc.index)!; + const acc = toolCallAccumulators.get(tc.index); + if (!acc) {continue;} if (tc.function?.name) {acc.name = tc.function.name;} if (tc.function?.arguments) {acc.arguments += tc.function.arguments;} } diff --git a/src/models/media.test.ts b/src/models/media.test.ts index 95e251b..3acf419 100644 --- a/src/models/media.test.ts +++ b/src/models/media.test.ts @@ -57,13 +57,13 @@ const mp3AudioAttachment: Attachment = makeAttachment({ filename: 'audio.mp3', }); -const wavAudioAttachment: Attachment = makeAttachment({ +const _wavAudioAttachment: Attachment = makeAttachment({ mimeType: 'audio/wav', data: 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=', // Base64 of a short WAV filename: 'audio.wav', }); -const m4aAudioAttachment: Attachment = makeAttachment({ +const _m4aAudioAttachment: Attachment = makeAttachment({ mimeType: 'audio/x-m4a', data: 'AAAAUGV0Zi4xLjAgc291cmNlIGZvciBzdGFydHBvaW50', // Base64 of M4A filename: 'audio.m4a', @@ -545,8 +545,11 @@ describe('buildUserMessageWithAudio', () => { const content = Array.isArray(result.content) ? result.content : [{ type: 'text' as const, text: result.content }]; const textPart = content.find((p) => p.type === 'text') as { type: 'text'; text: string } | undefined; expect(textPart).toBeDefined(); + if (!textPart) { + throw new Error('Expected text content part'); + } - const textContent = textPart!.text || ''; + const textContent = textPart.text || ''; const firstVoiceIndex = textContent.indexOf('[Voice message]:'); const textIndex = textContent.indexOf(textMessage); diff --git a/src/models/media.ts b/src/models/media.ts index 64a97b9..96c9ec7 100644 --- a/src/models/media.ts +++ b/src/models/media.ts @@ -241,9 +241,12 @@ export async function transcribeAudio( if (!config?.endpoint) { return '[Audio message received but no transcription service is configured]'; } + if (!attachment.data) { + return '[Audio message transcription failed]'; + } try { - const audioBuffer = Buffer.from(attachment.data!, 'base64'); + const audioBuffer = Buffer.from(attachment.data, 'base64'); const ext = mimeToExtension(attachment.mimeType); const formData = new FormData(); formData.append('file', new Blob([audioBuffer], { type: attachment.mimeType }), `audio.${ext}`); diff --git a/src/models/openai.baseurl.test.ts b/src/models/openai.baseurl.test.ts index d01edf4..c78f4de 100644 --- a/src/models/openai.baseurl.test.ts +++ b/src/models/openai.baseurl.test.ts @@ -1,10 +1,10 @@ import { describe, expect, it, vi } from 'vitest'; -let capturedOptions: any; +let capturedOptions: Record | undefined; vi.mock('openai', () => { class OpenAI { - constructor(options: any) { + constructor(options: Record) { capturedOptions = options; } } diff --git a/src/models/openai.test.ts b/src/models/openai.test.ts index ac4f24a..b4398ef 100644 --- a/src/models/openai.test.ts +++ b/src/models/openai.test.ts @@ -83,7 +83,8 @@ describe('OpenAIClient tool use', () => { expect(response.stopReason).toBe('tool_use'); expect(response.toolCalls).toHaveLength(1); - expect(response.toolCalls![0]).toEqual({ + const firstToolCall = response.toolCalls?.[0]; + expect(firstToolCall).toEqual({ id: 'call_1', name: 'shell.exec', args: { command: 'ls' }, diff --git a/src/models/synthetic.test.ts b/src/models/synthetic.test.ts index 3f1caf2..ebcb5b5 100644 --- a/src/models/synthetic.test.ts +++ b/src/models/synthetic.test.ts @@ -39,7 +39,10 @@ describe('SyntheticClient', () => { it('streams content + done', async () => { const client = new SyntheticClient({ model: 'fixed:stream-me' }); const events: string[] = []; - for await (const ev of client.chatStream!(makeRequest('x'))) { + if (!client.chatStream) { + throw new Error('Expected streaming support'); + } + for await (const ev of client.chatStream(makeRequest('x'))) { events.push(ev.type); } expect(events).toEqual(['content', 'done']); @@ -49,7 +52,10 @@ describe('SyntheticClient', () => { const client = new SyntheticClient({ model: 'echo' }); const types: string[] = []; const toolNames: string[] = []; - for await (const ev of client.chatStream!(makeRequest('@tool file.read {"path":"README.md"}'))) { + if (!client.chatStream) { + throw new Error('Expected streaming support'); + } + for await (const ev of client.chatStream(makeRequest('@tool file.read {"path":"README.md"}'))) { types.push(ev.type); if (ev.type === 'tool_use' && ev.toolCall) { toolNames.push(ev.toolCall.name); @@ -59,4 +65,3 @@ describe('SyntheticClient', () => { expect(toolNames).toEqual(['file.read']); }); }); - diff --git a/src/preferences.test.ts b/src/preferences.test.ts index d1da112..dbc505d 100644 --- a/src/preferences.test.ts +++ b/src/preferences.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { mkdtempSync, rmSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; +import { mkdtempSync, rmSync, readFileSync, writeFileSync } from 'fs'; import { resolve } from 'path'; import { tmpdir } from 'os'; import { loadPreferences, savePreference } from './preferences.js'; diff --git a/src/session/store.test.ts b/src/session/store.test.ts index f7de508..b8e4216 100644 --- a/src/session/store.test.ts +++ b/src/session/store.test.ts @@ -34,8 +34,12 @@ describe('SessionStore', () => { expect(messages[0].role).toBe('user'); expect(messages[0].content).toBe('Hello'); expect(messages[0].timestamp).toBeTypeOf('number'); - expect(messages[0].timestamp!).toBeGreaterThanOrEqual(before - 1000); - expect(messages[0].timestamp!).toBeLessThanOrEqual(after + 1000); + const timestamp = messages[0].timestamp; + if (typeof timestamp !== 'number') { + throw new Error('Expected numeric timestamp'); + } + expect(timestamp).toBeGreaterThanOrEqual(before - 1000); + expect(timestamp).toBeLessThanOrEqual(after + 1000); expect(messages[1].role).toBe('assistant'); expect(messages[1].content).toBe('Hi there!'); expect(messages[1].timestamp).toBeTypeOf('number'); diff --git a/src/skills/loader.ts b/src/skills/loader.ts index 086df61..f275c1d 100644 --- a/src/skills/loader.ts +++ b/src/skills/loader.ts @@ -13,126 +13,6 @@ import type { Skill, SkillManifest, SkillRequirements, SkillTier } from './types import { scanSkillDirectory } from './scanner.js'; import { auditLogger } from '../audit/index.js'; -function isStringArray(value: unknown): value is string[] { - return Array.isArray(value) && value.every((item) => typeof item === 'string'); -} - -function hasValidInstallers(manifest: unknown): boolean { - if (!manifest || typeof manifest !== 'object') { - return false; - } - - const candidate = manifest as { installers?: unknown }; - if (candidate.installers === undefined) { - return true; - } - - if (!Array.isArray(candidate.installers)) { - return false; - } - - return candidate.installers.every((installer) => { - if (!installer || typeof installer !== 'object') { - return false; - } - - const typedInstaller = installer as { type?: unknown; packages?: unknown; url?: unknown; destination?: unknown }; - if (typedInstaller.type === 'brew' || typedInstaller.type === 'node' || typedInstaller.type === 'go') { - return isStringArray(typedInstaller.packages); - } - - if (typedInstaller.type === 'download') { - if (typeof typedInstaller.url !== 'string') { - return false; - } - if (typedInstaller.destination !== undefined && typeof typedInstaller.destination !== 'string') { - return false; - } - return true; - } - - return false; - }); -} - -function isNumberArray(value: unknown): value is number[] { - return Array.isArray(value) && value.every((item) => typeof item === 'number' && Number.isFinite(item)); -} - -function hasValidPermissions(manifest: unknown): boolean { - if (!manifest || typeof manifest !== 'object') { - return false; - } - - const candidate = manifest as { permissions?: unknown }; - if (candidate.permissions === undefined) { - return true; - } - - if (!candidate.permissions || typeof candidate.permissions !== 'object') { - return false; - } - - const perms = candidate.permissions as { - tool_groups?: unknown; - tools?: unknown; - fs?: unknown; - net?: unknown; - secrets?: unknown; - execution_environment?: unknown; - }; - - if (perms.tool_groups !== undefined && !isStringArray(perms.tool_groups)) { - return false; - } - if (perms.tools !== undefined && !isStringArray(perms.tools)) { - return false; - } - - if (perms.fs !== undefined) { - if (!perms.fs || typeof perms.fs !== 'object') { - return false; - } - const fsPerms = perms.fs as { read?: unknown; write?: unknown }; - if (fsPerms.read !== undefined && !isStringArray(fsPerms.read)) { - return false; - } - if (fsPerms.write !== undefined && !isStringArray(fsPerms.write)) { - return false; - } - } - - if (perms.net !== undefined) { - if (!Array.isArray(perms.net)) { - return false; - } - for (const entry of perms.net) { - if (!entry || typeof entry !== 'object') { - return false; - } - const e = entry as { host?: unknown; ports?: unknown }; - if (typeof e.host !== 'string' || e.host.trim().length === 0) { - return false; - } - if (e.ports !== undefined && !isNumberArray(e.ports)) { - return false; - } - } - } - - if (perms.secrets !== undefined && !isStringArray(perms.secrets)) { - return false; - } - - if (perms.execution_environment !== undefined) { - if (perms.execution_environment !== 'sandbox' && perms.execution_environment !== 'host') { - return false; - } - } - - return true; -} - /** * Check whether a skill's system requirements are met. * @@ -237,7 +117,7 @@ export function loadSkill(directory: string, tier: SkillTier): Skill | null { tier, // Override tier from argument }; } - } catch (error) { + } catch { // Scanner will capture this and mark the skill unavailable. } } else { diff --git a/src/skills/registry.test.ts b/src/skills/registry.test.ts index 66f3a7b..bcd64d2 100644 --- a/src/skills/registry.test.ts +++ b/src/skills/registry.test.ts @@ -83,7 +83,11 @@ describe('SkillRegistry', () => { registry.register(replacement); expect(registry.list()).toHaveLength(1); - expect(registry.get('web')!.instructions).toBe('# Replacement'); + const webSkill = registry.get('web'); + if (!webSkill) { + throw new Error('Expected web skill'); + } + expect(webSkill.instructions).toBe('# Replacement'); }); it('unregisters a skill by name', () => { diff --git a/src/skills/scanner.ts b/src/skills/scanner.ts index 53af3ba..f715b19 100644 --- a/src/skills/scanner.ts +++ b/src/skills/scanner.ts @@ -248,7 +248,7 @@ export function scanSkillDirectory(directory: string, opts: SkillScanOptions = { issues.push({ severity: 'error', code: 'content.binary', message: 'manifest.json appears to be binary', path: manifestPath }); } else { const text = buf.toString('utf-8'); - const raw = safeJsonParse(text) as any; + const raw = safeJsonParse(text) as Record | null; if (!raw || typeof raw !== 'object') { issues.push({ severity: 'error', code: 'manifest.missing_required_fields', message: 'manifest.json must be an object with required fields', path: manifestPath }); } else { diff --git a/src/tools/builtin/audio-transcribe.ts b/src/tools/builtin/audio-transcribe.ts index fe702c9..8423452 100644 --- a/src/tools/builtin/audio-transcribe.ts +++ b/src/tools/builtin/audio-transcribe.ts @@ -71,7 +71,11 @@ function validateInput(args: AudioTranscribeArgs): { valid: boolean; error?: str } if (hasUrl) { - const urlValidation = validateUrl(args.url!); + const url = args.url; + if (!url) { + return { valid: false, error: 'URL is required when using url mode' }; + } + const urlValidation = validateUrl(url); if (!urlValidation.valid) { return urlValidation; } @@ -153,7 +157,7 @@ export function createAudioTranscribeTool(audioConfig?: AudioTranscriptionConfig 'audio/mp4': 'm4a', 'audio/x-m4a': 'm4a', }; - const ext = extMap[args.mime_type!] || 'bin'; + const ext = extMap[args.mime_type ?? ''] || 'bin'; filename = `audio.${ext}`; const mimeType = args.mime_type ?? 'audio/wav'; diff --git a/src/tools/builtin/browser/manager.test.ts b/src/tools/builtin/browser/manager.test.ts index a299399..479b507 100644 --- a/src/tools/builtin/browser/manager.test.ts +++ b/src/tools/builtin/browser/manager.test.ts @@ -3,8 +3,7 @@ import { BrowserManager } from './manager.js'; // Use vi.hoisted() so these are available inside the hoisted vi.mock() call const { - mockGoto, mockTitle, mockUrl, mockClose, mockIsClosed, - mockSetDefaultTimeout, mockPage, mockNewPage, mockPages, + mockClose, mockIsClosed, mockPage, mockNewPage, mockPages, mockBrowserClose, } = vi.hoisted(() => { const mockGoto = vi.fn().mockResolvedValue(undefined); diff --git a/src/tools/builtin/file-list.ts b/src/tools/builtin/file-list.ts index c3cce88..8be8db1 100644 --- a/src/tools/builtin/file-list.ts +++ b/src/tools/builtin/file-list.ts @@ -27,7 +27,7 @@ export const fileListTool: Tool = { try { let entries = readdirSync(args.path, { withFileTypes: true }); if (args.pattern) { - entries = entries.filter(e => matchGlob(e.name, args.pattern!)); + entries = entries.filter(e => matchGlob(e.name, args.pattern)); } const output = entries .map(e => e.isDirectory() ? `${e.name}/` : e.name) diff --git a/src/tools/builtin/image-analyze.ts b/src/tools/builtin/image-analyze.ts index ee69461..34ad926 100644 --- a/src/tools/builtin/image-analyze.ts +++ b/src/tools/builtin/image-analyze.ts @@ -86,7 +86,7 @@ export function createImageAnalyzeTool(modelClient: ModelClient): Tool { } : { type: 'base64' as const, - media_type: args.media_type!, + media_type: args.media_type ?? 'image/jpeg', data: args.data, }; diff --git a/src/tools/builtin/index.ts b/src/tools/builtin/index.ts index 3cc3b3c..7f9cbcf 100644 --- a/src/tools/builtin/index.ts +++ b/src/tools/builtin/index.ts @@ -40,8 +40,6 @@ import { filePatchTool } from './file-patch.js'; import { fileListTool } from './file-list.js'; import { systemInfoTool } from './system-info.js'; import { webFetchTool } from './web-fetch.js'; -import { createMediaSendTool } from './media-send.js'; -import { createImageAnalyzeTool } from './image-analyze.js'; import { createMemoryReadTool } from './memory-read.js'; import { createMemoryWriteTool } from './memory-write.js'; import { createMemorySearchTool } from './memory-search.js'; diff --git a/src/tools/builtin/process/manager.ts b/src/tools/builtin/process/manager.ts index 4047072..8d3ad0c 100644 --- a/src/tools/builtin/process/manager.ts +++ b/src/tools/builtin/process/manager.ts @@ -151,12 +151,16 @@ export class ProcessManager { stdio: ['ignore', 'pipe', 'pipe'], detached: false, }); + const pid = child.pid; + if (pid === undefined) { + throw new Error('Failed to start process: missing pid'); + } const proc: ManagedProcess = { id, command, cwd, - pid: child.pid!, + pid, status: 'running', outputBuffer, startedAt: Date.now(), diff --git a/src/tools/builtin/sessions.ts b/src/tools/builtin/sessions.ts index 552f8c1..bb6b48b 100644 --- a/src/tools/builtin/sessions.ts +++ b/src/tools/builtin/sessions.ts @@ -1,10 +1,6 @@ import type { Tool, ToolResult } from '../types.js'; import type { SessionManager } from '../../session/manager.js'; -interface SessionsListArgs { - // no args -} - interface SessionsHistoryArgs { sessionId: string; limit?: number; diff --git a/src/tools/executor.test.ts b/src/tools/executor.test.ts index 7cb86f9..09facbd 100644 --- a/src/tools/executor.test.ts +++ b/src/tools/executor.test.ts @@ -396,10 +396,10 @@ describe('ToolExecutor', () => { const fakeSandbox = { exec: async () => ({ stdout: 'sandbox-out', stderr: '' }), - } as any; + } as { exec: (command: string, opts?: { cwd?: string }) => Promise<{ stdout: string; stderr: string }> }; const fakeManager = { getOrCreate: async () => fakeSandbox, - } as any; + } as { getOrCreate: (sessionId: string) => Promise }; executor.setSandboxManager(fakeManager); const result = await executor.execute('shell.exec', { command: 'echo hi' }, { diff --git a/src/tools/policy.ts b/src/tools/policy.ts index fc86101..a74777b 100644 --- a/src/tools/policy.ts +++ b/src/tools/policy.ts @@ -288,11 +288,13 @@ export class ToolPolicy { */ getEffectiveProfile(context?: ToolPolicyContext): ToolProfile { // Check agent override first, then provider, then global - if (context?.agent && this.config.agents[context.agent]?.profile) { - return this.config.agents[context.agent].profile!; + const agentProfile = context?.agent ? this.config.agents[context.agent]?.profile : undefined; + if (agentProfile) { + return agentProfile; } - if (context?.provider && this.config.providers[context.provider]?.profile) { - return this.config.providers[context.provider].profile!; + const providerProfile = context?.provider ? this.config.providers[context.provider]?.profile : undefined; + if (providerProfile) { + return providerProfile; } return this.config.profile; } diff --git a/src/tools/registry.test.ts b/src/tools/registry.test.ts index 372bfff..d5cf2c7 100644 --- a/src/tools/registry.test.ts +++ b/src/tools/registry.test.ts @@ -1,6 +1,7 @@ import { describe, it, expect, vi } from 'vitest'; import { ToolRegistry } from './registry.js'; import type { Tool } from './types.js'; +import type { ToolPolicy } from './policy.js'; const echoTool: Tool = { name: 'test.echo', @@ -114,8 +115,13 @@ describe('ToolRegistry', () => { it('inherits the policy from original', () => { const reg = new ToolRegistry(); - const mockPolicy = { filterTools: vi.fn(), isAllowed: vi.fn(), resolveAllowedNames: vi.fn(), getEffectiveProfile: vi.fn() }; - reg.setPolicy(mockPolicy as any); + const mockPolicy: ToolPolicy = { + filterTools: vi.fn((tools) => tools), + isAllowed: vi.fn(() => true), + resolveAllowedNames: vi.fn(() => new Set()), + getEffectiveProfile: vi.fn(() => ({ profile: 'full', source: 'explicit' })), + }; + reg.setPolicy(mockPolicy); const cloned = reg.clone(); expect(cloned.getPolicy()).toBe(mockPolicy); @@ -131,8 +137,13 @@ describe('ToolRegistry', () => { replacementTool.description = 'Sandboxed version'; cloned.replace(replacementTool); - expect(cloned.get('shell.exec')!.description).toBe('Sandboxed version'); - expect(reg.get('shell.exec')!.description).toBe('Mock shell.exec'); + const clonedTool = cloned.get('shell.exec'); + const originalToolFromReg = reg.get('shell.exec'); + if (!clonedTool || !originalToolFromReg) { + throw new Error('Expected shell.exec to exist in both registries'); + } + expect(clonedTool.description).toBe('Sandboxed version'); + expect(originalToolFromReg.description).toBe('Mock shell.exec'); }); }); @@ -153,7 +164,11 @@ describe('ToolRegistry', () => { replacement.description = 'New description'; reg.replace(replacement); - expect(reg.get('tool.a')!.description).toBe('New description'); + const replaced = reg.get('tool.a'); + if (!replaced) { + throw new Error('Expected tool.a to be present'); + } + expect(replaced.description).toBe('New description'); }); it('throws if tool does not exist', () => {