Commit graph

682 commits

Author SHA1 Message Date
iamtoruk
f4fd6a1640 fix(pricing): restore Mythos 5 pricing dropped by gap-fill cleanup
Mythos 5 ($10/$50) is not in LiteLLM or the models.dev/OpenRouter gap-fill
yet (Fable is), so dropping the manual patch left it priced at $0. Add it to
MANUAL_ENTRIES + the snapshot and map the "Mythos 5" display name.
2026-06-10 00:09:31 +02:00
Gaurav Sisodia
a2e18d79e7
fix(cursor): period-aligned lookback; add Vercel AI Gateway provider (#432)
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.
2026-06-09 23:01:43 +02:00
KirDE
35a8518518
feat(antigravity-ide): added support for antigravity IDE (#418) 2026-06-09 22:12:01 +02:00
Tiago Santos
cf35854b09
feat: add devin provider (#444)
* feat: add devin provider

* feat: add devin to gnome extension and improve dev scripts

* fix: fix local installer

* chore: cleanup unecessary changes and address pr comments
2026-06-09 21:58:31 +02:00
Resham Joshi
02b16351be
fix(menubar-tests): add missing localModelSavings/savingsUSD args so swift tests compile (#465) 2026-06-09 21:56:13 +02:00
Resham Joshi
e83160f415
feat(pricing): proxy-aware cost attribution for subscription-backed Claude (#417) (#459)
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.
2026-06-09 21:30:57 +02:00
Resham Joshi
f5d1a8513d
fix(menubar): await terminationHandler instead of blocking a queue thread; cap CLI spawns (#462)
The #426 fix moved waitUntilExit and its timeout onto the same global(qos:.utility)
queue. Under sustained load every utility worker blocked in waitUntilExit, so the
timeout could never be scheduled to kill them and the menubar wedged on Loading
forever (confirmed via sample after ~a week of soak). Await process.terminationHandler
(fires on a Foundation queue, blocks no worker) so the timeout always has a free
thread. Add an actor-based async semaphore capping concurrent CLI spawns at 6.
2026-06-09 21:27:32 +02:00
Resham Joshi
ad251cfa3d
chore(pricing): drop manual Fable/Mythos patch; fable now gap-filled from models.dev/OpenRouter; keep Fable 5 name (#464) 2026-06-09 21:22:38 +02:00
Resham Joshi
a385f65dee
feat(pricing): automatic gap-fill from models.dev and OpenRouter (#457)
Keep model pricing automatic instead of hand-coding new models. The bundler
now layers three sources in priority order: LiteLLM (broad list prices),
hand-curated MANUAL_ENTRIES overrides, then a separate last-resort fallback
file gap-filled from models.dev first-party makers (official direct prices)
and OpenRouter (resale backstop). New models such as MiniMax-M3 ($0.6/$2.4)
now price correctly with no per-model code.

The fallback is written to its own pricing-fallback.json and consulted only
case-insensitively as the final step in getModelCosts, so a reseller variant
name can never shadow a canonical or aliased match.

Fixes surfaced while building and verifying this:
- Alias precedence: LiteLLM ships snowflake/claude-4-opus ($5), which the
  bundler strips to a bare claude-4-opus key that shadowed the curated alias
  to claude-opus-4 ($15 official). An explicit alias for a bare name now wins
  over a coincidental stripped reseller key; the prefixed gateway price is
  still returned for the fully-qualified id.
- Zero-stub guard: LiteLLM [0,0] price stubs (e.g. GigaChat-2-Max) are
  excluded from the case-insensitive index so a case-mismatched query stays
  null and keeps firing the unknown-model warning instead of silently
  reporting $0.
- Negative-sentinel guard: OpenRouter returns -1 for variable/BYOK-priced
  models. The bundler now rejects any non-positive rate pair (and strips the
  sentinel from cache fields) so a negative per-token cost can never ship and
  subtract from spend totals.

Bundler hardening: bareKey strips @pin and date suffixes to match the runtime
canonical form, seen-set dedupes on both full and bare key shapes, and it logs
MANUAL_ENTRIES now covered upstream plus models.dev allowlist drift. Extracted
buildCosts so the cache-cost heuristics live in one place. Added a data-hygiene
test that fails CI if a rebundle reintroduces negative, free, or unreachable
fallback entries.
2026-06-09 21:17:23 +02:00
Resham Joshi
c36f3afa76
chore(pricing): temp Fable 5 + Mythos 5 launch pricing ($10/$50 per M) + names until LiteLLM indexes them (#463) 2026-06-09 20:51:51 +02:00
Resham Joshi
8e460096e3
fix(codex): content-address fork dedupe key to stop undercounting divergent events (#458)
Some checks failed
CI / semgrep (push) Has been cancelled
Forked Codex sessions copy the parent's entire token_count history
(re-timestamped), so those replays are keyed into the parent's namespace
(forkedFromId) and deduped to avoid double-counting the parent's spend. But the
key used cumulativeTotal alone, which is too coarse: a genuine post-divergence
fork event whose running total coincidentally equals some parent total collided
with it and was dropped, undercounting real spend.

Add the cumulative token breakdown (input, cached, output, reasoning) to the
dedupe key. A fork replays those cumulative figures verbatim, so a true replay
still collides and parent and fork are not double-counted, while genuinely
different work that happens to reach the same cumulative total stays distinct.

The breakdown must come from the CUMULATIVE totals, not the per-event deltas:
the deltas are computed against a running `prev` that the fork advances
differently once the 5s replay cutoff skips some events, so a delta-based key
would spuriously diverge on a replay and double-count it (in the total-only
fallback branch).

This is the correct fix for issue #413. The reported "undercount" is mostly
replayed parent history being correctly credited to the parent, not lost; the
issue's suggested change (key on the fork's own session id) would instead
re-double-count the entire replayed history. Three regression tests pin the
invariants: a replay past the 5s cutoff counts once, a divergent fork event
sharing a parent's cumulative total is kept, and a total-only fork whose replay
straddles the cutoff does not overcount.
2026-06-07 07:29:42 +02:00
Resham Joshi
0edc8644aa
test(copilot): add JetBrains coverage; fix Windows path inference (#433 follow-up) (#456)
Some checks are pending
CI / semgrep (push) Waiting to run
Follow-up to #433. Adds tests for the JetBrains format: discovery of a jb
chat.jsonl, parsing into a call with inferred model/tools/user message, the
isJetBrainsFormat routing guard (legacy user.message files must not route to
the JB parser), and the id-less dedup fallback.

Also fixes inferJBProjectFromContent: it split homedir() on the platform
separator but the recorded tool path on a fixed '/', so project inference
always fell back to the raw session id on Windows. Split both on either
separator.
2026-06-06 23:32:56 +02:00
Zuber Khan
e7920fc6aa
feat: add JetBrains (IntelliJ/DataGrip) session discovery and parsing (#433)
* feat(copilot): add JetBrains (IntelliJ/DataGrip) session discovery and parsing
The Copilot provider previously only discovered sessions from:
- ~/.copilot/session-state/ (legacy VS Code format)
- VS Code workspace storage directories
IntelliJ and other JetBrains IDEs store Copilot chat sessions in
~/.copilot/jb/<session-id>/partition-*.jsonl using a slightly different
event format. This commit adds:
- discoverJetBrainsSessions(): finds all .jsonl files under ~/.copilot/jb/
- isJetBrainsFormat(): detects JB event format (no session.start header)
- parseJetBrainsEvents(): parses JB events (assistant.message with text/
  thinking, tool.execution_start for tool names, tool call ID prefixes
  for model inference)
- inferJBProjectName(): extracts project name from tool execution paths
Closes #N/A

* fix: address PR review feedback for JetBrains Copilot support
- Add 'sep' to path import (fixes ReferenceError in project inference)
- Use incrementing index as dedup key fallback when messageId is absent
  to prevent undercounting tokens/cost
- Move project inference to parse time using already-loaded content
  (avoids bypassing readSessionFile's 128MB safety cap)
- Tighten isJetBrainsFormat to only match user.message_rendered and
  partition.created (avoids false-positive on legacy files starting
  with user.message)
- Add dedicated inferModelFromEvents for JB format that checks
  data.model (100x weight) and tool call ID prefixes on
  tool.execution_start/complete events
- Remove dead first-pass loop that was never read
- Add jbDirOverride param to createCopilotProvider for test fixtures
2026-06-06 23:28:42 +02:00
Resham Joshi
4a384cd31d
feat(menubar): add macOS app icon (#455)
Adds assets/menubar-logo.png and generates AppIcon.icns (all standard sizes
via sips + iconutil) in package-app.sh, so the menubar app ships with a real
icon instead of the generic one. Info.plist already referenced AppIcon.

Icon and bundler change from @tvcsantos's #439; the unrelated tsconfig.json
change in that PR was left out.
2026-06-06 23:16:03 +02:00
Resham Joshi
fda117face
Merge pull request #454 from getagentseal/integrate/local-model-savings
feat(cli): local-model cost savings, re-homed onto current main (#421)
2026-06-06 22:38:23 +02:00
AgentSeal
a318375c6e Only show the Saved column when local-model savings exist
Without a model-savings mapping the Saved column was an unlabeled column of
dashes between Cost and Calls (menubar) and a dim '-' column (CLI models
report). Show it only when at least one model has savingsUSD > 0, so users
with no mapping keep the plain Cost/Calls layout. The menubar Hero caption was
already conditional.
2026-06-06 22:26:29 +02:00
AgentSeal
5a41f39944 Merge PR #423 (local-model savings) onto main, re-homing payload savings into usage-aggregator
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.
2026-06-06 22:14:13 +02:00
Resham Joshi
efa8593cc5
fix(parser): cache parse failures so broken files aren't re-read every run (#441 follow-up) (#453)
Some checks are pending
CI / semgrep (push) Waiting to run
Follow-up to #450. When a session file throws during parse it was excluded but
left uncached, so every refresh (~4x/min in the menubar) re-read and re-parsed
it, and only the first failing file per provider was ever surfaced.

- Add a negative-result marker: a failed file is cached as { fingerprint,
  turns: [], failed: true }. reconcileFile treats it as 'unchanged' at the same
  fingerprint, so it's skipped (no re-read) until the file changes. Empty turns
  => contributes no usage.
- Warn per offending file (with its path), capped at 5 per provider per run,
  instead of once-per-provider — so a systemic break surfaces more than one file
  without flooding. Cached markers keep it quiet across refreshes.

Tests: marker round-trips through save/load; reconcile stays 'unchanged' at the
same fingerprint and re-parses when the file changes.
2026-06-06 21:10:10 +02:00
Resham Joshi
e8009c4559
fix(parser): tolerate string message content; isolate per-file parse failures (#441) (#450)
Some checks are pending
CI / semgrep (push) Waiting to run
Some agents (Pi, and others for injected turns) write a message's `content`
as a string instead of an array of blocks. Parsers did `(content ?? []).filter`,
which throws on a string — and because the daily-cache backfill swallowed parse
errors, one bad session silently wiped the entire trend/history.

- Add normalizeContentBlocks(): string -> one text block, array -> as-is (by
  reference; drops null/undefined elements so the same crash can't happen one
  level down), else -> []. Applied across pi/codex/droid/cursor-agent and the
  Claude path in parser.ts.
- Isolate per-file parse failures in parseProviderSources: skip the offending
  file (warn once per provider) instead of re-throwing and aborting the whole
  backfill. The stale cache entry is already cleared, so the file is excluded.
- Surface backfill failures in hydrateCache via stderr instead of silently
  returning an empty cache.

Likely fixes #425 (previous-day always 0) for the throwing-file cause.
Tests: content-utils unit tests + a Pi string-content regression test.
2026-06-06 04:01:12 +02:00
Resham Joshi
f57ad64ce7
fix(network): add timeouts to critical-path fetches (#445) (#448)
The pricing fetch (loadPricing -> fetchAndCachePricing) runs on every CLI
invocation, and the macOS menubar shells out to the CLI and blocks on its
exit. fetch() had no timeout, so a half-open network after wake-from-sleep
made it hang forever once the 24h pricing cache expired — wedging the
menubar on its loading spinner until relaunch.

Add a shared fetchWithTimeout helper (8s default, AbortSignal.timeout) and
apply it to the two daily-critical-path fetches: pricing (models.ts) and
the currency rate (currency.ts). On timeout the existing catch falls back
to the bundled price snapshot / cached or USD rate.

Reproduced on main (stale cache + black-holed host -> hangs indefinitely);
with the timeout the same scenario aborts in 8s and renders via fallback.
2026-06-06 03:25:36 +02:00
Resham Joshi
77340e52b8
fix(models-report): stop double-counting cache reads (#447)
cacheReadInputTokens (Anthropic) and cachedInputTokens (OpenAI) name the
same tokens. ~11 providers populate both with the same value, so summing
them doubled the cache-read token figure in the models report. Take the
max instead. Cost is unaffected (calculateCost never used the sum).
2026-06-06 03:14:51 +02:00
Resham Joshi
cef1a6b669
refactor(mux): drop dead openai reasoning fallback (#446)
usage.reasoningTokens is the AI SDK v6 normalized field across all
provider families, so the providerMetadata.openai fallback never fired.
Removed it and the now-unused openai extraction.
2026-06-06 03:05:07 +02:00
Thomas Kosiewski
e0051fc403
feat(providers): add coder/mux as a datasource (#438)
* feat(providers): add coder/mux as a datasource

Reads coder/mux session data from ~/.mux/sessions/<workspaceId>/chat.jsonl and normalizes per-assistant-message token usage into ParsedProviderCall. Discovery resolves project names from config.json; the token decomposition matches mux's own inclusive input/output accounting, and cost is recomputed via LiteLLM. Includes unit tests and a provider doc.

Change-Id: Ie6ce9d41254d8bf05ff0965b443328dfa7b598de
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas Kosiewski <tk@coder.com>

* fix(providers): discover mux sub-agent transcripts

discoverSessions only globbed sessions/<id>/chat.jsonl and skipped each
spawned sub-agent's transcript at
sessions/<id>/subagent-transcripts/<childTaskId>/chat.jsonl. Against a real
~/.mux (629 workspaces) those nested files are 5,889 sessions and ~51% of all
assistant messages, so sub-agent spend was silently dropped: total mux usage
went from $17,113 to $21,564 (+26%) once counted.

Walk the subagent-transcripts dir per workspace and attribute each child
session to the parent's project. Dedup is unaffected: the parser keys off the
<childTaskId> dir name, which is disjoint from every workspace id, so each call
is still counted once. Cross-checked parsed per-model token totals against
mux's sibling session-usage.json.

Also corrects the provider doc, which wrongly claimed sub-agents get their own
top-level sessions/<id>/chat.jsonl, and documents the Google reasoning>output
decomposition edge case.

Change-Id: I1d43f26eec7c9c6c523e5ea541e2ff8d0c3aa07e
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 03:04:24 +02:00
Porun
42ef1ff97e
Add contribution heatmap insight (#437)
* Add contribution heatmap insight

* fix(menubar): heatmap layout polish — labels, spacing, pill order

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-06-06 02:49:08 +02:00
Resham Joshi
3cdd13881f
feat(menubar): configure CLAUDE_CONFIG_DIRS via Settings (#434) (#436)
Adds a "Config Directories" section under the Claude settings tab so users
can aggregate usage across multiple Claude config directories (work /
personal accounts) from the GUI. The menubar is an accessory app that
doesn't inherit the user's shell environment, so CLAUDE_CONFIG_DIRS was
previously unreachable from the app.

Rather than injecting the value as an env var into every spawned subprocess
(which would force arbitrary user paths through the shell/AppleScript
allowlist that deliberately excludes shell metacharacters), the list is
persisted to the shared ~/.config/codeburn/config.json. The CLI reads it
regardless of how it's launched, so both the menubar's data refresh and
terminal-launched `report`/`optimize` honor it consistently.

CLI: getClaudeConfigDirs() now reads a claudeConfigDirs array from config
as a fallback below the CLAUDE_CONFIG_DIRS / CLAUDE_CONFIG_DIR env vars
(env still wins for per-shell overrides), above the ~/.claude default.

Menubar: CLIClaudeConfig mirrors CLICurrencyConfig's flock-guarded write;
the Settings UI offers an add/remove list with a folder picker and forces
a refresh on edit.
2026-06-06 02:30:46 +02:00
Resham Joshi
14fbd4d6d1
fix(menubar): use SupportedCurrency enum in settings picker (#435)
Some checks failed
CI / semgrep (push) Has been cancelled
The currency picker hardcoded 8 currencies while the SupportedCurrency
enum defines 18. New currencies added to the enum (like CNY in #430)
required a separate SettingsView edit to appear in the picker.
2026-06-03 23:27:55 +02:00
Resham Joshi
a6601efc60
Merge pull request #430 from PorunC/feat/add-cny-currency
Add CNY currency support
2026-06-03 23:25:32 +02:00
AgentSeal
692bc09729 Merge feat/codeburn-mcp: MCP server for AI agent usage queries
Adds stdio-based MCP server (codeburn mcp) exposing get_usage and
get_savings tools. Includes pseudonymized project names with per-install
salt, session detail redaction, structured output schemas, and inflight
request coalescing.
2026-06-03 22:48:36 +02:00
Misaka
906c5338e4 Add CNY currency support 2026-06-03 10:45:39 +08:00
AgentSeal
f748d3463b fix(test): kilo-code tests fail on machines with KiloCode installed
Some checks are pending
CI / semgrep (push) Waiting to run
discoverSessions always checks the SQLite path at ~/.local/share/kilo
regardless of overrideDir, so the test found real sessions on the host.
Filter to override-dir sessions and avoid hardcoded /tmp paths.
2026-06-03 01:03:39 +02:00
AgentSeal
679f7ba5a6 fix(mcp): harden redaction, error responses, and pre-warm race
- 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
2026-06-03 00:56:30 +02:00
Resham Joshi
bec0491667
Merge pull request #426 from getagentseal/fix/menubar-dataclient-deadlock
fix(menubar): run CLI exit-wait and timeout off the cooperative pool
2026-06-02 15:56:11 -07:00
iamtoruk
94e6b77672 feat(mcp): add 'codeburn mcp' stdio command with stdout guard 2026-06-02 02:18:00 -07:00
iamtoruk
1e54967d97 feat(mcp): get_usage + get_savings tools with annotations, schemas, coalescing 2026-06-02 02:16:10 -07:00
iamtoruk
296085dff1 feat(mcp): markdown table renderers for usage and savings 2026-06-02 02:13:51 -07:00
iamtoruk
1e82a9cf21 feat(mcp): hash project names by default with opt-in reveal 2026-06-02 02:13:08 -07:00
iamtoruk
d5c8ad0bfd refactor: extract buildMenubarPayloadForRange for reuse by MCP 2026-06-02 02:12:27 -07:00
iamtoruk
2c46488f0f refactor: move buildPeriodData into usage-aggregator module 2026-06-02 02:01:45 -07:00
iamtoruk
4e61fd1369 build(mcp): add @modelcontextprotocol/sdk + zod, externalize in tsup 2026-06-02 01:38:45 -07:00
iamtoruk
0843efdecb docs(mcp): add implementation plan for codeburn MCP server 2026-06-02 01:33:08 -07:00
iamtoruk
dc7a1a7b0e docs(mcp): add design spec for codeburn MCP server 2026-06-02 01:20:58 -07:00
Resham Joshi
0431105052
Update README.md
Some checks are pending
CI / semgrep (push) Waiting to run
2026-06-01 23:46:00 -07:00
iamtoruk
4ffec37861 fix(menubar): run CLI exit-wait and timeout off the cooperative pool
The menubar wedged on "Loading Today…" for hours after an idle period.
Root cause: DataClient.runCLI called the blocking process.waitUntilExit()
from an async function on Swift's cooperative thread pool. On a 16-core
machine, 16 concurrent slow `codeburn` subprocesses pinned all 16
cooperative threads inside waitUntilExit; the 45s timeout — itself a Task
on that same pool — could then never be scheduled to kill them, so the
deadlock was permanent. Confirmed via sample: 16/16 cooperative threads
parked in waitUntilExit. PR #412 (AppStore inFlightKeys bookkeeping) was a
layer above the OS-thread deadlock and could not fix it.

Move both blocking points off the cooperative pool: bridge waitUntilExit
through a global (overcommit) queue via a continuation, and drive the
timeout from a DispatchSource on a global queue so it fires even when the
pool is saturated. Extract runProcess for testability; add a concurrency +
timeout smoke test and an output/exit-code test.
2026-06-01 02:09:05 -07:00
Justin Gheorghe
8ded6ad6fd feat(cli): track local-model cost savings against a paid baseline (#421)
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.)
2026-06-01 11:06:39 +03:00
iamtoruk
69b1736365 refactor(cli): share persistent-codeburn resolver; tighten Antigravity hook ownership
Some checks are pending
CI / semgrep (push) Waiting to run
Extract the duplicated codeburn-binary lookup (PATH building, npx-shim skipping,
default lookup dirs, resolver) from menubar-installer and the Antigravity
statusline installer into src/persistent-codeburn.ts. Both now import it and
keep their own user-facing "install globally" message.

Match the Antigravity statusLine hook on the trailing agy-statusline-hook token
instead of a bare substring, so a custom command that merely mentions the token
is treated as custom (protected, backed up) rather than silently overwritten.
2026-05-31 20:07:57 -07:00
iamtoruk
b246e822b7 Merge pull request #410 from ozymandiashh/codex/antigravity-hook-stale-path
Fix Antigravity hook stale CLI paths: install the statusLine hook through a
persistent codeburn binary resolved from PATH and repair stale CodeBurn-owned
hooks on re-install.
2026-05-31 20:04:11 -07:00
iamtoruk
3fa255fefc fix(models): match model names on version boundary, not bare prefix
A bare startsWith let an unlisted future minor collapse into the base entry
(gpt-5.6 -> "GPT-5"), the same mis-bucketing class as #420 but for non-Claude
families. Require an exact match or a key-plus-dash boundary in both
getShortModelName and the Codex provider so new versions fall through to their
raw id instead of a wrong name and pricing tier.
2026-05-31 05:37:12 -07:00
iamtoruk
0520ecbde8 fix(claude): derive model display names via getShortModelName
The Claude provider kept its own shortNames map with no claude-opus-4-8
entry, so the models table and menubar bucketed it into "Opus 4" — the
actual #420 path (the earlier models.ts map is unused here). Delegating to
getShortModelName removes the duplicate and picks up new versions for free.
2026-05-31 05:31:56 -07:00
iamtoruk
aa9bd9f0f1 feat(models): derive Claude names and fast multipliers automatically
New Claude releases no longer need a hand-maintained SHORT_NAMES entry or
FAST_MULTIPLIERS row. Display names are derived from the claude-<family>-<major>-<minor>
id, and the fast-mode multiplier rides along as a 5th element in the LiteLLM
snapshot tuple (provider_specific_entry.fast). Fixes #420: claude-opus-4-8 gets
its own line and correct pricing instead of falling into the Opus 4 bucket.
2026-05-31 05:17:14 -07:00
iamtoruk
ca41021a51 fix(menubar): treat the CLI credential store as the source of truth
The menubar kept its own copy of each provider's OAuth grant and refreshed
it on a timer, racing the CLI. Both Claude and Codex use single-use refresh
tokens that rotate on every refresh, so the menubar's self-rotation could
invalidate the user's own CLI login and surfaced as "disconnected" after a
long idle period.

Codex: read ~/.codex/auth.json fresh each cycle; only self-refresh when
last_refresh is older than 8 days; on 401 re-read the source before spending
our token; write rotated tokens back to auth.json (atomic, preserving other
keys); recover from reuse/invalid_grant by re-reading instead of going
terminal.

Claude: never HTTP-refresh the CLI-owned token. On expiry/401 re-read the
keychain with a no-UI query (LAContext.interactionNotAllowed) to adopt a
token the CLI already rotated; when none is available yet, report a transient
sourceTokenStale rather than a terminal disconnect.
2026-05-31 05:01:19 -07:00