Something went wrong. Try again.
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768import { spawn } from "node:child_process";import type { AuthorModel } from "./author";
/** "claude -p --tools ''" as an argument list: spaces separate, single or double quotes group, a backslash escapes the next character. No shell is involved. */export function splitCommand(text: string): string[] { const out: string[] = []; let cur = ""; let started = false; let quote: string | undefined; for (let i = 0; i < text.length; i++) { const c = text[i]!; if (quote !== undefined) { if (c === quote) quote = undefined; else cur += c; } else if (c === '"' || c === "'") { quote = c; started = true; } else if (c === "\\" && i + 1 < text.length) { cur += text[++i]; started = true; } else if (/\s/.test(c)) { if (started) { out.push(cur); cur = ""; started = false; } } else { cur += c; started = true; } } if (quote !== undefined) throw new Error("the model command has a quote that is never closed"); if (started) out.push(cur); if (out.length === 0) throw new Error("the model command is empty"); return out;}
/** Variables the model's process may have: enough to find programs and a home for its own login, and none of ours. */const KEEP = ["PATH", "HOME", "LANG", "LC_ALL", "TZ", "TMPDIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "ANTHROPIC_API_KEY"];
/** * A model that is a command: the prompt goes in on stdin, the answer comes out on stdout. The person chooses the command (for example `claude -p`, with the options they * want for tools); nothing is assumed about it. It gets none of aite's AITE_* variables, so no GitLab token. */export function commandModel(options: { command: string[]; timeoutMs?: number; env?: NodeJS.ProcessEnv }): AuthorModel { const timeoutMs = options.timeoutMs ?? 600_000; const source = options.env ?? process.env; const env = Object.fromEntries(KEEP.flatMap((k) => (source[k] !== undefined ? [[k, source[k]!]] : []))); return { propose: (prompt) => new Promise<string>((resolve, reject) => { const [bin, ...args] = options.command as [string, ...string[]]; const child = spawn(bin, args, { env, stdio: ["pipe", "pipe", "pipe"] }); let out = ""; let err = ""; let settled = false; const finish = (f: () => void) => { if (!settled) { settled = true; clearTimeout(timer); f(); } }; const timer = setTimeout(() => { child.kill("SIGKILL"); finish(() => reject(new Error(`the model command did not answer within ${timeoutMs} ms`))); }, timeoutMs); child.stdout.on("data", (c: Buffer) => (out += c.toString("utf8"))); child.stderr.on("data", (c: Buffer) => (err = (err + c.toString("utf8")).slice(-2000))); child.on("error", (e) => finish(() => reject(new Error(`cannot run the model command ${bin}: ${e.message}`)))); child.on("close", (code) => finish(() => { if (code !== 0) reject(new Error(`the model command ended with exit ${code}: ${err.trim().split("\n")[0] ?? ""}`)); else if (out.trim() === "") reject(new Error("the model command answered nothing")); else resolve(out); })); child.stdin.on("error", () => undefined); // a command that exits before reading everything child.stdin.end(prompt); }), };}