/** * Claude Code CLI Subprocess Manager — ARIA-Patch * * Basis: claude-max-api-proxy dist/subprocess/manager.js, plus die bisher per * sed in docker-compose.yml eingespielten Anpassungen (dangerously-skip- * permissions, system-prompt, 24h-Timeout, Prompt via stdin) — hier fest im * File, damit der groessere Zwischenruf-Umbau nicht per sed gefrickelt werden * muss. Wird per `cp` ueber die npm-Version gelegt (siehe docker-compose.yml). * * ZWISCHENRUF (interject): Statt den Prompt als Text zu schreiben und stdin * sofort zu schliessen (--print/text), laeuft claude jetzt im * `--input-format stream-json`-Modus. Der initiale Prompt geht als * stream-json User-Message rein, stdin bleibt OFFEN — so kann waehrend des * laufenden Turns per sendMessage() eine weitere User-Message reingeschoben * werden, die claude an der naechsten Tool-Grenze aufgreift (kein Abbruch). * Bei 'result' (Turn fertig) wird stdin geschlossen, damit claude sauber * beendet und die HTTP-Response (in routes.js an 'close' gebunden) rausgeht. */ import { spawn } from "child_process"; import { EventEmitter } from "events"; import { isAssistantMessage, isResultMessage, isContentDelta } from "../types/claude-cli.js"; const DEFAULT_TIMEOUT = 86400000; // 24h — lange Agent-Loops (Pentests etc.) export class ClaudeSubprocess extends EventEmitter { process = null; buffer = ""; timeoutId = null; isKilled = false; _stdinClosed = false; /** * Start the Claude CLI subprocess with the given prompt */ async start(prompt, options) { const args = this.buildArgs(prompt, options); const timeout = options.timeout || DEFAULT_TIMEOUT; return new Promise((resolve, reject) => { try { // Use spawn() for security - no shell interpretation this.process = spawn("claude", args, { cwd: options.cwd || process.cwd(), env: { ...process.env }, stdio: ["pipe", "pipe", "pipe"], }); // Set timeout this.timeoutId = setTimeout(() => { if (!this.isKilled) { this.isKilled = true; this.process?.kill("SIGTERM"); this.emit("error", new Error(`Request timed out after ${timeout}ms`)); } }, timeout); // Handle spawn errors (e.g., claude not found) this.process.on("error", (err) => { this.clearTimeout(); if (err.message.includes("ENOENT")) { reject(new Error("Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code")); } else { reject(err); } }); // stdin BLEIBT OFFEN: initialen Prompt als stream-json User- // Message schreiben; spaetere Zwischenrufe kommen via // sendMessage(). Geschlossen wird bei 'result' (s. processBuffer). this._writeUserMessage(prompt); // Falls stdin (z.B. EPIPE) frueh stirbt: nicht crashen. this.process.stdin?.on("error", () => {}); console.error(`[Subprocess] Process spawned with PID: ${this.process.pid}`); // Parse JSON stream from stdout this.process.stdout?.on("data", (chunk) => { const data = chunk.toString(); console.error(`[Subprocess] Received ${data.length} bytes of stdout`); this.buffer += data; this.processBuffer(); }); // Capture stderr for debugging this.process.stderr?.on("data", (chunk) => { const errorText = chunk.toString().trim(); if (errorText) { // Don't emit as error unless it's actually an error // Claude CLI may write debug info to stderr console.error("[Subprocess stderr]:", errorText.slice(0, 200)); } }); // Handle process close this.process.on("close", (code) => { console.error(`[Subprocess] Process closed with code: ${code}`); this.clearTimeout(); // Process any remaining buffer if (this.buffer.trim()) { this.processBuffer(); } this.emit("close", code); }); // Resolve immediately since we're streaming resolve(); } catch (err) { this.clearTimeout(); reject(err); } }); } /** * Build CLI arguments array */ buildArgs(prompt, options) { const args = [ "--print", // Non-interactive mode "--output-format", "stream-json", // JSON streaming output "--verbose", // Required for stream-json "--include-partial-messages", // Enable streaming chunks "--input-format", "stream-json", // ARIA: User-Messages via stdin (Zwischenruf) "--model", options.model, // Model alias (opus/sonnet/haiku) "--no-session-persistence", "--dangerously-skip-permissions", "--system-prompt", options.systemPrompt, "--safe-mode", ]; if (options.sessionId) { args.push("--session-id", options.sessionId); } return args; } /** * Eine User-Message im stream-json-Input-Format an stdin schreiben. * Genutzt fuer den initialen Prompt UND fuer Zwischenrufe (sendMessage). */ _writeUserMessage(text) { const p = this.process; if (!p || !p.stdin || p.stdin.destroyed || this._stdinClosed) return false; try { p.stdin.write(JSON.stringify({ type: "user", message: { role: "user", content: String(text) } }) + "\n"); return true; } catch (_) { return false; } } /** * Zwischenruf: waehrend eines laufenden Turns eine weitere User-Message * reinschieben. claude greift sie an der naechsten Tool-Grenze auf, ohne * den Turn abzubrechen. Kein Effekt, wenn stdin schon geschlossen ist * (Turn praktisch fertig) — dann ist der Zwischenruf schlicht zu spaet. */ sendMessage(text) { return this._writeUserMessage(text); } /** * stdin schliessen → claude beendet den stream-json-Input und exit't. */ _closeStdin() { if (this._stdinClosed) return; this._stdinClosed = true; try { this.process?.stdin?.end(); } catch (_) { } } /** * Process the buffer and emit parsed messages */ processBuffer() { const lines = this.buffer.split("\n"); this.buffer = lines.pop() || ""; // Keep incomplete line for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; try { const message = JSON.parse(trimmed); this.emit("message", message); if (isContentDelta(message)) { // Emit content delta for streaming this.emit("content_delta", message); } else if (isAssistantMessage(message)) { this.emit("assistant", message); } else if (isResultMessage(message)) { this.emit("result", message); // Turn fertig → stdin schliessen, sonst wartet claude im // stream-json-Input auf weitere Messages und der Prozess // (und damit die HTTP-Response) haengt fuer immer. this._closeStdin(); } } catch { // Non-JSON output, emit as raw this.emit("raw", trimmed); } } } /** * Clear the timeout timer */ clearTimeout() { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } } /** * Kill the subprocess */ kill(signal = "SIGTERM") { if (!this.isKilled && this.process) { this.isKilled = true; this.clearTimeout(); this.process.kill(signal); } } /** * Check if the process is still running */ isRunning() { return this.process !== null && !this.isKilled && this.process.exitCode === null; } } /** * Verify that Claude CLI is installed and accessible */ export async function verifyClaude() { return new Promise((resolve) => { const proc = spawn("claude", ["--version"], { stdio: "pipe" }); let output = ""; proc.stdout?.on("data", (chunk) => { output += chunk.toString(); }); proc.on("error", () => { resolve({ ok: false, error: "Claude CLI not found. Install with: npm install -g @anthropic-ai/claude-code", }); }); proc.on("close", (code) => { if (code === 0) { resolve({ ok: true, version: output.trim() }); } else { resolve({ ok: false, error: "Claude CLI returned non-zero exit code", }); } }); }); } /** * Check if Claude CLI is authenticated */ export async function verifyAuth() { return { ok: true }; } //# sourceMappingURL=manager.js.map