import type { UsageSummary } from "../types.js"; import { asString, asNumber, parseObject, parseJson } from "../utils.js"; export function parseClaudeStreamJson(stdout: string) { let sessionId: string | null = null; let model = ""; let finalResult: Record | null = null; const assistantTexts: string[] = []; for (const rawLine of stdout.split(/\r?\n/)) { const line = rawLine.trim(); if (!line) continue; const event = parseJson(line); if (!event) continue; const type = asString(event.type, ""); if (type === "system" && asString(event.subtype, "") === "init") { sessionId = asString(event.session_id, sessionId ?? "") || sessionId; model = asString(event.model, model); continue; } if (type === "assistant") { sessionId = asString(event.session_id, sessionId ?? "") || sessionId; const message = parseObject(event.message); const content = Array.isArray(message.content) ? message.content : []; for (const entry of content) { if (typeof entry !== "object" || entry === null || Array.isArray(entry)) continue; const block = entry as Record; if (asString(block.type, "") === "text") { const text = asString(block.text, ""); if (text) assistantTexts.push(text); } } continue; } if (type === "result") { finalResult = event; sessionId = asString(event.session_id, sessionId ?? "") || sessionId; } } if (!finalResult) { return { sessionId, model, costUsd: null as number | null, usage: null as UsageSummary | null, summary: assistantTexts.join("\n\n").trim(), resultJson: null as Record | null, }; } const usageObj = parseObject(finalResult.usage); const usage: UsageSummary = { inputTokens: asNumber(usageObj.input_tokens, 0), cachedInputTokens: asNumber(usageObj.cache_read_input_tokens, 0), outputTokens: asNumber(usageObj.output_tokens, 0), }; const costRaw = finalResult.total_cost_usd; const costUsd = typeof costRaw === "number" && Number.isFinite(costRaw) ? costRaw : null; const summary = asString(finalResult.result, assistantTexts.join("\n\n")).trim(); return { sessionId, model, costUsd, usage, summary, resultJson: finalResult, }; } function extractClaudeErrorMessages(parsed: Record): string[] { const raw = Array.isArray(parsed.errors) ? parsed.errors : []; const messages: string[] = []; for (const entry of raw) { if (typeof entry === "string") { const msg = entry.trim(); if (msg) messages.push(msg); continue; } if (typeof entry !== "object" || entry === null || Array.isArray(entry)) { continue; } const obj = entry as Record; const msg = asString(obj.message, "") || asString(obj.error, "") || asString(obj.code, ""); if (msg) { messages.push(msg); continue; } try { messages.push(JSON.stringify(obj)); } catch { // skip non-serializable entry } } return messages; } export function describeClaudeFailure(parsed: Record): string | null { const subtype = asString(parsed.subtype, ""); const resultText = asString(parsed.result, "").trim(); const errors = extractClaudeErrorMessages(parsed); let detail = resultText; if (!detail && errors.length > 0) { detail = errors[0] ?? ""; } const parts = ["Claude run failed"]; if (subtype) parts.push(`subtype=${subtype}`); if (detail) parts.push(detail); return parts.length > 1 ? parts.join(": ") : null; } export function isClaudeUnknownSessionError(parsed: Record): boolean { const resultText = asString(parsed.result, "").trim(); const allMessages = [resultText, ...extractClaudeErrorMessages(parsed)] .map((msg) => msg.trim()) .filter(Boolean); return allMessages.some((msg) => /no conversation found with session id|unknown session|session .* not found/i.test(msg), ); }