diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ee51a7..b25ede6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.9.0 - 2026-04-24 + +### Added (CLI) +- **Claude Max 5x plan preset.** `codeburn plan claude-max-5x` sets a $100/month budget for heavy Claude Code users. + +### Fixed (CLI) +- **Cursor provider failed on newer versions.** Cursor 0.50+ stores session data in `agentKv:blob:*` entries instead of `bubbleId:*`. Added fallback parser that extracts usage from the new format. +- **Cursor-agent provider missed Composer 2 sessions.** Composer 2 stores transcripts in `agent-transcripts//.jsonl` subdirectories instead of `.txt` files. Now scans both formats. Fixes #142. +- **Codex showed wrong model names.** Model info is now extracted from `turn_context` entries, showing exact names like "GPT-5.4" instead of generic "GPT-5". +- **Codex edit detection showed 0 edit turns.** Codex records file modifications as `patch_apply_end` events, not tool calls. Now tracks these events to enable one-shot rate and retry metrics. +- **Compare chart bar colors didn't match legend.** Non-winning model bars were grayed out despite the legend showing both colors. Bars now always display their assigned colors. + +### Fixed (macOS menubar) +- **Menubar icon invisible on macOS Tahoe (26.x).** Status item failed to render on macOS 26.4+ due to window server registration timing. Fixed by starting as regular app, activating, then switching to accessory mode after setup. Fixes #146. +- **High CPU usage (~14%).** Removed duplicate refresh timer, increased LaunchAgent interval to 30s, added 5-second debounce on wake events. + ## 0.8.9 - 2026-04-22 ### Fixed diff --git a/package.json b/package.json index 1c75335..6129e7f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeburn", - "version": "0.8.9", + "version": "0.9.0", "description": "See where your AI coding tokens go - by task, tool, model, and project", "type": "module", "main": "./dist/cli.js", diff --git a/src/providers/cursor-agent.ts b/src/providers/cursor-agent.ts index b0467a0..2cadb0a 100644 --- a/src/providers/cursor-agent.ts +++ b/src/providers/cursor-agent.ts @@ -160,6 +160,58 @@ function extractUserQuery(userBlock: string): string { return combined.slice(0, MAX_USER_TEXT_LENGTH) } +function parseJsonlTranscript(raw: string): { turns: ParsedTurn[]; recognized: boolean } { + const lines = raw.split(/\r?\n/).filter(l => l.trim()) + if (lines.length === 0) return { turns: [], recognized: false } + + const turns: ParsedTurn[] = [] + let currentUserMessage = '' + + for (const line of lines) { + let entry: { role?: string; message?: { content?: Array<{ type?: string; text?: string; name?: string }> } } + try { + entry = JSON.parse(line) + } catch { + continue + } + + if (entry.role === 'user') { + const texts = (entry.message?.content ?? []) + .filter(c => c.type === 'text') + .map(c => c.text ?? '') + const combined = texts.join(' ') + currentUserMessage = extractUserQuery(combined) || combined.slice(0, MAX_USER_TEXT_LENGTH) + continue + } + + if (entry.role === 'assistant' && currentUserMessage) { + const content = entry.message?.content ?? [] + const bodyParts: string[] = [] + const tools: string[] = [] + + for (const block of content) { + if (block.type === 'text' && block.text) { + bodyParts.push(block.text) + } else if (block.type === 'tool_use' && block.name) { + tools.push(`cursor:${block.name.toLowerCase()}`) + } + } + + turns.push({ + userMessage: currentUserMessage, + assistant: { + body: bodyParts.join('\n').trim(), + reasoning: '', + tools, + }, + }) + currentUserMessage = '' + } + } + + return { turns, recognized: turns.length > 0 } +} + function parseTranscript(raw: string): { turns: ParsedTurn[]; recognized: boolean } { const lines = raw.split(/\r?\n/) let recognized = false @@ -299,7 +351,8 @@ function createParser( } const transcript = await readFile(source.path, 'utf-8') - const parsed = parseTranscript(transcript) + const isJsonl = source.path.endsWith('.jsonl') + const parsed = isJsonl ? parseJsonlTranscript(transcript) : parseTranscript(transcript) if (!parsed.recognized) { process.stderr.write(`codeburn: skipped ${basename(source.path)}: unrecognized cursor-agent transcript format\n`) @@ -395,15 +448,32 @@ export function createCursorAgentProvider(baseDirOverride?: string): Provider { const transcriptEntries = await readdir(transcriptDir, { withFileTypes: true }) for (const transcript of transcriptEntries) { - if (!transcript.isFile()) continue - if (!transcript.name.endsWith('.txt')) continue + // Legacy format: .txt files directly in agent-transcripts/ + if (transcript.isFile() && transcript.name.endsWith('.txt')) { + const transcriptPath = join(transcriptDir, transcript.name) + sources.push({ + path: transcriptPath, + project: projectId, + provider: 'cursor-agent', + }) + continue + } - const transcriptPath = join(transcriptDir, transcript.name) - sources.push({ - path: transcriptPath, - project: projectId, - provider: 'cursor-agent', - }) + // Composer 2 format: UUID subdirectories with .jsonl files + if (transcript.isDirectory() && UUID_LIKE.test(transcript.name)) { + const subdir = join(transcriptDir, transcript.name) + const subEntries = await readdir(subdir, { withFileTypes: true }).catch(() => []) + for (const sub of subEntries) { + if (!sub.isFile()) continue + if (!sub.name.endsWith('.jsonl') && !sub.name.endsWith('.txt')) continue + const filePath = join(subdir, sub.name) + sources.push({ + path: filePath, + project: projectId, + provider: 'cursor-agent', + }) + } + } } }