import { spawn as defaultSpawn } from "node:child_process"; import { existsSync as defaultExistsSync } from "node:fs"; import type { ClientName, ExecResult, DebugInfo, ExecuteOptions } from "./types.js"; import { ClientNotFoundError, ExecError } from "./types.js"; export const CLIENT_ARGS: Record string[]> = { codex: (p) => ["exec", "--yolo", p], claude: (p) => ["-p", p, "--dangerously-skip-permissions"], opencode: (p) => ["run", "--dangerously-skip-permissions", p], }; /** * Known stderr noise patterns per client. * When exit code is 0, lines matching these patterns are stripped from the * returned stderr to prevent agents from misinterpreting informational * diagnostics as errors. The raw (unfiltered) stderr is preserved in * DebugInfo.rawStderr when --debug is active. */ const STDERR_NOISE_PATTERNS: Partial> = { codex: [ /^\d{4}-\d{2}-\d{2}T[\d:.]+Z\s+ERROR\s+codex_core::util:\s+ReasoningSummary\w*\s+/, ], }; function filterStderrNoise(client: ClientName, stderr: string, exitCode: number): string { if (exitCode !== 0) return stderr; const patterns = STDERR_NOISE_PATTERNS[client]; if (!patterns) return stderr; const lines = stderr.split("\n"); const filtered = lines.filter((line) => !patterns.some((p) => p.test(line))); return filtered.join("\n").replace(/\n+$/, ""); } export async function executePrompt( client: ClientName, prompt: string, options: ExecuteOptions = {} ): Promise { if (prompt.trim() === "") { throw new ExecError("Prompt cannot be empty", { stdout: "", stderr: "", exitCode: -1, client, durationMs: 0, }); } const spawnImpl = options.spawn ?? defaultSpawn; const existsSyncImpl = options.existsSync ?? defaultExistsSync; const timeoutMs = options.timeoutMs ?? 600_000; const command = options.clientPath ?? client; if (options.clientPath && !existsSyncImpl(options.clientPath)) { throw new ClientNotFoundError(client); } const argBuilder = (CLIENT_ARGS as Record string[]>)[client]; if (!argBuilder) { throw new ExecError(`Unknown client: ${client}`, { stdout: "", stderr: "", exitCode: -1, client, durationMs: 0, }); } const args = argBuilder(prompt); return new Promise((resolve, reject) => { let settled = false; let timedOut = false; let stdout = ""; let stderr = ""; let exitSignal: NodeJS.Signals | null = null; const startMs = Date.now(); const child = spawnImpl(command, args, { shell: false, stdio: ["pipe", "pipe", "pipe"], }); // Close stdin immediately so clients like codex don't hang waiting for input child.stdin?.end(); child.stdout?.on("data", (chunk: Buffer | string) => { stdout += chunk.toString(); }); child.stderr?.on("data", (chunk: Buffer | string) => { stderr += chunk.toString(); }); const timeout = setTimeout(() => { timedOut = true; child.kill(); }, timeoutMs); function settle( err?: Error | undefined, result?: ExecResult | undefined ): void { if (settled) return; settled = true; clearTimeout(timeout); const durationMs = Date.now() - startMs; const rawStderr = stderr; const cleanedStderr = filterStderrNoise(client, rawStderr, result?.exitCode ?? -1); if (options.debug || options.onDebug) { const effectiveExitCode = result?.exitCode ?? (err instanceof ExecError ? err.result.exitCode : null); const debugInfo: DebugInfo = { command, args, pid: child.pid ?? undefined, exitCode: effectiveExitCode, exitSignal, durationMs, stderrLength: rawStderr.length, stdoutLength: stdout.length, noisySuccess: effectiveExitCode === 0 && rawStderr.length > 0, rawStderr: rawStderr !== cleanedStderr ? rawStderr : undefined, }; options.onDebug?.(debugInfo); } if (err) { if (err instanceof ExecError) { err.result.stderr = cleanedStderr; err.result.client = client; err.result.durationMs = durationMs; } reject(err); } else { resolve({ ...result!, client, durationMs, stderr: cleanedStderr }); } } child.on("error", (err: NodeJS.ErrnoException) => { if (err.code === "ENOENT") { settle(new ClientNotFoundError(client)); } else { settle( new ExecError(err.message, { stdout, stderr, exitCode: -1, client, durationMs: 0 }) ); } }); child.on("close", (code: number | null, signal: NodeJS.Signals | null) => { exitSignal = signal; if (timedOut) { settle( new ExecError(`Execution timed out after ${timeoutMs}ms`, { stdout, stderr, exitCode: -1, client, durationMs: 0, }) ); } else { settle(undefined, { stdout, stderr, exitCode: code ?? -1, client, durationMs: 0 }); } }); }); }