import { spawn } from "node:child_process"; import { createWriteStream, readFileSync } from "node:fs"; const CLAUDE_BIN = process.env.CLAUDE_BIN || "claude"; /** * Run headless Claude Code and return the parsed JSON result. * The system prompt file is read and passed via --append-system-prompt so we don't * depend on the --append-system-prompt-file flag variant. */ export function runClaude({ prompt, systemPromptFile, model, mcpConfig, allowedTools, addDirs = [], cwd, logFile, permissionMode = "acceptEdits", // bypassPermissions is blocked under root }) { return new Promise((resolve, reject) => { const args = [ "-p", prompt, "--output-format", "json", "--permission-mode", permissionMode, ]; if (model) args.push("--model", model); if (systemPromptFile) { const sys = readFileSync(systemPromptFile, "utf8"); args.push("--append-system-prompt", sys); } if (mcpConfig) args.push("--mcp-config", mcpConfig); if (allowedTools) args.push("--allowed-tools", allowedTools); for (const d of addDirs) args.push("--add-dir", d); const log = logFile ? createWriteStream(logFile, { flags: "a" }) : null; if (log) log.write(`\n\n===== ${new Date().toISOString()} claude run =====\n`); const child = spawn(CLAUDE_BIN, args, { cwd, env: process.env, stdio: ["ignore", "pipe", "pipe"], }); let stdout = ""; let stderr = ""; child.stdout.on("data", (b) => { stdout += b; if (log) log.write(b); }); child.stderr.on("data", (b) => { stderr += b; if (log) log.write(b); }); child.on("error", reject); child.on("close", (code) => { if (log) log.end(); if (code !== 0) { return reject(new Error(`claude exited ${code}: ${stderr.slice(-2000) || stdout.slice(-2000)}`)); } try { resolve(JSON.parse(stdout)); } catch { resolve({ result: stdout.trim(), raw: true }); } }); }); }