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>
VS Code GitHub Copilot Chat users with no OTel agent-traces.db and no
~/.copilot/session-state/ saw $0.00 cost: codeburn never read VS Code's
core chat persistence, the only on-disk source carrying their token counts.
Add a fourth Copilot source that reads the VS Code chat delta-journals at
workspaceStorage/<hash>/chatSessions/*.jsonl and
globalStorage/emptyWindowChatSessions/*.jsonl. The files are a kind:0
snapshot / kind:1 path-set / kind:2 array-append journal; we replay it
(prototype-pollution-safe: __proto__/prototype/constructor segments are
rejected and containers use Object.create(null)) and read each request's
result.metadata.promptTokens / outputTokens (falling back to
completionTokens) and resolvedModel for pricing.
Dedup so users with multiple sources are not double-counted: prefer OTel
(skip chatSessions discovery when an OTel source is present), and skip a
workspace's legacy GitHub.copilot-chat/transcripts when that workspace has
chatSessions. New env overrides CODEBURN_COPILOT_GLOBAL_STORAGE_DIR and the
existing CODEBURN_COPILOT_WS_STORAGE_DIR keep discovery testable.
Tests cover the reporter's real request shape (promptTokens 32543 /
outputTokens 60 -> non-zero cost), empty sessions, emptyWindow discovery,
append-then-edit replay, requestId dedup, prototype-pollution paths, the
transcript skip, and the OTel-prefer skip.
The Skills & Agents panel was always empty for OpenCode sessions even
when skills and subagents were used. buildAssistantCall (shared by the
OpenCode SQLite and file-based parsers, and Kilo Code) counted the
skill/task tool invocations but never read the skill name or subagent
type from the tool-call input, so the dedicated breakdowns had no names
to aggregate.
In OpenCode's part files the identifier lives in state.input: the skill
tool carries input.name and the task tool carries input.subagent_type.
Extract both in buildAssistantCall and surface them on
ParsedProviderCall.skills / .subagentTypes, then stop hard-coding
skills: [] in providerCallToTurn and providerCallToCachedCall so the
classifier's subCategory and the subagent breakdown receive the data.
Verified against real OpenCode data: the previously empty skills[] and
subagents[] now populate (pipeline-investigation, plan-spec, splunk, ...
and explore/general subagents).
Fixes#556
Co-authored-by: Kevin Addison <kevin.addison3@tesco.com>
* fix(web): reject invalid dashboard periods without exiting
* test(web): assert invalid periods return 400 without exiting; drop redundant /api/devices re-parse
- Add tests/web-dashboard.test.ts: boots the dashboard on an ephemeral port and asserts
/api/usage and /api/devices answer 400 (not process.exit) for a bad period, and that
the server keeps serving afterward. runWebDashboard now returns the http.Server so it
can be exercised in-process; callers that ignore the return value are unaffected.
- /api/devices: resolve periodInfo once instead of validating then re-parsing it inside
localGetUsage (pullDevices invokes localGetUsage with the same already-validated query).
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* fix: add mssing support for ATIF v1.7
Co-Authored-By: bmcdonough <18721778+bmcdonough@users.noreply.github.com>
* fix(devin): drop unused MCP-coupled types, dead guard, harden metrics fallback
- Remove the @modelcontextprotocol/sdk JsonSchemaType import and the unused
ToolDefinition/FunctionDefinition types it served; type Agent.tool_definitions
as unknown since the parser never reads it (no dependency warranted).
- Remove isImageContentPart: unused, and its `"image" in part` check could never
be true (image parts carry `source`/`type`, not `image`).
- Use the `type` discriminant in isTextContentPart instead of property presence.
- getMetricsFromStep: fall back to legacy metadata.metrics when step.metrics is
present but empty, so a partial metrics object cannot silently zero usage.
- Tests: cover the empty step.metrics fallback and image-only message normalization.
---------
Co-authored-by: bmcdonough <18721778+bmcdonough@users.noreply.github.com>
Co-authored-by: AgentSeal <hello@agentseal.org>
The 2GB stream cap silently dropped whole Codex session files over the limit, so a live 2.4GB session (about 308M tokens of real gpt-5.5 spend) was excluded and reported as zero. Raise MAX_STREAM_SESSION_FILE_BYTES to 4GB (the line-by-line reader is bounded-memory and handles multi-GB files), and surface any remaining oversize skip via an always-on notice instead of a verbose-gated warning so a dropped session is never silent. Adds a maxBytes option for testability and tests covering the cap boundary.
Two post-0.9.12 cleanups.
Report model names: getShortModelName resolves a model's pricing alias before looking up its display name, so models priced via a sibling alias (ZCode/Hermes GLM-5.2 via glm-5p1, Grok Build via grok-build-0.1) leaked the internal pricing key as the model name in report --format json and the menubar model breakdown. Grok Composer was unmapped and showed raw. Add SHORT_NAMES entries so each resolves to its real name (GLM-5.2, Grok Build, Grok Composer 2.5 Fast). The models command was already correct because it uses each provider's own modelDisplayName.
Daily cache: providers added since the v8 rollup (Grok, Hermes, ZCode) parse usage that older binaries skipped, so days cached at v8 omit them and report 0 across history. Bump DAILY_CACHE_VERSION and MIN_SUPPORTED_VERSION to 9 to force a one-time full re-hydration so new providers backfill without a manual cache clear.
* fix(antigravity): read current agy antigravity-cli on-disk layout
* fix(antigravity): propagate SQLITE_BUSY from the .db read so the run retries
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* fix(cursor-agent): ingest workspace-less CLI transcript layout
* fix(cursor-agent): bump parse version so cached sessions pick up the new ingestion
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
gpt-5.3-codex-spark is a distinct model variant, but the longest-first
startsWith(key+'-') matcher (intended for reasoning suffixes -high/-low)
swept it into the base 'GPT-5.3 Codex' label, making a distinct model look
like the retired base in today/models/status output. Add an explicit
display entry in SHORT_NAMES and the codex provider so the longer key wins;
-high/-low still fall through to the base. Pricing is unaffected (LiteLLM
has no spark entry; cost falls back to the base rate as before).
Fixes#461
- Read the populated sessions.cwd column for project grouping; fall back to
scraping the transcript, then the profile name. The current Hermes build no
longer writes "Current working directory:" lines into messages.
- Set costIsEstimated when the figure is the LiteLLM fallback (no recorded cost).
- Re-throw SQLITE_BUSY from the discovery and read paths so a transient lock on
the live state.db is retried instead of being cached as an empty result.
- Tests: add the cwd column to fixtures; cover cwd grouping, the estimated flag,
and tool-result tool_name extraction.
Reads session-level usage from Hermes SQLite state databases
(~/.hermes/state.db and per-profile state.dbs). Tracks input,
output, cache read/write, reasoning tokens, stored costs, tools,
shell commands, and inferred projects.
- Provider reads both root and profile state databases
- Cost fallback: actual_cost_usd > estimated_cost_usd > LiteLLM pricing
- inferProject filters to user/system messages only
- Discovery query capped at LIMIT 10000
- sanitizeProject handles repeated leading separators
- All five review items from PR #386 addressed
- Tested against real Hermes data (557MB state.db with 198 sessions)
Closes#368. Supersedes #386.
The 'By Project' panel collapsed sibling Claude Code projects to a parent
folder in two cases:
1) resolveCanonicalProjectPath walked up to ANY ancestor .git and used it as
the canonical project. A stray .git in a parent of several projects (a
dotfiles bare repo, an accidental git init) made every sibling resolve to
that parent ('Projects'/'home'). Now only a real linked worktree (.git is a
FILE pointing at <main>/.git/worktrees/<name>) canonicalizes to its main
repo; an ordinary repo or a stray ancestor .git keeps the session's own cwd
as the project. Genuine worktree grouping is preserved.
2) When a Claude session has no cwd metadata, the dir slug was unsanitized by
replacing every '-' with '/', inventing path segments that overview then
split on, so 'Projects-Content-OS' rendered as 'os'. The lossy slug is now
kept intact, and overview only basenames ABSOLUTE paths.
Fixes#493
Reads ZCode CLI usage from ~/.zcode/cli/db/db.sqlite. The model_usage
table has exact per-request tokens; cost is computed from the pricing
table since ZCode stores none (GLM-5.2 runs on the z.ai start-plan
subscription).
- Split cached tokens out of input_tokens (OpenAI-style) so fresh input
and cache reads price correctly
- Attach each turn's tool calls to one request to avoid double-counting
- Map GLM-5.2 to glm-5p1 (GLM-5.1 rate) until LiteLLM lists it
- Register as a lazy SQLite provider; add test and provider doc
* feat(overview): cache in/out tokens table, roomier tables
Add a Tokens breakdown table (Input / Output / Cache in / Cache out /
Total with share %) so cache write/read are shown clearly instead of
crammed into the Totals line, and widen table cell padding to 2 spaces so
columns aren't congested.
* feat(overview): full comma-grouped numbers for a precise, spacious look
Render token counts as full thousands-grouped integers (e.g.
3,926,923,819) instead of abbreviated M/B, so the roomy tables read like
a precise statement. Update the overview test to match.
* feat(web): device discovery + pairing endpoints for the dashboard
Add /api/identity, /api/devices/scan (mDNS browse with confirm code and
paired status), and /api/devices/pair (approve-style pairing via
linkRemote). Backs the Search local devices button so pairing can happen
from the browser instead of the CLI.
* feat(web): two-panel dashboard with eywa-inspired warm theme
Redesign the dashboard into a left sidebar (device switcher + Search
local devices) and a right content panel, restyled to a warm-paper,
forest-green archival theme (serif display numbers, ink text, hairline
borders) in place of the dark midnight theme. The Search local devices
modal discovers nearby devices over mDNS and pairs in one click (approve
on the other device), backed by /api/devices/scan and /api/devices/pair.
* feat(web): use the CodeBurn flame logo in the dashboard header
Replace the placeholder triangle with the flame mark (the repo's
codeburn-symbolic vector), tinted the theme's forest green, and set the
same flame as the favicon.
* feat(web): color the All-devices chart by device, add task-level combined views
In the All devices view the daily chart now stacks by device (one color
per device) instead of by model, and the combined panels add a By task
breakdown plus in/out token detail. Devices that cannot be reached are
hidden entirely rather than shown as error rows.
* feat(web): use the flame image as the dashboard logo and favicon
Replace the inline vector flame with the brand flame PNG
(public/codeburn-flame.png) in the header and as the favicon.
* fix(web): trim flame logo padding and tighten brand spacing
Crop the transparent border off the flame PNG (it was 184px of padding
each side) and reduce the header gap so the mark sits close to the
wordmark.
* feat(web): cost/tokens toggle, cache read/write, full feature panels
Add a Cost/Tokens unit toggle that switches the hero number and the chart
between dollars and tokens. Surface cache write/read token totals as
metric cards. Add per-device panels for subagents, skills, MCP servers,
and a savings / retry-tax / routing-waste summary. The combined view
gains cache totals and the unit toggle too.
* feat(web): use CodeBurn website logos and add community links
Use the website's navbar flame (logo.png) with the Code+Burn wordmark in
the header, and the three-flame app icon (icon.png) as the favicon.
Add Website, Discord, and X links to the sidebar footer.
* fix(web): use the single-flame logo for the favicon too
The three-flame icon was wrong for the tab; use the same single flame as
the navbar everywhere and drop the unused three-flame asset.
* chore(web): set dashboard title to CodeBurn - Local Dashboard
* harden sharing + dashboard for public launch
Security:
- pairing: cap PIN attempts (close window after 5 wrong guesses) so a
6-digit PIN cannot be brute-forced within the TTL on a 0.0.0.0 listener.
- web dashboard: reject non-loopback Host (defeats DNS rebinding that
could read unsanitized local usage) and cross-origin requests (CSRF);
require application/json on the pair endpoint.
- store tokens with 0600 perms (was world-readable), 0700 dir.
Robustness:
- client: per-request timeout so a hung/asleep peer cannot hang a pull;
pullDevices fetches remotes concurrently and isolates failures.
- dashboard: normalize peer payloads at the boundary and add an error
boundary so a peer on a different version cannot white-screen the SPA;
finite-guard fmtNum/compactUsd.
Tests: PIN attempt-cap test added (1269 pass).
* feat(web): share this device from the dashboard, with browser approval
Add a Share this device toggle to the dashboard sidebar. It runs the
secure share server in-process (mTLS + mDNS advertise) so no terminal is
needed, with a Keep sharing always option (persisted; otherwise it stops
after idle). Incoming approve-style pairings are queued and surfaced in
the browser as an Approve/Deny prompt with the matching code, instead of
a terminal prompt. The shared payload is sanitized; start degrades
gracefully if the port is already held by a CLI share.
* fix(sharing): keep a paired device from dropping on a transient pull
- client: 8s -> 15s timeout and a fresh socket per request (agent:false) so
the pinned-fingerprint check always reads this connection's cert.
- share server: wrap request handling so a getUsage error returns a fast
500 instead of hanging the caller (which times out and drops the device).
- dashboard: re-pull paired devices every 20s so a brief drop self-heals
instead of staying gone until you change tabs.
* fix(sharing): re-sanitize remote payloads on receipt
A peer might run an older build that does not strip its own project
names/sessions. Sanitize every remote payload when we receive it too, so
project names never cross into our dashboard regardless of the sender's
version. Aggregate numbers (cost, tokens, models, tools, daily) are kept.
* harden dashboard sharing: version-skew, lifecycle, server errors
- normalize remote daily-history entries so a peer on an older build (daily
rows missing topModels) can no longer crash the chart / brick the page.
- ShareController: commit always/sharing state only after listen() binds, so
a port-in-use start no longer reports sharing when it is not.
- attach durable error/tlsClientError handlers to both HTTPS servers so a
malformed peer connection cannot crash the process.
- reset view/provider selection when the viewed device disappears or lacks
the selected provider (no more empty/no-selection state on sleep-wake).
- by-device chart: stable per-device color/key so bars do not reshuffle when
a device drops or returns between polls.
* polish: device identity, approval guard, queue cap, formatting
- give each device a stable unique id (cert fingerprint for remotes,
'local' for this device); key the sidebar, selection, and by-device
chart by id so two devices sharing a hostname no longer collide.
- approval prompt: drop a request from the UI the moment it is answered
so it cannot be double-clicked.
- share controller: cap concurrent pending approvals and allow only one
per device, so a LAN peer cannot flood the prompt.
- usd(): render negatives as -$5.00, consistent with compactUsd.
* 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.
* fix: fix and improve test isolation and collision with environment
* docs: remove unnecessary comment
* test(env-isolation): clear CODEBURN_FORCE_MACOS_MAJOR and pin TZ
Two env vars read in src/ were not isolated: CODEBURN_FORCE_MACOS_MAJOR
(now cleared so it cannot leak between tests) and TZ (now pinned to UTC,
since clearing it falls back to the OS zone and would shift date buckets
versus a clean CI runner).
---------
Co-authored-by: AgentSeal <hello@agentseal.org>
* 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.
OpenCode 1.1+ stores sessions as file-based JSON under
storage/{session,message,part}/ instead of a SQLite DB, so the
SQLite-only provider discovered zero sessions on current installs and
reported no usage at all.
Add a file-based discovery and parser path, preferred when present and
falling back to the SQLite DB for pre-migration installs. Extract the
shared message-to-call logic (token extraction, tool and bash parsing,
cost) into session-message.ts so the file path, the SQLite path, and
Kilo Code stay identical.
Adds Grok Build (xAI coding CLI) as an eager provider with caching- and compaction-aware token estimation, real tool/shell extraction, Skills & Agents from spawn_subagent, grok-build pricing, and SuperGrok plan presets. Tested against real sessions; full suite green.
* Add zerostack provider scaffold
Register zerostack (gi-dellav/zerostack) as a core provider reading
plain-text sessions from $XDG_DATA_HOME/zerostack/sessions/. Parser is
modeled on pi.ts; on-disk schema is unverified and must be confirmed
against real sessions before a PR (see docs/providers/zerostack.md).
* Rework zerostack provider against real session schema
Replace the initial scaffold (modeled on pi.ts JSONL) with the verified
format from a real local run and the zerostack source:
- Sessions are one JSON file per session under the platform data dir
(~/Library/Application Support/zerostack/sessions on macOS,
$XDG_DATA_HOME/zerostack/sessions on Linux; ZS_DATA_DIR overrides).
- Tokens are cumulative session totals, not per-call, so emit one
ParsedProviderCall per session from total_input/output_tokens.
- Recompute cost via LiteLLM (calculateCost); OpenRouter ids arrive
route-prefixed and resolve through canonical names.
- zerostack persists only final assistant text, so tools/bash are empty.
Verified against a real session (DeepSeek v4 Pro via OpenRouter):
34.1K in / 961 out / $0.016, matching the recorded total_cost. Adds a
fixture-based test and rewrites the provider doc to match.
The export schema emitted tools and shell-commands but no MCP section, and
the tools list is built from extractCoreTools which strips mcp__ names, so
MCP usage never appeared in codeburn export output even when sessions had
MCP activity (issue #496).
Add an mcp section to the JSON export and an mcp.csv to the CSV export,
sourced from session.mcpBreakdown (server -> calls, with share). Mirrors the
existing tools section.
The underlying parse fix (Codex mcp_tool_call_end attribution) landed in
#513; this makes that data visible in exports. Validated on real local data:
the JSON export now reports mcp: [{Server: node_repl, Calls: 5, ...}].
Fixes#496
Recent Codex sessions report completed MCP tool calls as an event_msg with
type mcp_tool_call_end instead of a function_call response_item. The parser
only built its tool list from function_call and patch_apply_end events, so
these MCP calls were never attributed: token usage was still counted, but the
MCP tool itself was missing from recent lookback windows (today/week/month)
while older history still showed it.
Add a handler that reads invocation.server and invocation.tool from the
event and rebuilds the canonical mcp__<server>__<tool> name the classifier
recognizes. Bump the Codex result cache to v4 so sessions cached under v3
re-parse and pick up the previously dropped MCP calls.
Validated on real local data: 5 mcp_tool_call_end events that were dropped
before are now attributed as mcp__node_repl__js, with no change to the total
call count.
Fixes#478
Large Cursor DBs were scanned as "the most-recent 250k bubbles by ROWID",
which dropped in-range older sessions from long-range reports and warned even
when the requested window fit comfortably. The bubble timestamp lives in the
JSON value (no index), so the date filter forces a full decode per row, which
is why a scan bound exists.
Replace the blind cap with a range-aware paged scan: for DBs over the budget,
page ROWID-descending, keep only rows within the window (createdAt > timeFloor),
and stop once a full page falls past the window floor. The hard budget remains
as a backstop for genuinely enormous in-range scans, and the "older sessions
may be missing" warning now fires only when that budget is actually hit.
Effect: short ranges decode far fewer rows and no longer warn; long ranges
return the full window when it fits the budget; truncation keeps the newest
in-range bubbles and warns only then. Small DBs are unchanged (un-paged query).
Budget is overridable via CODEBURN_CURSOR_MAX_BUBBLES for tests.
* feat(codex): compute Codex credit usage (#408, #495)
Codex/ChatGPT subscription users consume credits, a unit separate from API
dollars: usage is billed as credits-per-million-tokens at per-model rates that
differ from the API USD pricing CodeBurn uses for cost. So the reported dollar
cost does not match what credits actually consume.
Add a credit engine sourced from the official Codex credit rates
(developers.openai.com/codex/pricing): GPT-5.5 125/12.5/750, GPT-5.4
62.5/6.25/375, GPT-5.4 mini 18.75/1.875/113 credits per 1M input/cached/output
tokens. Surface per-model credit usage in `codeburn models` JSON output
(credits field; null for non-Codex or unknown models). models-report already
folds reasoning into output and keeps non-cached input + cached-read separately,
which is exactly what the credit rates expect, so the figure is exact.
Engine + computation are unit-tested. UI display surfaces (the models table,
the TUI dashboard, the menubar "credits" view) are intentionally left for a
follow-up so the display choice can be decided.
* feat(menubar): opt-in Codex credits display metric (#408, #495)
Surface Codex credit usage in the menubar as a selectable metric, without
changing the default. Cost ($) stays the default in both the menubar and the
CLI; credits only appear when explicitly chosen.
- TS: buildMenubarPayloadForRange computes the period's Codex credits (via the
tested aggregateModels, so reasoning/cached are handled) and exposes
current.codexCredits in the menubar JSON.
- Swift: new DisplayMetric.credits, a "Credits (Codex)" option in the metric
picker, decodes codexCredits, and renders it in the menu-bar title. Default
metric remains .cost.
Each test spawns 'tsx src/cli.ts' several times, re-transpiling the CLI on
every spawn. Under full-suite parallel load these spawns contend for CPU and
the slowest test could exceed the 5s default timeout, even though it runs in
~1.7s in isolation and the spawnSync calls already allow 30s each. Set the
file testTimeout to 30s so the wrapper matches the per-spawn cap. Full suite
is green across repeated runs.
buildMenubarPayloadForRange('today') parsed the developer's real on-disk
session data under a fixed 5s timeout, so on a heavy-usage day the test
timed out locally (it stayed green on CI, which has no real data). Point
HOME and the config dirs at an empty temp dir so the payload is built from
an empty dataset: deterministic and fast (~0.3s) while still asserting the
payload shape and the optimize:false short-circuit.
The Kiro provider previously only detected sessions from the Kiro IDE
(VS Code-based) stored in globalStorage. This adds support for Kiro CLI
(`kiro-cli chat`) sessions stored in ~/.kiro/sessions/cli/.
CLI sessions use a different format:
- .jsonl files with Prompt/AssistantMessage/ToolResults entries
- Companion .json files with session metadata, model info, and
per-turn metering usage (credits)
Changes:
- Add CLI session discovery (scans ~/.kiro/sessions/cli/ for .jsonl)
- Add CLI JSONL parser that extracts turns, tools, and cost from
metering_usage credits
- Add CLI tool name mappings (read, write, shell, grep, glob)
- Make CLI sessions dir configurable via third param to
createKiroProvider for testability
- Add unit tests for CLI session discovery and parsing
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
Keeps the perf win from PR #486 (yesterday stays cached, no re-parse every
run) but restores a narrow safety guard: drop any cached entry dated today or
later before computing the gap range. The cache only ever stores complete past
days, so a >= today entry can only come from the clock moving backward or a
stale older cache; without this it would be served frozen instead of being
recomputed live. Yesterday and earlier are untouched, so this does not
re-parse already-cached days. Adds a regression test.
Route the menubar installer's GitHub downloads through an undici ProxyAgent when HTTP(S)_PROXY is set, honoring NO_PROXY for bypass. Falls back to direct fetch when no proxy is configured. Closes#473.
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.
collectJsonlFiles only read agent transcripts directly under `subagents/`, but
the workflow feature nests them at `subagents/workflows/<wf>/agent-*.jsonl`, so
their tokens were dropped — undercounting whenever workflow/ultracode was on.
Walk the `subagents/` subtree recursively. Verified on real data: a workflow-
using project recovered ~16% of its cost, with no change to other projects.
The synthetic source path has no on-disk file, so fingerprintFile returned
null and parseProviderSources dropped the source before parsing — the provider
always reported $0 even with a key set. Add a `network` provider flag; such
sources skip the fingerprint gate, re-fetch each run with a synthetic
fingerprint, and keep the gateway's reported cost (instead of recomputing from
tokens, which would be $0 for any model codeburn can't price). Adds an
end-to-end test through parseAllSessions.
Opt-in Vercel AI Gateway provider (AI_GATEWAY_API_KEY/VERCEL_OIDC_TOKEN), inert without a key. Cursor lookback now period-aligned with a 6-month floor. Gateway fetch hardened on merge with an 8s timeout and try/catch fallback.
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.