## File: README.md

| [](https://claude.com/open-source-max) | [](https://www.producthunt.com/products/codeburn?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-codeburn-2) |
| --- | --- |













| **Desktop**
      
      [](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**
      
      `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](https://github.com/BerriAI/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)](#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.

## Menu bar

```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/`](gnome/):

```bash
git clone https://github.com/getagentseal/codeburn && cd codeburn/gnome
./install.sh
gnome-extensions enable codeburn@codeburn.dev
```

See [gnome/README.md](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](https://github.com/getagentseal/codeburn/issues).

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/](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](https://github.com/BerriAI/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](https://en.wikipedia.org/wiki/ISO_4217#List_of_ISO_4217_currency_codes) is supported (162 currencies). Exchange rates fetched from [Frankfurter](https://www.frankfurter.app/) (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](https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json) or a canonical name from the fallback table (e.g. `claude-sonnet-4-6`, `claude-opus-4-5`, `gpt-4o`). Built-in aliases ship for known proxy model name variants. 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](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](https://vercel.com/docs/ai-gateway/capabilities/custom-reporting) (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](https://github.com/BerriAI/litellm). Exchange rates from [Frankfurter](https://www.frankfurter.app/).

Built by [AgentSeal](https://agentseal.org).

---

## 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.log`s; 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

```
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`:

```
/* 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`:

```
/* 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`:

```
/* 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`:

```
/* 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](cline-cli.md); 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:

```
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`](vscode-cline-parser.md) 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:

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

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

```
~/.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:

```
~/.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.