import type { TranscriptEntry } from "@paperclipai/adapter-utils"; import { normalizeCursorStreamLine } from "../shared/stream.js"; function safeJsonParse(text: string): unknown { try { return JSON.parse(text); } catch { return null; } } function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value === null || Array.isArray(value)) return null; return value as Record; } function asString(value: unknown, fallback = ""): string { return typeof value === "string" ? value : fallback; } function asNumber(value: unknown, fallback = 0): number { return typeof value === "number" && Number.isFinite(value) ? value : fallback; } function stringifyUnknown(value: unknown): string { if (typeof value === "string") return value; if (value === null || value === undefined) return ""; try { return JSON.stringify(value, null, 2); } catch { return String(value); } } /** Max chars of stdout/stderr to show in run log for shell tool results. */ const SHELL_OUTPUT_TRUNCATE = 2000; /** * Format shell tool result for run log: exit code + stdout/stderr (truncated). * If the result is not a shell-shaped object, returns full stringify. */ function formatShellToolResultForLog(result: unknown): string { const obj = asRecord(result); if (!obj) return stringifyUnknown(result); const success = asRecord(obj.success); if (!success) return stringifyUnknown(result); const exitCode = asNumber(success.exitCode, NaN); const stdout = asString(success.stdout).trim(); const stderr = asString(success.stderr).trim(); const hasShellShape = Number.isFinite(exitCode) || stdout.length > 0 || stderr.length > 0; if (!hasShellShape) return stringifyUnknown(result); const lines: string[] = []; if (Number.isFinite(exitCode)) lines.push(`exit ${exitCode}`); if (stdout) { const out = stdout.length > SHELL_OUTPUT_TRUNCATE ? stdout.slice(0, SHELL_OUTPUT_TRUNCATE) + "\n... (truncated)" : stdout; lines.push(""); lines.push(out); } if (stderr) { const err = stderr.length > SHELL_OUTPUT_TRUNCATE ? stderr.slice(0, SHELL_OUTPUT_TRUNCATE) + "\n... (truncated)" : stderr; lines.push(""); lines.push(err); } return lines.join("\n"); } /** Return compact input for run log when tool is shell/shellToolCall (command only). */ function compactShellToolInput(rawInput: unknown, payload?: Record): unknown { const cmd = asString(payload?.command ?? asRecord(rawInput)?.command); if (cmd) return { command: cmd }; return rawInput; } function parseUserMessage(messageRaw: unknown, ts: string): TranscriptEntry[] { if (typeof messageRaw === "string") { const text = messageRaw.trim(); return text ? [{ kind: "user", ts, text }] : []; } const message = asRecord(messageRaw); if (!message) return []; const entries: TranscriptEntry[] = []; const directText = asString(message.text).trim(); if (directText) entries.push({ kind: "user", ts, text: directText }); const content = Array.isArray(message.content) ? message.content : []; for (const partRaw of content) { const part = asRecord(partRaw); if (!part) continue; const type = asString(part.type).trim(); if (type !== "output_text" && type !== "text") continue; const text = asString(part.text).trim(); if (text) entries.push({ kind: "user", ts, text }); } return entries; } function parseAssistantMessage(messageRaw: unknown, ts: string): TranscriptEntry[] { if (typeof messageRaw === "string") { const text = messageRaw.trim(); return text ? [{ kind: "assistant", ts, text }] : []; } const message = asRecord(messageRaw); if (!message) return []; const entries: TranscriptEntry[] = []; const directText = asString(message.text).trim(); if (directText) { entries.push({ kind: "assistant", ts, text: directText }); } const content = Array.isArray(message.content) ? message.content : []; for (const partRaw of content) { const part = asRecord(partRaw); if (!part) continue; const type = asString(part.type).trim(); if (type === "output_text" || type === "text") { const text = asString(part.text).trim(); if (text) entries.push({ kind: "assistant", ts, text }); continue; } if (type === "thinking") { const text = asString(part.text).trim(); if (text) entries.push({ kind: "thinking", ts, text }); continue; } if (type === "tool_call") { const name = asString(part.name, asString(part.tool, "tool")); const rawInput = part.input ?? part.arguments ?? part.args ?? {}; const input = name === "shellToolCall" || name === "shell" ? compactShellToolInput(rawInput, asRecord(rawInput) ?? undefined) : rawInput; entries.push({ kind: "tool_call", ts, name, toolUseId: asString(part.tool_use_id) || asString(part.toolUseId) || asString(part.call_id) || asString(part.id) || undefined, input, }); continue; } if (type === "tool_result") { const toolUseId = asString(part.tool_use_id) || asString(part.toolUseId) || asString(part.call_id) || asString(part.id) || "tool_result"; const rawOutput = part.output ?? part.result ?? part.text; const contentText = typeof rawOutput === "object" && rawOutput !== null ? formatShellToolResultForLog(rawOutput) : asString(rawOutput) || stringifyUnknown(rawOutput); const isError = part.is_error === true || asString(part.status).toLowerCase() === "error"; entries.push({ kind: "tool_result", ts, toolUseId, content: contentText, isError, }); } } return entries; } function parseCursorToolCallEvent(event: Record, ts: string): TranscriptEntry[] { const subtype = asString(event.subtype).trim().toLowerCase(); const callId = asString(event.call_id) || asString(event.callId) || asString(event.id) || "tool_call"; const toolCall = asRecord(event.tool_call ?? event.toolCall); if (!toolCall) { return [{ kind: "system", ts, text: `tool_call${subtype ? ` (${subtype})` : ""}` }]; } const [toolName] = Object.keys(toolCall); if (!toolName) { return [{ kind: "system", ts, text: `tool_call${subtype ? ` (${subtype})` : ""}` }]; } const payload = asRecord(toolCall[toolName]) ?? {}; const rawInput = payload.args ?? asRecord(payload.function)?.arguments ?? payload; const isShellTool = toolName === "shellToolCall" || toolName === "shell"; const input = isShellTool ? compactShellToolInput(rawInput, payload) : rawInput; if (subtype === "started" || subtype === "start") { return [{ kind: "tool_call", ts, name: toolName, toolUseId: callId, input, }]; } if (subtype === "completed" || subtype === "complete" || subtype === "finished") { const result = payload.result ?? payload.output ?? payload.error ?? asRecord(payload.function)?.result ?? asRecord(payload.function)?.output; const isError = event.is_error === true || payload.is_error === true || asString(payload.status).toLowerCase() === "error" || asString(payload.status).toLowerCase() === "failed" || asString(payload.status).toLowerCase() === "cancelled" || payload.error !== undefined; const content = result !== undefined ? isShellTool ? formatShellToolResultForLog(result) : stringifyUnknown(result) : `${toolName} completed`; return [{ kind: "tool_result", ts, toolUseId: callId, content, isError, }]; } return [{ kind: "system", ts, text: `tool_call${subtype ? ` (${subtype})` : ""}: ${toolName}`, }]; } export function parseCursorStdoutLine(line: string, ts: string): TranscriptEntry[] { const normalized = normalizeCursorStreamLine(line); if (!normalized.line) return []; const parsed = asRecord(safeJsonParse(normalized.line)); if (!parsed) { return [{ kind: "stdout", ts, text: normalized.line }]; } const type = asString(parsed.type); if (type === "system") { const subtype = asString(parsed.subtype); if (subtype === "init") { const sessionId = asString(parsed.session_id) || asString(parsed.sessionId) || asString(parsed.sessionID); return [{ kind: "init", ts, model: asString(parsed.model, "cursor"), sessionId }]; } return [{ kind: "system", ts, text: subtype ? `system: ${subtype}` : "system" }]; } if (type === "assistant") { const entries = parseAssistantMessage(parsed.message, ts); return entries.length > 0 ? entries : [{ kind: "assistant", ts, text: asString(parsed.result) }]; } if (type === "user") { return parseUserMessage(parsed.message, ts); } if (type === "thinking") { const textFromTopLevel = asString(parsed.text); const textFromDelta = asString(asRecord(parsed.delta)?.text); const text = textFromTopLevel.length > 0 ? textFromTopLevel : textFromDelta; const subtype = asString(parsed.subtype).trim().toLowerCase(); const isDelta = subtype === "delta" || asRecord(parsed.delta) !== null; if (!text.trim()) return []; return [{ kind: "thinking", ts, text: isDelta ? text : text.trim(), ...(isDelta ? { delta: true } : {}) }]; } if (type === "tool_call") { return parseCursorToolCallEvent(parsed, ts); } if (type === "result") { const usage = asRecord(parsed.usage); const inputTokens = asNumber(usage?.input_tokens, asNumber(usage?.inputTokens)); const outputTokens = asNumber(usage?.output_tokens, asNumber(usage?.outputTokens)); const cachedTokens = asNumber( usage?.cached_input_tokens, asNumber(usage?.cachedInputTokens, asNumber(usage?.cache_read_input_tokens)), ); const subtype = asString(parsed.subtype, "result"); const errors = Array.isArray(parsed.errors) ? parsed.errors.map((value) => stringifyUnknown(value)).filter(Boolean) : []; const errorText = asString(parsed.error).trim(); if (errorText) errors.push(errorText); const isError = parsed.is_error === true || subtype === "error" || subtype === "failed"; return [{ kind: "result", ts, text: asString(parsed.result), inputTokens, outputTokens, cachedTokens, costUsd: asNumber(parsed.total_cost_usd, asNumber(parsed.cost_usd, asNumber(parsed.cost))), subtype, isError, errors, }]; } if (type === "error") { const message = asString(parsed.message) || stringifyUnknown(parsed.error ?? parsed.detail) || normalized.line; return [{ kind: "stderr", ts, text: message }]; } // Compatibility with older stream-json event shapes. if (type === "step_start") { const sessionId = asString(parsed.sessionID); return [{ kind: "system", ts, text: `step started${sessionId ? ` (${sessionId})` : ""}` }]; } if (type === "text") { const part = asRecord(parsed.part); const text = asString(part?.text).trim(); if (!text) return []; return [{ kind: "assistant", ts, text }]; } if (type === "tool_use") { const part = asRecord(parsed.part); const toolUseId = asString(part?.callID, asString(part?.id, "tool_use")); const toolName = asString(part?.tool, "tool"); const state = asRecord(part?.state); const input = state?.input ?? {}; const output = asString(state?.output).trim(); const status = asString(state?.status).trim(); const exitCode = asNumber(asRecord(state?.metadata)?.exit, NaN); const isError = status === "failed" || status === "error" || status === "cancelled" || (Number.isFinite(exitCode) && exitCode !== 0); const entries: TranscriptEntry[] = [ { kind: "tool_call", ts, name: toolName, input, }, ]; if (status || output) { const lines: string[] = []; if (status) lines.push(`status: ${status}`); if (Number.isFinite(exitCode)) lines.push(`exit: ${exitCode}`); if (output) { if (lines.length > 0) lines.push(""); lines.push(output); } entries.push({ kind: "tool_result", ts, toolUseId, content: lines.join("\n").trim() || "tool completed", isError, }); } return entries; } if (type === "step_finish") { const part = asRecord(parsed.part); const tokens = asRecord(part?.tokens); const cache = asRecord(tokens?.cache); const reason = asString(part?.reason); return [{ kind: "result", ts, text: reason, inputTokens: asNumber(tokens?.input), outputTokens: asNumber(tokens?.output), cachedTokens: asNumber(cache?.read), costUsd: asNumber(part?.cost), subtype: reason || "step_finish", isError: reason === "error" || reason === "failed", errors: [], }]; } return [{ kind: "stdout", ts, text: normalized.line }]; }