mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-05-17 03:56:45 +00:00
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:
parent
0bebe6e5d0
commit
ed7d76567b
3 changed files with 96 additions and 10 deletions
16
CHANGELOG.md
16
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/<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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue