Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## Unreleased

### Added (CLI)
- **VSCodium storage discovery for VS Code-family providers.** GitHub
Copilot, Roo Code, and KiloCode now scan VSCodium storage roots in addition
to VS Code and VS Code Insiders, so usage created from VSCodium is included
without a custom override path.
- **Multiple subscription plans can be tracked at the same time.**
`codeburn plan set` now stores plans in a provider-keyed `plans` map, so
setting a Codex custom plan no longer overwrites an existing Claude plan.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,11 @@ The `--provider` flag filters any command to a single provider: `codeburn report

**Kiro** stores conversations as `.chat` JSON files. Token counts are estimated from content length. The underlying model is not exposed, so sessions are labeled `kiro-auto` and costed at Sonnet rates.

**GitHub Copilot** reads from both `~/.copilot/session-state/` (legacy CLI) and VS Code's `workspaceStorage/*/GitHub.copilot-chat/transcripts/`. The VS Code format has no explicit token counts; tokens are estimated from content length and the model is inferred from tool call ID prefixes.
**GitHub Copilot** reads from both `~/.copilot/session-state/` (legacy CLI) and VS Code/VSCodium `workspaceStorage/*/GitHub.copilot-chat/transcripts/`. The editor transcript format has no explicit token counts; tokens are estimated from content length and the model is inferred from tool call ID prefixes.

**OpenClaw** reads JSONL agent logs from `~/.openclaw/agents/` and also checks legacy paths (`.clawdbot`, `.moltbot`, `.moldbot`).

**Roo Code and KiloCode** are Cline-family VS Code extensions. CodeBurn reads `ui_messages.json` from each task directory and extracts token usage from `api_req_started` entries.
**Roo Code and KiloCode** are Cline-family VS Code/VSCodium extensions. CodeBurn reads `ui_messages.json` from each task directory and extracts token usage from `api_req_started` entries.

**Claude with multiple config directories.** If you run Claude Code under more than one account or profile (e.g. `~/.claude-work` and `~/.claude-personal`), point `CLAUDE_CONFIG_DIRS` at all of them at once: `CLAUDE_CONFIG_DIRS=~/.claude-work:~/.claude-personal codeburn`. Sessions across every directory are merged into one row per project so the totals reflect all your Claude usage in one place. Use `:` on POSIX, `;` on Windows. Missing or unreadable directories in the list are skipped.

Expand Down Expand Up @@ -389,7 +389,7 @@ These are starting points, not verdicts. A 60% cache hit on a single experimenta

**OpenClaw** stores agent sessions as JSONL at `~/.openclaw/agents/*.jsonl`. Also checks legacy paths `.clawdbot`, `.moltbot`, `.moldbot`. Token usage comes from assistant message `usage` blocks; model from `modelId` or `message.model` fields.

**Cline / Roo Code / KiloCode** are Cline-family coding agents. CodeBurn reads `ui_messages.json` from each task directory, filtering `type: "say"` entries with `say: "api_req_started"` to extract token counts. Cline scans both VS Code's `globalStorage/saoudrizwan.claude-dev` and `~/.cline/data`.
**Cline / Roo Code / KiloCode** are Cline-family coding agents. CodeBurn reads `ui_messages.json` from each task directory, filtering `type: "say"` entries with `say: "api_req_started"` to extract token counts. Cline scans both VS Code's `globalStorage/saoudrizwan.claude-dev` and `~/.cline/data`; Roo Code and KiloCode scan VS Code, VS Code Insiders, and VSCodium `globalStorage` roots.

**IBM Bob** stores IDE task history in `User/globalStorage/ibm.bob-code/tasks/<task-id>/` under the IBM Bob application data directory. CodeBurn reads `ui_messages.json` for API request token/cost records and `api_conversation_history.json` for the selected model, with support for both GA (`IBM Bob`) and preview (`Bob-IDE`) app data folders.

Expand Down
30 changes: 18 additions & 12 deletions src/providers/copilot.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync } from 'fs'
import { readdir, readFile, stat } from 'fs/promises'
import { basename, dirname, join } from 'path'
import { basename, dirname, join, posix, win32 } from 'path'
import { homedir } from 'os'

import { readSessionFile } from '../fs-utils.js'
Expand Down Expand Up @@ -106,7 +106,8 @@ function parseLegacyEvents(content: string, sessionId: string, seenKeys: Set<str
}

if (event.type === 'user.message') {
pendingUserMessage = event.data.content ?? ''
const userEvent = event as Extract<LegacyCopilotEvent, { type: 'user.message' }>
pendingUserMessage = userEvent.data.content ?? ''
continue
}

Expand Down Expand Up @@ -330,25 +331,30 @@ function getCopilotSessionStateDir(override?: string): string {
return override ?? join(homedir(), '.copilot', 'session-state')
}

function getVSCodeWorkspaceStorageDirs(): string[] {
if (process.platform === 'darwin') {
export function getVSCodeWorkspaceStorageDirs(homeDir = homedir(), platform = process.platform): string[] {
const pathJoin = platform === 'win32' ? win32.join : posix.join

if (platform === 'darwin') {
return [
join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'workspaceStorage'),
join(homedir(), 'Library', 'Application Support', 'Code - Insiders', 'User', 'workspaceStorage'),
pathJoin(homeDir, 'Library', 'Application Support', 'Code', 'User', 'workspaceStorage'),
pathJoin(homeDir, 'Library', 'Application Support', 'Code - Insiders', 'User', 'workspaceStorage'),
pathJoin(homeDir, 'Library', 'Application Support', 'VSCodium', 'User', 'workspaceStorage'),
]
}

if (process.platform === 'win32') {
if (platform === 'win32') {
return [
join(homedir(), 'AppData', 'Roaming', 'Code', 'User', 'workspaceStorage'),
join(homedir(), 'AppData', 'Roaming', 'Code - Insiders', 'User', 'workspaceStorage'),
pathJoin(homeDir, 'AppData', 'Roaming', 'Code', 'User', 'workspaceStorage'),
pathJoin(homeDir, 'AppData', 'Roaming', 'Code - Insiders', 'User', 'workspaceStorage'),
pathJoin(homeDir, 'AppData', 'Roaming', 'VSCodium', 'User', 'workspaceStorage'),
]
}

return [
join(homedir(), '.config', 'Code', 'User', 'workspaceStorage'),
join(homedir(), '.config', 'Code - Insiders', 'User', 'workspaceStorage'),
join(homedir(), '.vscode-server', 'data', 'User', 'workspaceStorage'),
pathJoin(homeDir, '.config', 'Code', 'User', 'workspaceStorage'),
pathJoin(homeDir, '.config', 'Code - Insiders', 'User', 'workspaceStorage'),
pathJoin(homeDir, '.config', 'VSCodium', 'User', 'workspaceStorage'),
pathJoin(homeDir, '.vscode-server', 'data', 'User', 'workspaceStorage'),
]
}

Expand Down
2 changes: 1 addition & 1 deletion src/providers/kilo-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Provider, SessionSource, SessionParser } from './types.js'

const EXTENSION_ID = 'kilocode.kilo-code'

export function createKiloCodeProvider(overrideDir?: string): Provider {
export function createKiloCodeProvider(overrideDir?: string | string[]): Provider {
return {
name: 'kilo-code',
displayName: 'KiloCode',
Expand Down
2 changes: 1 addition & 1 deletion src/providers/roo-code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Provider, SessionSource, SessionParser } from './types.js'

const EXTENSION_ID = 'rooveterinaryinc.roo-cline'

export function createRooCodeProvider(overrideDir?: string): Provider {
export function createRooCodeProvider(overrideDir?: string | string[]): Provider {
return {
name: 'roo-code',
displayName: 'Roo Code',
Expand Down
42 changes: 32 additions & 10 deletions src/providers/vscode-cline-parser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readdir, readFile, stat } from 'fs/promises'
import { basename, join } from 'path'
import { basename, join, posix, win32 } from 'path'
import { homedir } from 'os'

import { calculateCost } from '../models.js'
Expand All @@ -12,19 +12,41 @@ type UiMessage = {
ts?: number
}

export function getVSCodeGlobalStoragePath(extensionId: string): string {
if (process.platform === 'darwin') {
return join(homedir(), 'Library', 'Application Support', 'Code', 'User', 'globalStorage', extensionId)
export function getVSCodeGlobalStoragePaths(extensionId: string, homeDir = homedir(), platform = process.platform): string[] {
const pathJoin = platform === 'win32' ? win32.join : posix.join

if (platform === 'darwin') {
return [
pathJoin(homeDir, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', extensionId),
pathJoin(homeDir, 'Library', 'Application Support', 'Code - Insiders', 'User', 'globalStorage', extensionId),
pathJoin(homeDir, 'Library', 'Application Support', 'VSCodium', 'User', 'globalStorage', extensionId),
]
}
if (process.platform === 'win32') {
return join(homedir(), 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', extensionId)

if (platform === 'win32') {
return [
pathJoin(homeDir, 'AppData', 'Roaming', 'Code', 'User', 'globalStorage', extensionId),
pathJoin(homeDir, 'AppData', 'Roaming', 'Code - Insiders', 'User', 'globalStorage', extensionId),
pathJoin(homeDir, 'AppData', 'Roaming', 'VSCodium', 'User', 'globalStorage', extensionId),
]
}
return join(homedir(), '.config', 'Code', 'User', 'globalStorage', extensionId)

return [
pathJoin(homeDir, '.config', 'Code', 'User', 'globalStorage', extensionId),
pathJoin(homeDir, '.config', 'Code - Insiders', 'User', 'globalStorage', extensionId),
pathJoin(homeDir, '.config', 'VSCodium', 'User', 'globalStorage', extensionId),
]
}

export function getVSCodeGlobalStoragePath(extensionId: string): string {
return getVSCodeGlobalStoragePaths(extensionId)[0]!
}

export async function discoverClineTasks(extensionId: string, providerName: string, displayName: string, overrideDir?: string): Promise<SessionSource[]> {
const baseDir = overrideDir ?? getVSCodeGlobalStoragePath(extensionId)
return discoverClineTasksInBaseDirs([baseDir], providerName, displayName)
export async function discoverClineTasks(extensionId: string, providerName: string, displayName: string, overrideDir?: string | string[]): Promise<SessionSource[]> {
const baseDirs = overrideDir
? (Array.isArray(overrideDir) ? overrideDir : [overrideDir])
: getVSCodeGlobalStoragePaths(extensionId)
return discoverClineTasksInBaseDirs(baseDirs, providerName, displayName)
}

export async function discoverClineTasksInBaseDirs(baseDirs: string[], providerName: string, displayName: string): Promise<SessionSource[]> {
Expand Down
16 changes: 14 additions & 2 deletions tests/providers/copilot.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises'
import { join } from 'path'
import { join, posix, win32 } from 'path'
import { tmpdir } from 'os'

import { copilot, createCopilotProvider } from '../../src/providers/copilot.js'
import { copilot, createCopilotProvider, getVSCodeWorkspaceStorageDirs } from '../../src/providers/copilot.js'
import type { ParsedProviderCall } from '../../src/providers/types.js'

let tmpDir: string
Expand Down Expand Up @@ -365,6 +365,18 @@ describe('copilot provider - discoverSessions', () => {
expect(sessions[0]!.project).toBe('myapp')
expect(sessions[0]!.path).toContain('session-1.jsonl')
})

it('includes VSCodium workspaceStorage paths on all supported platforms', () => {
expect(getVSCodeWorkspaceStorageDirs('/Users/test', 'darwin')).toContain(
posix.join('/Users/test', 'Library', 'Application Support', 'VSCodium', 'User', 'workspaceStorage'),
)
expect(getVSCodeWorkspaceStorageDirs('C:\\Users\\test', 'win32')).toContain(
win32.join('C:\\Users\\test', 'AppData', 'Roaming', 'VSCodium', 'User', 'workspaceStorage'),
)
expect(getVSCodeWorkspaceStorageDirs('/home/test', 'linux')).toContain(
posix.join('/home/test', '.config', 'VSCodium', 'User', 'workspaceStorage'),
)
})
})

describe('copilot provider - metadata', () => {
Expand Down
58 changes: 58 additions & 0 deletions tests/providers/vscode-cline-parser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises'
import { join, posix, win32 } from 'path'
import { tmpdir } from 'os'

import { discoverClineTasks, getVSCodeGlobalStoragePaths } from '../../src/providers/vscode-cline-parser.js'

let tmpDir: string

beforeEach(async () => {
tmpDir = await mkdtemp(join(tmpdir(), 'vscode-cline-parser-test-'))
})

afterEach(async () => {
await rm(tmpDir, { recursive: true, force: true })
})

async function writeTask(baseDir: string, taskId: string): Promise<void> {
const taskDir = join(baseDir, 'tasks', taskId)
await mkdir(taskDir, { recursive: true })
await writeFile(join(taskDir, 'ui_messages.json'), '[]')
}

describe('VS Code Cline-family storage discovery', () => {
it('includes VSCodium globalStorage paths on all supported platforms', () => {
const extensionId = 'example.extension'

expect(getVSCodeGlobalStoragePaths(extensionId, '/Users/test', 'darwin')).toContain(
posix.join('/Users/test', 'Library', 'Application Support', 'VSCodium', 'User', 'globalStorage', extensionId),
)
expect(getVSCodeGlobalStoragePaths(extensionId, 'C:\\Users\\test', 'win32')).toContain(
win32.join('C:\\Users\\test', 'AppData', 'Roaming', 'VSCodium', 'User', 'globalStorage', extensionId),
)
expect(getVSCodeGlobalStoragePaths(extensionId, '/home/test', 'linux')).toContain(
posix.join('/home/test', '.config', 'VSCodium', 'User', 'globalStorage', extensionId),
)
})

it('discovers tasks across multiple VS Code-compatible storage roots', async () => {
const codeRoot = join(tmpDir, 'Code', 'User', 'globalStorage', 'example.extension')
const codiumRoot = join(tmpDir, 'VSCodium', 'User', 'globalStorage', 'example.extension')
await writeTask(codeRoot, 'task-code')
await writeTask(codiumRoot, 'task-codium')

const sessions = await discoverClineTasks(
'example.extension',
'example-provider',
'Example Provider',
[codeRoot, codiumRoot],
)

expect(sessions).toHaveLength(2)
expect(sessions.map(s => s.path).sort()).toEqual([
join(codeRoot, 'tasks', 'task-code'),
join(codiumRoot, 'tasks', 'task-codium'),
].sort())
})
})
Loading