chore(lint): burn down remaining warnings to zero

This commit is contained in:
William Valentin
2026-02-15 23:14:21 -08:00
parent 49b752e8b0
commit 948d4ac6d8
67 changed files with 235 additions and 256 deletions
+8 -2
View File
@@ -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);
});
});
-1
View File
@@ -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()', () => {
+1 -2
View File
@@ -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<AuditEvent[]> {
+7 -5
View File
@@ -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<AuditEvent, 'timestamp'>): 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<void> {
if (this.writeStream) {
const writeStream = this.writeStream;
if (writeStream) {
await new Promise<void>((resolve) => {
this.writeStream!.end(() => resolve());
writeStream.end(() => resolve());
});
this.writeStream = null;
}
+1 -1
View File
@@ -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. */
+4 -4
View File
@@ -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];
+4 -1
View File
@@ -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
+2 -2
View File
@@ -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<Response>(() => {});
}
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' });
+4 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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);
+5 -1
View File
@@ -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) {
+4 -4
View File
@@ -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<void> {
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);
}
};
+8 -2
View File
@@ -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);
});
});
+2 -2
View File
@@ -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<string, any>): Promise<void> {
const automation = config.automation as Record<string, any> | undefined;
export async function runGoogleAuth(p: Prompter, config: Record<string, unknown>): Promise<void> {
const automation = config.automation as Record<string, unknown> | undefined;
if (!automation) {return;}
const pending: { name: string; authCmd: string }[] = [];
+3 -4
View File
@@ -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', () => {
+2 -2
View File
@@ -155,8 +155,8 @@ export class ConfigBuilder {
this.config.automation = automation;
}
build(): Record<string, any> {
return structuredClone(this.config) as Record<string, any>;
build(): Record<string, unknown> {
return structuredClone(this.config) as Record<string, unknown>;
}
toYaml(): string {
+1 -1
View File
@@ -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', () => {
+2 -2
View File
@@ -39,8 +39,8 @@ export async function setupMemory(p: Prompter, builder: ConfigBuilder): Promise<
p.println('✓ Vector search enabled');
}
function findReusableApiKey(config: Record<string, any>, embeddingProvider: string): string | undefined {
const models = config.models ?? {};
function findReusableApiKey(config: Record<string, unknown>, embeddingProvider: string): string | undefined {
const models = (config.models as Record<string, { provider?: string; api_key?: string }>) ?? {};
for (const tier of ['default', 'fast', 'complex', 'local']) {
const m = models[tier];
if (m?.provider === embeddingProvider && m?.api_key) {return m.api_key;}
+1 -1
View File
@@ -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';
+3 -2
View File
@@ -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', () => {
+1 -1
View File
@@ -1,4 +1,4 @@
export function renderSummary(config: Record<string, any>): string {
export function renderSummary(config: Record<string, unknown>): string {
const lines: string[] = [];
const models = config.models ?? {};
+10 -2
View File
@@ -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;
+14 -7
View File
@@ -3,21 +3,28 @@ import { readFileSync } from 'fs';
import { parse } from 'yaml';
describe('config/default.yaml', () => {
const asRecord = (value: unknown): Record<string, unknown> => (
value && typeof value === 'object' ? value as Record<string, unknown> : {}
);
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');
});
});
+6 -3
View File
@@ -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', () => {
+1 -1
View File
@@ -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', () => {
+1 -1
View File
@@ -39,7 +39,7 @@ export async function initAgents(deps: AgentsDeps): Promise<AgentsResult> {
if (sandboxManager) {
lifecycle.onShutdown(async () => {
await sandboxManager!.destroyAll();
await sandboxManager.destroyAll();
console.log('Docker sandboxes destroyed');
});
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect } from 'vitest';
import { Lifecycle } from './lifecycle.js';
describe('Lifecycle', () => {
+4 -1
View File
@@ -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 = {
+4 -3
View File
@@ -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');
});
}
+3 -2
View File
@@ -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') {
+1 -1
View File
@@ -20,7 +20,7 @@ function formatTokens(n: number): string {
}
export const StatusBar = memo(function StatusBar({
sessionId,
sessionId: _sessionId,
messageCount,
model,
tokenUsage,
+3 -2
View File
@@ -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,
});
+3 -2
View File
@@ -59,7 +59,8 @@ export function createSystemHandlers(deps: SystemHandlerDeps) {
},
'system.restart': async (request: GatewayRequest): Promise<OutboundMessage> => {
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);
});
});
+1 -1
View File
@@ -49,7 +49,7 @@ export class LaneQueue {
// Otherwise, queue the work and return a deferred promise
return new Promise<T>((resolve, reject) => {
lane!.queue.push({
lane.queue.push({
work: work as () => Promise<unknown>,
resolve: resolve as (value: unknown) => void,
reject,
+3 -3
View File
@@ -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));
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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');
+1 -1
View File
@@ -217,7 +217,7 @@ function updateActiveRequests(requestsData) {
`;
}
function updateChannels(channelsData) {
function _updateChannels(channelsData) {
const el = document.getElementById('ops-channels');
if (!el) {return;}
+1 -1
View File
@@ -17,7 +17,7 @@ let _el = null;
async function loadSettings() {
if (!_client || !_el) {return;}
let config, tools, channels;
let config, tools;
let services;
try {
+8 -3
View File
@@ -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', () => {
+3 -2
View File
@@ -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' },
+3 -2
View File
@@ -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 () => {
+3 -2
View File
@@ -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(
+1 -1
View File
@@ -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;
+9 -3
View File
@@ -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);
+4 -3
View File
@@ -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;}
}
+6 -3
View File
@@ -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);
+4 -1
View File
@@ -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}`);
+2 -2
View File
@@ -1,10 +1,10 @@
import { describe, expect, it, vi } from 'vitest';
let capturedOptions: any;
let capturedOptions: Record<string, unknown> | undefined;
vi.mock('openai', () => {
class OpenAI {
constructor(options: any) {
constructor(options: Record<string, unknown>) {
capturedOptions = options;
}
}
+2 -1
View File
@@ -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' },
+8 -3
View File
@@ -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']);
});
});
+1 -1
View File
@@ -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';
+6 -2
View File
@@ -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');
+1 -121
View File
@@ -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 {
+5 -1
View File
@@ -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', () => {
+1 -1
View File
@@ -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<string, unknown> | 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 {
+6 -2
View File
@@ -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';
+1 -2
View File
@@ -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);
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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,
};
-2
View File
@@ -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';
+5 -1
View File
@@ -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(),
-4
View File
@@ -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;
+2 -2
View File
@@ -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<typeof fakeSandbox> };
executor.setSandboxManager(fakeManager);
const result = await executor.execute('shell.exec', { command: 'echo hi' }, {
+6 -4
View File
@@ -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;
}
+20 -5
View File
@@ -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', () => {