Orchestrator setups pin different agents to different models (planner on
Opus, implementers on Sonnet, scouts on Haiku). The dashboard shows
"Skills & Agents" and "By Model" as separate panels, so there was no way
to see which agent drove which model's spend, or catch an agent that
silently switched models. `codeburn models --by-agent` closes that gap:
one row per (provider, model, agent), mirroring the existing --by-task
pattern across the table, markdown, CSV, and JSON renderers.
The agent label comes from each Claude subagent transcript session's own
agentType crossed with that session's model calls, aggregated through the
same assistantCalls path --by-task uses (not modelBreakdown), so cache-tier
dedup, savings, and Codex-credit logic all carry over unchanged. This is
the authoritative pairing; main-session subagentTypes spawn markers are
deliberately not joined to models, since a spawn marker names an agent but
not the model that agent actually ran on.
Ordinary main sessions (no agentType) and every non-Claude provider have
no agent, so their spend buckets under the label "main" rather than being
dropped or fabricated into a false agent. --by-agent is mutually exclusive
with --by-task; passing both errors politely.
Exposing subagentType + per-record model on `codeburn export` (so users
can build custom crosses over the export) is a separate follow-up and is
intentionally out of scope here.
- prefetch fires exactly once per provider per session; memo sized to
detected providers so base keys never evict (was: 12 full-history
parses every 30s forever from LRU thrash re-triggering the effect)
- day-aggregator buckets whole turns by user-message timestamp,
matching the report/headline path: trend bars and provider breakdown
now reconcile exactly (midnight-straddling turns were split);
DAILY_CACHE_VERSION 13 with an IANA tz guard that rehydrates when
the cache crosses timezones
- overview's redundant second all-provider scan collapsed (~11% faster
warm); numbers parity-guarded
- lock takeover cleans stale hydrating.lock; SIGINT/SIGTERM release it
- scanner skips EPERM/EACCES provider dirs (progress shows skipped)
instead of aborting; onboarding hints Full Disk Access
- rateLimited quota copy render tests; export -f json emits valid
empty JSON when no data
Root 2023 (+4), app 327 (+3), typechecks clean.
Ported onto current main after the locale-grouping change in overview.ts;
review follow-ups included:
- explicit note: budgets count the full would-be API cost including
subscription-proxied spend, consistent with the overview Cost total
- percent display floors the raw value on both surfaces, so 99.6 percent
renders as 99 percent in warn state and 100 percent appears only when the
state is over; regression tests cover the boundary on overview and --check
- overview keeps main's en-US locale-grouped formatting and the
unpriced-model warning block; the budget line renders after it
- CODEBURN_PROGRESS=1 emits newline-JSON progress on stderr (provider
list, per-provider start/done, per-file ticks) for GUI first-run
loaders; plain CLI output untouched
- parseAllSessions saves the session cache atomically every 5s during
hydration and after each provider group, so an interrupted cold scan
leaves a warm partial cache instead of nothing
- status --no-timeline skips the granular timeline computation
(~300-450ms per call) for clients that never render it; default
unchanged for the menubar
* fix(report): surface estimated costs distinctly
Providers that estimate tokens or price (kiro, cursor, warp, copilot,
grok, hermes, codewhale, and the codex proxy path) set costIsEstimated
on their parsed calls, but the flag died at the parser boundary: it was
never carried onto ParsedApiCall or into the session aggregates, so a
figure whose tokens were synthesized from content length rendered with
the same authority as a metered one.
Plumb the truth through, mirroring the savingsUSD/isLocalSavings pattern:
a call-level boolean (ParsedApiCall.isEstimated, CachedCall.isEstimated,
persisted so it survives the session-cache round trip) and an additive
aggregate amount (estimatedCostUSD on the model breakdown, session, and
project totals, plus PeriodData and the menubar payload). The amount is
carried rather than a bare boolean so a row that is mostly metered with a
small estimated slice is not indistinguishable from a fully guessed one.
Display: report (TUI) and overview per-model rows prefix the cost with a
tilde and print one legend line; the MCP tables carry the same marker and
legend, and the machine surfaces (report --json, MCP get_usage, menubar /
web payload) expose estimatedCostUSD. Totals math is unchanged; the flag
is display/metadata only.
Bump PROVIDER_PARSE_VERSIONS for every provider that sets the flag so
already-cached sessions reparse once and pick it up. Copilot is excluded:
it is a durable provider, so changing its env fingerprint would discard
OTel cache entries whose source rows may already be pruned.
Also fix the cross-provider project merge, which summed totalCostUSD but
dropped merged-in projects' totalEstimatedCostUSD, undercounting the
project/period estimated total (the same latent gap still affects
totalSavingsUSD, left untouched here).
* test(parser): pin estimated dollars through the cross-provider merge
The merge fix for dropped totalEstimatedCostUSD was not covered: deleting
the summing line left every test green. Extract the merge into an exported
mergeProjectsByCrossProviderKey (no behavior change) and pin both the
measured-plus-estimated and both-estimated merge cases.
* docs(parser): honest merge-comment scope and load-bearing overwrite note
Re-review nits: the merge doc claimed all additive totals are summed there
while totalSavingsUSD still is not (pre-existing gap, tracked separately)
and totalProxiedCostUSD is re-derived post-merge; say so. Mark the
buildPeriodData overwrite in usage-aggregator as load-bearing for the
estimated marker so nobody optimizes it away trusting the daily cache.
* feat(doctor): add per-provider detection diagnostics command
When a provider reports zero (or a number that looks wrong) there was no way
to see why: whether the tool is not installed, an env override points at the
wrong directory, the data dir is empty, or parsing failed. Users had to file
issues like "OpenCode does not work" with nothing to go on.
`codeburn doctor` closes that gap. For every provider (or one via --provider)
it shows the exact directories/dbs probed with any env override and whether the
path exists, how many session files were discovered, how many of a bounded
sample parsed cleanly, the cached file count and parser version, and a one-line
verdict: OK (n sessions), NOTHING FOUND with a concrete likely cause, or
ERRORS (n parse failures). Output is a human table by default or --json.
The collect logic is a pure function separated from rendering so tests exercise
it without a TTY. It runs fully offline and read-only (never writes caches or
config, and skips the one network provider's parse), and isolates each provider
in its own try/catch so a single thrower becomes an error row instead of
crashing the report. Providers expose their scan roots via an optional
probeRoots() so the exact paths are shown even when zero sessions are found,
reusing each provider's own path resolution rather than duplicating it.
* fix(doctor): make the inert promise actually true
Adversarial re-review reproduced two false guarantees. Cursor's parser
writes its results cache to disk before its first yield, so a doctor run
created ~/.cache/codeburn/cursor-results.json on a clean machine; the
collect pass now sets CODEBURN_SUPPRESS_CACHE_WRITES (restored after) and
the cursor writer honors it. Antigravity's parse probes for a live
language server (spawns ps and lsof, RPCs the IDE when found); doctor now
skips its parse sample the way network providers are skipped, keeping
discovery counts meaningful.
Also stop blaming CODEBURN_CACHE_DIR in NOTHING FOUND hints (it is our
cache location, not a discovery path) and correct the sample-cap comment:
the cap truncates yields, eager parsers still do whole-file work.
Resolves menubar-json conflicts: main's granular-history timeline
param unions cleanly with this branch's providerDetails and currency
payload additions; both new tests kept.
* feat(antigravity): add support for Antigravity IDE storage on Windows
CodeBurn previously only detected Antigravity CLI usage (.pb files under
.gemini/antigravity/). Antigravity IDE on Windows stores session state in
VSCode-style storage at %APPDATA%\Antigravity IDE\User\globalStorage\state.vscdb,
which was not detected.
Add support for reading Antigravity IDE sessions from the VSCode-style storage:
- Extend CONVERSATION_ROOTS to include APPDATA Antigravity IDE path
- Refine path classification to properly identify IDE vs CLI sessions
- Handle missing per-call timestamps by stamping file mtime as fallback
- Bump CACHE_VERSION to 4 for cache invalidation
Fixes: CodeBurn reports zero usage when Antigravity IDE is actively used.
* fix(antigravity): stamp mtime fallback at emission, tighten path classification
- Apply the mtime fallback to a copy at emission instead of mutating and
persisting it into the cache, so a later file rewrite can't retro-date a
session's history to the new mtime. SQLite gen_metadata rows have no real
per-call time; the RPC path still uses chatStartMetadata.createdAt.
- Drop the unreachable APPDATA classifier branch (discovery only walks the
~/.gemini roots; APPDATA state.vscdb holds no token usage). This also removes
the misroute of base-antigravity paths under an "Antigravity IDE" profile dir.
- Bump CACHE_VERSION to invalidate caches that persisted a synthesized mtime.
* fix(antigravity): stabilize untimestamped call times across DB rewrites
Extract ChatStartMetadata.created_at from proto-encoded data and decode multiple timestamp formats (ISO string, Timestamp submessage, unix varint). Implement assignStableTimestamps() to preserve first-seen timestamps across file rewrites, preventing retro-dating of sessions when .db files are modified. Make conversation roots dynamic (computed per call) to honor environment overrides in tests. Add comprehensive timestamp stability tests verifying that timestamps remain fixed across file mtime changes while respecting date-range filters.
* test(antigravity): assert today range excludes first-seen timestamps
Adds a test case to verify that the 'today' date range filter correctly excludes sessions based on their first-seen timestamp, not file modification time. This ensures that even if a database file is rewritten with a later mtime, sessions with earlier first-seen dates are properly filtered out.
Refs #411
yield always analyzed all providers, so provider-filtered GUI panels
(cost per outcome, reverts, abandoned) showed all-provider numbers
under a filtered header.
The desktop app's getOptimizeReport handler already appends --from/--to
when a custom range is active; without these options commander rejects
the invocation and the Optimize screen errors under a custom range.
Adds an opt-in `codeburn sync` command group: setup, push, status,
logout, reset. Developers authenticate via standard OIDC browser login
(Authorization Code + PKCE, provider-agnostic — Cognito, Auth0, Okta,
Keycloak, etc.) and push per-call usage metadata to a team-operated
OTLP/HTTP endpoint.
Privacy is structural: the payload builder has no field that can carry
prompts, responses, file contents, or bash commands. Only metadata
leaves the machine (provider, model, tokens, cost, project, tool names,
timestamps). There is deliberately no flag to include more.
Setup derives everything from a single URL:
GET {base}/.well-known/codeburn-export.json
→ issuer, client_id, traces_path, max_batch_size
Modules:
- discovery.ts discovery doc fetch/validation (version-gated)
- auth.ts OIDC discovery, PKCE, localhost callback (:19876-78),
token exchange/refresh/revoke
- credentials.ts refresh token in OS keychain (macOS security / Linux
libsecret / Windows DPAPI / 0600 file fallback). All
subprocess calls use execFileSync arg arrays; tokens
flow via stdin or env var — never shell-interpolated
- config.ts ~/.config/codeburn/sync.json
- ledger.ts client-side sent-ledger for exact dedup (no watermark
races), 6-month prune
- otlp.ts ParsedApiCall[] → ExportTraceServiceRequest JSON with
deterministic IDs: span=SHA256(dedupKey)[:8],
trace=SHA256(sessionId)[:16] — every retry idempotent
- push.ts orchestration with typed outcomes; pushes run to
completion — 429 Retry-After honored (≤120s/wait,
3 retries/batch) before deferring; partial_success is
batch-atomic (nothing ledgered, whole batch retries);
50K safety valve, no routine cap
- cli.ts command layer; --dry-run reports exact counts/cost
without sending
Preview feature: protocol may change between releases.
AI-Origin: human
Adds JSON list mode so the desktop Settings screen can read current
config: `model-alias --list --format json` emits AliasRow[] ({from,to},
sorted by from) and `proxy-path --list --format json` emits string[].
Text output and all mutation behavior are unchanged.
Addresses T1 review: discovery.ts logs mDNS bind errors to stderr and returns
already-found devices instead of masking failures as empty; spend-flow uses a
collision-proof "other" sentinel and a local-date period label; the spend/date
flags get friendly error handling; devices-json tests are deterministic (mocked
discovery) and assert the read-only mutation guards. 13 tests green, tsc clean.
Implemented by Codex gpt-5.5 (high); committed by Fable (git blocked in Codex sandbox).
New `codeburn spend --format flow-json` emits the model x project cost matrix
(uncapped, then top-8 each side with an "other" rollup) for the desktop Spend
Sankey. Adds --format json to devices / devices scan / share status / identity,
matching the web /api/* shapes. Read-only; text output unchanged when --format
is omitted. discovery.ts degrades mDNS bind errors to an empty scan result.
Implemented by Codex gpt-5.5 (high); committed by Fable (git was blocked in the Codex sandbox).
Scopes the menubar to one Claude config for multi-config (CLAUDE_CONFIG_DIRS)
setups, with All as the default. Rebased onto main and fixed the review
findings from the original #635:
- Fix a TS2206 build break (a 'type' modifier inside an import type block).
- Reject --claude-config-source with a non-Claude --provider, and scan Claude
only in the scoped branch (a config is Claude-only): fixes provider data
leaking into a scoped query and avoids parsing every provider's corpus.
- Scope the macOS menu-bar figure to the selected config (badge matched the
popover), clear the selection when switching to a non-Claude provider tab,
and stop the on-disk badge fallback from showing an unscoped number while
scoped.
- Tag Claude Desktop / Cowork sessions as their own 'claude-desktop' source so
they are a selectable bucket instead of silently vanishing from per-config
views (sum of options now equals All).
- Skip the redundant Claude discovery walk for plain single-config users while
keeping idle configs and Claude Desktop selectable.
Reviewed by Codex 5.6; all findings addressed. Full suite: 1581 TS tests, 76
Swift tests, tsc clean.
Models missing from the pricing tables silently contributed $0 to every
total with no indication anywhere (the warning was gated behind
CODEBURN_VERBOSE). Add render-time detection that flags aggregated model
rows with usage but $0 cost whose pricing lookup fails right now, and
surface it in overview, report/today/month JSON (unpricedModels), the
TUI By Model panel, and the MCP get_usage summary.
Render-time detection covers cached sessions (cost is computed at parse
time) and heals as soon as pricing data, an alias, or a price override
arrives. Rows with real cost are never flagged: aggregation keys rows by
display name, which the pricing lookup misses; $0 display-name rows are
reverse-resolved to their raw id before flagging so sessions cached
before their model's pricing landed don't get a misleading alias hint.
Local models and model-savings mappings stay excluded: $0 is correct
for them.
Fixes#638
Bug-hunt follow-ups on the realized-savings report:
- Scale the displayed estimate to the measured window so the Estimated and
Realized columns are comparable: mcp/archive use the per-session baseline
times the post-window session count, read-edit uses deficitThen times the
same edits denominator as realized (so realized never exceeds it). The
at-apply estimate stays in --json as estimatedAtApply next to
estimatedForWindow; the footer states the scaling and that mcp/archive
realized figures are derived from session counts, not independently
measured.
- Never crash on a corrupt journal: records without a string status or a
parseable string `at` are skipped and surfaced ("N malformed records
skipped"); the optimize header computation is additionally wrapped so any
error just drops the header.
- Zero post-window sessions now reads "not measurable: no sessions in the
window yet" instead of measured-zero.
- The optimize header sums only normal-confidence measured rows (under-claim);
low-confidence rows stay visible in act report only.
- Tests: floor discipline on non-integer mcp and read-edit products (a ceil
mutation fails), the project-scope keeper subtraction, the archive
estimate==realized tautology, malformed-journal robustness, and the
low-confidence header exclusion.
Capture a trailing-14-day before-baseline when a fix is applied and
re-measure it against the post-apply window so optimize can show realized
numbers next to estimates.
- ActionBaseline (windowDays, capturedAt, estimatedTokens, sessions, metrics)
persisted by runAction; captured in the optimize --apply flow and at guard
install time.
- codeburn act report [--json]: applied, not-undone actions older than 3 days,
re-running the detectors over apply-date-to-now (capped 30 days). Per-kind
realized deltas: MCP/archive tokens-per-session times saved sessions with
reverted-by-user detection; read-edit deficit reduction; guard yield split
labeled correlation. Bash cap is marked not measurable (result sizes are not
retained). Low confidence under 20 post-window sessions or past a 2x volume
shift. Realized numbers rounded down, estimate kept visible.
- optimize gains one header line only when a measured action exists, and
appends "(previously applied <date>, re-flagged)" to re-triggered findings.
No change for users with no applied actions.
Reuses scanAndDetect helpers over a date-bounded range; exports the token
constants and read/edit tool sets rather than duplicating the math.
codeburn guard install|uninstall|status|refresh|allow plus the internal
hook/statusline handlers. Off by default, fully local, cleanly removable.
- Settings edits go through the action journal (guard-install / guard-uninstall)
with expectedHash, appending our entries and removing exactly ours by command
prefix; a byte-identical uninstall is asserted by test.
- PreToolUse budget cap (soft warn once, hard block with a per-session allow
override), Stop yield checkpoint (expensive with no edits and no commit, once),
and a flagged-project SessionStart opener built from the optimize detectors.
- Incremental per-session cache keyed by session id: resumes the transcript
parse from the last complete-line byte offset via readSessionLines, folding
only the tail into running totals. Warm invocation ~0.28s against a 90MB
transcript, dominated by CLI startup; the tail parse itself is negligible.
- All handlers fail open: any error, malformed stdin, or missing transcript
exits 0 with no output so a broken guard can never block a session.
- Hook protocol verified against the live docs (dated block at the top of
hooks.ts); zero new dependencies.
Add stable kebab-case finding ids to every optimize detector and to the
JSON report, then route the config-class findings through the action
journal so they can be applied and undone.
- optimize.ts: id on every WasteFinding and OptimizeJsonReport entry;
appliable findings also carry a machine-readable apply payload (mcp
server list, project-scope keepers, archive names).
- act/plans.ts: planFor(finding) builds concrete, journaled file
mutations for mcp-remove, mcp-project-scope, skill/agent/command
archives, CLAUDE.md rule blocks, and the bash output cap. JSON edits
preserve the rest of the document (2-space indent, trailing newline);
unparseable config files are reported and skipped, not fatal.
- act/optimize-apply.ts: codeburn optimize --apply with a plain-readline
confirm, plus --yes, --dry-run, and --only; prints each journal id and
the undo hint. --apply with --json exits 2.
Tests cover mcp remove/undo, project-scope global-to-project move,
unparseable-file skip, archive collision suffixing, CLAUDE.md marker
idempotency, a byte-identical dry-run tree hash, and a finding-id guard.
Add src/act, a dependency-free framework for journaling and reverting any
file CodeBurn modifies. runAction is the single mutation path: it snapshots
every target, applies the changes, then appends a JSONL record, rolling back
completed steps and journaling nothing if a mutation throws midway. Undo
checks each file against its post-apply sha256 and refuses on drift unless
forced, restoring edits, deletes (created files), and moves. A pid plus
timestamp lockfile (stale after 60s) guards apply and undo, and the journal
reader tolerates corrupt lines with last-line-wins status updates.
Wire up `codeburn act list` (table or --json) and
`codeburn act undo <id|--last> [--force]`. Storage lives under the existing
config home via the config.ts resolver. Tests cover apply with backups and
afterHash, byte-identical undo per op type, drift refusal and --force,
mid-apply rollback, and corrupt-journal tolerance.
* feat(context): add experimental context command with per-session token tree
Reconstructs context breakdown for a Claude Code session
from its transcript: messages and tokens by role, block type, and tool,
split into full-session history vs the live window since the last
compaction (following preservedSegment.headUuid).
Block tokens are chars/4 estimates, images estimated from sniffed PNG or
JPEG dimensions, reasoning derived per message from output_tokens minus
visible output since transcripts strip thinking text. The exact context
size comes from the last assistant message's API usage, and the gap to
the estimate is shown as derived system prompt and tool overhead.
* feat(context): show session titles in context --list
Reads the latest ai-title entry (summary entry as fallback for older
sessions) from one tail and one head chunk of each transcript, so the
list stays fast on 100MB files.
* feat(dash): context explorer page with Claude Code and Codex session trees
Adds a Context page to the web dashboard: a navbar toggle, a provider
picker (Claude Code / Codex), the 15 most recent sessions with titles,
and per-session expandable details showing the context token tree, the
exact live context vs window, and a live/full-history scope toggle.
Server side adds /api/context/sessions and /api/context/tree with an
mtime-keyed tree cache, plus a Codex rollout builder: response items
feed the tree, compacted entries (with replacement_history) split live
window from full history, reasoning comes exact from cumulative
token_count totals, and model_context_window gives the real window.
* feat(context): interactive TUI for the context command
codeburn context with no arguments now opens an ink TUI: arrow keys to
move, enter to expand a session's token tree inline (with a spinner
while the transcript reads), tab to switch between Claude Code and
Codex, f to toggle live window vs full history, q to quit. Flag and id
forms keep the plain output for scripts, and non-TTY runs fall back to
the static list. Tree rows are shared with the static renderer via
snapshotRows.
* fix(context): review fixes across window detection, codex parsing, and surfaces
Window sizes are no longer guessed from token counts alone: opus-4-8 and
[1m] model ids map to the 1M window (a 190K opus-4-8 session used to
render as 95% of a 200K window), and Codex omits the percentage instead
of borrowing Anthropic constants when the rollout lacks
model_context_window.
Codex compaction accounting now reads the encrypted compaction item and
developer messages from replacement_history, so post-compaction windows
stop undercounting. Both builders stream with largeLineAsBuffer so one
oversized line cannot silently truncate the walk.
The tree API stops sending session.filePath, serves the flattened rows
so the dash renders the same tree as the CLI and TUI, resolves ids
directly instead of re-scanning every session file per request, and the
tree cache evicts LRU. Session discovery honors CLAUDE_CONFIG_DIRS,
CLAUDE_CONFIG_DIR, and CODEX_HOME, stats files in parallel, and id
lookups stat only matching files.
CLI gains --provider codex and machine-readable --list --json; --full
now opens the TUI in full scope instead of bypassing it. The TUI shows
build errors instead of a frozen spinner and drops the ref-plus-counter
repaint for plain state. The dashboard scopes the usage error banner and
device sidebar to the Usage page.
Adds a per-(provider, model) audit table showing the raw token fields each
provider records (input, output, reasoning, cache write/read) alongside the
totals codeburn displays and prices. It makes the normalizations explicit:
reasoning folds into output, the Anthropic cacheReadInput / OpenAI cached
vocabularies collapse to a per-call max, and cache write/read use the 1.25x /
0.1x input multipliers when a model omits explicit cache rates.
--format json adds per-component cost, the applied rates, both raw cache-read
fields, and recomputed vs attributed cost, so any token-to-cost drift is
visible in one command. Same options as `codeburn models`.
The web server is long-lived, so cache what the per-invocation CLI cannot:
- Cache the parsed local payload in-memory (single-flight, 180s TTL matched to
the parser session cache, expired entries pruned on write). /api/usage and the
local half of /api/devices now return from a Map hit after the first parse.
- Prewarm today at startup and inline that payload into index.html as a
bootstrap, so the SPA paints today's numbers with no round-trip. Only the
local device is embedded; '<' is escaped so a name cannot break the script
tag; served no-store; the seeded view refetches at once so live peers appear.
- Default the web command and dashboard to today.
- Cap the peer connect phase at 3s. req.setTimeout does not abort a stalled TCP
connect, so an offline paired device hung ~75s on the OS timeout; it now
degrades to an unreachable row in ~3s. The cap clears on TCP connect, so the
TLS handshake and the 65s pairing-approval wait are unaffected.
Measured: /api/usage 0.0007s warm (was ~0.22s), /api/devices ~3s with an offline
peer (was ~75s), first paint instant.
* feat(menubar): expose combined multi-device usage in menubar-json (#566)
The macOS menubar reads `codeburn status --format menubar-json`, which only
ever reflected the local machine. Multi-device "Combined" usage existed only in
the terminal `devices` command. This exposes combined + per-device usage through
the menubar-json contract so the menubar can mirror the combined dashboard.
- Extract the per-device summary + combined reduce that was inline in
renderDevices into a reusable pure `summarizeDeviceUsage(results, window?)`.
Error/unreachable devices are excluded from the combined sums (kept in
perDevice); deviceCount counts all, reachableCount counts the reachable ones.
renderDevices now formats from it with byte-identical output.
- Add an optional `combined?: CombinedUsage` block to MenubarPayload (perDevice
list + combined totals incl. calls/sessions). Absent by default.
- Add `--scope local|combined` to `status` (default local). `combined` builds the
local payload, pulls paired devices (pullDevices isolates per-peer failures),
and attaches the summary.
- Correctness guards: reject `--scope combined` with `--days` (non-contiguous,
not representable over the sharing query) and with `--provider`/`--project`/
`--exclude` (the sharing query carries no filters, so peers would report
unfiltered usage). Window-scope the cache-token sum to the selected period
(cache lives in 365-day daily history; current carries no cache counts).
TS/CLI only. The menubar Local/Combined toggle + render is a follow-up.
* fix(menubar): never let combined enrichment break the base local payload
The status --format menubar-json --scope combined path pulls paired devices.
Wrap that best-effort enrichment in a guard so an unexpected failure (corrupt
remotes store, peer I/O) can never take down the base local menubar payload —
on error the local payload is still emitted with combined omitted. Add a test
that a corrupt remote-devices.json still yields a valid combined (local-only) payload.
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* feat(sharing): pairing, token, and device-identity core
First piece of local device sharing: self-cert fingerprint identity
(trust-on-first-use), a one-time expiring pairing PIN, fingerprint-bound
tokens, and a peer store that authorizes a pull only when both the token
and the TLS peer fingerprint match the same paired device. Pure logic,
fully unit-tested; the TLS share server and host pull build on this.
* feat(sharing): secure mutual-TLS transport + pairing handshake
Add device identity (self-signed cert, persisted; fingerprint = sha256 of
the cert DER), an HTTPS share server (mutual TLS: presents its cert, reads
the client's, and serves /api/usage only when the bearer token AND the
client fingerprint match the same paired peer), a one-time-PIN pairing
endpoint, and a fingerprint-pinning client. Verified end to end on
loopback: PIN pairing, pinned authed pull, and rejection of a wrong PIN,
a token replayed from another device, and a mismatched server
fingerprint. Adds the selfsigned dep (Node cannot generate certs natively).
* feat(sharing): share + devices CLI (pair, pull, combine)
Phase 3 terminal flow: codeburn share runs the secure server on-demand
(stops after 10 min idle; --always to persist, --pair to add a device),
and codeburn devices add <host> --pin <pin> pairs and pins a remote.
codeburn devices pulls this machine plus every paired device, keeps each
separate, and prints a per-device table with a simple summed Combined
row (no server-side merge). Persists identity and peers under the config
dir. Host pair-and-pull flow covered by a loopback integration test.
* feat(sharing): mDNS discovery + approve-style (no-PIN) pairing
Add bonjour-service discovery (advertise/browse over the LAN), a short
confirmation code derived from both cert fingerprints (Bluetooth-style
'do these match?' check), and an approve-style pairing endpoint that
prompts the owner instead of requiring a typed PIN. Loopback-tested:
approved device gets a working token with a matching code on both sides,
declined device is rejected.
* feat(sharing): AirDrop-style discover + approve UX
codeburn share now advertises on the LAN and approves incoming devices
interactively (confirm the matching code, no typed PIN). codeburn devices
add (no args) discovers nearby devices, lets you pick one, shows the
confirmation code, and waits for the owner to approve. Manual
add <host> --pin stays as a fallback for networks that block mDNS.
* feat(sharing): share only aggregates, never project names or paths
Sanitize each device's payload before it leaves the machine: drop
topProjects and topSessions (project names + session detail) and send
only aggregate numbers (cost, tokens, models, tools, activities, daily).
What you are working on stays local; only the totals travel.
Add a Vite + React 19 + Tailwind v4 + Recharts SPA (dash/) and a 'web'
command that serves the built UI plus a local /api/usage endpoint backed
by the existing menubar aggregation. Midnight theme with CodeBurn's
orange chart ramp: hero cost, a stacked-by-model daily bar chart with a
custom tooltip and hover-dim, metric cards, by-tool and by-activity bar
lists, and top-projects and tools tables. Period and provider filters,
react-query with skeleton loading. Stays 100% local.
build:dash builds the SPA into dist/dash (shipped via package files,
served at runtime); a missing build falls back to a helpful message.
* feat(overview): plain-text monthly usage overview command
Add 'codeburn overview', a copy-pasteable text report. Defaults to the
current month, with --from/--to to filter and --no-color for plain
output. Renders Totals, By tool, Top models, Highest-value days, Top
projects, a daily table, By activity, and Tools, with colored section
headings (stripped under --no-color or when piped).
Reuses the existing session aggregation. Cost gains thousands
separators and tokens roll up to a B unit for readability via
display-only wrappers, without changing the shared formatters. Project
names use the path basename instead of the sanitized full path.
* fix(cli): exit cleanly on EPIPE when the stdout pipe closes early
Piping output to a reader that closes early (| head, quitting less, a
missing command) made stdout writes throw EPIPE and crash with an
unhandled error event. Handle EPIPE on process.stdout and exit 0 so
piping the overview and other commands behaves normally.
* docs(readme): document and highlight the overview command
Add a 'Your month at a glance' section featuring codeburn overview with
examples and sample output, a Commands-table entry, and the provider
flag note.
Two CLI polish fixes found during a full command sweep:
- Validate --provider against the known provider set (report, status, today,
month, export, optimize, compare, models). An unknown provider previously
produced a silently empty report; it now exits 1 with a clear message
listing valid values, matching how --period and --format already behave.
Provider names are exposed via a lazy allProviderNames() helper so importing
the providers module never depends on every provider object being defined.
- Make the non-interactive report/today/month render deterministic: yield a
tick so ink flushes the one-shot frame to stdout before unmounting, instead
of unmounting synchronously which could race the flush and drop the output.
Adds CLI provider-validation tests (rejection, valid name, all sentinel, and
a drift guard that allProviderNames covers every loadable provider).
Dashboard integrations need machine-readable optimize findings and yield ratios without parsing terminal output. This threads the existing analysis results into conservative JSON serializers while preserving text output as the default.
Constraint: Issue #419 asks for dashboard-friendly JSON for optimize and yield
Rejected: Build a separate analysis path | would risk drift from terminal output
Confidence: high
Scope-risk: narrow
Tested: npm test
Tested: npm run build
Tested: npm run dev -- optimize -p today --format json
Tested: npm run dev -- yield -p today --format json
Not-tested: Real downstream dashboard integration
Surfaces real subagent-transcript spend grouped by agentType
(workflow-subagent / Explore / general-purpose / …), read from each agent's
sibling .meta.json (cached on CachedFile; session cache bumped 3→4). This makes
ultra/workflow usage visible — the existing Task-input-based "subagents" section
never sees workflow agents. Shown in `report --format json` (claudeAgentTypes)
and a "Claude Agent Types" dashboard panel that renders only when Claude agent
transcripts exist, so it never appears for the other providers.
Claude Code routed through a GitHub-Copilot-backed proxy (ANTHROPIC_BASE_URL
-> claude-code-over-github-copilot / claudegate) costs ~$0 marginal but was
priced at full Anthropic API rates, producing a misleading cost figure. The
JSONL records only the model name and no endpoint, so proxying cannot be
auto-detected; the user declares it.
Add a `proxyPaths` config (managed via a new `codeburn proxy-path` subcommand)
listing project directories served through a subscription proxy. Sessions whose
canonical path is under one keep their full API-rate totalCostUSD (the billable
would-be figure is never destroyed) and additionally report totalProxiedCostUSD,
so the JSON report overview exposes cost / proxiedCost / netCost
(netCost = cost - proxiedCost). With no proxyPaths configured the new fields are
0 and every existing consumer is unchanged.
Design: "full cost, flagged" was chosen over zeroing cost so a misconfigured or
since-changed path can never silently erase real Anthropic spend. Attribution is
project-level (one canonical path per project), computed in a single helper
(summarizeProject) that all ProjectSummary builders route through, including the
cross-provider merge in parseAllSessions (re-derived from the merged total so a
repo used with both Claude and Codex stays correct). The in-memory session-cache
key folds in a proxy-config hash so toggling proxyPaths in a long-lived process
cannot serve stale attribution. Path matching is segment-boundary anchored
(/foo does not match /foobar), trailing-slash and backslash tolerant, leading-
slash agnostic (so a non-Claude provider's slash-stripped path matches the same
way Claude's absolute path does), and case-folded only on case-insensitive
filesystems (macOS/Windows, not Linux). The proxy-path CLI sanitizes a
hand-edited config.json (non-array / non-string entries) rather than crashing.
Tested: isProxiedPath matching matrix (boundary, case, Windows, empty, root,
multi-path, leading-slash form); config-hash distinctness/order-independence;
end-to-end attribution through parseAllSessions incl. the critical negative
cases (real spend must not be zeroed); cross-provider Claude+Codex merge;
Codex-only project under a proxy path; date-range-filtered attribution;
cache-staleness after a config change; and the proxy-path CLI add/list/remove,
malformed-config robustness, and the report --format json overview.
Scope note: proxiedCost/netCost currently surface in `report --format json`
only; wiring them into the TUI dashboard and menubar payload is a follow-up.
Resolves conflicts from the post-PR refactor that moved buildPeriodData,
hydrateCache, and the menubar payload builder out of main.ts into
usage-aggregator.ts. The PR's savings additions to those functions are
re-homed there; config.ts keeps both new fields; parser.ts keeps both imports;
redact.ts session details carry savingsUSD.
- Add per-install random salt to pseudonym hash to prevent rainbow table
reversal of common project names
- Redact session details (dates, models) when project names are hashed
to prevent re-identification via cost fingerprinting
- Return structuredContent in error responses to satisfy strict MCP
clients that validate against declared outputSchema
- Remove pre-warm fire-and-forget that raced with the inflight map
- Fix empty markdown table producing malformed cells
- Add tests for session detail redaction and cross-field consistency
Add a new `localModelSavings` config and `codeburn model-savings` CLI
that maps a local-model name (e.g. llama3.1:8b) to a paid baseline
(e.g. gpt-4o). The local call still costs $0; the new `savingsUSD`
field tracks the counterfactual spend avoided by running locally and
is reported separately from `costUSD` everywhere a number is shown.
* Parser normalization (`applyLocalModelSavings`) runs on Claude
parse, direct provider calls, and the cached-call path. It forces
`costUSD` to 0 and attaches `savingsUSD` + `savingsBaselineModel`
+ `isLocalSavings` on the `ParsedApiCall`. Local-savings wins for
actual cost even when the same model is also in `modelAliases`.
* Session, project, day, model, category, activity, skill, and
subagent rollups all carry `savingsUSD` alongside `costUSD`.
* `status --format json` adds `today.savings` and `month.savings`.
* `status --format menubar-json` adds a `current.localModelSavings`
block (totalUSD, calls, byModel, byProvider) plus savings on
topModels, topProjects, topSessions, topActivities, and history
daily entries. Schema fields default-decode for backward compat.
* `report --format json` adds savings across overview/daily/
projects/models/activities/skills/subagents/topSessions, with
the active paid baseline name on each model row.
* `models` command gains a `Saved` column on table/markdown/CSV
and a `savingsUSD`/`savingsBaselineModel` pair in JSON. Default
`--min-cost 0.01` filter now ORs in `savingsUSD >= minCost` so
local models with $0 actual cost but >0 savings still surface.
* CSV/JSON exports add a `Saved (CODE)` column on summary/daily/
models/projects/sessions.
* Dashboard TUI shows a green 'saved $X by local models' footer
line in the overview when any savings are present.
* macOS Swift payload gains a `LocalModelSavings` Codable block
and savings fields on every model/activity/session/daily
struct. Hero shows a green leaf 'Saved $X' caption, models
section gets a green `Saved` column. `swift build` clean.
* GNOME indicator adds 'saved $X' to the hero meta line and a
`codeburn-model-saved` column to the model row.
* Daily cache schema bumped to v8 (`savingsUSD` on day/model/
category/provider). `savingsConfigHash` invalidates the cache
when the user changes their baseline mapping so historical
saved-spend numbers never lie about a stale baseline.
* Defensive `Object.hasOwn` lookup in `getLocalSavingsBaseline`
blocks the prototype-pollution test that previously surfaced via
the savings path with a hostile `__proto__` model name.
* New tests (5 files, 25 tests, 549 lines) cover pricing helpers,
end-to-end parser normalization, day aggregator savings,
menubar payload savings, CLI set/list/remove, and
daily-cache hash invalidation. Existing tests for daily-cache
/ day-aggregator / models-report updated for the new fields.
Full vitest suite: 1028/1028 passing across 73 test files.
`tsc --noEmit` clean. `npm run build` clean.
(Note: `mac/Tests` has a pre-existing `no such module 'Testing'`
environment error on the installed Swift toolchain, confirmed
on `main` before this PR; not caused by these changes.)
Add subagentTypes to session-cache validateCall for consistency with
other string-array fields (tools, bashCommands, skills).
Stabilize Forge dedup key fallback: use model+tokens instead of array
index so deletions between scans don't cause double-counting.
Add mutual exclusivity validation for --day/--days/--from/--to on the
status command, matching the report command's existing checks.
In commit 5800179, the Commander configuration for `antigravity-hook` and `agy-statusline-hook` commands in `src/main.ts` was accidentally deleted during conflict resolution. This commit restores them along with the missing imports.
* Add multi-day calendar selection with custom popover UI
Replace native DatePicker with a custom-themed CalendarPopover supporting
non-contiguous multi-day selection. Add --days CLI flag for comma-separated
date filtering that post-filters from the bounding range scan.
CLI: parseDaysFlag() parses and validates dates, filterProjectsByDays()
filters turns to only selected days. Verified that sum of individual --day
calls matches --days output across cost, calls, activities, models, and
providers.
Swift: DataClient passes --days for multi-day, with single-day fallback.
AppStore tracks selectedDays as Set<String> with PayloadCacheKey support.
CalendarPopover uses pending state committed only on Done tap.
* Clear + Done in calendar resets to current period
When user clears all selected days and taps Done, switch back to the
active period instead of silently keeping the old day selection.
* Capture Antigravity CLI usage via statusline
* Harden statusLine hook: cap stdin at 1MB, fix JSONL file permissions
- readStdin() now rejects input exceeding 1MB to prevent OOM from
a buggy or runaway caller.
- Switch from appendFile (mode only on creation) to open(path, 'a', 0o600)
so the JSONL file is always created with private permissions.
- Set cache directory mode to 0o700.
---------
Co-authored-by: iamtoruk <hello@agentseal.org>
* Fix Antigravity provider detection, Codex fork double-counting, and tab ordering
Antigravity:
- Handle ephemeral port (--https_server_port 0) via lsof fallback
- Force reparse when cached turns are 0 (server may have been unavailable)
- Persist precomputed costUSD in session cache for correct pricing
- Map gemini-pro-agent to gemini-3.1-pro pricing
- Add display names for gemini-pro-agent and gemini-3.5-flash-low
Codex:
- Detect forked sessions via forked_from_id in session_meta
- Skip replayed parent events within 5s of fork creation time
- Use parent session ID in dedup key so parent+fork don't double-count
Menubar:
- Sort provider tabs by cost descending instead of enum declaration order
* Add tooling breakdowns to CLI dashboard and menubar
Surface skills, subagents, tools, and MCP server usage in both the
text dashboard and menubar app. Parse subagent types from Agent/Task
tool calls, aggregate with skills into a merged "Skills & Agents"
panel. Remove Top Sessions panel from CLI dashboard.
* Fix per-provider data loss, division-by-zero, and decode fragility
- Per-provider multi-day queries only merged cost/calls from cache,
dropping categories/models/sessions/tokens. Remove broken cache
shortcut and always do full parse for per-provider periods.
- Remove per-provider daily history double-counting from overlapping
cache + live data.
- Guard maxCost against zero in ActivitySection and ModelsSection to
prevent NaN in bar width calculations.
- Use offset-based ForEach ID in BarTooltipCard to avoid duplicate
model name collisions.
- Make cacheHitPercent, topActivities, topModels, providers use
decodeIfPresent for backward compat with older CLI versions.
- Skip currency switch when FX rate fetch fails with no cache,
preventing rate/symbol desync.
- Use readSessionFile in Gemini parser for 128MB size cap.
- Truncate Codex userMessage to 500 chars like other providers.
* Restore cache-backed trend history for provider-filtered views
The previous commit removed the broken per-provider cache shortcut but
also dropped cache-backed daily history, causing provider-filtered views
to lose trend data outside the selected period range.
Use allCacheDays for historical days (cost/calls per provider is accurate
in cache) and today's entry from the full parse. No overlap since cache
ends at yesterday.
* Add CodeBurn Pro Mac App Store app
SwiftUI MenuBarExtra with litellm-snapshot pricing, Claude/Codex/Copilot
parsers, session discovery, auto-refresh timer, and dashboard UI matching
the real menubar design.
* Add appstore/ to .gitignore
Private Mac App Store build, not for the public repo.
* Add Optimize tab, token display modes, daily budget alerts, and project drill-down
- New Optimize insight tab with Retry Tax and Routing Waste computations
(pure math from session data, no LLM required)
- Retry tax: shows money wasted on failed edit retries, per-model breakdown
- Routing waste: counterfactual savings vs cheapest reliable model, per-model
- Token display modes: Cost ($), Tokens (up/down split), Total Tokens
- Daily budget alert: configurable threshold, flame turns yellow when exceeded
- Project drill-down: click project rows to see per-session cost, tokens, models
- Period-aware top sessions: 30-day view now shows 30-day costliest sessions
- Friendly project names: show directory name instead of full path, Home for ~
- Session cache TTL bumped to 180s to prevent re-parsing on non-today periods
- Audit fixes: array mutation, percentage rounding, ForEach ID collision,
baseline minimum threshold, editTurns scoping, first-of-month edge case
* Fix model badge ForEach ID collision and budget warning provider filter
- SessionDetailsList model badges used id: \.name which silently drops
duplicate model entries. Switched to enumerated offset-based ID.
- Hero budget warning compared against provider-filtered payload instead
of todayPayload (all providers). Now matches the menubar flame tint.