Release 0.9.0: Cursor Composer 2 support and provider fixes

- Fix cursor-agent provider to detect Composer 2 JSONL sessions (#142)
- Bump version to 0.9.0
- Update changelog with all 0.9.0 changes
This commit is contained in:
AgentSeal 2026-04-24 20:24:49 +02:00
parent 0bebe6e5d0
commit ed7d76567b
3 changed files with 96 additions and 10 deletions

View file

@ -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

View file

@ -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",

View file

@ -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',
})
}
}
}
}