import { spawnSync } from "node:child_process"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { lookupContextTokens } from "../agents/context.js"; import { DEFAULT_CONTEXT_TOKENS, DEFAULT_MODEL } from "../agents/defaults.js"; import type { WarelayConfig } from "../config/config.js"; import type { SessionEntry, SessionScope } from "../config/sessions.js"; import type { ThinkLevel, VerboseLevel } from "./thinking.js"; type ReplyConfig = NonNullable["reply"]; type StatusArgs = { reply: ReplyConfig; sessionEntry?: SessionEntry; sessionKey?: string; sessionScope?: SessionScope; storePath?: string; resolvedThink?: ThinkLevel; resolvedVerbose?: VerboseLevel; now?: number; webLinked?: boolean; webAuthAgeMs?: number | null; heartbeatSeconds?: number; }; type AgentProbe = { ok: boolean; detail: string; label: string; }; const formatAge = (ms?: number | null) => { if (!ms || ms < 0) return "unknown"; const minutes = Math.round(ms / 60_000); if (minutes < 1) return "just now"; if (minutes < 60) return `${minutes}m ago`; const hours = Math.round(minutes / 60); if (hours < 48) return `${hours}h ago`; const days = Math.round(hours / 24); return `${days}d ago`; }; const formatKTokens = (value: number) => `${(value / 1000).toFixed(value >= 10_000 ? 0 : 1)}k`; const abbreviatePath = (p?: string) => { if (!p) return undefined; const home = os.homedir(); if (p.startsWith(home)) return p.replace(home, "~"); return p; }; const probeAgentCommand = (command?: string[]): AgentProbe => { const bin = command?.[0]; if (!bin) { return { ok: false, detail: "no command configured", label: "not set" }; } const commandLabel = command .slice(0, 3) .map((c) => c.replace(/\{\{[^}]+}}/g, "{…}")) .join(" ") .concat(command.length > 3 ? " …" : ""); const looksLikePath = bin.includes("/") || bin.startsWith("."); if (looksLikePath) { const exists = fs.existsSync(bin); return { ok: exists, detail: exists ? "binary found" : "binary missing", label: commandLabel || bin, }; } try { const res = spawnSync("which", [bin], { encoding: "utf-8", timeout: 1500, }); const found = res.status === 0 && res.stdout ? res.stdout.split("\n")[0]?.trim() : ""; return { ok: Boolean(found), detail: found || "not in PATH", label: commandLabel || bin, }; } catch (err) { return { ok: false, detail: `probe failed: ${String(err)}`, label: commandLabel || bin, }; } }; const formatTokens = ( total: number | null | undefined, contextTokens: number | null, ) => { const ctx = contextTokens ?? null; if (total == null) { const ctxLabel = ctx ? formatKTokens(ctx) : "?"; return `unknown/${ctxLabel}`; } const pct = ctx ? Math.min(999, Math.round((total / ctx) * 100)) : null; const totalLabel = formatKTokens(total); const ctxLabel = ctx ? formatKTokens(ctx) : "?"; return `${totalLabel}/${ctxLabel}${pct !== null ? ` (${pct}%)` : ""}`; }; const readUsageFromSessionLog = ( sessionId?: string, storePath?: string, ): | { input: number; output: number; total: number; model?: string; } | undefined => { // Prefer the coding-agent session log (pi-mono) if present. // Path resolution rules (priority): // 1) Store directory sibling file .jsonl // 2) PI coding agent dir: ~/.pi/agent/sessions/.jsonl if (!sessionId) return undefined; const candidatePaths: string[] = []; if (storePath) { const dir = path.dirname(storePath); candidatePaths.push(path.join(dir, `${sessionId}.jsonl`)); } const piDir = path.join(os.homedir(), ".pi", "agent", "sessions"); candidatePaths.push(path.join(piDir, `${sessionId}.jsonl`)); const logPath = candidatePaths.find((p) => fs.existsSync(p)); if (!logPath) return undefined; try { const lines = fs.readFileSync(logPath, "utf-8").split(/\n+/); let input = 0; let output = 0; let model: string | undefined; for (const line of lines) { if (!line.trim()) continue; try { const parsed = JSON.parse(line) as { message?: { usage?: { input?: number; output?: number; total?: number }; model?: string; }; usage?: { input?: number; output?: number; total?: number }; model?: string; }; const usage = parsed.message?.usage ?? parsed.usage; if (usage) { input += usage.input ?? 0; output += usage.output ?? 0; } model = parsed.message?.model ?? parsed.model ?? model; } catch { // ignore bad lines } } const total = input + output; if (total === 0) return undefined; return { input, output, total, model }; } catch { return undefined; } }; export function buildStatusMessage(args: StatusArgs): string { const now = args.now ?? Date.now(); const entry = args.sessionEntry; let model = entry?.model ?? args.reply?.agent?.model ?? DEFAULT_MODEL; let contextTokens = entry?.contextTokens ?? args.reply?.agent?.contextTokens ?? lookupContextTokens(model) ?? DEFAULT_CONTEXT_TOKENS; let totalTokens = entry?.totalTokens ?? (entry?.inputTokens ?? 0) + (entry?.outputTokens ?? 0); // Fallback: derive usage from the session transcript if the store lacks it if (!totalTokens || totalTokens === 0) { const logUsage = readUsageFromSessionLog(entry?.sessionId, args.storePath); if (logUsage) { totalTokens = logUsage.total; if (!model) model = logUsage.model ?? model; if (!contextTokens && logUsage.model) { contextTokens = lookupContextTokens(logUsage.model) ?? contextTokens; } } } const agentProbe = probeAgentCommand(args.reply?.command); const thinkLevel = args.resolvedThink ?? args.reply?.thinkingDefault ?? "auto"; const verboseLevel = args.resolvedVerbose ?? args.reply?.verboseDefault ?? "off"; const webLine = (() => { if (args.webLinked === false) { return "Web: not linked — run `clawdis login` to scan the QR."; } const authAge = formatAge(args.webAuthAgeMs); const heartbeat = typeof args.heartbeatSeconds === "number" ? ` • heartbeat ${args.heartbeatSeconds}s` : ""; return `Web: linked • auth refreshed ${authAge}${heartbeat}`; })(); const sessionLine = [ `Session: ${args.sessionKey ?? "unknown"}`, `scope ${args.sessionScope ?? "per-sender"}`, entry?.updatedAt ? `updated ${formatAge(now - entry.updatedAt)}` : "no activity", args.storePath ? `store ${abbreviatePath(args.storePath)}` : undefined, ] .filter(Boolean) .join(" • "); const contextLine = `Context: ${formatTokens( totalTokens, contextTokens ?? null, )}${entry?.abortedLastRun ? " • last run aborted" : ""}`; const optionsLine = `Options: thinking=${thinkLevel} | verbose=${verboseLevel} (set with /think , /verbose on|off)`; const agentLine = `Agent: ${agentProbe.ok ? "ready" : "check"} — ${agentProbe.label}${agentProbe.detail ? ` (${agentProbe.detail})` : ""}${model ? ` • model ${model}` : ""}`; const helpersLine = "Shortcuts: /new reset | /restart relink"; return [ "⚙️ Status", webLine, agentLine, contextLine, sessionLine, optionsLine, helpersLine, ].join("\n"); }