chore(lint): burn down remaining warnings to zero
This commit is contained in:
@@ -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,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
@@ -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
@@ -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,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. */
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }[] = [];
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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,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
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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', () => {
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Lifecycle } from './lifecycle.js';
|
||||
|
||||
describe('Lifecycle', () => {
|
||||
|
||||
@@ -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
@@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -20,7 +20,7 @@ function formatTokens(n: number): string {
|
||||
}
|
||||
|
||||
export const StatusBar = memo(function StatusBar({
|
||||
sessionId,
|
||||
sessionId: _sessionId,
|
||||
messageCount,
|
||||
model,
|
||||
tokenUsage,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -217,7 +217,7 @@ function updateActiveRequests(requestsData) {
|
||||
`;
|
||||
}
|
||||
|
||||
function updateChannels(channelsData) {
|
||||
function _updateChannels(channelsData) {
|
||||
const el = document.getElementById('ops-channels');
|
||||
if (!el) {return;}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ let _el = null;
|
||||
async function loadSettings() {
|
||||
if (!_client || !_el) {return;}
|
||||
|
||||
let config, tools, channels;
|
||||
let config, tools;
|
||||
let services;
|
||||
|
||||
try {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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}`);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user