codeburn Documentation

Free, local tool to track AI coding token usage and cost across 31 tools and agents (Claude Code, Cursor, Codex, Gemini and more), by model, project, and task.

9,207 stars TypeScript TypeScript 1 file ยท ~29,666 tokens #ai-coding#claude-code#cli#codex#cost-tracking#cursor-ide#developer-tools#menubar
RAW Doc

File: README.md

| Desktop

text
[](https://github.com/getagentseal/codeburn/releases/download/desktop-v0.9.20/CodeBurn-0.9.20-arm64.dmg)
  [](https://github.com/getagentseal/codeburn/releases/download/desktop-v0.9.20/CodeBurn-0.9.20.dmg)
  [](https://apps.microsoft.com/detail/9P0R4ZL5XMB8)
  [](https://github.com/getagentseal/codeburn/releases/download/desktop-v0.9.20/codeburn-desktop_0.9.20_amd64.deb)
  [](https://github.com/getagentseal/codeburn/releases/download/desktop-v0.9.20/codeburn-desktop-0.9.20.x86_64.rpm)
  [](https://github.com/getagentseal/codeburn/releases/download/desktop-v0.9.20/CodeBurn-0.9.20.AppImage) | **Web**
  
  `npx codeburn web` |

| --- | --- |
| Terminal

text
`npx codeburn` | **macOS Menubar**
  
  `codeburn menubar` |

CodeBurn is a free, open-source, local-first tool that tracks AI coding token usage and cost across 40 tools and agents (Claude Code, Cursor, Codex, Gemini, Grok and more), broken down by model, project, and task.

You pay for Claude, Codex, Cursor, and a stack of other AI tools. The bill tells you the total. It never tells you that half of it went to conversation instead of code, or that an expensive model burned your budget on work a cheaper one would have one-shot.

CodeBurn does. It reads the session files your tools already write to disk and breaks down every token and dollar by task, model, tool, and project, across 40 AI tools.

Everything runs locally. No wrapper, no proxy, no API keys, nothing leaves your machine. Pricing comes from LiteLLM, refreshed daily.

Quick start

Run it instantly, no install needed:

bash
npx codeburn

That opens the interactive dashboard (last 7 days by default). Arrow keys switch periods, q quits. That is the 30-second version. You now know where your AI budget goes.

Install it for a permanent codeburn command:

bash
npm install codeburn

Also runs via bunx codeburn or pnpm dlx codeburn, or brew install codeburn on macOS.

Menu bar app for macOS, with your spend always in the menu bar:

bash
codeburn menubar

On Linux, a GNOME Shell extension gives the same panel view; see Linux (GNOME).

Requires Node.js 22.13+ and at least one supported tool with session data on disk. For Cursor and OpenCode, better-sqlite3 installs automatically.

Your month at a glance

bash
codeburn overview                                    # this month, clean tables
codeburn overview --no-color                         # plain text, ready to paste
codeburn overview --from 2026-06-01 --to 2026-06-15  # any date range
codeburn overview -p all                             # last 6 months
codeburn overview -p lifetime                        # full history (uncapped)
codeburn overview --provider claude                  # one tool only

codeburn overview prints a copy-pasteable summary of where your AI spend went: totals (cost, tokens, cache hit), a breakdown by tool and by top model, your highest-value days, top projects, a per-day table, and activity and tool usage. Pipe it anywhere (into pbcopy, a PR, Slack, or a tweet); color drops automatically when the output is not a terminal, or pass --no-color.

text
CodeBurn  June 2026

Totals
  Cost       $2,795.10
  Tokens     3.49B   in 23.9M / out 20.2M / cache-w 72.5M / cache-r 3.38B
  Calls      14,755   sessions 753
  Cache hit  99.3%

By tool
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Tool     โ”‚      Cost โ”‚ Tokens โ”‚ Share โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ claude   โ”‚ $2,662.37 โ”‚  3.34B โ”‚   95% โ”‚
โ”‚ codex    โ”‚   $119.12 โ”‚ 128.1M โ”‚    4% โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

(plus Top models, Highest-value days, Top projects, a per-day table, By activity, and Tools)

Find and fix waste

bash
codeburn optimize                       # scan the last 30 days
codeburn optimize -p today              # today only
codeburn optimize -p week               # last 7 days
codeburn optimize --provider claude     # restrict to one provider
codeburn optimize --format json         # setup health + findings as JSON

codeburn optimize scans your sessions and your ~/.claude/ setup for waste patterns:

  • Files Claude re-reads across sessions (same content, same context, over and over)
  • Low Read:Edit ratio (editing without reading leads to retries and wasted tokens)
  • Wasted bash output (uncapped BASH_MAX_OUTPUT_LENGTH, trailing noise)
  • Unused MCP servers still paying their tool-schema overhead every session
  • Ghost agents, skills, and slash commands defined in ~/.claude/ but never invoked
  • Bloated CLAUDE.md files (with @-import expansion counted)
  • Cache creation overhead and junk directory reads
  • Context-heavy sessions where effective input/cache tokens swamp output
  • Possibly low-worth expensive sessions with no edit turns or repeated retries
    when no git/gh delivery command is observed

Each finding shows the estimated token and dollar savings plus a ready-to-paste fix: a CLAUDE.md line, an environment variable, or a mv command to archive unused items. Findings are ranked by urgency (impact weighted against observed waste) and rolled up into an A to F setup health grade. Repeat runs classify each finding as new, improving, or resolved against a 48-hour recent window.

You can also open it inline from the dashboard: press o when a finding count appears in the status bar, b to return.

Apply fixes, undo anytime

bash
codeburn optimize --apply             # review and apply fixes interactively
codeburn optimize --apply --dry-run   # print the plan, change nothing
codeburn optimize --apply --yes       # apply every appliable fix without prompting
codeburn act list                     # every change CodeBurn has made
codeburn act undo --last              # roll the most recent change back
codeburn act report                   # realized vs estimated savings

codeburn optimize finds the waste; --apply fixes the config-class findings for you: settings values, environment variables, archiving unused agents and skills. Every change is backed up and journaled before it lands. codeburn act list shows the history and codeburn act undo restores the original files (it refuses if the files changed since being applied, unless you pass --force).

The loop closes on honesty: once an applied fix is at least 3 days old, codeburn act report compares its estimated savings against what your sessions actually did, and later codeburn optimize runs show that realized figure in the header. Estimates get checked against reality, not just claimed.

Guard your budget

bash
codeburn guard install            # hooks into this project's .claude/settings.json
codeburn guard install --global   # or into ~/.claude/settings.json
codeburn guard status             # caps, install locations, flagged projects
codeburn guard uninstall          # removes cleanly, leaves your own hooks alone

Guard installs opt-in hooks into Claude Code that watch session cost while you work:

  • Soft cap (default $5): a one-time in-session warning when a session passes it.
  • Hard cap (default $15): stops the session; codeburn guard allow lifts it for that session only.
  • Checkpoint (default $3): if a session ends past this with no edits and no commits, a nudge suggests starting fresh with a named deliverable.
  • Session openers: projects where optimize found waste get a one-line flag at session start.

Caps are edited in ~/.config/codeburn/guard.json (set a value to null to disable it). Add --statusline to show session cost in the Claude Code status line. Installs go through the same journal as everything else, so codeburn act undo removes them too. Hooks fail open: a broken guard never blocks a session.

Compare models

bash
codeburn compare                        # interactive model picker (default: last 6 months)
codeburn compare -p week                # last 7 days
codeburn compare -p today               # today only
codeburn compare --provider claude      # Claude Code sessions only

Which model is actually better for your work? Press c in the dashboard, or run codeburn compare. Arrow keys switch periods, b to return.

Section Metric What it measures
Performance One-shot rate Edits that succeed without retries
Performance Retry rate Average retries per edit turn
Performance Self-correction Turns where the model corrected its own mistake
Efficiency Cost per call Average cost per API call
Efficiency Cost per edit Average cost per edit turn
Efficiency Output tokens per call Average output tokens per call
Efficiency Cache hit rate Proportion of input from cache

Also compares per-category one-shot rates, delegation rate, planning rate, average tools per turn, and fast mode usage.

Track what shipped

bash
codeburn yield                  # last 7 days (default)
codeburn yield -p today         # today only
codeburn yield -p 30days        # last 30 days
codeburn yield -p month         # this calendar month
codeburn yield --format json    # productive/reverted/abandoned/ambiguous spend as JSON

Did the spend actually ship? codeburn yield correlates AI sessions with git commits by timestamp:

Category Meaning
Productive Commits from this session landed in main
Reverted Commits were later reverted
Abandoned No commits near session, or commits never merged
Ambiguous Session ran parallel to another and its window's commits were attributed to the tighter one

Attribution is timestamp-window based (heuristic): each commit is credited to at most one session, the tightest window containing it. The JSON report carries methodology: "timestamp-window".

Requires a git repository. Run from your project directory.

Browser dashboard

bash
codeburn web                    # opens http://localhost:4747 in your browser
codeburn web -p 30days          # start on a different period
codeburn web --port 8080        # pick a port (falls back to a free one if taken)
codeburn web --no-open          # start the server without opening a browser

A local web dashboard with the same task, model, tool, and project breakdowns as the TUI, rendered with charts. The usage graph follows the selected period with 15-minute, hourly, or daily buckets and can switch between per-session and per-model lines. Everything is read from disk on your machine and the server binds to localhost; nothing is uploaded.

Combine usage across your devices

See one total across your laptop, desktop, and work machine on the same network. On each other device, share its usage:

bash
codeburn share --pair           # opens a pairing window and prints a PIN

Then add it once from your main device (the PIN authorizes the pairing):

bash
codeburn devices add            # find nearby devices and pair, or: add <host> --pin 
codeburn devices                # combined totals by machine
codeburn devices rm <name>      # forget a device

Pairing is PIN-authorized and stays on your local network. You can also discover and pair devices straight from the browser dashboard.

bash
codeburn menubar

One command: downloads the latest .app, installs into ~/Applications, and launches it. Re-run with --force to reinstall. The native Swift and SwiftUI app lives in mac/ (see mac/README.md for build details).

The menubar icon shows the spend period selected in Settings (Today by default; Week, Month, and 6 Months are also available). Non-today periods add a short suffix such as $42 / mo so the menu bar value stays clear. Click to open a popover with agent tabs, period switcher (Today, 7 Days, 30 Days, Month, All), Trend, Forecast, Pulse, Stats, and Plan insights, activity and model breakdowns, optimize findings, and CSV/JSON export. Refreshes every 30 seconds.

You can also set the menubar status period from Terminal:

bash
defaults write org.agentseal.codeburn-menubar CodeBurnMenubarPeriod -string month

Allowed values are today, week, month, and sixMonths. Relaunch the app to apply external defaults changes.

Compact mode shrinks the menubar item to fit the text, dropping decimals (e.g. $110 instead of $110.20):

bash
defaults write org.agentseal.codeburn-menubar CodeBurnMenubarCompact -bool true

Relaunch the app to apply. To revert: defaults delete org.agentseal.codeburn-menubar CodeBurnMenubarCompact.

Refresh cadence is set in Settings under Usage Refresh. Auto (the default) refreshes every 30 seconds on AC power and backs off on battery, in Low Power Mode, and while the display sleeps; fixed 1, 5, or 15 minute cadences and a Manual mode (refresh only when you open the popover or click Refresh Now) are also available. From Terminal:

bash
defaults write org.agentseal.codeburn-menubar CodeBurnMenubarRefreshSeconds -int 300

Seconds between refreshes: 60, 300, or 900; 0 is Manual and -1 is Auto. Takes effect on the next refresh tick, no relaunch needed.

Preferred terminal decides where Full Report and Optimize open. Set it in Settings โ†’ General โ†’ Terminal, or from Terminal:

bash
defaults write org.agentseal.codeburn-menubar CodeBurnPreferredTerminal -string iterm2

Allowed values are terminal (macOS Terminal.app, the default) and iterm2. Anything else falls back to terminal. Only terminals that can script a command into a live window are offered; if the chosen app is missing or fails to accept the command, CodeBurn tries Terminal.app and then runs the command in the background, logging each step to Console.app. Takes effect on the next launch of a command, no relaunch needed.

Linux (GNOME)

Linux gets the same ambient view through a GNOME Shell extension (GNOME 45+): spend in the top panel, period switcher, compact mode, and daily budget alerts. It lives in gnome/:

bash
git clone https://github.com/getagentseal/codeburn && cd codeburn/gnome
./install.sh
gnome-extensions enable [email protected]

See gnome/README.md for settings and development notes. On Windows, codeburn web is the always-on view for now.

CodeBurn in your agent (MCP)

bash
claude mcp add codeburn -- npx -y codeburn mcp

codeburn mcp runs a local MCP server over stdio, so Claude Code, Cursor, or any MCP client can ask "where did my tokens go this week?" or "how do I spend less?" mid-conversation. It exposes two tools:

Tool What it returns
get_usage Spend and usage with breakdowns by tool, model, project, and task (fast)
get_savings Cost reductions: waste findings, retry tax, routing waste (slower, deeper analysis)

Everything is read from local disk, same as the CLI. Project names are pseudonymized by default; the agent only sees real names if it asks with include_project_names: true. For other MCP clients, configure a stdio server with command npx and args -y codeburn mcp.

Supported tools

CodeBurn auto-detects which AI tools you use. Each logo links to its provider doc.

If multiple providers have session data on disk, press p in the dashboard to toggle between them.

Each provider doc lists the exact data location, storage format, and known quirks. Linux and Windows paths are detected automatically. If a path has changed or is wrong, please open an issue.

The --provider flag filters any command to a single provider: codeburn report --provider claude, codeburn today --provider codex, codeburn export --provider cursor. Works on all commands: report, today, month, overview, status, export, web, optimize, compare, yield.

Adding a new provider is a single file. See src/providers/codex.ts for an example.

Commands

All commands and keyboard shortcuts

Run codeburn for the dashboard, or use a subcommand below. Most commands also accept --provider, --project / --exclude, and a period flag (-p today|week|30days|month|all|lifetime).

Dashboard & reports

Command What it does
codeburn Interactive dashboard, last 7 days (the default view)
codeburn today Today's usage
codeburn month This calendar month's usage
codeburn overview Plain-text monthly summary, copy-pasteable (--no-color, --from/--to)
codeburn report -p 30days Rolling 30-day window
codeburn report -p all Every recorded session
codeburn report --from 2026-04-01 --to 2026-04-10 An exact date range
codeburn report --format json Full dashboard data as JSON, printed to stdout
codeburn report --refresh 60 Auto-refresh every 60s (the minimum and default; --refresh 0 disables)

Status & export

Command What it does
codeburn status Compact one-liner: today + month totals
codeburn status --format json The same totals as JSON
codeburn export CSV covering today, 7 days, and 30 days
codeburn export -f json Export as JSON instead of CSV

Sync (team telemetry) preview

Command What it does
codeburn sync setup One-time setup: OIDC login via browser, stores token securely
codeburn sync push Push unsent usage to remote endpoint (default: last 7 days)
codeburn sync push --since 30d Push a larger window
codeburn sync status Show endpoint, auth state, last sync time
codeburn sync logout Revoke token and remove credentials
codeburn sync reset --confirm Clear sent-ledger (re-send all data on next push)

Sync sends token counts, costs, models, and projects, never prompts or code. This feature is in preview; the protocol may change between releases. See docs/sync/ for details.

Web & devices

Command What it does
codeburn web Local browser dashboard with charts (http://localhost:4747)
codeburn share --pair Share this device's usage to your other devices (PIN pairing)
codeburn devices add Find and pair a nearby device
codeburn devices Combined usage totals across your paired devices

Analysis

Command What it does
codeburn doctor Per-provider detection status: paths probed, sessions found, parse health (--json, --provider)
codeburn audit Per provider-model token source table: where every number comes from
codeburn context What fills a session's context window: interactive browser (Claude Code and Codex)
codeburn context --json The same context tree, scriptable
codeburn optimize Scan for waste and print copy-paste fixes (last 30 days)
codeburn optimize -p week Scope the waste scan to the last 7 days
codeburn compare Side-by-side model comparison
codeburn yield Productive vs reverted/abandoned spend, correlated against git
codeburn yield -p 30days Yield analysis for the last 30 days

Fix & control

Command What it does
codeburn optimize --apply Interactively apply config-class fixes (--yes, --dry-run, --only )
codeburn act list Every change CodeBurn has applied, newest first
codeburn act undo Roll a change back (--last for the most recent, --force if files drifted)
codeburn act report Realized vs estimated savings for applied fixes
codeburn guard install Budget-cap hooks for Claude Code (--global, --statusline)
codeburn guard status Show caps, install locations, and flagged projects
codeburn guard allow Lift the hard cap for the current session
codeburn mcp MCP server (stdio) exposing usage and savings to AI agents

Models

Command What it does
codeburn models Per-model token + cost table (last 30 days)
codeburn models --by-task Break each model into per-task-type rows
codeburn models --by-agent Break each model into per-agent rows: which agent drove which model's spend ((main) covers non-agent sessions; --min-cost 0 shows sub-cent agents)
codeburn models --top 10 Only the 10 most expensive models
codeburn models --format markdown Emit a paste-friendly markdown table
codeburn models --task feature Filter to feature-development work
codeburn models --provider claude Filter to a single provider

Left/right arrow keys switch between Today, 7 Days, 30 Days, Month, 6 Months, and Lifetime (use --from / --to for an exact historical window). Up/down scroll the full dashboard one line, Page Up/Page Down move one screen, and Home/End jump to either end. The main Daily Activity panel shows at least 10 dates from scrollable full history: use j/k to move one day, Shift+Space/Space to page, and g/G to jump to either end. Panels flow in the same order across three columns at maximum width, two at medium width, and one when narrow. In the three-column layout, all panels widen equally by one character for every three additional terminal columns until the dashboard reaches the lesser of 256 characters or the widest renderable source row. Press q to quit, 1 2 3 4 5 6 as period shortcuts, c to open model comparison, or o to open optimize. Today, 7 Days, and concrete-day views refresh in place at most once per minute by default (--refresh 0 to disable) without changing the active view or scroll position. The heavier aggregate views remain static between deliberate navigation changes. The dashboard also shows average cost per session and the five most expensive sessions across all projects.

Features

Pricing, task categories, plans, currency, filtering, and more

Pricing

Prices every API call using input, output, cache read, cache write, and web search token counts, with a fast mode multiplier for Claude. Prices are fetched from LiteLLM and cached locally for 24 hours at ~/.cache/codeburn/. Hardcoded fallbacks for all Claude and GPT-5 models prevent fuzzy-matching mispricing.

Task Categories

13 categories classified from tool usage patterns and user message keywords. No LLM calls, fully deterministic.

Category What triggers it
Coding Edit, Write tools
Debugging Error/fix keywords + tool usage
Feature Dev "add", "create", "implement" keywords
Refactoring "refactor", "rename", "simplify"
Testing pytest, vitest, jest in Bash
Exploration Read, Grep, WebSearch without edits
Planning EnterPlanMode, TaskCreate tools
Delegation Agent tool spawns
Git Ops git push/commit/merge in Bash
Build/Deploy npm build, docker, pm2
Brainstorming "brainstorm", "what if", "design"
Conversation No tools, pure text exchange
General Skill tool, uncategorized

Breakdowns

Daily cost chart, per-project, per-model (Opus, Sonnet, Haiku, GPT-5, GPT-4o, Gemini, Kiro, and more), per-activity with one-shot rate, core tools, shell commands, and MCP servers.

One-Shot Rate

For categories that involve code edits, CodeBurn tracks file-aware retry cycles. A retry is when the same file is re-edited after a shell command in between (Edit foo.ts, Bash, Edit foo.ts). Editing different files across shell steps is not a retry. The one-shot column shows the percentage of edit turns that succeeded without retries. Coding at 90% means the AI got it right first try 9 out of 10 times. File-level tracking is available for Claude, Codex, and Goose; other providers fall back to tool-name-based detection.

Plans

bash
codeburn plan set claude-max                                  # $200/month
codeburn plan set claude-pro                                  # $20/month
codeburn plan set cursor-pro                                  # $20/month
codeburn plan set custom --monthly-usd 200 --provider codex   # ChatGPT Pro-style custom plan
codeburn plan reset --provider codex                          # remove one provider plan
codeburn plan set none                                        # disable plan view
codeburn plan                                                 # show configured plans
codeburn plan reset                                           # remove plan config

Subscription tracking for Claude Pro, Claude Max, Cursor Pro, and custom provider plans. Plans are stored per provider, so you can track Claude and Codex/Cursor subscriptions at the same time; the dashboard shows one overage line per active provider plan. A legacy/custom all plan remains a single aggregate plan and is replaced when you add a provider-specific plan, avoiding double-counted overage rows. Existing single-plan config is still read as a fallback. Presets use publicly stated plan prices (as of April 2026); they do not model exact token allowances, because vendors do not publish precise consumer-plan limits.

Currency

bash
codeburn currency GBP          # set to British Pounds
codeburn currency AUD          # set to Australian Dollars
codeburn currency JPY          # set to Japanese Yen
codeburn currency CNY          # set to Chinese Yuan
codeburn currency RON          # set to Romanian Leu
codeburn currency              # show current setting
codeburn currency --reset      # back to USD

Any ISO 4217 currency code is supported (162 currencies). Exchange rates fetched from Frankfurter (European Central Bank data, free, no API key) and cached for 24 hours. Config stored at ~/.config/codeburn/config.json. The currency setting applies everywhere: dashboard, status bar, menu bar, CSV/JSON exports, and JSON API output.

Model Aliases

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

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 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. User-configured aliases take precedence over built-ins.

Local Models, Custom Prices, and Proxies

bash
codeburn price-override my-model --input 0.27 --output 1.10   # USD per 1M tokens
codeburn model-savings "llama3.1:8b" gpt-4o                   # local model, counted as savings
codeburn proxy-path ~/work/copilot-repo                       # subscription-covered project

price-override sets exact rates for any model (input, output, cache read, cache creation), useful for private deployments or models LiteLLM prices wrong. model-savings maps a free local model to a paid baseline: the local calls stay $0, and the dashboard shows what the same tokens would have cost on the baseline. proxy-path marks a project routed through a subscription-backed proxy (e.g. Claude Code over GitHub Copilot), so its API-rate cost is reported as subscription-covered and your net out-of-pocket stays honest. All three support --list and --remove.

Filtering

bash
codeburn report --project myapp                  # show only projects matching "myapp"
codeburn report --exclude myapp                  # show everything except "myapp"
codeburn report --exclude myapp --exclude tests  # exclude multiple projects
codeburn month --project api --project web       # include multiple projects
codeburn export --project inventory              # export only "inventory" project data

Filter by provider, project name (case-insensitive substring), or exact date range. The --project and --exclude flags work on all commands and can be combined with --provider.

bash
codeburn report --from 2026-04-01 --to 2026-04-10   # explicit window
codeburn report --from 2026-04-01                    # this date through today
codeburn report --to 2026-04-10                      # earliest data through this date

Either flag alone is valid. Inverted or malformed dates exit with a clear error. In the TUI, the custom range sets the initial load only; pressing 1 through 6 switches back to predefined periods.

Diagnosing detection

When a tool shows zero (or a number that looks wrong), codeburn doctor explains why. It runs fully offline and read-only, and never writes to caches or config.

bash
codeburn doctor                     # every provider, human-readable table
codeburn doctor --provider opencode # diagnose one provider
codeburn doctor --json              # machine-readable, pipe to jq

For each provider it shows the exact directories or databases probed (with any env override such as CLAUDE_CONFIG_DIR, CODEX_HOME, or OPENCODE_DATA_DIR and whether the path exists), how many session files were found, how many of a bounded sample parsed cleanly, the cached file count, and a one-line verdict: OK (n sessions), NOTHING FOUND with the likely cause (directory missing, override points at an empty dir, or the tool is not installed), or ERRORS (n parse failures). A provider that throws is caught and reported as its own error row, never crashing the rest of the report.

JSON Output

report, today, and month support --format json to output the full dashboard data as structured JSON to stdout:

bash
codeburn report --format json             # 7-day JSON report
codeburn today --format json              # today's data as JSON
codeburn month --format json              # this month as JSON
codeburn report -p 30days --format json   # 30-day window

The JSON includes all dashboard panels: overview (cost, calls, sessions, cache hit %), daily breakdown, projects (with avgCostPerSession), models with token counts, activities with one-shot rates, core tools, MCP servers, and shell commands. Pipe to jq for filtering:

bash
codeburn report --format json | jq '.projects'
codeburn today --format json | jq '.overview.cost'

For lighter output, use status --format json (today and month totals only), optimize --format json (setup health, findings, and copy-paste fixes), yield --format json (productive/reverted/abandoned/ambiguous spend), or file exports (export -f json).

Reading the dashboard

Signals and what they might mean

CodeBurn surfaces the data; you read the story. A few patterns worth knowing:

Signal you see What it might mean
Cache hit < 80% System prompt or context is not stable, or caching is not enabled
Lots of Read calls per session Agent re-reading same files, missing context
Low 1-shot rate (Coding 30%) Agent struggling with edits, retry loops
Opus 4.8 dominating cost on small turns Overpowered model for simple tasks
dispatch_agent / task heavy Sub-agent fan-out, expected or excessive
No MCP usage shown Either you don't use MCP servers, or your config is broken
Bash dominated by git status, ls Agent exploring instead of executing
Conversation category dominant Agent talking instead of doing

These are starting points, not verdicts. A 60% cache hit on a single experimental session is fine. A persistent 60% cache hit across weeks of work is a config issue.

How it reads your data

Per-tool data locations and parsing

Provider Data location Notes
Claude Code ~/.claude/projects/<sanitized-path>/<session-id>.jsonl Each assistant entry carries model name, token usage (input, output, cache read, cache write), tool_use blocks, and timestamps.
Claude (multiple config dirs) Set via CLAUDE_CONFIG_DIRS (e.g. ~/.claude-work:~/.claude-personal) Scans every listed directory and merges sessions into one row per project so totals reflect all your Claude usage. Use : on POSIX, ; on Windows; overrides CLAUDE_CONFIG_DIR. Missing or unreadable directories are skipped.
Codex (OpenAI) ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl, ~/.codex/archived_sessions/rollout-*.jsonl Reads token_count events (per-call and cumulative usage) and function_call entries for tool tracking; attributes cost by project working directory. codeburn report --provider codex views Codex alone.
Cursor SQLite state.vscdb under globalStorage: macOS ~/Library/Application Support/Cursor/User/globalStorage/, Linux ~/.config/Cursor/User/globalStorage/, Windows %APPDATA%/Cursor/User/globalStorage/; results cached at ~/.cache/codeburn/cursor-results.json Input tokens come from Cursor's own per-conversation context meter (composerData.promptTokenBreakdown), credited once per conversation on a stable anchor; tool calls and shell commands are read from the agent stream (agentKv), and Composer house models are priced from Cursor's published rates. Output is a reply-text estimate and cache tokens are server-side only, so figures are marked estimated and undercount the Cursor admin console for long conversations. The cache auto-invalidates when the database changes; the first run on a large database can take a minute.
OpenCode SQLite ~/.local/share/opencode/opencode*.db or file-based ~/.local/share/opencode/storage/ (respects XDG_DATA_HOME; OPENCODE_DATA_DIR/OPENCODE_DB_PREFIX for renamed/forked builds) Queries session, message, and part read-only and recalculates cost via LiteLLM (falling back to OpenCode's own cost field for unpriced models). Subtask sessions (parent_id IS NOT NULL) are excluded to avoid double counting; multiple channel databases are supported.
Gemini CLI ~/.gemini/tmp//chats/session-*.json One JSON file per session with real token counts (input, output, cached, thoughts) per message, so no estimation is needed. Input is reported inclusive of cached, so CodeBurn subtracts cached before pricing to avoid double charging.
Antigravity (CLI & IDE) Session files under .gemini/ folders, plus the running language server Pulls granular trajectory and pricing from the language server process. For the short-lived CLI, optionally install a status-line hook with codeburn antigravity-hook install so usage is captured between menubar refreshes. The IDE is detected via the --app-data-dir antigravity-ide flag on Windows.
GitHub Copilot ~/.copilot/session-state/ (legacy CLI); VS Code/VSCodium workspaceStorage/* chat sessions, GitHub.copilot-chat/transcripts/, and the agent-traces.db OpenTelemetry store; JetBrains IDEs (IntelliJ, PyCharm, โ€ฆ) under ~/.config/github-copilot//<kind>/<storeId>/copilot-*-nitrite.db The OTel SQLite store is preferred when present (it carries real input/output/cache token counts). Other sources carry no explicit counts, so tokens are estimated from content length and the model is inferred from tool call ID prefixes. JetBrains sessions read from a Nitrite (H2 MVStore) .db; project comes from the plugin's projectName field (else the .git root of a referenced file). See docs/providers/copilot.md.
Kiro .chat JSON files Token counts are estimated from content length. The model is not exposed, so sessions are labeled kiro-auto and costed at Sonnet rates.
Mistral Vibe ~/.vibe/logs/session/ (or $VIBE_HOME/logs/session/); each folder has meta.json + messages.jsonl Reads cumulative prompt/completion totals and model pricing from meta.json, then the first user prompt and tool calls from messages.jsonl. Emits one record per session (source data is cumulative, not per turn); subagent sessions under agents/ are counted separately.
OpenClaw ~/.openclaw/agents/*.jsonl (legacy .clawdbot, .moltbot, .moldbot) Token usage comes from assistant message usage blocks; the model from modelId or message.model.
OpenClaude ~/.openclaude/projects/<slug>/*.jsonl (or $CODEBURN_OPENCLAUDE_DIR/projects/) Claude Code fork routed to any LLM backend; transcripts are Claude-Code schema. Only usage-bearing assistant lines become calls; no cost field exists, so every call is priced from the shared tables and flagged estimated. Sidechain (subagent) lines are counted as real spend.
Warp ~/Library/Group Containers/2BBY89MBSN.dev.warp/Library/Application Support/dev.warp.Warp-Stable/warp.sqlite (Preview fallback) Reads agent_conversations, ai_queries, and blocks, emitting one call per finalized exchange. Exchange token share is estimated from prompt-size weighting normalized to conversation totals; run_command blocks attach to the nearest preceding exchange by timestamp.
Zed SQLite ~/Library/Application Support/Zed/threads/threads.db (Linux ~/.local/share/zed/threads/) One row per agent thread; the blob is zstd-compressed JSON with per-request token usage (input, output, cache read, cache write) and the thread's model. Threads are topped up to the exact cumulative counter so totals match the store. Needs Node 22.15+ for built-in zstd.
Forge SQLite ~/.forge/.forge.db Queries conversations read-only and parses context.messages. Assistant usage entries provide prompt, completion, and cached counts; CodeBurn subtracts cached from prompt for input pricing, emits one call per assistant message, and extracts tool calls plus shell commands.
Pi / OMP ~/.pi/agent/sessions/<sanitized-cwd>/*.jsonl (Pi), ~/.omp/agent/sessions/<sanitized-cwd>/*.jsonl (OMP) Each assistant message carries usage (input, output, cacheRead, cacheWrite) plus inline toolCall blocks. Tool names normalize to the standard set (bash โ†’ Bash, dispatch_agent โ†’ Agent); bash commands come from toolCall.arguments.command.
Codebuff (formerly Manicode) ~/.config/manicode/projects//chats/<chatId>/chat-messages.json (honors CODEBUFF_DATA_DIR; walks manicode-dev / manicode-staging) Bills in credits, so each completed assistant message is costed at the public rate of $0.01/credit via msg.credits. When an upstream provider's stashed RunState records token-level usage (message.metadata.runState.sessionState.mainAgentState.messageHistory[*].providerOptions), the real tokens and LiteLLM cost take precedence. Native tool names (read_files, str_replace, run_terminal_command, spawn_agents) normalize to Read, Edit, Bash, Agent.
Cline / Roo Code / KiloCode VS Code globalStorage across VS Code, VS Code Insiders, and VSCodium (Cline at saoudrizwan.claude-dev, plus ~/.cline/data) Cline-family agents. CodeBurn reads ui_messages.json from each task directory, extracting token counts from type: "say" entries with say: "api_req_started".
Cline CLI ~/.cline/data/sessions/<session-id>/ (honors CLINE_SESSION_DATA_DIR, CLINE_DATA_DIR, CLINE_DIR) The Cline command-line agent, whose layout is unrelated to the VS Code extension's. Reads <session-id>.json for session metadata and the rolled-up usage, and <session-id>.messages.json for the per-message metrics block (input, output, cacheRead, cacheWrite, cost) that becomes one call each.
CodeWhale ~/.codewhale/sessions/*.json plus unmigrated legacy ~/.deepseek/sessions/*.json; $CODEWHALE_HOME/sessions is an exact override Emits one cumulative record per saved session. CodeWhale exposes only total_tokens, so CodeBurn preserves that aggregate in the input column rather than inventing an input/output split. Cost is the exact stored parent-session plus subagent USD total; model pricing is used only when the cost snapshot is absent. Tool blocks, shell commands, skills, and subagent types are retained.
IBM Bob User/globalStorage/ibm.bob-code/tasks/<task-id>/ (GA IBM Bob and preview Bob-IDE app folders) Reads ui_messages.json for API request token/cost records and api_conversation_history.json for the selected model.
Kimi Code CLI $KIMI_SHARE_DIR/sessions/<workdir-hash>/<session-id>/ or ~/.kimi/sessions/<workdir-hash>/<session-id>/ Reads wire.jsonl StatusUpdate.token_usage records, mapping input_other, input_cache_read, input_cache_creation, and output into the standard token columns; includes subagents under each session's subagents/ folder.
LingTai TUI ~/.lingtai/<agent>/logs/token_ledger.jsonl plus project homes from ~/.lingtai-tui/registry.jsonl (/.lingtai/<agent>/logs/token_ledger.jsonl); honors LINGTAI_HOME / LINGTAI_TUI_HOME Reads LingTai's append-only token ledger, mapping input - cached to fresh input, cached to cache reads, output to output, and thinking to reasoning. Nested daemon ledgers are skipped because parent ledgers already mirror daemon usage with source/run_id tags.
Vercel AI Gateway Vercel AI Gateway reporting API (cloud, not local logs) Set AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN (from vercel env pull / vercel dev); requires a Vercel plan with Custom Reporting. Without credentials, it's skipped silently in the combined dashboard.

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

Environment Variables

Override data directories and paths

Variable Description
CLAUDE_CONFIG_DIR Override Claude Code data directory (default: ~/.claude)
CLAUDE_CONFIG_DIRS OS-delimited list of Claude data directories to scan together (e.g. ~/.claude-work:~/.claude-personal). Sessions merge into one row per project. Overrides CLAUDE_CONFIG_DIR when set.
CODEX_HOME Override Codex data directory (default: ~/.codex)
CODEBUFF_DATA_DIR Override Codebuff data directory (default: ~/.config/manicode)
CODEWHALE_HOME Override the exact CodeWhale home directory; sessions are read from <CODEWHALE_HOME>/sessions
FACTORY_DIR Override Droid data directory (default: ~/.factory)
KIMI_SHARE_DIR Override Kimi Code CLI share directory (default: ~/.kimi)
KIMI_MODEL_NAME Override Kimi model name when Kimi sessions do not record the model
LINGTAI_HOME Override LingTai data directory (default: ~/.lingtai)
LINGTAI_TUI_HOME Alternate override for LingTai data directory; LINGTAI_HOME takes precedence
LINGTAI_TUI_GLOBAL_DIR Override LingTai TUI global directory used for project registry discovery (default: ~/.lingtai-tui)
OPENCODE_DATA_DIR Override the OpenCode-compatible data directory (default: $XDG_DATA_HOME/opencode or ~/.local/share/opencode). Point at a renamed/forked build, e.g. ~/.local/share/mimicode. Exact directory; no opencode suffix is appended.
OPENCODE_DB_PREFIX Override the SQLite DB filename prefix for OpenCode discovery (default: opencode, matching opencode*.db); e.g. mimicode discovers mimicode*.db. SQLite storage only.
QWEN_DATA_DIR Override Qwen data directory (default: ~/.qwen/projects)
VIBE_HOME Override Mistral Vibe home directory (default: ~/.vibe)
WARP_DB_PATH Override Warp database path (default: Warp Stable, then Warp Preview)

Sponsoring CodeBurn

CodeBurn is free, runs entirely on your machine, and exists to cut your AI bill. If it has already saved you more than a sponsorship costs, consider sending a little of that back.

Keeping 40 integrations accurate is constant work. The tools underneath change every week: Cursor reshapes its database, Claude moves a config path, new models ship at new prices. Sponsorship keeps CodeBurn current with all of it, so the numbers you see are always the real ones.

Where your sponsorship goes:

  • Honest numbers. New models and price changes are mapped quickly, so your cost is the real cost, not a guess.
  • More tools. Every one of the 40 providers started as a single file. Sponsorship funds the next one.
  • Fast fixes. When a vendor breaks something, paid time is what gets it patched now instead of someday.

Sponsoring as a team or company? Your logo lands right here, in front of every developer who opens the repo. The first sponsor gets it to themselves until the next one shows up.

Star History

License

MIT.

CodeBurn is an AgentSeal open-source project and is not affiliated with CodeBurn Bt. or codeburn.hu.

Credits

Pricing data from LiteLLM. Exchange rates from Frankfurter.

Built by AgentSeal.


File: docs/design/codeburn-mcp.md

CodeBurn MCP Server โ€” Design Spec

  • Date: 2026-06-02
  • Status: Approved design (pre-implementation)
  • Author: brainstormed + validator/devil's-advocate hardened

1. Context & Goal

CodeBurn already aggregates rich AI-coding usage/cost data (by task, model, project, provider; retry tax; routing waste; optimize findings; 365-day history). This spec adds an MCP server that exposes that data to AI agents (Claude Code, Cursor, Claude Desktop) so an agent can answer "where did my tokens go?" and "how do I spend less?" mid-conversation.

Why (and why not): This is a product / differentiation play, not a downloads play. We measured that MCP is a niche download lever (@ccusage/mcp โ‰ˆ 1.1% of ccusage's downloads; competitor tokscale ships no MCP), and the real download lever is npx-first CLI positioning โ€” tracked separately. The MCP's value is agent-facing utility on data competitors don't expose (retry tax, routing waste, one-shot rate, task attribution).

2. Decisions (from brainstorming)

  1. Use case: unified โ€” serves both live self-optimization and historical analysis from one tool set.
  2. Output contract: every tool returns a ready-to-display markdown table and the same data as structured JSON.
  3. Architecture: Approach A โ€” a codeburn mcp subcommand on the existing CLI (no second package), reusing the existing aggregation; run as a long-lived in-process stdio server.
  4. Tool surface: 2 tools โ€” get_usage and get_savings. (compare_periods cut โ€” no reusable backend, overlap-incoherent.)
  5. Privacy: project/session names hashed by default (project-<6hex>); real names only when include_project_names: true. Absolute paths never exposed.

3. Architecture

Process model

  • codeburn mcp starts a resident, in-process MCP server over stdio (StdioServerTransport). Not exec-per-call โ€” a resident process is required so the existing in-process session cache (180s TTL, src/parser.ts) amortizes across tool calls. Measured cost otherwise: --period all is ~17.6s even warm when the process exits between calls.
  • First line of the mcp action: console.log = console.error. The aggregation path is stdout-clean today (writes go to stderr), but the global preAction hook and runOptimize contain stdout console.logs; reassigning immunizes the JSON-RPC stream against any present or future stdout write. (Verified: scanAndDetect, parseAllSessions, providers, caches, buildMenubarPayload do not write to stdout.)
  • Pre-warm today usage on boot so the first interactive call is fast.

Modules

  • src/usage-aggregator.ts (new) โ€” extracted from the inline logic in the status handler (src/main.ts ~476โ€“760):
    • buildUsage(period, opts): Promise<MenubarPayload> โ€” cheap path, no optimize pass.
    • buildSavings(period, opts): Promise<MenubarPayload> โ€” adds scanAndDetect (optimize findings + retry tax + routing waste).
    • opts: { provider?: string; project?: string[]; exclude?: string[]; range?: ResolvedRange }. The provider and project/range filters are required for parity โ€” the status body forks on isAllProviders and resolves a range from day/days/from/to before period. status --format menubar-json is refactored to call these (one shared path).
  • src/mcp/server.ts (new) โ€” builds McpServer, registers the 2 tools, connects StdioServerTransport, owns the in-flight coalescing map and the empty-state messages.
  • src/mcp/tables.ts (new) โ€” compact markdown renderers per slice, reusing format.ts and models-report.ts where they already render tables.
  • src/mcp/redact.ts (new) โ€” stable pseudonym hashing for project/session names; applied unless the caller passes include_project_names: true.
  • src/main.ts โ€” add .command('mcp') that dynamic-import()s ./mcp/server.js and starts it.

Dependencies & build

  • Add to dependencies: @modelcontextprotocol/sdk@^1.29.0 (v1 line; import paths @modelcontextprotocol/sdk/server/mcp.js and /server/stdio.js) and zod@^3.25 (NOT transitive โ€” it's a peer dep of the SDK and absent from the lockfile today).
  • Add to tsup.config.ts: external: ['@modelcontextprotocol/sdk', 'zod']. The current config bundles all deps (splitting: false, no external), so without this a dynamic import would inline the SDK (~MBs) into dist/main.js. Externalizing keeps dist small and makes the dynamic import a real lazy load from node_modules. (files: ["dist"] means externalized deps must be runtime dependencies.)
  • Pin the SDK major: a separately-named v2 package exists with different import paths; ^1.29.0 keeps the v1 paths valid.

4. Tool Surface

Period enum (LLM-clear names, mapped internally): todayโ†’today, last_7_daysโ†’week, last_30_daysโ†’30days, month_to_dateโ†’month, last_6_monthsโ†’all. Documented: last_6_months is the maximum window (codeburn's "all" = ~6 months); history is summarized, not dumped.

Both tools carry annotations { readOnlyHint: true, openWorldHint: false, idempotentHint: true, title }, a zod inputSchema and outputSchema, and return { content: [{ type:'text', text: <markdown table> }], structuredContent: <object matching outputSchema> }.

4.1 `get_usage`

  • title: "CodeBurn โ€” usage & cost"
  • description (agent-facing): "Show AI coding token spend and usage for a period. Omit by for a headline summary; set by to break it down by project, model, task, or provider. Fast (does not run the deeper savings analysis). Data is local to this machine and current as of the last scan."
  • inputSchema:
    • period?: enum (default today)
    • by?: "project" | "model" | "task" | "provider"
    • limit?: number (default 20, max 100) โ€” row cap for breakdowns
    • include_project_names?: boolean (default false)
  • Behavior: no by โ†’ headline (cost, calls, sessions, input/output tokens, cache-hit %, one-shot rate) + top-N models/projects/tasks. With by โ†’ one ranked table for that dimension (projectโ†’topProjects, modelโ†’topModels, taskโ†’topActivities, providerโ†’providers cost map). Uses buildUsage (cheap path).
  • outputSchema: the relevant subset of MenubarPayload.current (typed).

4.2 `get_savings`

  • title: "CodeBurn โ€” savings opportunities"
  • description (agent-facing): "Find ways to reduce AI coding cost for a period: optimization findings, retry tax (money spent re-doing work), and routing waste (what you'd have saved on a cheaper model). Runs a deeper analysis, so it is slower than get_usage."
  • inputSchema: period?: enum (default last_7_days โ€” never default to the slow last_6_months), include_project_names?: boolean (default false).
  • Behavior: runs buildSavings; returns optimize findings (title, impact, $ saved), retry tax (total + by model), routing waste (total savings, baseline model, by model).
  • outputSchema: { optimize, retryTax, routingWaste } (typed).

4.3 Server `instructions`

"CodeBurn exposes local AI-coding spend data. Use get_usage for spend/usage and breakdowns (fast); use get_savings to find cost reductions (slower). Project names are pseudonymized unless you pass include_project_names: true. All data is read locally from this machine; last_6_months is the widest window. Numbers reflect the most recent scan and may lag the current in-flight session by a short interval."

5. Data Flow

text
agent tool call
  โ†’ zod inputSchema validation        (invalid params โ†’ protocol error, auto)
  โ†’ in-flight coalesce on {kind, period, opts-hash}  (parallel callers share one scan)
  โ†’ buildUsage / buildSavings(period, {provider:'all', ...})
        โ†’ parseAllSessions with PER-PROVIDER allSettled isolation โ†’ degraded[]
        โ†’ existing 180s in-process session cache amortizes
  โ†’ redact project/session names unless include_project_names
  โ†’ render markdown table + build structuredContent (validated vs outputSchema)
  โ†’ return { content:[{type:'text',text}], structuredContent }   (isError:true on failure)

6. Privacy / Redaction

  • Name-bearing fields โ€” topProjects[].name, topSessions[].project, topProjects[].sessionDetails โ€” are replaced with stable pseudonyms project-<6hex(sha256(name))> unless include_project_names: true.
  • Absolute paths are never emitted (they aren't in the payload today; the MCP must not enrich with them).
  • Rationale: an MCP is an egress surface to possibly-remote/cloud agents; codeburn's brand is "data never leaves your machine." Hashing keeps breakdowns coherent (stable pseudonyms) without leaking identity; local users who want real names opt in per call.

7. Performance

  • Two paths: get_usage uses the cheap aggregation (--no-optimize seam already exists; ~3s today, 5s all). get_savings runs the optimize pass (+13s) โ€” isolated to the one tool that needs it. Without this split, every tool paid the 13s tax.
  • Resident process + existing 180s session cache โ†’ warm calls are cheap.
  • In-flight coalescing: concurrent calls for the same {kind, period, opts} await a single scan (agents fire tools in parallel).
  • Pre-warm today usage on boot.
  • Token discipline: breakdowns capped at limit (default 20); history is summarized (totals + top-N), never the full 365-day array; tables are compact.

8. Error Handling

  • Invalid params โ†’ zod โ†’ MCP protocol error (auto).
  • No data for period (fresh install) โ†’ isError: false with a friendly "no usage recorded for yet โ€” run some coding sessions" message (not a zero-filled table).
  • One provider fails to parse โ†’ isolate via allSettled, return partial data, list skipped providers in degraded[] and a table footer note.
  • Aggregation throw / payload-shape mismatch โ†’ isError: true with a clear message (incl. version-skew hint); never hang the transport.

9. Testing

  • Parity: buildUsage('today', {provider:'all'}) deep-equals current status --format menubar-json --period today (plus one more period). Guards the extraction.
  • Per-tool: fixture payload โ†’ expected markdown table (snapshot) + structuredContent keys; by each dimension; redaction on/off (no real names/paths leak when off; pseudonyms stable); empty-state message.
  • MCP protocol smoke: in-memory transport โ€” listTools returns the 2 tools with annotations + outputSchema; call each; assert result shape (content + structuredContent) and isError paths; concurrent identical calls coalesce to one scan.
  • Build: assert SDK + zod are externalized (absent from dist) and declared in dependencies.

10. Out of Scope (v1 โ€” YAGNI)

  • HTTP/SSE transport (stdio only).
  • compare_periods (agent can diff two get_usage calls; backend is net-new and overlap-incoherent).
  • Write/mutating tools, auth, multi-machine/remote data.
  • MCP prompts and resources primitives (tools-only v1; revisit if a "cost-review" prompt template proves useful).
  • README "MCP" section and MCP-registry listings (lobehub/Smithery/mcp.so) โ€” follow-up tasks, not code.

11. Provenance โ€” review findings incorporated

Hardened by a validator + devil's-advocate pass:

  • BLOCKER in-process resident server (caches don't help exec-per-call) โ€” ยง3, ยง7.
  • BLOCKER split cheap vs optimize path (optimize โ‰ˆ 70% of cost) โ€” ยง7.
  • BLOCKER redact names by default (raw repo names + dated spend would egress) โ€” ยง6.
  • MAJOR buildUsage/buildSavings signature carries provider/project/range for parity โ€” ยง3.
  • MAJOR tsup external + deps in dependencies; pin SDK ^1.29.0; add zod explicitly โ€” ยง3.
  • MAJOR per-provider allSettled isolation + degraded[] โ€” ยง5, ยง8.
  • MAJOR in-flight coalescing โ€” ยง5, ยง7.
  • MAJOR drop compare_periods; rename period enums; default today โ€” ยง4, ยง10.
  • MINOR console.logโ†’console.error stdout guard; friendly empty-state; run compiled dist not tsx; validate payload shape; last_6_months is the real max โ€” ยง3, ยง8, ยง4.

File: docs/design/codeburn-mcp-plan.md

CodeBurn MCP Server โ€” Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a codeburn mcp stdio MCP server exposing CodeBurn's usage/cost data to AI agents via two tools (get_usage, get_savings), each returning a markdown table plus typed structured JSON.

Architecture: Extract the existing status --format menubar-json aggregation into one reusable buildMenubarPayloadForRange(periodInfo, opts) (with an optimize boolean โ€” the only expensive call, scanAndDetect, is skipped for get_usage). A long-lived in-process McpServer registers the two tools, injects the aggregator for testability, coalesces concurrent calls, hashes project names by default, and relies on the existing 180 s parser cache for warm reuse.

Tech Stack: TypeScript (ESM, type: module, node โ‰ฅ 22.13), commander, @modelcontextprotocol/sdk@^1.29 (v1), zod, tsup, vitest.


File Structure

  • Create src/usage-aggregator.ts โ€” owns buildPeriodData (moved from main.ts) and the extracted buildMenubarPayloadForRange(periodInfo, opts): Promise<MenubarPayload>. Single responsibility: turn a resolved date range + filters into a MenubarPayload.
  • Create src/mcp/redact.ts โ€” pseudonym() + redactProjectNames(payload, include). Privacy only.
  • Create src/mcp/tables.ts โ€” markdown renderers (renderSummaryTable, renderBreakdownTable, renderSavingsTable). Presentation only.
  • Create src/mcp/server.ts โ€” createServer(deps) (tool registration + handlers, aggregator injectable) and startStdioServer(version) (loadPricing + pre-warm + stdio transport).
  • Modify src/main.ts โ€” import buildPeriodData/buildMenubarPayloadForRange from the aggregator; refactor the status menubar branch to call the aggregator; add .command('mcp').
  • Modify package.json โ€” add @modelcontextprotocol/sdk + zod deps.
  • Modify tsup.config.ts โ€” external: ['@modelcontextprotocol/sdk', 'zod'].
  • Create tests โ€” tests/usage-aggregator.test.ts, tests/mcp-redact.test.ts, tests/mcp-tables.test.ts, tests/mcp-server.test.ts.

Internal MCP period names map to CodeBurn's: todayโ†’today, last_7_daysโ†’week, last_30_daysโ†’30days, month_to_dateโ†’month, last_6_monthsโ†’all (โ‰ˆ6 months).


Task 1: Add dependencies and externalize them in the bundle

Files:

  • Modify: package.json (dependencies)

  • Modify: tsup.config.ts

  • Step 1: Add runtime deps

Run: npm install @modelcontextprotocol/sdk@^1.29.0 zod@^3.25.0 --save-exact=false
Expected: both appear under "dependencies" in package.json; node_modules/@modelcontextprotocol/sdk and node_modules/zod exist.

  • Step 2: Externalize them so they are not inlined into dist/main.js

Edit tsup.config.ts to:

ts
import { defineConfig } from 'tsup'

export default defineConfig({
  entry: ['src/main.ts'],
  format: ['esm'],
  target: 'node20',
  outDir: 'dist',
  clean: true,
  splitting: false,
  sourcemap: true,
  dts: false,
  external: ['@modelcontextprotocol/sdk', 'zod'],
})
  • Step 3: Build and confirm the SDK is external (not bundled)

Run: npm run build && node -e "const s=require('fs').readFileSync('dist/main.js','utf8'); if(/from'@modelcontextprotocol\/sdk|require\(.@modelcontextprotocol\/sdk./.test(s.replace(/\s/g,''))||!/McpServer/.test(s)){} console.log('built, bytes', s.length)"
Expected: build succeeds, prints built, bytes <n>. (The SDK isn't imported anywhere yet, so this just verifies the config builds.)

  • Step 4: Commit
bash
git add package.json package-lock.json tsup.config.ts
git commit -m "build(mcp): add @modelcontextprotocol/sdk + zod, externalize in tsup"

Task 2: Move `buildPeriodData` into a shared module

buildPeriodData is currently a private function in main.ts:410. The aggregator needs it, so move it to the new module and import it back into main.ts.

Files:

  • Create: src/usage-aggregator.ts

  • Modify: src/main.ts:410 (remove local def), import section

  • Step 1: Create the module with buildPeriodData moved verbatim

Create src/usage-aggregator.ts. Move the entire function buildPeriodData(label: string, projects: ProjectSummary[]): PeriodData { ... } body from main.ts into it, exported, with imports it needs:

ts
import type { ProjectSummary } from './types.js'
import { type PeriodData } from './menubar-json.js'

export function buildPeriodData(label: string, projects: ProjectSummary[]): PeriodData {
  // ... exact body moved from main.ts:410 ...
}

(Copy the body unchanged. Add any imports the body references โ€” e.g. getShortModelName from ./models.js โ€” until npx tsc --noEmit is clean.)

  • Step 2: Update main.ts to import it and delete the local copy

Remove the function buildPeriodData(...) {...} at main.ts:410. Add to the import block near the top:

ts
import { buildPeriodData } from './usage-aggregator.js'
  • Step 3: Typecheck + run the full suite (parity guard)

Run: npx tsc --noEmit && npm test
Expected: typecheck clean; all existing tests pass (the status paths still use buildPeriodData, now imported).

  • Step 4: Commit
bash
git add src/usage-aggregator.ts src/main.ts
git commit -m "refactor: move buildPeriodData into usage-aggregator module"

Task 3: Extract `buildMenubarPayloadForRange`

Move the status menubar-json aggregation block (main.ts:485โ€“759, everything after periodInfo/now are computed, ending at the console.log) into the aggregator as a function that returns the payload instead of printing it.

Files:

  • Modify: src/usage-aggregator.ts (add the function)

  • Modify: src/main.ts:476โ€“761 (call it)

  • Test: existing tests/cli-status-menubar.test.ts is the parity guard

  • Step 1: Add the function signature + types to src/usage-aggregator.ts

ts
import { homedir } from 'node:os'
import type { ProjectSummary, DateRange } from './types.js'
import { type PeriodData, type ProviderCost, type BreakdownArrays, type MenubarPayload, buildMenubarPayload } from './menubar-json.js'
import { parseAllSessions, getAllProviders, filterProjectsByName, filterProjectsByDays } from './parser.js'
import { aggregateProjectsIntoDays, buildPeriodDataFromDays } from './day-aggregator.js'
import { aggregateModelEfficiency } from './model-efficiency.js'
import { scanAndDetect } from './optimize.js'
import { hydrateCache, loadDailyCache, getDaysInRange, toDateString, BACKFILL_DAYS, type DailyCache } from './daily-cache.js'

export type PeriodInfo = { range: DateRange; label: string }
export type AggregateOpts = {
  provider?: string
  project?: string[]
  exclude?: string[]
  daysSelection?: { range: DateRange; label: string; days: Set<string> } | null
  optimize?: boolean
}

export async function buildMenubarPayloadForRange(
  periodInfo: PeriodInfo,
  opts: AggregateOpts = {},
): Promise<MenubarPayload> {
  const pf = opts.provider ?? 'all'
  const daysSelection = opts.daysSelection ?? null
  const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project ?? [], opts.exclude ?? [])
  // ... moved block from main.ts:485โ€“757 (now/todayStart/... through breakdowns) ...
  const optimize = opts.optimize === false ? null : await scanAndDetect(scanProjects, scanRange)
  return buildMenubarPayload(currentData, providers, optimize, dailyHistory, retryTax, routingWaste, breakdowns)
}

Move lines main.ts:485โ€“757 (from const now = new Date() through the breakdowns IIFE) into the body unchanged. The block already references only the symbols imported above plus homedir(). hydrateCache is imported from ./daily-cache.js (same module as loadDailyCache); if tsc reports a different source, follow the import it suggests.

  • Step 2: Precondition note โ€” pricing

The block assumes loadPricing() already ran. The status action calls it at main.ts:473; the MCP server will call it at startup (Task 6). Do not call loadPricing() inside the function.

  • Step 3: Refactor the status menubar branch to call it

Replace main.ts:485โ€“760 (the inline block + console.log + return) with:

ts
const payload = await buildMenubarPayloadForRange(periodInfo, {
  provider: pf,
  project: opts.project,
  exclude: opts.exclude,
  daysSelection,
  optimize: opts.optimize !== false,
})
console.log(JSON.stringify(payload))
return

Keep main.ts:477โ€“484 (the daysSelection/customRange/daySelection/periodInfo resolution) โ€” periodInfo and daysSelection are the inputs now passed in. Add buildMenubarPayloadForRange to the existing import from ./usage-aggregator.js.

  • Step 4: Typecheck + parity test

Run: npx tsc --noEmit && npx vitest run cli-status-menubar
Expected: clean typecheck; tests/cli-status-menubar.test.ts passes โ€” i.e. status --format menubar-json output is unchanged (parity).

  • Step 5: Add a direct unit test for the aggregator

Create tests/usage-aggregator.test.ts:

ts
import { describe, expect, it } from 'vitest'
import { buildMenubarPayloadForRange } from '../src/usage-aggregator.js'
import { getDateRange } from '../src/cli-date.js'

describe('buildMenubarPayloadForRange', () => {
  it('returns a zero payload with no data and skips optimize when optimize:false', async () => {
    const payload = await buildMenubarPayloadForRange(getDateRange('today'), { provider: 'all', optimize: false })
    expect(payload.current.cost).toBe(0)
    expect(payload.current.calls).toBe(0)
    expect(payload.optimize.findingCount).toBe(0)
    expect(Array.isArray(payload.current.topProjects)).toBe(true)
  })
})

Run: npx vitest run usage-aggregator
Expected: PASS (uses the empty real environment; scanAndDetect not called because optimize:false).

  • Step 6: Commit
bash
git add src/usage-aggregator.ts src/main.ts tests/usage-aggregator.test.ts
git commit -m "refactor: extract buildMenubarPayloadForRange for reuse by MCP"

Task 4: Project-name redaction

Files:

  • Create: src/mcp/redact.ts

  • Test: tests/mcp-redact.test.ts

  • Step 1: Write the failing test

Create tests/mcp-redact.test.ts:

ts
import { describe, expect, it } from 'vitest'
import { pseudonym, redactProjectNames } from '../src/mcp/redact.js'
import type { MenubarPayload } from '../src/menubar-json.js'

function payload(): MenubarPayload {
  const base = {
    name: 'secret-client-repo', cost: 5, sessions: 2, avgCostPerSession: 2.5, sessionDetails: [],
  }
  return {
    generated: '', optimize: { findingCount: 0, savingsUSD: 0, topFindings: [] }, history: { daily: [] },
    current: {
      label: 'Today', cost: 5, calls: 10, sessions: 2, oneShotRate: null, inputTokens: 0, outputTokens: 0,
      cacheHitPercent: 0, topActivities: [], topModels: [], providers: {},
      topProjects: [base], modelEfficiency: [],
      topSessions: [{ project: 'secret-client-repo', cost: 5, calls: 10, date: '2026-06-01' }],
      retryTax: { totalUSD: 0, retries: 0, editTurns: 0, byModel: [] },
      routingWaste: { totalSavingsUSD: 0, baselineModel: '', baselineCostPerEdit: 0, byModel: [] },
      tools: [], skills: [], subagents: [], mcpServers: [],
    },
  } as MenubarPayload
}

describe('redact', () => {
  it('pseudonym is stable and path-free', () => {
    expect(pseudonym('a')).toBe(pseudonym('a'))
    expect(pseudonym('secret-client-repo')).toMatch(/^project-[0-9a-f]{6}$/)
  })
  it('hashes project names by default, preserves numbers', () => {
    const out = redactProjectNames(payload(), false)
    expect(out.current.topProjects[0]!.name).toMatch(/^project-[0-9a-f]{6}$/)
    expect(out.current.topSessions[0]!.project).toMatch(/^project-[0-9a-f]{6}$/)
    expect(out.current.topProjects[0]!.cost).toBe(5)
  })
  it('keeps real names when include=true', () => {
    const out = redactProjectNames(payload(), true)
    expect(out.current.topProjects[0]!.name).toBe('secret-client-repo')
  })
})
  • Step 2: Run to verify failure

Run: npx vitest run mcp-redact
Expected: FAIL ("Cannot find module '../src/mcp/redact.js'").

  • Step 3: Implement

Create src/mcp/redact.ts:

ts
import { createHash } from 'node:crypto'
import type { MenubarPayload } from '../menubar-json.js'

export function pseudonym(name: string): string {
  return `project-${createHash('sha256').update(name).digest('hex').slice(0, 6)}`
}

export function redactProjectNames(payload: MenubarPayload, includeNames: boolean): MenubarPayload {
  if (includeNames) return payload
  return {
    ...payload,
    current: {
      ...payload.current,
      topProjects: payload.current.topProjects.map(p => ({ ...p, name: pseudonym(p.name) })),
      topSessions: payload.current.topSessions.map(s => ({ ...s, project: pseudonym(s.project) })),
    },
  }
}
  • Step 4: Run to verify pass

Run: npx vitest run mcp-redact
Expected: PASS (3 tests).

  • Step 5: Commit
bash
git add src/mcp/redact.ts tests/mcp-redact.test.ts
git commit -m "feat(mcp): hash project names by default with opt-in reveal"

Task 5: Markdown table renderers

Files:

  • Create: src/mcp/tables.ts

  • Test: tests/mcp-tables.test.ts

  • Step 1: Write the failing test

Create tests/mcp-tables.test.ts:

text
/* Detailed source-code truncated for AI context efficiency. */
  • Step 2: Run to verify failure

Run: npx vitest run mcp-tables
Expected: FAIL ("Cannot find module '../src/mcp/tables.js'").

  • Step 3: Implement

Create src/mcp/tables.ts:

text
/* Detailed source-code truncated for AI context efficiency. */
  • Step 4: Run to verify pass

Run: npx vitest run mcp-tables
Expected: PASS (4 tests).

  • Step 5: Commit
bash
git add src/mcp/tables.ts tests/mcp-tables.test.ts
git commit -m "feat(mcp): markdown table renderers for usage and savings"

Task 6: MCP server (tools, schemas, handlers, coalescing)

Files:

  • Create: src/mcp/server.ts

  • Test: tests/mcp-server.test.ts

  • Step 1: Write the failing test (in-memory transport, injected aggregator)

Create tests/mcp-server.test.ts:

text
/* Detailed source-code truncated for AI context efficiency. */
  • Step 2: Run to verify failure

Run: npx vitest run mcp-server
Expected: FAIL ("Cannot find module '../src/mcp/server.js'").

  • Step 3: Implement the server

Create src/mcp/server.ts:

text
/* Detailed source-code truncated for AI context efficiency. */

If the installed SDK rejects the raw-shape inputSchema/outputSchema (object of zod validators), wrap each in z.object({ ... }) โ€” the in-memory test in Step 4 will surface this immediately. Likewise, if InMemoryTransport's import path differs, it is exported from @modelcontextprotocol/sdk/inMemory.js in v1.

  • Step 4: Run to verify pass

Run: npx vitest run mcp-server
Expected: PASS (5 tests).

  • Step 5: Commit
bash
git add src/mcp/server.ts tests/mcp-server.test.ts
git commit -m "feat(mcp): get_usage + get_savings tools with annotations, schemas, coalescing"

Task 7: Wire the `codeburn mcp` command

Files:

  • Modify: src/main.ts (add command + stdout guard)

  • Step 1: Add the command

In src/main.ts, alongside the other .command(...) registrations (e.g. after the status block), add:

ts
program
  .command('mcp')
  .description('Run a Model Context Protocol server (stdio) exposing usage + savings to AI agents')
  .action(async () => {
    // stdout MUST carry only JSON-RPC; route stray logs to stderr.
    console.log = ((...args: unknown[]) => process.stderr.write(args.join(' ') + '\n')) as typeof console.log
    const { startStdioServer } = await import('./mcp/server.js')
    await startStdioServer(version)
  })

(version is already in scope at main.ts:30.)

  • Step 2: Build

Run: npm run build
Expected: succeeds. src/mcp/server.ts is reachable from the main.ts import graph (via the dynamic import), so tsup bundles it; the @modelcontextprotocol/sdk and zod imports stay external (Task 1).

  • Step 3: Smoke-test the built server over stdio

Run:

bash
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"smoke","version":"1"}}}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | node dist/cli.js mcp 2>/dev/null | head -2

Expected: two JSON-RPC result lines on stdout; the second lists get_usage and get_savings. No non-JSON noise on stdout (warnings, if any, went to stderr).

  • Step 4: Verify the SDK is external in the bundle

Run: node -e "const s=require('fs').readFileSync('dist/main.js','utf8'); console.log('McpServer source inlined?', /class McpServer/.test(s))"
Expected: McpServer source inlined? false (it's imported from node_modules at runtime, not bundled).

  • Step 5: Commit
bash
git add src/main.ts
git commit -m "feat(mcp): add 'codeburn mcp' stdio command with stdout guard"

Task 8: Full suite + final verification

Files: none (verification)

  • Step 1: Run everything

Run: npx tsc --noEmit && npm test && npm run build
Expected: typecheck clean; all tests pass (incl. the pre-existing suite โ€” parity preserved); build succeeds.

  • Step 2: Manual end-to-end against real data (optional but recommended)

Run: node dist/cli.js mcp then, in another shell, point a local MCP client (or the smoke command from Task 7) at it and call get_usage {"period":"today"} and get_savings {"period":"last_7_days"}. Confirm tables render and project names are hashed.

  • Step 3: Commit any cleanup
bash
git add -A && git commit -m "chore(mcp): final verification" || echo "nothing to commit"

Deferred (not in v1 โ€” see spec ยง10 and note below)

  • Per-provider graceful degradation (degraded[]). The spec (ยง5/ยง8) called for allSettled per-provider isolation. That requires changing the shared parseAllSessions loop (parser.ts:2133), which every command uses โ€” out of scope for v1 to avoid destabilizing the parser. v1 handles failures at the tool boundary (isError: true with the message). A malformed provider aborting a scan is a pre-existing behavior shared with the status/menubar path, not introduced here. This is the one intentional deviation from the committed spec.
  • HTTP/SSE transport, compare_periods, MCP prompts/resources, README + registry listings.

Self-Review

  • Spec coverage: in-process stdio server (Task 6/7) โœ“; cheap vs optimize split via optimize flag (Task 3, refined โ€” scanAndDetect is the only expensive call) โœ“; 2 tools with annotations + outputSchema + isError (Task 6) โœ“; hash-by-default redaction (Task 4) โœ“; in-flight coalescing + pre-warm (Task 6) โœ“; tsup external + pinned deps (Task 1) โœ“; stdout guard (Task 7) โœ“; period-enum rename + last_6_months semantics (Task 6 + instructions) โœ“; empty-state message (Task 6) โœ“; token discipline via limit + summarized history (Task 6, no daily array returned) โœ“. Deviation: per-provider degraded[] deferred (documented above).
  • Placeholders: none โ€” every code step has full source; the only "move verbatim" steps (Tasks 2โ€“3) are mechanical relocations of existing, cited code, gated by the existing parity test.
  • Type consistency: buildMenubarPayloadForRange(PeriodInfo, AggregateOpts), redactProjectNames(MenubarPayload, boolean), renderBreakdownTable(payload, BreakdownBy, limit), createServer({version, aggregate}) โ€” names/signatures consistent across tasks and tests.

File: docs/providers/antigravity.md

Antigravity

Google Antigravity (CLI and IDE). CodeBurn discovers session files on disk and queries the local language-server RPC endpoint to parse them.

  • Source: src/providers/antigravity.ts
  • Loading: lazy via src/providers/index.ts. Lazy because the protobuf dependency is heavy.
  • Test: focused helper coverage in tests/providers/antigravity.test.ts.

Where it reads from

CodeBurn discovers Antigravity sessions from local directories on disk, then queries the live language-server process (if running) to fetch detailed trajectory generator metadata:

  1. Session Discovery: It scans the following folders for .pb or .db files:
    • Antigravity CLI: %USERPROFILE%\.gemini\antigravity-cli\conversations (and implicit)
    • Antigravity App/older path: %USERPROFILE%\.gemini\antigravity\conversations
    • Antigravity IDE: %USERPROFILE%\.gemini\antigravity-ide\conversations (and implicit). The IDE also maintains VSCode-style global state at %APPDATA%\Antigravity IDE\User\globalStorage\state.vscdb, but that DB stores trajectory metadata (titles, timestamps, workspace paths) โ€” not token usage. Token usage data still comes from the .db conversation files.
  2. Language Server RPC Query: It locates the active language-server process via ps on POSIX or Get-CimInstance Win32_Process on Windows. It extracts the port and CSRF token from the process arguments, and queries the local HTTPS RPC endpoint GetCascadeTrajectoryGeneratorMetadata to parse the session.
  3. Cache Fallback: If the language server is not running, it falls back to the local results cache.

Antigravity exposes slightly different process flags across platforms:
POSIX builds have used --https_server_port and --csrf_token; Windows
builds can expose --extension_server_port and
--extension_server_csrf_token. Both space-separated and --flag=value
forms are supported. The parser identifies the target app type using the --app-data-dir flag (e.g., antigravity, antigravity-cli, or antigravity-ide).

For Antigravity CLI (agy), CodeBurn can also install an opt-in status line
hook with codeburn antigravity-hook install. The hook records the CLI's
sanitized context_window.current_usage payload while agy is still alive,
without prompts or local working-directory paths. It also attempts a best-effort
RPC snapshot for full response metadata. The installed command points at a
persistent codeburn binary from PATH rather than a local build artifact, and
running codeburn antigravity-hook install again repairs older CodeBurn-owned
statusLine commands that used stale absolute paths. Remove it with codeburn antigravity-hook uninstall; if --force replaced an existing statusLine
command, uninstall restores that previous command.

Storage format

Protobuf. Cascade and response objects map to ParsedProviderCall directly.

Caching

Custom file cache at $CODEBURN_CACHE_DIR/antigravity-results.json (defaults to ~/.cache/codeburn/). The cache is also used as the data source when the RPC endpoint is unavailable, not just as an optimization. Bumping the cache version forces a recompute.

Deduplication

Per <cascadeId>:<responseId> for RPC data. The status line fallback collapses
repeated identical usage snapshots, ignores singleton intermediate snapshots
when a later stabilized usage total is observed for the same conversation, and
uses positive deltas for monotonic snapshots so cumulative counters are not
double-counted.

Quirks

  • Antigravity is the only provider that requires a live process. A user who closes Antigravity loses the most-recent data until next launch (the cache covers older runs).
  • Antigravity CLI has a shorter capture window than the desktop app. agy
    exposes its language server only while the CLI session is active. The status
    line hook closes that gap for future sessions; older CLI .pb files still
    cannot be priced exactly unless an RPC snapshot was captured.
  • The 16 MB cap on RPC responses is necessary because individual cascades can balloon. Raising it risks OOM on the user's machine.
  • Token types are split across inputTokens, responseOutputTokens, and thinkingOutputTokens. Thinking is billed at output rate.

When fixing a bug here

  1. Reproducing the full provider path requires Antigravity running locally.
    The unit tests cover process flag parsing and wrapped/unwrapped RPC response
    extraction, but they do not stand up a live Antigravity RPC endpoint.
  2. Before any change, capture a sample protobuf response (anonymized) so future regressions can be tested against a recording.
  3. If the bug is "no data after Antigravity update", the protobuf schema may have shifted. The parser's response handling is the place to look.
  4. If the bug is "stale data", check whether the RPC is reachable; the cache fallback can mask connectivity issues.

File: docs/providers/claude.md

Claude

Anthropic Claude Code CLI and Claude Desktop's local agent mode.

  • Source: src/providers/claude.ts
  • Loading: eager (src/providers/index.ts:1)
  • Test: none directly. Coverage comes from tests/parser-claude-cwd.test.ts, tests/parser-filter.test.ts, and tests/parser-mcp-inventory.test.ts, which exercise src/parser.ts end-to-end against fixture session files.

Where it reads from

Source Path
Claude Code CLI $CLAUDE_CONFIG_DIR if set, otherwise ~/.claude/projects/
Claude Desktop (macOS) ~/Library/Application Support/Claude/local-agent-mode-sessions/
Claude Desktop (Windows, classic) %APPDATA%/Claude/local-agent-mode-sessions/
Claude Desktop (Windows, MSIX) %LOCALAPPDATA%/Packages/<Claude package>/LocalCache/Roaming/Claude/local-agent-mode-sessions/
Claude Desktop (Linux) ~/.config/Claude/local-agent-mode-sessions/

For Desktop, findDesktopProjectDirs walks up to 8 levels deep looking for projects/ subdirectories, skipping node_modules and .git.

Desktop session roots are resolved in this order:

  1. A non-empty CODEBURN_DESKTOP_SESSIONS_DIR overrides discovery and is the
    only returned root.
  2. macOS uses the single path shown above.
  3. Windows always includes the classic path first. It then scans
    %LOCALAPPDATA%/Packages for package directories whose names start with
    Claude_ or contain .Claude_, sorted by package name, and includes only
    packages whose full MSIX sessions path exists as a directory.
  4. Other platforms use the single Linux path shown above.

All returned roots are absolute, resolved, and deduplicated. Missing or
unreadable Windows package directories are ignored.

Storage format

JSONL, one event per line, per session file. Sessions live under /<sessionId>.jsonl.

Parser

createSessionParser returns an empty async generator (claude.ts:101-105). Claude is a special case: src/parser.ts reads Claude JSONL files directly with full turn grouping, dedup of streaming message IDs, and MCP tool inventory extraction. The provider object exists only so discoverSessions can return Claude session sources alongside the others.

Pricing

Claude Code reports total cache-write tokens in usage.cache_creation_input_tokens.
When available, it also splits those writes by duration in
usage.cache_creation.ephemeral_5m_input_tokens and
usage.cache_creation.ephemeral_1h_input_tokens. CodeBurn keeps the existing
aggregate cache-write token total for reports, but prices the 1-hour portion at
2x base input cost (1.6x the 5-minute cache-write rate exposed by LiteLLM).
If the split fields are missing, the parser falls back to the legacy behavior
and prices every cache write at the 5-minute rate.

Caching

None at the provider level. The daily aggregation cache (src/daily-cache.ts) reuses prior computed days.

Quirks

  • The parser is in src/parser.ts, not in src/providers/claude.ts. Anything that changes Claude parsing belongs in parser.ts.
  • Streaming responses produce duplicate message IDs across resumed sessions; parser.ts strips them via the global seenMsgIds Set.
  • Model display names are mapped in claude.ts:7-20; add new versions there when Anthropic releases them.

When fixing a bug here

  1. Confirm whether the bug is in discovery (sessions not picked up) or parsing (sessions found but data wrong).
  2. Discovery bugs live in claude.ts:78-99. Verify the directory layout you expect actually matches what Claude writes today.
  3. Parsing bugs live in src/parser.ts. Look for parseSessionFile, groupIntoTurns, and dedupeStreamingMessageIds.
  4. Add a fixture under tests/fixtures/ and a test under tests/parser-claude-cwd.test.ts (or a new file). Do not mock the filesystem.

File: docs/providers/cline.md

Cline

Cline VS Code extension and Cline home-data task storage.

Sessions from the Cline command-line agent use an unrelated layout and are handled by Cline CLI; this provider does not see them.

  • Source: src/providers/cline.ts
  • Loading: eager (src/providers/index.ts:2)
  • Test: tests/providers/cline.test.ts

Where it reads from

These task roots are scanned:

  1. VS Code extension globalStorage for saoudrizwan.claude-dev, in every supported VS Code variant: stable (Code), Insiders (Code - Insiders), and VSCodium. The per-platform paths come from getVSCodeGlobalStoragePaths in src/providers/vscode-cline-parser.ts, the same helper Roo Code and KiloCode use.
  2. Cline's home-data root at ~/.cline/data.

Every root is expected to contain a tasks/ child directory. Discovery is delegated to discoverClineTasks in src/providers/vscode-cline-parser.ts, so a task is only included when it has a ui_messages.json file.

Storage format

Per-task directories with:

text
tasks/<taskId>/
  ui_messages.json
  api_conversation_history.json
  task_metadata.json

ui_messages.json provides the api_req_started usage entries. api_conversation_history.json is used for model extraction. See vscode-cline-parser for the full schema description.
task_metadata.json is part of Cline's task layout but is not read by CodeBurn today.

Caching

None at the provider level; delegates to the shared helper and normal parser/cache layers.

Deduplication

Discovery deduplicates by task id across all Cline roots so a task that exists in more than one root (a migration, or the same extension storage seen by two VS Code variants) is not scanned twice. If the same task id exists in multiple roots, the one with the newest ui_messages.json wins. Parsing still uses the shared per-call key: :<taskId>:.

Quirks

  • This provider is intentionally a thin wrapper over the shared Cline-family parser.
  • Cline can keep data in both VS Code globalStorage and ~/.cline/data, depending on version and workflow.
  • A user can run Cline in VS Code stable, Insiders, and VSCodium at the same time; each variant has its own globalStorage tree, so all of them must be scanned.
  • If Cline changes the JSON shape, fix vscode-cline-parser.ts only if Roo Code and KiloCode still pass. Branch provider-specific parsing rather than duplicating the whole parser.

When fixing a bug here

  1. Reproduce with a minimal task directory containing ui_messages.json and api_conversation_history.json.
  2. Run tests/providers/cline.test.ts, plus tests/providers/roo-code.test.ts and tests/providers/kilo-code.test.ts if the shared parser changes.
  3. Keep the provider name cline; downstream filters and dedup keys depend on it.

File: docs/providers/codex.md

Codex

OpenAI Codex CLI.

  • Source: src/providers/codex.ts
  • Loading: eager (src/providers/index.ts:2)
  • Test: tests/providers/codex.test.ts (1075 lines)

Where it reads from

$CODEX_HOME if set, otherwise ~/.codex. Active sessions are nested by date:

text
~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-*.jsonl

Archived sessions are stored in a flat directory and are included in usage reports:

text
~/.codex/archived_sessions/rollout-*.jsonl

The active-session discovery walk uses strict regex (^\d{4}$, ^\d{2}$) on each path component.

Storage format

JSONL. Validation of the first line is structural: it must parse as JSON, have type === "session_meta", and carry a payload that is a plain object (not missing, not a scalar, not an array). Files that fail this check are silently skipped.

payload.originator is deliberately not part of the check. It is a free-form client identity string, not a format marker: Codex CLI writes codex-tui / codex_exec / codex_cli_rs, Codex Desktop writes Codex Desktop, and third-party frontends driving codex app-server write their own values (t3code_desktop, JetBrains.IntelliJ IDEA, ...) into structurally identical rollouts. Gating discovery on the spelling silently dropped those sessions from every report and required a new allowlist entry per client (issues #626, #873). Directory ownership decides the provider instead: codex.ts is the only provider that reads ~/.codex, and the walk only visits rollout-*.jsonl under the strict YYYY/MM/DD path or archived_sessions/. originator is still parsed into the session meta entry, but nothing downstream reads it.

Because admission no longer implies a known client, every payload field is treated as untrusted JSON. payload.cwd in particular is type-guarded before it reaches sanitizeProject (discovery) or projectPath/workingDirectory (parse): a non-string cwd falls back to the unknown project instead of throwing out of discoverSessions, which safeDiscoverSessions would have turned into an empty session list for the entire provider.

The first line read is capped at 1 MB (FIRST_LINE_READ_CAP). Codex CLI 0.128+ embeds the full system prompt in session_meta, which can run 20-27 KB; the cap leaves headroom while bounding memory if a corrupt file has no newline.

Caching

src/codex-cache.ts writes ~/.cache/codeburn/codex-results.json (or $CODEBURN_CACHE_DIR/codex-results.json). Each entry is keyed by absolute file path and validated against mtimeMs + sizeBytes. Cached entries are returned wholesale.

A session that yielded zero parseable lines does not write to the cache (codex.ts:419); this prevents a transient read failure from pinning an empty result against a fingerprint.

Deduplication

codex:<sessionId>:<timestamp>:<cumulativeTotal> for accounted events, plus codex:<sessionId>:<timestamp>:est<n> for estimated events that fall back to char-counting.

Quirks

  • Codex CLI emits both last_token_usage (per turn) and total_token_usage (cumulative). The parser handles three modes:
    1. last_token_usage present: use it directly.
    2. Only cumulative: compute deltas against the prior turn.
    3. Neither: estimate from message text length (CHARS_PER_TOKEN = 4).
  • prevCumulativeTotal is initialized to null, not 0. A session whose first event reports total = 0 would otherwise be dropped as a "duplicate" of the initial state.
  • prev* token counters are advanced on every event, including ones that used last_token_usage. Earlier code only updated them on the fallback branch, which double-counted any session that mixed modes.
  • OpenAI counts cached tokens inside input_tokens. The parser subtracts them so the rest of the codebase can assume Anthropic semantics (cached are separate).

Live quota (ChatGPT subscription)

Separate from the log parser above: the desktop app and the macOS menubar read
live quota from GET https://chatgpt.com/backend-api/wham/usage using the Codex
OAuth token. Two independent implementations of the same decoder, which must be
kept in sync:

  • app/electron/quota/codex.ts: decodeCodexUsage() is the pure, exported decoder.
  • mac/Sources/CodeBurnMenubar/Data/CodexSubscriptionService.swift: decodeUsage().

Seat-based plans (Plus, Pro, Team)

rate_limit.primary_window / secondary_window carry used_percent,
reset_at and limit_window_seconds. The window label is inferred from the
duration (5-hour, Weekly, โ€ฆ), never from the plan, because window size is dynamic per
account. additional_rate_limits[] holds per-model limits (Codex Spark, etc.)
and is only surfaced when utilization is non-zero.

Credit-metered plans (Business, Edu, Enterprise on flexible pricing)

These workspaces have no rate-limit windows: rate_limit comes back
null. Usage scales with credits, and an admin sets a monthly per-user credit
allowance. That allowance is the account's only limit and lives in
spend_control:

jsonc
"spend_control": {
  "reached": false,
  "individual_limit": {
    "source": "workspace_spend_controls",
    "limit": "10000",              // string
    "used": "3028.9909675121307",  // string
    "used_percent": 30,            // number
    "remaining_percent": 70,
    "reset_after_seconds": 441896, // time *remaining*, not window length
    "reset_at": 1785542400
  }
}

Notes that have bitten us:

  • Number encodings are mixed within the same object: limit and used
    arrive as strings while used_percent arrives as a number. Every numeric
    field is decoded flexibly (number | string) on both sides.
  • reset_after_seconds is not the window length. Pace projection needs the
    whole-window duration, so it is derived as the calendar month preceding
    reset_at, resolved in UTC: reset_at is a UTC boundary, and a local
    calendar would make the month length depend on the viewer's timezone (a
    2026-03-01Z reset spans 28 days in UTC but 31 in Toronto).
  • Two other positions for this object have been observed in other clients
    (top-level individual_limit, and nested under rate_limit), in both
    snake_case and camelCase. All are accepted; spend_control wins.
  • credits.has_credits means the account settles in credits, not dollars, so
    credits.balance must not be rendered with a currency symbol in that case.
    credits.unlimited means credit-metered but deliberately uncapped.
  • has_credits is not "is credit-metered". The live Enterprise workspace
    above is credit-metered (it has a spend_control allowance) yet reports
    has_credits: false with a null balance, so the flag tracks whether the
    account holds a credit balance, which is orthogonal to the allowance. Do not
    derive one from the other. The has_credits: true rendering path has not been
    observed against a real account; if a seat-based account ever reports it
    alongside a dollar balance, the footer would drop the $ and round to whole
    units.

`plan_type` cannot distinguish Business from Enterprise

A live ChatGPT Enterprise workspace reports plan_type: "business" on this
endpoint, and the id_token's https://api.openai.com/auth โ†’ chatgpt_plan_type
claim says "business" too, even though ChatGPT's own workspace switcher
displays "Enterprise". Neither source carries the distinction, so the label
CodeBurn shows is faithfully what OpenAI returns. Do not try to infer a tier
from the presence of a spend control.

The switcher renders from the accounts endpoints, and those are not reachable
with a Codex token
, verified against a live Enterprise workspace:

Endpoint Result
/backend-api/accounts/check/v4-2023-04-27 403
/backend-api/accounts/check 403
/backend-api/me 403
/backend-api/settings/account_user_setting 403

Not an expiry or a missing-header problem: the same token returns 200 on
/wham/usage (and on /backend-api/gizmo_creator_profile) in the same run. The
Codex OAuth access token carries scopes openid profile email offline_access api.connectors.read api.connectors.invoke with audience
https://api.openai.com/v1, with no ChatGPT web-app account scope, so the accounts
surfaces reject it by design. Adding a ChatGPT-Account-Id header does not
change this. Business is therefore the correct label to display; closing
this gap would need a different credential, not a different endpoint.

Composite tiers (enterprise_cbp_usage_based, self_serve_business_usage_based)
are normalized down to their base tier before lookup.

Reset credits

rate_limit_reset_credits is carried inline on the usage payload
(available_count). The dedicated GET /wham/rate-limit-reset-credits
endpoint is only called when the inline block is absent. It is the sole source
of per-credit expires_at values, so the "next expires" caption is omitted on
the inline path.

When fixing a bug here

  1. Reproduce against a real rollout-*.jsonl if you can. Drop a redacted copy under tests/fixtures/codex/ and reference it from tests/providers/codex.test.ts.
  2. If the bug is "zero tokens reported", first check whether the file is being skipped by isValidCodexSession.
  3. If the bug is "tokens counted twice", look at prevCumulativeTotal and the prev-counter advancement.
  4. If you change the dedup key shape, run tests/providers/codex.test.ts and tests/parser-filter.test.ts together; cross-provider dedup happens via the global seenKeys Set.

File: docs/providers/copilot.md

Copilot

GitHub Copilot Chat (CLI, VS Code core chat sessions, VS Code extension transcripts, and JetBrains IDE sessions).

  • Source: src/providers/copilot.ts
  • Loading: eager (src/providers/index.ts:3)
  • Test: tests/providers/copilot.test.ts

Where it reads from

Three JSONL locations plus an optional OpenTelemetry SQLite source (see below). OTel is
preferred when present; chatSessions are only discovered when no OTel source is found.
Other discovered sources are walked on every run; results merge and dedupe.

  1. Legacy CLI sessions: ~/.copilot/session-state/
  2. VS Code core chat sessions: ~/Library/Application Support/Code/User/workspaceStorage/<hash>/chatSessions/*.jsonl plus ~/Library/Application Support/Code/User/globalStorage/emptyWindowChatSessions/*.jsonl and equivalents on Windows / Linux
  3. VS Code transcripts: ~/Library/Application Support/Code/User/workspaceStorage/<hash>/GitHub.copilot-chat/transcripts/ and equivalents on Windows / Linux
  4. OTel SQLite store: VS Code Copilot Chat's agent-traces.db (see the OTel section). Preferred when present because it carries full input / output / cache token counts; legacy JSONL sources only record output tokens.
  5. JetBrains IDE sessions: ~/.config/github-copilot//<kind>/<storeId>/copilot-*-nitrite.db (see the JetBrains section). Covers IntelliJ IDEA, PyCharm, RubyMine, etc.

Storage format

JSONL in the first three locations (schemas differ; the parser switches by source type / event shape), a SQLite DB for the OTel source, and a Nitrite (H2 MVStore) .db for the JetBrains source. VS Code core chat sessions use a delta journal: kind:0 sets the root object, kind:1 writes a value at path k, and kind:2 appends items to an array path.

OpenTelemetry (OTel) source

When VS Code Copilot Chat's agent-traces.db exists, the parser reads per-LLM-call token
breakdowns (input, output, cache-read, cache-creation) from it, which the JSONL sources do
not record. Discovery is skipped with CODEBURN_COPILOT_DISABLE_OTEL=1, and the DB path
can be overridden with CODEBURN_COPILOT_OTEL_DB.

If OTel discovery finds at least one source, workspace chatSessions/*.jsonl and
emptyWindowChatSessions/*.jsonl are skipped. Those journals can mirror the same Copilot
turns under IDs that do not match OTel turn IDs, so CodeBurn prefers the richer OTel data
instead of trying to dedupe across stores.

  • Requires Node 22+. The OTel source uses the built-in node:sqlite module (the same
    backend as Cursor / OpenCode). On Node 20, or if the DB is missing / locked / corrupt /
    wrong-schema, OTel is skipped and the JSONL/transcript sources are used as a fallback.
  • Durable cache (monotonic totals). Copilot is marked durableSources: OTel-derived
    cache entries are never evicted when VS Code prunes old spans from the DB, so
    month-to-date totals do not drop as the DB rotates. Entries age out after 90 days.
  • Upgrade note. The first run after upgrading to the OTel version bumps the copilot
    parse version, which discards the prior copilot cache. Spans already pruned from the DB
    before the upgrade cannot be recovered, so monotonicity starts from the upgrade point,
    not retroactively.

JetBrains IDEs (IntelliJ, PyCharm, โ€ฆ)

The JetBrains Copilot plugin does not write to any of the VS Code or CLI
locations above. It persists chat/agent sessions under the shared GitHub Copilot
config root, in one store directory per session store:

text
~/.config/github-copilot//<kind>/<storeId>/
  copilot-*-nitrite.db     # Nitrite (H2 MVStore) โ€” the session content
  blobs/

`` is a per-IDE dir (iu for IntelliJ IDEA Ultimate, intellij for the
community edition, PyCharm2025.2, โ€ฆ). <kind> โˆˆ chat-agent-sessions,
chat-sessions, chat-edit-sessions (agent / ask / edit mode). The root follows
XDG rules: $XDG_CONFIG_HOME/github-copilot when set, else
~/.config/github-copilot (macOS / Linux) or %LOCALAPPDATA%\github-copilot
(Windows).

Storage: the Nitrite .db. An H2 MVStore file (header
H:2,block:9,โ€ฆformat:3) of Java-serialized Nitrite documents (NtAgentSession,
NtAgentTurn). It is read as latin1 (byte-offset-stable, lossless) and scanned
โ€” no Java deserializer, no new deps, and it is not SQLite so node:sqlite is
not used. Each assistant reply is a {"__first__":{"type":"Subgraph",โ€ฆ}} blob.
extractResponseText recovers the reply by unescaping one level at a time and,
at the first depth where the record markers appear bare, reading the reply
structurally (the payload is parsed as a delimited JSON-string literal, so a
reply containing its own quotes is never truncated).

Two turn shapes, both handled (a blob is one or the other โ€” verified across
every observed store that they never coexist):

  • Ask mode โ€” the reply is a Markdown record's text.
  • Agent / plan mode (agent sessions, /plan โ€ฆ, e.g. in PyCharm) โ€” the reply
    is the reply field of an AgentRound record; here the Markdown records
    hold the user's prompt instead. The mode is decided by the presence of an
    AgentRound record, and only its reply is read โ€” so an agent turn with an
    empty reply (a failed turn or a pure tool-call round) is billed $0 rather
    than falling back to the prompt. A multi-round blob contributes every non-empty
    round's reply.

Sidecar records that plan/agent mode also writes โ€” Thinking (chain-of-thought),
PendingChanges (proposed code diff, stored under content not data),
AskQuestion, Notification, SubTurn, and file-read text results โ€” are
not billable assistant output and are deliberately skipped. User prompts are
the simpler {"":{"type":"Value",โ€ฆ}} value-maps.

Old plugin format (โ‰ค1.5.x, e.g. 1.5.59-243). Older plugins do not write
per-turn __first__/Subgraph blobs at all โ€” they store the whole session as ONE
binary-framed outer Nitrite document of UUID-keyed Value entries, with the
AgentRound records one escaping level deeper. When the Subgraph scan finds no
turns but the raw file contains AgentRound text, a fallback locates that outer
document (extractJetBrainsDbTurns), runs it through the same
extractResponseText depth-unescape, and emits one session-level call per
document (all rounds' replies joined). Cost and tokens are correct; only the
per-turn call-count granularity is coarser than the new format โ€” an accepted
tradeoff for legacy data. The fallback is gated on the new-format scan yielding
nothing, so current sessions are never affected or double-counted.

(Store dirs may also contain a legacy 00000000000.xd Xodus log from older
plugin versions. On every installation observed it is either empty or shadowed
by the .db, so CodeBurn reads only the .db. If a real .xd-only session ever
surfaces, add a reader with a captured fixture.)

  • No token accounting. No store records token counts. Output tokens are
    estimated from the reply text via estimateTokens (CHARS_PER_TOKEN = 4,
    as for Cursor and legacy Copilot JSONL); input tokens are 0; every JetBrains
    call is marked costIsEstimated: true.

  • Errored turns. A failed generation ("Sorry, an error occurred โ€ฆ") is stored
    as an assistant blob with an error status and no reply text; it is detected and
    billed $0 (not conflated with an empty success). In agent mode a failed turn
    has an empty AgentRound reply โ€” the parser does not fall back to the prompt
    Markdown, so the user's words are never billed as the assistant's output.

  • Per-turn model. The model varies per turn within one .db. It is recovered
    from inside the assistant blob when present, else a store-wide default, else a
    generic Copilot bucket. Dotted Claude names are normalised to canonical ids
    (claude-opus-4.5 โ†’ claude-opus-4-5); GPT/Gemini names kept verbatim.

  • Duplicates. The store keeps several byte-copies of each reply (original,
    lowercased, revisions); assistant turns are de-duplicated by reply content.

  • One .db holds many chat tabs. A single store .db contains multiple
    conversations, each with an internal GUID and an evolving title
    (New Agent Session โ†’ auto-name โ†’ final title). CodeBurn recovers the
    GUID โ†’ title map (extractJetBrainsConversations, keeping the latest
    non-default title), attributes each turn to the nearest preceding conversation
    GUID, and emits one session per conversation (not one per .db). Reply
    content is de-duplicated per conversation.

  • Project. Resolved in three tiers, most authoritative first:

    1. projectName field (plugin 1.12+). Recent plugins serialize the repo
      label directly on the session doc (extractJetBrainsProjectName) โ€” the
      JetBrains analogue of the OTel source's github.copilot.git.repository.
      Cross-kind join: the billable turns live in chat-agent-sessions, but
      the projectName is usually written only into the sibling
      chat-sessions / chat-edit-sessions store. Discovery
      (resolveJetBrainsProjectNames) joins them by store id so the agent
      session inherits the label from whichever store recorded it. Read
      length-prefixed (Java TC_STRING) so an embedded quote/newline can't
      truncate it.
    2. .git walk-up (older plugins / no projectName). For each file://
      URI a chat referenced, walk UP the real filesystem to the nearest ancestor
      containing a .git and use that repo's basename (e.g. pinot).
    3. copilot-jetbrains bucket when neither signal exists (chat referenced
      no files and no projectName was recorded, or the repo no longer exists on
      disk).

    The conversation title is a chat-thread name, NOT a project โ€” it is the
    session label (userMessage) and deliberately kept out of project so it does
    not pollute the By-Project view. Note that bg-agent-sessions/ (a newer kind
    dir holding copilot-agent-snapshots.db / copilot-session-metadata.db) is
    not scanned: those DBs carry file snapshots and metadata, not billable
    turns, and the same session's turns are already read from
    chat-agent-sessions.

  • Override the root with CODEBURN_COPILOT_JETBRAINS_DIR.

Caching

None for the JSONL sources. The OTel source uses a durable cache (see above).

Deduplication

Legacy JSONL and transcript sessions dedupe per messageId. Core chat sessions dedupe per copilot-chatsession:<sessionId>:<requestId>, and are not discovered when an OTel source is present. JetBrains .db turns dedupe per copilot:jb:<conversationId>:<turnIndex> (a per-conversation index, plus reply-content dedup within each conversation). These sources otherwise touch disjoint locations from the VS Code / CLI sources.

If a workspace hash contains at least one chatSessions/*.jsonl file, the provider skips that hash's legacy GitHub.copilot-chat/transcripts/ directory. The core chat session journal is the modern token-bearing source for the same conversations, so reading both would inflate call counts.

Model inference

Copilot does not always tag the model on each message. The parser infers it from the tool-call ID prefix:

Prefix Inferred model family
toolu_bdrk_, toolu_vrtx_, tooluse_, toolu_ Anthropic
call_ OpenAI

See copilot.ts:176-213.

Quirks

  • toolRequests can be missing or non-array on older sessions; the parser guards against that (copilot.ts:126, :260).
  • When outputTokens is missing the parser falls back to char-counting (CHARS_PER_TOKEN = 4, copilot.ts:252-254).
  • A single chat may be mirrored across both legacy and transcript paths if the user upgraded; the dedup messageId collision handles this.

When fixing a bug here

  1. Determine which schema reproduces the bug. The two parsers share little code on purpose; do not unify them unless you understand both formats.
  2. If the model is misidentified, look at the tool-call ID prefix list and consider whether a new prefix should be added.
  3. New fixtures go under tests/fixtures/copilot/ and are referenced from tests/providers/copilot.test.ts.

File: docs/providers/crush.md

Crush

Charmbracelet's Crush TUI coding agent.

  • Source: src/providers/crush.ts
  • Loading: lazy (src/providers/index.ts). Lazy because Crush ships per-project SQLite databases and we use node:sqlite to read them.
  • Test: tests/providers/crush.test.ts (10 tests, fixture-based)

Where it reads from

Crush keeps a global registry that lists every project it has touched, and a separate SQLite database per project.

File Path
Registry (project list) $CRUSH_GLOBAL_DATA/projects.json, otherwise $XDG_DATA_HOME/crush/projects.json, otherwise ~/.local/share/crush/projects.json (Linux/macOS) or %LOCALAPPDATA%/crush/projects.json (Windows).
Per-project db //crush.db where data_dir defaults to .crush.

The registry shape is an object keyed by project id (modern Crush) or an array (older builds and tokscale's sample fixtures). The parser accepts both.

Storage format

SQLite. Schema verified against charmbracelet/crush v0.66.1 (internal/db/migrations/20250424200609_initial.sql plus subsequent additive migrations).

Two tables matter for codeburn:

sql
CREATE TABLE sessions (
  id TEXT PRIMARY KEY,
  parent_session_id TEXT,
  title TEXT NOT NULL,
  message_count INTEGER NOT NULL DEFAULT 0,
  prompt_tokens INTEGER NOT NULL DEFAULT 0,
  completion_tokens INTEGER NOT NULL DEFAULT 0,
  cost REAL NOT NULL DEFAULT 0.0,
  updated_at INTEGER NOT NULL,
  created_at INTEGER NOT NULL,
  ...
);

CREATE TABLE messages (
  id TEXT PRIMARY KEY,
  session_id TEXT NOT NULL,
  role TEXT NOT NULL,
  parts TEXT NOT NULL DEFAULT '[]',
  model TEXT,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL,
  ...
);

Caching

None at the provider level.

Deduplication

Per crush:<sessionId> (crush.ts).

What we extract

codeburn field Crush source
inputTokens sessions.prompt_tokens
outputTokens sessions.completion_tokens
costUSD sessions.cost (already in dollars)
model dominant value of messages.model for the session, picked by GROUP BY model ORDER BY COUNT(*) DESC LIMIT 1. Falls back to unknown.
timestamp sessions.updated_at if set, otherwise created_at

Cache tokens, reasoning tokens, web-search counts, tools, and bash commands are all left as zero / empty. Crush does not record per-message token data, so per-turn attribution is not available.

Quirks worth knowing

  • Timestamps are seconds, not milliseconds. The Crush schema comments in the upstream migration claim millisecond timestamps, but every actual INSERT/UPDATE in internal/db/sql/{sessions,messages}.sql uses strftime('%s', 'now'), which returns Unix seconds. The parser multiplies by 1000 before constructing a Date. Tokscale's parser (junhoyeo/tokscale#346) gets this wrong and is off by 1000x. Confirmed against Crush v0.66.1.
  • Cost is stored in dollars as a REAL. No conversion needed.
  • Child sessions are skipped. Only rows with parent_session_id IS NULL are surfaced. Crush sub-agents inherit cost into the parent.
  • Zero-spend rows are filtered. Discovery skips sessions with cost = 0 AND prompt_tokens = 0 AND completion_tokens = 0.
  • Optimize detectors that depend on tools (detectJunkReads, detectDuplicateReads, detectLowReadEditRatio) will not flag Crush sessions. That is correct: Crush does not log per-tool calls in a way we can read today.
  • detectLowWorthSessions may flag Crush sessions because it looks for cost without edits. That is a known false positive; if it becomes noisy, we can branch the detector on provider.

When fixing a bug here

  1. Confirm the issue against a real Crush install (brew install charmbracelet/tap/crush) before assuming the schema has changed. Migrations in the last six months have only added columns to sessions/messages, never removed any of the ones we read.
  2. If the bug is "Crush sessions show timestamps from 1970-something", check whether someone "fixed" the seconds-vs-milliseconds handling by removing the * 1000. The schema comment is wrong; the data is in seconds.
  3. If the bug is "Crush model column shows unknown", the session has no messages with a non-null model. Some early Crush builds did not record provider on every message; add LIKE matching against provider if you want a stronger fallback.
  4. If the bug is "no sessions discovered", the registry path probably has not been verified for the user's setup. Print getRegistryPath() and have them confirm the file exists at that location.
  5. New fixtures go under the inline schema in tests/providers/crush.test.ts; keep the CREATE TABLE literal and synchronized with the upstream migration.

File: docs/providers/cursor.md

Cursor

Cursor IDE chat history.

  • Source: src/providers/cursor.ts
  • Loading: lazy (src/providers/index.ts:44-57). The node:sqlite import is the heavy dependency that justifies lazy loading.
  • Test: tests/providers/cursor.test.ts (77 lines), tests/providers/cursor-bubble-dedup.test.ts (176 lines)

Where it reads from

A single SQLite database per platform:

Platform Path
macOS ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
Windows %APPDATA%/Cursor/User/globalStorage/state.vscdb
Linux ~/.config/Cursor/User/globalStorage/state.vscdb

Storage format

SQLite. Two parallel sources within the same db:

  1. Bubbles (cursor.ts:201-331): per-message rows. The richer source.
  2. agentKv (cursor.ts:350-460): per-conversation key-value blobs. The fallback for older sessions.

The parser tries both and dedupes via seenKeys.

Caching

src/cursor-cache.ts writes ~/.cache/codeburn/cursor-results.json (override with $CODEBURN_CACHE_DIR). The fingerprint is dbMtimeMs + dbSizeBytes of state.vscdb. Atomic write via temp + rename.

Deduplication

  • Bubbles: per bubbleId (cursor.ts:282).
  • agentKv: per requestId (cursor.ts:429).

Quirks

  • 180-day lookback. The bubbles query bounds itself to the trailing 180 days (cursor.ts:205). Older history is ignored. If a user reports "Cursor data missing", confirm the date range first.
  • 250 000 bubble cap. Power users with massive history are capped to prevent unbounded memory. If you need to raise this, also raise the cache size budget.
  • Per-conversation user-message queue. The parser caches the user-message stream per conversation to avoid an O(n) shift on every turn (cursor.ts:171-191).
  • agentKv has no per-message timestamp. The DB file's mtime is used as the timestamp for every agentKv-derived call (cursor.ts:358-363). This is wrong but consistent.
  • Cursor v3 reports zero token counts. The parser falls back to char-counting (CHARS_PER_TOKEN = 4) for those rows (cursor.ts:265-272).

When fixing a bug here

  1. Always reproduce against a fixture, not a real db. SQLite over the live db is racy; the user might be using Cursor while you read.
  2. If the bug is "tokens are zero", check whether the row is a v3 zero-token bubble, in which case the char-fallback should kick in.
  3. If the bug is "duplicate counts", check both bubbleId dedup and the cross-provider seenKeys dedup.
  4. Cache poisoning is the most common failure mode after a Cursor schema change. Bump CURSOR_CACHE_VERSION in src/cursor-cache.ts so old caches are invalidated.