#!/usr/bin/env node // progress.mjs — render a `pi --mode json` event stream as human-readable // progress on stdout, and write the final assistant message to a report file. // // Usage: pi --mode json "prompt" | node progress.mjs [--quiet] // // Quiet mode still writes the report file (for harness logs); it only // suppresses the progress display. import { writeFileSync } from "node:fs"; const args = process.argv.slice(2); const reportPath = args.find((a) => !a.startsWith("--")); const quiet = args.includes("--quiet"); if (!reportPath) { console.error("usage: progress.mjs [--quiet]"); process.exit(2); } const useColor = process.stdout.isTTY && !process.env.NO_COLOR; const c = { dim: (s) => (useColor ? `\x1b[2m${s}\x1b[0m` : s), cyan: (s) => (useColor ? `\x1b[36m${s}\x1b[0m` : s), red: (s) => (useColor ? `\x1b[31m${s}\x1b[0m` : s), yellow: (s) => (useColor ? `\x1b[33m${s}\x1b[0m` : s), green: (s) => (useColor ? `\x1b[32m${s}\x1b[0m` : s), }; const out = (s = "") => { if (!quiet) process.stdout.write(s + "\n"); }; function truncate(s, n) { s = String(s ?? "").replace(/\s+/g, " ").trim(); return s.length > n ? s.slice(0, n - 1) + "…" : s; } function summarizeTool(name, a = {}) { switch (name) { case "bash": return "$ " + truncate(a.command ?? a.cmd, 110); case "read": return truncate(a.path, 90) + (a.offset ? `:${a.offset}` : ""); case "write": return truncate(a.path, 90); case "edit": return truncate(a.path, 90); case "grep": return `${truncate(a.pattern, 40)} in ${truncate(a.path ?? ".", 60)}`; case "find": return truncate(a.pattern, 40) + (a.path ? " in " + truncate(a.path, 60) : ""); case "ls": return truncate(a.path ?? ".", 90); default: { let s = ""; try { s = JSON.stringify(a); } catch { s = String(a); } return truncate(s, 100); } } } function textOf(message) { if (!message) return ""; if (typeof message.content === "string") return message.content; if (Array.isArray(message.content)) return message.content.filter((b) => b.type === "text").map((b) => b.text).join(""); return ""; } function wrap(text, indent, width = 100) { const pad = " ".repeat(indent); const lines = []; for (const raw of text.split("\n")) { if (!raw) { lines.push(""); continue; } let line = raw; while (line.length > width - indent) { let cut = line.lastIndexOf(" ", width - indent); if (cut < 20) cut = width - indent; lines.push(pad + line.slice(0, cut).trimEnd()); line = line.slice(cut).trimStart(); } lines.push(pad + line); } return lines.join("\n"); } let sessionId = ""; let lastUsage = null; let lastAssistantText = ""; let sawError = false; function handle(ev) { switch (ev.type) { case "session": sessionId = ev.id ?? ""; break; case "tool_execution_start": out(" " + c.cyan("⏺ " + ev.toolName) + " " + c.dim(summarizeTool(ev.toolName, ev.args))); break; case "tool_execution_end": if (ev.isError) out(" " + c.red("✗ " + ev.toolName + " failed")); break; case "message_update": if (ev.usage) lastUsage = ev.usage; break; case "message_end": { const m = ev.message ?? {}; if (m.role === "assistant") { const text = textOf(m); if (text) { lastAssistantText = text; out(wrap(text, 2)); } if (m.stopReason === "error") { sawError = true; out(c.red(" ✗ " + (m.errorMessage ?? "assistant error"))); } } break; } default: break; } } let buf = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { buf += chunk; let idx; while ((idx = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, idx).trim(); buf = buf.slice(idx + 1); if (!line) continue; let ev; try { ev = JSON.parse(line); } catch { out(c.yellow(" ? " + truncate(line, 120))); continue; } handle(ev); } }); process.stdin.on("end", () => { writeFileSync( reportPath, lastAssistantText ? lastAssistantText + "\n" : "(no final assistant message — see the .err log)\n", ); const u = lastUsage ?? {}; const bits = []; if (u.input) bits.push(`↑${u.input}`); if (u.output) bits.push(`↓${u.output}`); if (u.cacheRead) bits.push(`R${u.cacheRead}`); if (sessionId) bits.push("session " + String(sessionId).slice(0, 8)); if (bits.length) out(c.dim(" · " + bits.join(" "))); out(sawError ? c.red(" · child finished with errors") : c.green(" · child done")); });