#!/usr/bin/env node /** * pi-coding-agent bridge (stdio JSON). * * Input (stdin): * { * "workspace": "/abs/path", * "model": "claude-sonnet-4-5", * "prompt": "...", * "targets": ["docs/RD-UN.md"] * } * * Output (stdout, last line JSON): 同 cursor-bridge / opencode-bridge 协议 * * 设计:与 cursor-bridge / opencode-bridge 完全相同的协议层。 * * 调用的 pi 命令: * workspace: --tools read,write,edit --approve * reasoning: --no-tools,不加载扩展 * 未知 tool_profile 直接失败,不退回读写工具 * * 工作目录:Go 端 cmd.Dir 设(pi 没有 --cwd flag;node 桥 spawn 时再覆盖一次) * * 完成信号: * - agent_settled → 成功("最稳妥" 标志,自动重试/队列全走完才算) * - agent_error → 失败 * - turn_end → 单轮结束,不一定等于整体任务完成 */ import { spawn } from "node:child_process"; import { readFileSync, existsSync } from "node:fs"; import { resolve, relative, isAbsolute } from "node:path"; import { buildPiLaunchArgs } from "../../deploy/pi-agent/pi-args.mjs"; async function readStdin() { const chunks = []; for await (const chunk of process.stdin) { chunks.push(chunk); } return Buffer.concat(chunks).toString("utf8"); } function fail(reason, extra = {}, stderr = "") { process.stdout.write( JSON.stringify({ ok: false, patches: [], fallback_reason: reason, stderr: stderr ? stderr.slice(0, 400) : undefined, ...extra, }) + "\n", ); process.exit(0); } function safeRel(workspace, target) { const abs = isAbsolute(target) ? target : resolve(workspace, target); const rel = relative(workspace, abs); if (!rel || rel.startsWith("..") || isAbsolute(rel)) { return null; } return rel.replace(/\\/g, "/"); } async function main() { let req; try { const raw = await readStdin(); req = JSON.parse(raw); } catch (e) { fail("invalid_json: " + (e && e.message ? e.message : String(e))); return; } const workspace = req.workspace; const model = req.model || process.env.PI_AGENT_MODEL || "claude-sonnet-4-5"; const provider = req.provider || process.env.PI_AGENT_PROVIDER || "anthropic"; const prompt = req.prompt || ""; const targets = Array.isArray(req.targets) ? req.targets : []; if (!workspace || !existsSync(workspace)) { fail("workspace_missing"); return; } if (!prompt.trim()) { fail("prompt_empty"); return; } const allowed = new Set(); for (const t of targets) { const rel = safeRel(workspace, t); if (rel) allowed.add(rel); } if (allowed.size === 0) { fail("no_targets"); return; } // 1. spawn pi --mode rpc const piBin = process.env.PI_AGENT_BIN || "pi"; const sysPromptFile = process.env.PI_AGENT_SYSTEM_PROMPT_FILE || "INSTRUCTIONS.md"; const absSysPrompt = resolve(workspace, sysPromptFile); let args; try { args = buildPiLaunchArgs({ toolProfile: req.tool_profile, provider, model, systemPromptFile: existsSync(absSysPrompt) ? sysPromptFile : "", workspaceTools: "read,write,edit", }); } catch (e) { if (e && e.code === "UNKNOWN_TOOL_PROFILE") { fail("unknown_tool_profile"); return; } throw e; } const env = { ...process.env }; env.PI_AGENT_MODEL = model; env.PI_AGENT_PROVIDER = provider; let agentId = ""; let runId = ""; let lastErr = ""; let succeeded = false; try { const child = spawn(piBin, args, { cwd: workspace, env, stdio: ["pipe", "pipe", "pipe"], }); // 2. 写 JSONL prompt 命令到 stdin const cmdId = "req-" + Date.now(); child.stdin.write( JSON.stringify({ id: cmdId, type: "prompt", message: prompt, }) + "\n", ); // 3. 按行解析 ndjson 事件流 let buf = ""; child.stdout.on("data", (chunk) => { buf += chunk.toString("utf8"); let nl; while ((nl = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1); if (!line) continue; let ev; try { ev = JSON.parse(line); } catch { continue; } if (!ev || typeof ev !== "object") continue; const t = ev.type || ""; if (t === "session" && ev.id) { agentId = ev.id; } if (t === "agent_settled") { succeeded = true; // agent_settled 表示任务真正结束;关闭 stdin 让 pi 退出 child.stdin.end(); } if (t === "agent_error") { lastErr = ev.error?.message || ev.message || "agent_error"; } } }); let stderrBuf = ""; child.stderr.on("data", (chunk) => { stderrBuf += chunk.toString("utf8"); lastErr = (stderrBuf + " " + lastErr).slice(-400); }); await new Promise((resolveDone) => { child.on("close", () => resolveDone()); child.on("error", (err) => { lastErr = err && err.message ? err.message : String(err); resolveDone(); }); }); } catch (e) { fail("startup_or_run: " + (e && e.message ? e.message : String(e))); return; } if (!succeeded) { fail(lastErr || "agent_not_settled", { agent_id: agentId, run_id: runId }, lastErr); return; } // 4. 读 workspace 内目标文件 const patches = []; for (const rel of allowed) { const abs = resolve(workspace, rel); if (!existsSync(abs)) continue; const content = readFileSync(abs, "utf8"); patches.push({ rel_path: rel, content }); } if (patches.length === 0) { fail("empty_patches", { agent_id: agentId, run_id: runId }); return; } process.stdout.write( JSON.stringify({ ok: true, agent_id: agentId, run_id: runId, patches, fallback_reason: "", }) + "\n", ); } main().catch((e) => { fail("unhandled: " + (e && e.message ? e.message : String(e))); });