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
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
<img src="https://raw.githubusercontent.com/getagentseal/codeburn/main/assets/dashboard.jpg" alt="CodeBurn TUI dashboard" width="620" />
</p>

By task type, tool, model, MCP server, and project. Supports **Claude Code**, **Codex** (OpenAI), **Cursor**, **cursor-agent**, **OpenCode**, **Pi**, and **GitHub Copilot** with a provider plugin system. Tracks one-shot success rate per activity type so you can see where the AI nails it first try vs. burns tokens on edit/test/fix retries. Interactive TUI dashboard with gradient charts, responsive panels, and keyboard navigation. Native macOS menubar app in `mac/`. CSV/JSON export.
By task type, tool, model, MCP server, and project. Supports **Claude Code**, **Codex** (OpenAI), **Cursor**, **cursor-agent**, **OpenCode**, **Pi**, **[OMP](https://github.com/can1357/oh-my-pi)** (Oh My Pi), and **GitHub Copilot** with a provider plugin system. Tracks one-shot success rate per activity type so you can see where the AI nails it first try vs. burns tokens on edit/test/fix retries. Interactive TUI dashboard with gradient charts, responsive panels, and keyboard navigation. Native macOS menubar app in `mac/`. CSV/JSON export.

Works by reading session data directly from disk. No wrapper, no proxy, no API keys. Pricing from LiteLLM (auto-cached, all models supported).

Expand All @@ -36,7 +36,7 @@ npx codeburn
### Requirements

- Node.js 22+
- Claude Code (`~/.claude/projects/`), Codex (`~/.codex/sessions/`), Cursor, OpenCode, Pi (`~/.pi/agent/sessions/`), and/or GitHub Copilot (`~/.copilot/session-state/`)
- Claude Code (`~/.claude/projects/`), Codex (`~/.codex/sessions/`), Cursor, OpenCode, Pi (`~/.pi/agent/sessions/`), OMP (`~/.omp/agent/sessions/`), and/or GitHub Copilot (`~/.copilot/session-state/`)
- For Cursor/OpenCode support: uses Node's built-in `node:sqlite` (Node 22+)

## Usage
Expand Down Expand Up @@ -112,6 +112,7 @@ codeburn report --provider cursor-agent # cursor-agent CLI only
codeburn report --provider opencode # OpenCode only
codeburn report --provider pi # Pi only
codeburn report --provider copilot # GitHub Copilot only
codeburn report --provider omp # OMP only
codeburn today --provider codex # Codex today
codeburn export --provider claude # export Claude data only
```
Expand Down Expand Up @@ -155,6 +156,7 @@ Either flag alone is valid. Inverted or malformed dates exit with a clear error.
| Cursor | `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` | Supported |
| OpenCode | `~/.local/share/opencode/` (SQLite) | Supported |
| Pi | `~/.pi/agent/sessions/` | Supported |
| OMP | `~/.omp/agent/sessions/` | Supported |
| GitHub Copilot | `~/.copilot/session-state/` | Supported (output tokens only) |
| Amp | -- | Planned (provider plugin system) |

Expand All @@ -168,6 +170,22 @@ GitHub Copilot only logs output tokens in its session state, so Copilot cost row

The provider plugin system makes adding a new provider a single file. Each provider implements session discovery, JSONL parsing, tool normalization, and model display names. See `src/providers/codex.ts` for an example.

## Model aliases

If you see `$0.00` for some models, the model name reported by your provider doesn't match any entry in the LiteLLM pricing data. This commonly happens when using a proxy that rewrites model names.

Map any model name to a canonical one:

```bash
codeburn model-alias "my-proxy-model" "claude-opus-4-6" # add alias
codeburn model-alias --list # show configured aliases
codeburn model-alias --remove "my-proxy-model" # remove alias
```

Aliases are stored in `~/.config/codeburn/config.json` and applied at runtime before pricing lookup. The target name can be anything in the [LiteLLM model list](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json) or a canonical name from the fallback table (e.g. `claude-sonnet-4-6`, `claude-opus-4-5`, `gpt-4o`).

Built-in aliases ship for known proxy model name variants (such as `anthropic--claude-4.6-opus`). User-configured aliases take precedence over built-ins.

## Currency

By default, costs are shown in USD. To display in a different currency:
Expand Down Expand Up @@ -325,9 +343,9 @@ All metrics are computed from your local session data. No LLM calls, fully deter

**OpenCode** stores sessions in SQLite databases at `~/.local/share/opencode/opencode*.db`. CodeBurn queries the `session`, `message`, and `part` tables read-only, extracts token counts and tool usage, and recalculates cost using the LiteLLM pricing engine. Falls back to OpenCode's own cost field for models not in our pricing data. Subtask sessions (`parent_id IS NOT NULL`) are excluded to avoid double-counting. Supports multiple channel databases and respects `XDG_DATA_HOME`.

**Pi** stores sessions as JSONL at `~/.pi/agent/sessions/<sanitized-cwd>/*.jsonl`. Each assistant message carries token usage (input, output, cacheRead, cacheWrite) plus inline `toolCall` content blocks. CodeBurn extracts token counts, normalizes Pi's lowercase tool names to the standard set (`bash` -> `Bash`, `dispatch_agent` -> `Agent`), and pulls bash commands from `toolCall.arguments.command` for the shell breakdown.
**Pi / OMP** stores sessions as JSONL at `~/.pi/agent/sessions/<sanitized-cwd>/*.jsonl` (Pi) and `~/.omp/agent/sessions/<sanitized-cwd>/*.jsonl` (OMP). Each assistant message carries token usage (input, output, cacheRead, cacheWrite) plus inline `toolCall` content blocks. CodeBurn extracts token counts, normalizes tool names to the standard set (`bash` -> `Bash`, `dispatch_agent` -> `Agent`), and pulls bash commands from `toolCall.arguments.command` for the shell breakdown.

CodeBurn reads these files, deduplicates messages (by API message ID for Claude, by cumulative token cross-check for Codex, by conversation/timestamp for Cursor, by session+message ID for OpenCode, by responseId for Pi), filters by date range per entry, and classifies each turn.
CodeBurn reads these files, deduplicates messages (by API message ID for Claude, by cumulative token cross-check for Codex, by conversation/timestamp for Cursor, by session+message ID for OpenCode, by responseId for Pi/OMP), filters by date range per entry, and classifies each turn.

## Environment variables

Expand Down Expand Up @@ -362,13 +380,14 @@ src/
fs-utils.ts Bounded file readers with stream support
providers/
types.ts Provider interface definitions
index.ts Provider registry (lazy-loads Cursor, OpenCode)
index.ts Provider registry (lazy-loads Cursor, OpenCode, cursor-agent)
claude.ts Claude Code session discovery
codex.ts Codex session discovery and JSONL parsing
copilot.ts GitHub Copilot session state parsing
cursor.ts Cursor SQLite parsing, language extraction
cursor-agent.ts cursor-agent CLI session discovery and parsing
opencode.ts OpenCode SQLite session discovery and parsing
pi.ts Pi agent JSONL session discovery and parsing
pi.ts Pi/OMP agent JSONL session discovery and parsing
```

## Star History
Expand Down
54 changes: 53 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Command } from 'commander'
import { installMenubarApp } from './menubar-installer.js'
import { exportCsv, exportJson, type PeriodExport } from './export.js'
import { loadPricing } from './models.js'
import { loadPricing, setModelAliases } from './models.js'
import { parseAllSessions, filterProjectsByName } from './parser.js'
import { convertCost } from './currency.js'
import { renderStatusBar } from './format.js'
Expand Down Expand Up @@ -158,6 +158,8 @@ program.hook('preAction', async (thisCommand) => {
if (thisCommand.opts<{ verbose?: boolean }>().verbose) {
process.env['CODEBURN_VERBOSE'] = '1'
}
const config = await readConfig()
setModelAliases(config.modelAliases ?? {})
await loadCurrency()
})

Expand Down Expand Up @@ -835,6 +837,56 @@ program
console.log(` Config saved to ${getConfigFilePath()}\n`)
})

program
.command('model-alias [from] [to]')
.description('Map a provider model name to a canonical one for pricing (e.g. codeburn model-alias my-model claude-opus-4-6)')
.option('--remove <from>', 'Remove an alias')
.option('--list', 'List configured aliases')
.action(async (from?: string, to?: string, opts?: { remove?: string; list?: boolean }) => {
const config = await readConfig()
const aliases = config.modelAliases ?? {}

if (opts?.list || (!from && !opts?.remove)) {
const entries = Object.entries(aliases)
if (entries.length === 0) {
console.log('\n No model aliases configured.')
console.log(` Config: ${getConfigFilePath()}\n`)
} else {
console.log('\n Model aliases:')
for (const [src, dst] of entries) {
console.log(` ${src} -> ${dst}`)
}
console.log(` Config: ${getConfigFilePath()}\n`)
}
return
}

if (opts?.remove) {
if (!(opts.remove in aliases)) {
console.error(`\n Alias not found: ${opts.remove}\n`)
process.exitCode = 1
return
}
delete aliases[opts.remove]
config.modelAliases = Object.keys(aliases).length > 0 ? aliases : undefined
await saveConfig(config)
console.log(`\n Removed alias: ${opts.remove}\n`)
return
}

if (!from || !to) {
console.error('\n Usage: codeburn model-alias <from> <to>\n')
process.exitCode = 1
return
}

aliases[from] = to
config.modelAliases = aliases
await saveConfig(config)
console.log(`\n Alias saved: ${from} -> ${to}`)
console.log(` Config: ${getConfigFilePath()}\n`)
})

program
.command('optimize')
.description('Find token waste and get exact fixes')
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type CodeburnConfig = {
symbol?: string
}
plan?: Plan
modelAliases?: Record<string, string>
}

function getConfigDir(): string {
Expand Down
32 changes: 28 additions & 4 deletions src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,38 @@ export async function loadPricing(): Promise<void> {
}
}

// Known model name variants that providers emit but LiteLLM/fallback don't index under.
// OMP emits 'anthropic--claude-4.6-opus' (double-dash, dot version, tier-last).
// getCanonicalName strips any 'provider/' prefix first, so only the post-strip
// forms need to be listed here.
const BUILTIN_ALIASES: Record<string, string> = {
'anthropic--claude-4.6-opus': 'claude-opus-4-6',
'anthropic--claude-4.6-sonnet': 'claude-sonnet-4-6',
'anthropic--claude-4.5-opus': 'claude-opus-4-5',
'anthropic--claude-4.5-sonnet': 'claude-sonnet-4-5',
'anthropic--claude-4.5-haiku': 'claude-haiku-4-5',
}

let userAliases: Record<string, string> = {}

// Called once during CLI startup after config is loaded.
// User aliases take precedence over built-ins.
export function setModelAliases(aliases: Record<string, string>): void {
userAliases = aliases
}

function resolveAlias(model: string): string {
return userAliases[model] ?? BUILTIN_ALIASES[model] ?? model
}
function getCanonicalName(model: string): string {
return model
.replace(/@.*$/, '')
.replace(/-\d{8}$/, '')
.replace(/@.*$/, '') // strip pin: claude-sonnet-4-6@20250929 -> claude-sonnet-4-6
.replace(/-\d{8}$/, '') // strip date: claude-sonnet-4-20250514 -> claude-sonnet-4
.replace(/^[^/]+\//, '') // strip provider prefix: anthropic/foo -> foo
}

export function getModelCosts(model: string): ModelCosts | null {
const canonical = getCanonicalName(model)
const canonical = resolveAlias(getCanonicalName(model))

if (pricingCache?.has(canonical)) return pricingCache.get(canonical)!

Expand Down Expand Up @@ -174,7 +198,7 @@ export function calculateCost(
}

export function getShortModelName(model: string): string {
const canonical = getCanonicalName(model)
const canonical = resolveAlias(getCanonicalName(model))
const shortNames: Record<string, string> = {
'claude-opus-4-7': 'Opus 4.7',
'claude-opus-4-6': 'Opus 4.6',
Expand Down
3 changes: 2 additions & 1 deletion src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { claude } from './claude.js'
import { codex } from './codex.js'
import { copilot } from './copilot.js'
import { pi } from './pi.js'
import { omp } from './pi.js'
import type { Provider, SessionSource } from './types.js'

let cursorProvider: Provider | null = null
Expand Down Expand Up @@ -49,7 +50,7 @@ async function loadCursorAgent(): Promise<Provider | null> {
}
}

const coreProviders: Provider[] = [claude, codex, copilot, pi]
const coreProviders: Provider[] = [claude, codex, copilot, pi, omp]

export async function getAllProviders(): Promise<Provider[]> {
const [cursor, opencode, cursorAgent] = await Promise.all([loadCursor(), loadOpenCode(), loadCursorAgent()])
Expand Down
Loading
Loading