import { execFile } from 'child_process'; export type ExternalBackendName = 'claude_code' | 'opencode' | 'codex' | 'gemini' | 'pi_embedded'; export interface ExternalBackendRequest { prompt: string; history: Array<{ role: 'user' | 'assistant'; content: string }>; } export interface ExternalBackend { name: ExternalBackendName; process(input: ExternalBackendRequest): Promise; } interface ExternalCliBackendOptions { name: ExternalBackendName; command: string; args?: string[]; timeoutMs?: number; } const DEFAULT_TIMEOUT_MS = 120_000; function buildPrompt(request: ExternalBackendRequest): string { const lines: string[] = []; for (const item of request.history) { if (!item.content.trim()) { continue; } lines.push(`${item.role.toUpperCase()}: ${item.content}`); } lines.push(`USER: ${request.prompt}`); return lines.join('\n\n'); } function inferArgs(name: ExternalBackendName, prompt: string): string[] { if (name === 'claude_code') { return ['--print', prompt]; } if (name === 'opencode') { return ['run', '--format', 'default', prompt]; } return ['-p', prompt]; } export class ExternalCliBackend implements ExternalBackend { readonly name: ExternalBackendName; private readonly command: string; private readonly args: string[]; private readonly timeoutMs: number; constructor(options: ExternalCliBackendOptions) { this.name = options.name; this.command = options.command; this.args = options.args ?? []; this.timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS; } async process(input: ExternalBackendRequest): Promise { const prompt = buildPrompt(input); const configuredArgs = this.args .map((arg) => arg.includes('{prompt}') ? arg.replaceAll('{prompt}', prompt) : arg); const hasPromptPlaceholder = this.args.some((arg) => arg.includes('{prompt}')); const args = hasPromptPlaceholder ? configuredArgs : [...configuredArgs, ...inferArgs(this.name, prompt)]; const output = await execFileAsync(this.command, args, this.timeoutMs); const trimmed = output.trim(); if (!trimmed) { throw new Error(`External backend "${this.name}" returned no output`); } return trimmed; } } export class ClaudeCodeBackend extends ExternalCliBackend { constructor(path?: string, args?: string[], timeoutMs?: number) { super({ name: 'claude_code', command: path ?? 'claude', args, timeoutMs }); } } export class OpenCodeBackend extends ExternalCliBackend { constructor(path?: string, args?: string[], timeoutMs?: number) { super({ name: 'opencode', command: path ?? 'opencode', args, timeoutMs }); } } export class CodexBackend extends ExternalCliBackend { constructor(path?: string, args?: string[], timeoutMs?: number) { super({ name: 'codex', command: path ?? 'codex', args, timeoutMs }); } } export class GeminiBackend extends ExternalCliBackend { constructor(path?: string, args?: string[], timeoutMs?: number) { super({ name: 'gemini', command: path ?? 'gemini', args, timeoutMs }); } } function execFileAsync(command: string, args: string[], timeoutMs: number): Promise { return new Promise((resolve, reject) => { execFile(command, args, { timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => { if (error) { const details = [stderr, stdout] .map((entry) => entry.trim()) .filter((entry) => entry.length > 0) .join('\n'); reject(new Error(details ? `${error.message}\n${details}` : error.message)); return; } resolve(stdout || ''); }); }); }