// Runs Claude Code for one profile and returns its final result text. import { createInterface } from "node:readline"; import type { Readable } from "node:stream"; import { isClaudeAuthFailure } from "./auth.ts"; import type { ClaudeFailureFields } from "./auth.ts"; import { chunks } from "./command.ts"; import { agentCommand } from "./isolation.ts"; import { isObject } from "./json.ts"; import type { JsonObject } from "./json.ts"; import { AgentAuthError, agentEnvironment } from "./profiles.ts"; import { PROCESS_ENV, WORKSPACE } from "./run-context.ts"; import type { RuntimeProfile } from "./runtime.ts"; // Claude Code's stream-json `result` event, in its own field names. type ClaudeResult = ClaudeFailureFields & Partial> & { subtype: string; }; // Echoes stderr as it arrives and resolves with all of it. async function copyError(stream: Readable): Promise { const decoder = new TextDecoder(); let error = ""; for await (const chunk of chunks(stream)) { const text = decoder.decode(chunk, { stream: true }); error += text; console.error(text); } return error + decoder.decode(); } // Prints the visible text of each streamed message part. function printParts(event: JsonObject): void { const content = isObject(event.message) ? event.message.content : undefined; for (const part of Array.isArray(content) ? content : []) { if (isObject(part)) { const text = part.thinking ?? part.text ?? part.name; if (typeof text === "string" && text !== "") { console.log(text); } } } } function toResult(event: JsonObject): ClaudeResult { const { is_error: isError, result, subtype, api_error_status: apiErrorStatus } = event; return { ...(typeof isError === "boolean" && { is_error: isError }), ...(typeof result === "string" && { result }), subtype: String(subtype), ...((typeof apiErrorStatus === "number" || apiErrorStatus === null) && { api_error_status: apiErrorStatus, }), }; } // Prints each streamed message part and returns the final `result` event. async function readEvents(stream: Readable): Promise { let result: ClaudeResult | undefined; for await (const line of createInterface({ input: stream, crlfDelay: Infinity })) { if (line !== "") { const event: unknown = JSON.parse(line); if (isObject(event)) { printParts(event); if (event.type === "result") { result = toResult(event); } } } } return result; } function checkResult(result: ClaudeResult | undefined, error: string): string { if (result?.is_error === true) { const failure = new Error(result.result ?? `claude ended with ${result.subtype}`); if (isClaudeAuthFailure(result)) { throw new AgentAuthError("claude sign-in failed", { cause: failure }); } throw failure; } if (result?.result === undefined) { throw new Error(error.trim() || `claude ended with ${result?.subtype ?? "no result"}`); } return result.result; } export async function runClaude( profile: RuntimeProfile, token: string, prompt: string, ): Promise { const args = [ "--print", "--dangerously-skip-permissions", "--model", profile.model, "--output-format", "stream-json", "--verbose", prompt, ]; const claude = agentCommand(profile, "claude", args, { cwd: WORKSPACE, env: { ...agentEnvironment(PROCESS_ENV, profile, token), IS_SANDBOX: "1" }, clearEnv: true, stdout: "piped", stderr: "piped", }).spawn(); const errorText = copyError(claude.stderr); const result = await readEvents(claude.stdout); const [status, error] = await Promise.all([claude.status, errorText]); const body = checkResult(result, error); if (!status.success) { throw new Error(error.trim() || `claude exited with ${status.code}`); } return body; }