import { asString, asNumber, parseObject, parseJson } from "@paperclipai/adapter-utils/server-utils"; import { normalizeCursorStreamLine } from "../shared/stream.js"; function asErrorText(value: unknown): string { if (typeof value === "string") return value; const rec = parseObject(value); const message = asString(rec.message, "") || asString(rec.error, "") || asString(rec.code, "") || asString(rec.detail, ""); if (message) return message; try { return JSON.stringify(rec); } catch { return ""; } } function collectAssistantText(message: unknown): string[] { if (typeof message === "string") { const trimmed = message.trim(); return trimmed ? [trimmed] : []; } const rec = parseObject(message); const direct = asString(rec.text, "").trim(); const lines: string[] = direct ? [direct] : []; const content = Array.isArray(rec.content) ? rec.content : []; for (const partRaw of content) { const part = parseObject(partRaw); const type = asString(part.type, "").trim(); if (type === "output_text" || type === "text") { const text = asString(part.text, "").trim(); if (text) lines.push(text); } } return lines; } function readSessionId(event: Record): string | null { return ( asString(event.session_id, "").trim() || asString(event.sessionId, "").trim() || asString(event.sessionID, "").trim() || null ); } export function parseCursorJsonl(stdout: string) { let sessionId: string | null = null; const messages: string[] = []; let errorMessage: string | null = null; let totalCostUsd = 0; const usage = { inputTokens: 0, cachedInputTokens: 0, outputTokens: 0, }; for (const rawLine of stdout.split(/\r?\n/)) { const line = normalizeCursorStreamLine(rawLine).line; if (!line) continue; const event = parseJson(line); if (!event) continue; const foundSession = readSessionId(event); if (foundSession) sessionId = foundSession; const type = asString(event.type, "").trim(); if (type === "assistant") { messages.push(...collectAssistantText(event.message)); continue; } if (type === "result") { const usageObj = parseObject(event.usage); usage.inputTokens += asNumber( usageObj.input_tokens, asNumber(usageObj.inputTokens, 0), ); usage.cachedInputTokens += asNumber( usageObj.cached_input_tokens, asNumber(usageObj.cachedInputTokens, asNumber(usageObj.cache_read_input_tokens, 0)), ); usage.outputTokens += asNumber( usageObj.output_tokens, asNumber(usageObj.outputTokens, 0), ); totalCostUsd += asNumber(event.total_cost_usd, asNumber(event.cost_usd, asNumber(event.cost, 0))); const isError = event.is_error === true || asString(event.subtype, "").toLowerCase() === "error"; const resultText = asString(event.result, "").trim(); if (resultText && messages.length === 0) { messages.push(resultText); } if (isError) { const resultError = asErrorText(event.error ?? event.message ?? event.result).trim(); if (resultError) errorMessage = resultError; } continue; } if (type === "error") { const message = asErrorText(event.message ?? event.error ?? event.detail).trim(); if (message) errorMessage = message; continue; } if (type === "system") { const subtype = asString(event.subtype, "").trim().toLowerCase(); if (subtype === "error") { const message = asErrorText(event.message ?? event.error ?? event.detail).trim(); if (message) errorMessage = message; } continue; } // Compatibility with older stream-json shapes. if (type === "text") { const part = parseObject(event.part); const text = asString(part.text, "").trim(); if (text) messages.push(text); continue; } if (type === "step_finish") { const part = parseObject(event.part); const tokens = parseObject(part.tokens); const cache = parseObject(tokens.cache); usage.inputTokens += asNumber(tokens.input, 0); usage.cachedInputTokens += asNumber(cache.read, 0); usage.outputTokens += asNumber(tokens.output, 0); totalCostUsd += asNumber(part.cost, 0); continue; } } return { sessionId, summary: messages.join("\n\n").trim(), usage, costUsd: totalCostUsd > 0 ? totalCostUsd : null, errorMessage, }; } export function isCursorUnknownSessionError(stdout: string, stderr: string): boolean { const haystack = `${stdout}\n${stderr}` .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean) .join("\n"); return /unknown\s+(session|chat)|session\s+.*\s+not\s+found|chat\s+.*\s+not\s+found|resume\s+.*\s+not\s+found|could\s+not\s+resume/i.test( haystack, ); }