/** * Short-lived ACP probe — opens a session and reads models/modes from the response. */ import { spawn } from 'node:child_process'; import { ClientSideConnection, type Client, type NewSessionResponse, type ReadTextFileRequest, type ReadTextFileResponse, type WriteTextFileRequest, type WriteTextFileResponse, type CreateTerminalRequest, type CreateTerminalResponse, type RequestPermissionRequest, type RequestPermissionResponse, } from '@agentclientprotocol/sdk'; import { deriveModesFromACP, deriveModelDefinitionsFromACP } from './acp-derive.js'; import { getManifestDefaultModeId, getManifestModes } from './provider-manifest.js'; import { resolveAcpSpawnArgs } from './acp-spawn.js'; import { createAcpNdJsonStream } from './acp-stream.js'; import type { ProviderModel, ProviderMode } from './provider-types.js'; import type { AgentCommand } from './agent-commands-cache.js'; const PROBE_TIMEOUT_MS = 30_000; export interface AcpProbeResult { ok: boolean; models: ProviderModel[]; modes: ProviderMode[]; defaultModeId: string | null; commands: AgentCommand[]; error?: string; } function parseSessionResponse(session: NewSessionResponse, agent: string): AcpProbeResult { const fallbackModes = getManifestModes(agent); const { modes, currentModeId } = deriveModesFromACP( fallbackModes, session.modes, session.configOptions, ); const models = deriveModelDefinitionsFromACP(session.models, session.configOptions); return { ok: true, models, modes, defaultModeId: currentModeId ?? getManifestDefaultModeId(agent), commands: [], }; } export async function probeAcpProvider( agent: string, installPath: string, cwd: string, ): Promise { const args = resolveAcpSpawnArgs(agent); if (!args) { return { ok: false, models: [], modes: getManifestModes(agent), defaultModeId: getManifestDefaultModeId(agent), commands: [], error: 'no ACP spawn args', }; } const child = spawn(installPath, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env }, }); let killed = false; const kill = () => { if (!killed) { killed = true; child.kill('SIGTERM'); setTimeout(() => child.kill('SIGKILL'), 2_000); } }; const timeout = setTimeout(kill, PROBE_TIMEOUT_MS); const probedCommands: AgentCommand[] = []; try { const stream = createAcpNdJsonStream(child); const connection = new ClientSideConnection( (_agentInterface): Client => ({ async sessionUpdate(params) { const update = params.update; if (update.sessionUpdate === 'available_commands_update') { for (const cmd of update.availableCommands) { probedCommands.push({ name: cmd.name, description: cmd.description ?? undefined, }); } } }, async requestPermission(params: RequestPermissionRequest): Promise { const first = params.options[0]; if (first) { return { outcome: { outcome: 'selected', optionId: first.optionId } }; } return { outcome: { outcome: 'cancelled' } }; }, async readTextFile(_params: ReadTextFileRequest): Promise { return { content: '' }; }, async writeTextFile(_params: WriteTextFileRequest): Promise { return {}; }, async createTerminal(_params: CreateTerminalRequest): Promise { return { terminalId: 'noop' }; }, }), stream, ); await connection.initialize({ protocolVersion: 1, clientInfo: { name: 'boocoder-probe', version: '2.2.0' }, clientCapabilities: {}, }); const session = await connection.newSession({ cwd, mcpServers: [] }); const result = parseSessionResponse(session, agent); result.commands = probedCommands; await connection.closeSession({ sessionId: session.sessionId }).catch(() => {}); return result; } catch (err) { const message = err instanceof Error ? err.message : String(err); return { ok: false, models: [], modes: getManifestModes(agent), defaultModeId: getManifestDefaultModeId(agent), commands: probedCommands, error: message, }; } finally { clearTimeout(timeout); kill(); await new Promise((resolve) => { child.on('close', resolve); setTimeout(resolve, 2_000); }); } }