* feat: Command Code support
Reads the cmd CLI's session transcripts at
~/.commandcode/projects/<slug>/<id>.jsonl (CODEBURN_COMMANDCODE_DIR moves
the root). Checkpoint and prompt sidecars are skipped.
usage.inputTokens includes cacheReadTokens, so fresh input is
inputTokens - cacheReadTokens. The recorded costUsd is kept per call
(costFromBilling); a call without one is priced from tokens and marked
estimated. Forks copy parent message ids into a new file, so calls dedupe
by message id across sessions.
Desktop: Electron sandbox read path, macOS and Windows change watchers,
Windows tab colour. No quota adapter.
* fix(command-code): dedupe on meta.messageId, not the 8-hex line id
The top-level id is randomUUID().slice(0, 8) and only unique within one
file, so calls from unrelated sessions could collide and vanish.
meta.messageId is a full uuid that survives forks. Fixture ids now use
the real 8-hex shape.
* test(command-code): check sidecar skipping by file name, not a posix path
* Add regressions for redirected editor storage roots
* Honor redirected data roots for Copilot and Cursor
* Verify redirected Copilot OTel discovery on Linux
* Reproduce Cursor cache collisions across storage roots
* Bind Cursor result caches to their database and backfill redirected history
* Verify redirected discovery against the latest daily cache
* Update env guard and document editor storage overrides
* feat: Amp support - read the local thread mirror (#1572)
Amp (Sourcegraph's coding agent) mirrors every thread it serves as one
JSON document per thread at ~/.local/share/amp/threads/ (AMP_DATA_DIR
honored, comma-separated). Each file is { id, messages,
usageLedger?{events} }; the ledger's events are the per-request record
(tokens.input/output, cache tokens joined from the billed message
through toMessageId -> messages[].messageId), with a per-assistant-
message usage fallback for threads without a ledger and a total-only
record billing as output.
Amp bills a subscription and records no charged dollars, so every call
is priced from the underlying model's token rates and marked
costIsEstimated; the recorded credits field is ignored (no published
per-credit dollar rate). Thread files carry no cwd, so sessions group
under the provider name. Registered eager; parse version
usage-ledger-v1; no artwork yet (mono-initial fallback, like Codebuff).
Surfaces: CLI registry + doctor probe roots, Electron sandbox read
paths + menubar catalog (Amp after Cursor, matching the Swift order),
Windows tray tab color + usage_guard root, GNOME prefs dropdown, macOS
ProviderConnectionCatalog, docs/providers/amp.md, README tool list,
CHANGELOG.
* fix: address Amp provider review feedback
* fix(opencode): price Vertex turns on a Vertex row and keep an unpriceable recorded cost through the cache (#1547)
OpenCode records the cost of each turn. For an id CodeBurn cannot price the
parser fell back to that cost, but the session cache stored no cost and
re-priced the cached tokens on every read, so the call showed $0. The
recorded cost now rides on the cached call as fallbackCostUSD and applies
only while the tokens price at $0, so a later catalog row or price
override still wins.
The session-level rollup used provider/model while per-message turns use
the bare id since #1543; it now uses the bare id too. google-vertex and
google-vertex-anthropic map to a metered Vertex route.
OpenCode and Kilo Code re-parse once; daily cache v37 re-derives settled days.
* fix(opencode): keep a declared-free id at $0 instead of its recorded cost
A model the user marked flat-rate, priced at a zero rate, or that looks
local prices at $0 on purpose. The recorded-cost fallback now skips those
ids at parse time and on cache read, through one guard in models.ts.
* feat(windows): discover provider roots inside WSL distros
Agents run inside a WSL distro write their history to the distro's Linux
home, exposed to Windows as \\wsl$\<distro>\... and never under the user
profile, so a Windows-only scan reported zero sessions for them.
src/wsl.ts lists distros through %SystemRoot%\System32\wsl.exe (absolute,
so nothing dropped next to the CLI can impersonate it) with a 3s timeout,
decodes its UTF-16LE output, drops container-runtime distros, and
enumerates each distro's home/* and root. Running distros only by
default: touching \\wsl$ boots a stopped distro. CODEBURN_WSL=all|off
opts in or out. Inert off win32 and memoized per process.
claude and codex append <wslHome>/.claude and <wslHome>/.codex to their
existing root lists, so multi-root discovery, probeRoots() and doctor
cover them unchanged. Roots are additive, never an override.
CODEBURN_WSL stays out of PROVIDER_ENV_VARS: cache entries are keyed by
source path, so it can only add or drop paths, never make a kept entry
stale, while declaring it would force a re-parse on every toggle.
Fingerprints drop dev/ino for \\wsl$ paths in both caches. The 9P share
synthesizes them per mount, so keying on them would re-parse every WSL
session on every run; mtime+size still catches edits and appends.
Refs #1059
* test(windows): cover WSL root discovery and the 9P carve-outs
UTF-16 output parsing, utility-distro filtering, CODEBURN_WSL modes, UNC
classification, claude/codex root injection and discovery, probeRoots,
fingerprint reconciliation with zeroed dev/ino, and the win32 guard that
keeps a Linux cwd from being walked on Windows.
Refs #1059
* docs(windows): document WSL discovery and CODEBURN_WSL
Refs #1059
* fix(windows): harden WSL discovery from adversarial review
Probe \\wsl$\ before \\wsl.localhost\. The older spelling works on every
WSL build; probing the newer one first sends builds without it through
MUP, SMB and DNS resolution for a host named "wsl.localhost" — seconds
of stall per distro, every process.
Only accept lines shaped like a distro name. With nothing installed,
wsl.exe prints prose on stdout whose lines otherwise read as distro
names and get probed over UNC. A name must be a single token with none
of the characters Windows bans from a path component and no trailing
period, which discards that message in any UI language.
Do not evict a \\wsl$ cache entry for being undiscovered, and serve it
in write mode too. A stopped distro drops its whole root out of
discovery; treating that as a deletion re-parsed everything over 9P on
the next start and made totals drop while the read-only serve path still
showed them. The exemption is scoped to isWslUncPath — an ordinary path
whose file is gone is still evicted.
Also: getCachedCodexProject was the one fingerprint site still keying on
dev/ino for WSL paths; doctor now prints why it probed no WSL roots; and
a distro contributing several homes labels them per user rather than
letting makeUniqueLabels number them.
Refs #1059
* fix(windows): re-validate running WSL distros on a TTL before UNC access
wslHomes() memoized the running-distro homes for the process lifetime. In
long-lived serve/menubar use a distro cached as running could be shut down
and the next refresh would still touch its \\wsl$ UNC path — which can hang
win32 fs calls — while a distro started after an empty discovery stayed
invisible until restart.
Discovery now caches for a 60s TTL only; on expiry the wsl.exe -l --running
probe (timeout-bounded) re-validates the running list before any UNC path
is touched, and a failed probe fails closed to native-only discovery rather
than serving stale homes. Covers both shutdown and later-start transitions.
Addresses the blocking review finding on PR #1062.
* fix(windows): harden WSL cache reconciliation
* fix(windows): bound WSL orphan refresh to the discovery TTL
refreshWslHomes re-spawned wsl.exe on every full parse for a stopped-but-
cached distro, ignoring the 60s TTL — up to ~3s twice per parse in the
resident serve/menubar. Reuse the TTL-bounded wslHomes() probe instead; the
same TTL already reflects the only transition the orphan check must catch,
offline->running.
* test(windows): skip on-disk POSIX-worktree walk case on real win32
The fixture builds a worktree under tmpdir() and treats it as a POSIX cwd the
win32 guard should reject. On a real Windows runner tmpdir() is a C:\ path, so
the guard rightly walks it and canonicalizes to the main repo, failing the
assertion. The case is only constructible on a POSIX host (mocked win32); the
literal /home/... test already covers the guard on Windows.
---------
Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
* feat(cli): filter usage by billing route and mode
Carry provider-observed billing provenance through Hermes and the session cache, register the real OpenRouter route, and add call-level route/billing filters to models, sessions, export, and audit.
Keep unknown billing unclassified, preserve existing pricing, and repair legacy included Hermes ledger baselines without rewriting genuine multi-observation history.
* feat(opencode): preserve OpenRouter route provenance
Carry OpenCode providerID through legacy SQLite, v2, file, and session-rollup paths so OpenRouter calls remain distinct from direct traffic. Invalidate OpenCode and KiloCode caches and cover strict route matching.
* feat(opencode): classify Amazon Bedrock route
Map OpenCode's exact amazon-bedrock providerID to the existing metered Bedrock route, including model families that cannot rely on Anthropic/OpenAI ID shapes. Invalidate OpenCode and KiloCode caches across the route expansion.
* docs(opencode): document billing route provenance
---------
Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
The same model billed through a different door is its own row on every surface.
The route rides on the call as a field: the model id names it when the door renames the model (Bedrock's <vendor>.<model>[-vN:M], with the cross-region profile kept as its own SKU), and the provider's own column names it when it does not (Hermes billing_provider). Only bedrock is registered; openrouter and bedrock-mantle wait for real sessions.
Every report keys a model row on one function, modelRowKey: short name plus the door's label. models-report folds on that row key rather than the route id, so two doors sharing one label can never print two rows a reader cannot tell apart. codeburn models --format json carries route per row.
Daily cache moves to v33: day.models is keyed by the row key so a column-sourced route survives into a finalized day, and hermes joins the pending-rederive table. Hermes sessions re-parse once.
Pricing never consults the route and runs on the raw id. granular-history still keys on the raw model id, so the desktop Trend timeline is the one surface outside this.
Verified on a real cache: 142 finalized days, $37,898.851498 and 272,985 calls unchanged.
Mixed-version binaries were clobbering the unsuffixed Codex, Cursor,
and Antigravity result files and each re-parsed the whole corpus.
Write *-results.v<n>.json like the daily cache. Leave the unsuffixed
file for older binaries; adopt a matching-version copy once.
The month-scoped load a ranged query takes is a behaviour change on a warm
cache with no way back except deleting it. Drop the scope in loadCache, the
one place every caller (including the resident serve) routes through, so a
suspect scoped read can be compared against a full one in place.
Read policy only: deliberately not in PROVIDER_ENV_VARS, so setting or
unsetting it invalidates nothing.
README gains a Windows section next to the macOS and GNOME ones, a download
badge on the menubar card, and an honest note that the Tauri tray builds on
Linux but is unreleased there. docs/architecture.md picks up windows/ in the
surfaces diagram and gets a section covering the crate layout, the PATH and
System32 spawn rules, and the Claude quota parity.
`codeburn menubar` on Windows now points at the windows-v release page instead
of failing with "macOS only". The generalized installer from the source branch
is not brought over: it is 1182 commits behind this file and would drop the
proxy support, retry/backoff, checksum and bundle verification, and persistent
CLI path handling that landed since.
Groups sessions by their opening block (whitespace/ANSI-normalized, hashed
over the first 2 KB) and flags a block of at least 1.5 KB that opens five or
more sessions. Class nudge: CodeBurn will not move the user's own text into
CLAUDE.md, so the fix asks Claude to give the block a permanent home. Only
the repeats count as savings, sized from the block's bytes because provider
usage is per API call and cannot isolate the paste. The opener comes from
the session scan that already runs, so nothing extra is read.
Adds docs/optimize.md (what optimize scans, the three classes, the exact
files --apply may touch plus undo, measured vs estimated, the health
grade bands, the --yes CLAUDE.md guardrail), links it from the README
waste section, and corrects the detector count in docs/architecture.md
(14 -> 19).
docs/providers/NEW_PROVIDER.md items the PR had not reached yet, plus the two
surfaces that are functional rather than cosmetic:
- docs/providers/dsh.md and its row in the provider index, documenting the
storage layout, the JSONL-backend-only scope (the opt-in SQLite persistence
backend is not read), and that DSH is a developer preview whose format
version 0 implies no compatibility.
- CHANGELOG entry under Unreleased.
- README provider count 40 -> 41 and a data-locations row.
- app/package.json: $HOME/.dsh in the snap personal-files allowlist, without
which the Linux snap build cannot read DSH sessions at all.
- UsageDataChangeGuard: the DSH sessions root, without which the menubar never
notices a new session and does not refresh.
- Bumps the dsh parse version, since the parser's attribution changed.
Three fixes from review, all measured on this box.
The workload gate was files OR bytes. The files arm is wrong: 250 pending files
holding 117 KB between them spawned 5 threads and ran ~5% SLOWER than serial,
and a file count only starts paying for itself around 400. Gate on bytes alone;
the count still takes max(files / 50, bytes / 200 MB), so a few hundred huge
rollouts keep their threads.
The flat 256 MB per-worker memory budget was contradicted by the Codex workload:
a 260 MB rollout peaks near 430 MB in its worker, linearly across the pool. It is
now derived per parse as clamp(256 MB, 2 x average pending file + 128 MB, 1 GB),
which leaves a corpus of small Claude transcripts where it was and stops
over-subscribing on rollouts. The parent's buffer of up to pool.size finished
results is part of that peak and is named in the comment.
The worker/file pairing at both install sites was positional, guarded only by
position (Claude) or a path membership check (Codex). Each worker now echoes its
path and the parent asserts it, outside the per-file try: a misalignment would
install one session's turns under another's path -- a wrong number nobody would
ever notice -- so it fails the run rather than being swallowed as a parse
failure. On the Claude side that meant hoisting the whole worker-result block
above the try, which is safe because an append never consumes a result in either
its shortcut or its straddled-fallthrough case.
The comment at the install site claimed only that an overlapping worker result
'is discarded'. State why the empty-set result is installable at all — an empty
id intersection is proof a serial parse would have dropped nothing — and why the
tempting shortcut is wrong: parsedTurnsToCachedTurns delta-encodes gitBranch
across turns, so dropping one turn changes whether a LATER turn carries a
gitBranch key. Overlap discards the whole file, never individual turns.
Tests: the end-to-end determinism check now runs both parses over the SAME
corpus, so cache shard BODIES are compared byte for byte instead of just their
keys, and a new resumed-session fixture (a transcript restating another file's
message ids, in both filename orders) makes install order decide the answer.
Verified by mutation: removing the discard guard fails it, and yielding worker
results out of order fails it.
CODEBURN_VERBOSE now reports how many worker results were re-parsed in-process
on id overlap, which is what the new test asserts on. The worker bundle's source
map is excluded from the published package (-1.8 MB).
os.freemem() reports free pages on macOS, not available memory: on an idle
128 GB machine it reads a few hundred MB, so the 2 GB gate switched the worker
pool on and off between runs on the platform the desktop app ships to. The gate
and the budget now use process.availableMemory() (cgroup/rlimit-aware in a
container), falling back to os.totalmem(): serial under 4 GB available, budget
min(0.25 * available, 2 GB). An 8 GB box earns 8 threads, a 4 GB box none.
The verbose line now carries every decision input — cores, available GB, pending
files and bytes — on both the gate and the go path, so one support log explains
itself.
vitest's default glob reached the Electron app's specs under app/, which carry their
own vitest config and their own jsdom in app/node_modules. From a root install that
fails with ERR_MODULE_NOT_FOUND: jsdom, so the command CONTRIBUTING documents and the
one RELEASING.md names as the pre-release gate both error out.
Move the scoping CI already applies into package.json: test runs tests/ minus the
parallelism-sensitive cache-refresh-lock suites, test:locks runs those three serially,
test:watch keeps watch mode at the same scope. The first two are byte-identical to the
invocations .github/workflows/tests.yml spells out, so the workflow can be pointed at
the scripts to stop the two drifting apart again; that edit is left out of this PR so it
needs no workflow permissions. test plus test:locks together still cover all 192 files
under tests/.
Scoping the script changes what a trailing path argument means: vitest ORs positional
filters, so 'npm test -- tests/providers/hermes.test.ts' would no longer narrow to that
file, it would run the whole suite. Rewrite those to 'npx vitest run <path>' everywhere
they appear - four provider guides and the MCP design plan, thirteen lines in all.
Also refresh the stale test docs: 42 files/568 tests (now 192 under tests/), the
per-directory counts, the line claiming vitest does not run in CI which stopped being
true when tests.yml landed, and the provider test-gap list, which still named
antigravity and gemini after both gained test files.
Record the cache-refresh-lock naming convention in CONTRIBUTING, since the split makes
it load-bearing: a lock test that misses the prefix runs under the full worker pool and
flakes, and one that matches it but is absent from test:locks never runs at all.
* Add IBM Bob provider
* Add workspace extraction for Cline-family providers
Extract project name from workspace directory in api_conversation_history.json
so sessions show actual folder names instead of the provider display name.
Thread projectPath through ParsedProviderCall to avoid unsanitizePath mangling
hyphenated folder names.
---------
Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
Co-authored-by: iamtoruk <hello@agentseal.org>
Closes#278.
Adds Charmbracelet Crush as a lazy-loaded provider:
- src/providers/crush.ts: walks ~/.local/share/crush/projects.json
(XDG_DATA_HOME and CRUSH_GLOBAL_DATA aware), opens each project's
crush.db read-only, queries root sessions where parent_session_id
IS NULL. Emits one ParsedProviderCall per session with real
prompt_tokens, completion_tokens, cost (dollars), and the
dominant model resolved from messages.model.
- src/providers/index.ts: register crush alongside cursor, goose,
opencode, antigravity, cursor-agent in the lazy import path.
- tests/providers/crush.test.ts: 10 fixture-based tests covering
discovery, parsing, missing-registry, malformed JSON, missing db,
child session exclusion, dominant model selection, dedup, and
array-shaped legacy registry.
Schema source: charmbracelet/crush@v0.66.1
internal/db/migrations/20250424200609_initial.sql, verified by
spawning a research agent against upstream. The schema *comments*
in that migration claim millisecond timestamps but every actual
INSERT/UPDATE uses strftime('%s', 'now') which returns Unix
seconds; the parser treats values as seconds. Tokscale's
parser (junhoyeo/tokscale#346) gets this wrong and is off by
1000x, plus its parser misses the prompt_tokens/completion_tokens
columns that exist in Crush's schema. Our integration uses both,
so Crush sessions get real per-model attribution.
Menubar:
- mac/Sources/CodeBurnMenubar/AppStore.swift: add .crush case to
ProviderFilter and its cliArg switch.
- mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift: add
Crush color to the per-tab color extension. The visibleFilters
computed property already filters by detected providers, so the
Crush tab appears automatically when a user has Crush data.
README:
- Replace the provider table with an icon-led layout. Icons live
under assets/providers/<name>.<ext>. 14 icons sourced from
junhoyeo/tokscale (MIT) under nominative fair use, 4 sourced
separately: codex (OpenAI org avatar), cursor-agent (reuses the
Cursor icon), kiro (kiro.dev favicon, ico->png via sips), omp
(can1357/oh-my-pi icon.svg, MIT). Attribution line added.
- Add Crush row.
Docs:
- docs/providers/crush.md: full per-provider doc with verified
schema excerpt, the seconds-vs-milliseconds quirk, and a
"when fixing a bug here" checklist.
- docs/architecture.md: provider count 17 -> 18, test count
41 -> 42, and crush in the lazy list.
- docs/providers/README.md: add Crush row to the lazy index.
- CONTRIBUTING.md: bump test count to 568 (was 558).
All 568 tests pass locally; swift build clean.
Document the contributor onboarding path:
- CONTRIBUTING.md: setup, npm scripts, coding conventions, PR process,
the block-claude-coauthor enforcement, and the five providers without
test coverage today (claude, gemini, goose, qwen, antigravity).
- docs/architecture.md: 12-command CLI surface, parser pipeline, three
cache layers, 14 optimize detectors, and the mac / gnome / build
layouts with cited line numbers.
- docs/providers/: one file per provider (17 providers plus the shared
vscode-cline-parser helper). Each covers data path, storage format,
caching, dedup key, quirks, and a "when fixing a bug here" checklist.
Also fix two pre-existing documentation issues surfaced while writing
the new docs:
- RELEASING.md claimed GitHub Actions auto-publishes the CLI when a
v* tag is pushed. There is no such workflow; CLI publishing is
manual via npm publish. Updated the CLI section to reflect reality
and kept the menubar (mac-v* tag) automation accurate.
- .gitignore had CLAUDE.md unanchored, which on case-insensitive
filesystems also matched docs/providers/claude.md. Anchored to
/CLAUDE.md so the root-level memory file stays ignored without
affecting subdirectory docs.
All cited file paths, line numbers, function names, and test counts
were verified against current code (41 test files, 558 tests passing).