import { readdirSync, readFileSync, statSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; import { collectRecentLogEvents, formatMergedLogEvent, getProjectSessionKey, } from "./cli-logs.js"; import type { QuerySnapshot } from "./headless-query.js"; interface StatusArgs { watch: boolean; } interface StatusDeps { basePath: string; sfHome?: string; stdout?: Pick; stderr?: Pick; } interface CurrentModel { provider: string; model: string; } function parseStatusArgs(argv: string[]): StatusArgs { const args = argv.slice(1); return { watch: args.includes("--watch"), }; } function formatRef( ref: { id: string; title?: string } | string | null | undefined, ): string { if (!ref) return "n/a"; if (typeof ref === "string") return ref; return ref.title ? `${ref.id} ${ref.title}` : ref.id; } function formatDispatch(snapshot: QuerySnapshot): string { const next = snapshot.next; if (next.action === "dispatch") { return `${next.unitType ?? "unit"} ${next.unitId ?? "n/a"}`; } if (next.action === "stop") return `stop: ${next.reason ?? snapshot.state.nextAction}`; return `skip: ${next.reason ?? snapshot.state.nextAction}`; } function formatCost(snapshot: QuerySnapshot): string { const total = snapshot.cost.total; const workers = snapshot.cost.workers.length; return `$${total.toFixed(4)}${workers > 0 ? ` (${workers} worker${workers === 1 ? "" : "s"})` : ""}`; } function readSolverStatus(basePath: string): string | null { let state: Record; try { state = JSON.parse( readFileSync( join(basePath, ".sf", "runtime", "autonomous-solver", "active.json"), "utf-8", ), ); } catch { return null; } const checkpoint = state.latestCheckpoint ?? {}; const parts = [ `${state.unitType ?? "unit"} ${state.unitId ?? "n/a"}`, `iter ${state.iteration ?? "?"}/${state.maxIterations ?? "?"}`, `outcome ${checkpoint.outcome ?? "none"}`, ]; const remaining = Array.isArray(checkpoint.remainingItems) ? checkpoint.remainingItems.length : null; if (remaining !== null) parts.push(`${remaining} remaining`); if (checkpoint.blockerReason) parts.push(`blocker: ${checkpoint.blockerReason}`); if (checkpoint.decisionQuestion) parts.push(`decision: ${checkpoint.decisionQuestion}`); if (checkpoint.summary) parts.push(String(checkpoint.summary)); return parts.join(" ยท "); } function latestJsonlFile(dir: string): string | null { try { const entries = readdirSync(dir) .filter((file) => file.endsWith(".jsonl")) .map((file) => { const path = join(dir, file); try { return { path, mtimeMs: statSync(path).mtimeMs }; } catch { return null; } }) .filter( (entry): entry is { path: string; mtimeMs: number } => entry !== null, ); entries.sort((a, b) => b.mtimeMs - a.mtimeMs); return entries[0]?.path ?? null; } catch { return null; } } function extractCurrentModelFromSession(filePath: string): CurrentModel | null { let current: CurrentModel | null = null; let raw: string; try { raw = readFileSync(filePath, "utf-8"); } catch { return null; } for (const line of raw.split(/\n/)) { if (!line.trim()) continue; let entry: Record; try { entry = JSON.parse(line) as Record; } catch { continue; } if (entry.type === "model_change") { const provider = String(entry.provider ?? ""); const model = String(entry.modelId ?? ""); if (provider || model) current = { provider, model }; continue; } if (entry.type === "message") { const message = entry.message as Record | undefined; if (message?.role === "assistant") { const provider = String(message.provider ?? ""); const model = String(message.model ?? ""); if (provider || model) current = { provider, model }; } } } return current; } function getCurrentModel( basePath: string, sfHome: string, ): CurrentModel | null { const key = getProjectSessionKey(basePath); const sessionFile = latestJsonlFile(join(sfHome, "agent", "sessions", key)) ?? latestJsonlFile(join(sfHome, "sessions", key)); return sessionFile ? extractCurrentModelFromSession(sessionFile) : null; } function formatModel(model: CurrentModel | null): string { if (!model) return "n/a"; if (model.provider && model.model) return `${model.provider}/${model.model}`; return model.model || model.provider || "n/a"; } export function renderLiveStatus( snapshot: QuerySnapshot, opts: { basePath?: string; model: CurrentModel | null; recentEvents: string[]; }, ): string { const lines: string[] = []; lines.push("SF Status"); lines.push("---------"); lines.push( `Milestone: ${formatRef(snapshot.state.activeMilestone ?? snapshot.state.lastCompletedMilestone)}`, ); lines.push(`Slice: ${formatRef(snapshot.state.activeSlice)}`); lines.push(`Task: ${formatRef(snapshot.state.activeTask)}`); lines.push(`Phase: ${snapshot.state.phase}`); lines.push(`Dispatch: ${formatDispatch(snapshot)}`); lines.push(`Cost: ${formatCost(snapshot)}`); lines.push(`Model: ${formatModel(opts.model)}`); const solverStatus = opts.basePath ? readSolverStatus(opts.basePath) : null; if (solverStatus) lines.push(`Solver: ${solverStatus}`); lines.push(""); lines.push("Last Events:"); if (opts.recentEvents.length === 0) { lines.push(" n/a"); } else { for (const event of opts.recentEvents) { lines.push(` ${event.trimEnd()}`); } } return lines.join("\n") + "\n"; } async function buildStatusText( basePath: string, sfHome: string, ): Promise { const { buildQuerySnapshot } = await import("./headless-query.js"); const snapshot = await buildQuerySnapshot(basePath); const recentEvents = collectRecentLogEvents({ basePath, sfHome, limit: 50, }) .filter((event) => event.source === "notif" || event.source === "activity") .slice(-10) .map(formatMergedLogEvent); return renderLiveStatus(snapshot, { basePath, model: getCurrentModel(basePath, sfHome), recentEvents, }); } export async function runStatusCli( argv: string[], deps: StatusDeps, ): Promise { const stdout = deps.stdout ?? process.stdout; const stderr = deps.stderr ?? process.stderr; const sfHome = deps.sfHome ?? process.env.SF_HOME ?? join(homedir(), ".sf"); const args = parseStatusArgs(argv); const renderOnce = async () => { try { const text = await buildStatusText(deps.basePath, sfHome); if (args.watch && stdout.isTTY) stdout.write("\x1b[2J\x1b[H"); stdout.write(text); } catch (err) { stderr.write( `sf status: ${err instanceof Error ? err.message : String(err)}\n`, ); throw err; } }; if (!args.watch) { await renderOnce(); return 0; } await renderOnce(); await new Promise((resolve) => { const timer = setInterval(() => { renderOnce().catch(() => { clearInterval(timer); resolve(); }); }, 2000); const stop = () => { clearInterval(timer); process.off("SIGINT", stop); process.off("SIGTERM", stop); resolve(); }; process.on("SIGINT", stop); process.on("SIGTERM", stop); }); return 0; }