Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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/<UUID>/<UUID>.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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
88 changes: 79 additions & 9 deletions src/providers/cursor-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`)
Expand Down Expand Up @@ -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',
})
}
}
}
}

Expand Down
Loading