Commit graph

285 commits

Author SHA1 Message Date
iamtoruk
ded4e4e844 feat(cli): scan progress protocol, incremental cache saves, --no-timeline
- 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
2026-07-16 10:51:18 -07:00
Resham Joshi
3163ccb5c4
fix(report): surface estimated costs distinctly (#688)
* 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.
2026-07-16 10:46:21 -07:00
Resham Joshi
3dd1d1c793
feat(doctor): per-provider detection diagnostics (#685)
* 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.
2026-07-16 10:38:08 -07:00
Resham Joshi
98051b4263
fix(copilot): bill CLI input and cache tokens from session.shutdown (#684)
* fix(copilot): bill CLI input and cache tokens from session.shutdown

Copilot CLI sessions (~/.copilot/session-state/*/events.jsonl) recorded
only outputTokens, because the parser read assistant.message events and
ignored the session.shutdown rollup. That rollup is the sole on-disk
source of a CLI session's input, cache-read and cache-write tokens, so
cost was underreported (in the reported case 35k+ cache-read tokens were
dropped entirely).

Handle session.shutdown in the CLI (non-transcript) JSONL path and emit
one supplementary call per model from modelMetrics.<model>.usage. The
per-turn assistant.message events keep owning output tokens, tools and
turn structure; the shutdown call contributes only the input and cache
tokens they lack, with output excluded, so no dimension double-counts and
the combined cost equals the full CLI-measured cost. usage.inputTokens is
cache-inclusive (input + cache_read + cache_write), so the cache
components are subtracted to recover the uncached input calculateCost
expects. These are real counts written by the CLI, so costIsEstimated is
false. Sessions without session.shutdown (crashed or still running) keep
the exact previous output-only behavior, and the VS Code, JetBrains and
OTel paths are untouched.

Bump the copilot entry in PROVIDER_PARSE_VERSIONS so already-cached CLI
sessions re-parse and pick up the corrected tokens and cost.

* fix(copilot): never emit an empty-timestamp shutdown call

A session.shutdown without its own timestamp and without sessionStartTime
produced a call with an empty timestamp, which the date-range filters in
parser.ts drop silently, erasing exactly the tokens this feature bills.
Fall back to the last stamped event in the session.

* fix(cache): durable orphans survive provider fingerprint changes

Bumping a durable provider's PROVIDER_PARSE_VERSIONS changed its env
fingerprint, and getOrCreateProviderSection replaced the whole section on
mismatch. For copilot OTel orphans the discarded cache entry is the only
remaining record of pruned spans, so the first post-upgrade run silently
and permanently erased month-to-date history. Carry forward exactly the
entries whose source no longer exists; present sources still drop and
re-parse under the new fingerprint, which is the point of a bump.

Regression test forges a pre-upgrade fingerprint on a pruned-source
orphan and asserts it stays counted and cached across the transition.
2026-07-16 10:37:58 -07:00
Resham Joshi
b7e3bac263
Merge pull request #679 from getagentseal/feat/desktop-app
CodeBurn Desktop: standalone Electron app
2026-07-16 07:54:05 -07:00
iamtoruk
1b600d1c82 Merge origin/main into feat/desktop-app
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.
2026-07-16 07:52:28 -07:00
kronos
68ad0be18b
fix(antigravity): stamp mtime fallback at emission, tighten path classification (#612)
* 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
2026-07-16 07:49:54 -07:00
Resham Joshi
4307542b62
feat(claude): account for /advisor escalations under the advisor model (#659)
* feat(claude): account for /advisor escalations under the advisor model

Claude Code's advisor tool (/advisor) escalates hard decisions mid-turn
to a stronger model. Those tokens are recorded inside
message.usage.iterations as advisor_message entries with their own model
and are excluded from the top-level message.usage totals. The parser read
only the top-level totals, so advisor spend was silently dropped and never
attributed to the advisor model.

Emit a separate call per advisor_message iteration priced under the
advisor model, preserve those iterations through entry compaction and the
large-line (>32KB) byte-scanner path, and fold advisor cost into the guard
budget. Bump the claude parser and guard cache versions so existing caches
re-parse and pick up the advisor spend.

* test(guard): pin advisor cost fold in the live budget

The guard's advisor fold in computeSessionUsage had no test; dropping
advisorCost from the line cost left all 30 guard tests green. Pins the
fold (advisor line costs more than the same line without iterations)
and the per-message replace semantics under streaming rewrites.
2026-07-16 07:42:49 -07:00
ihearttokyo
ccd8764e5b
feat(dashboard): make Daily Activity history directly scrollable (#672)
Some checks are pending
CI / semgrep (push) Waiting to run
* feat(dashboard): add inline daily history paging

* refactor(dashboard): make daily activity directly scrollable

* fix(dashboard): keep the empty state for users with no history at all

Scrollable mode kept the dashboard shell up whenever full history exists,
but a truly-new user with zero data anywhere saw a zeroed skeleton instead
of the No usage data message. Extract the decision into showEmptyState so
the boundary is test-pinned: empty period with history renders the shell,
empty everything renders the empty state, and the loading pass never
flashes it.

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-07-16 04:10:08 -07:00
iamtoruk
699ca53979 feat(cli): price-override --list --format json 2026-07-16 03:45:36 -07:00
ozymandiashh
df78e50052
feat: add CodeWhale provider support (#674)
Co-authored-by: AgentSeal <hello@agentseal.org>
2026-07-16 03:12:28 -07:00
ozymandiashh
5d366fb03b
feat(dashboard): add granular session and model timelines (#673)
* feat(dashboard): add granular usage timelines

* fix(dashboard): readable timeline legend and month-scale buckets

Session legend labels now use the real projectPath's last two segments
(app/web) instead of the sanitized project dir, which rendered as an
unreadable -Users-name-... dump and truncated in the legend.

Hourly buckets now cap at 8 days; longer ranges bucket daily. A 30-day
window rendered 720 overlapping hourly spikes, unreadable as a chart.

The browser no longer gives the backend session_other/model_other
aggregate a top-N slot, which rendered two identical Other legend
entries whenever the backend fold out-ranked a real series.

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-07-16 03:04:54 -07:00
ozymandiashh
1e54c693b6
fix(opencode): support custom data dir and db prefix (#620)
Co-authored-by: AgentSeal <hello@agentseal.org>
2026-07-16 02:18:19 -07:00
ozymandiashh
e2d006be9d
fix(claude): support Windows Desktop sessions via APPDATA (#615)
* fix(claude): use APPDATA for Windows Desktop sessions

* test(claude): harden withPlatform mock against cross-file leak

The process.platform mock omitted configurable:true and never restored
the value when the property had no own descriptor, leaving win32 mocked
for later tests on the same Vitest worker. Add configurable + an else
branch that deletes the override to expose the real inherited value.

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-07-16 02:13:11 -07:00
ozymandiashh
4ef32c27cb
fix(optimize): exclude a young project's founding session from cost-outlier detection (#666)
A project's first/bootstrap session is a different kind of work (one-off
scaffolding), not an expensive instance of routine work. While a project is
still young its peer sample is thin, so the leave-one-out average is dominated
by a few routine sessions and the founding session's legitimately high one-off
cost trips the >2x outlier multiplier. Exclude each young project's earliest
costed session from the outlier finding only; mature projects are unaffected so
genuine outliers still surface. Young = fewer than 2x MIN_SESSIONS_FOR_OUTLIER
costed sessions.

Fixes #664
2026-07-16 02:03:06 -07:00
ozymandiashh
e2cba003a8
Include archived Codex sessions in usage reports (#667)
Co-authored-by: zhangshihao03 <zhangshihao03@baijia.com>
2026-07-16 01:55:25 -07:00
iamtoruk
599764f39c feat(cli): menubar payload carries provider ids and active currency
ProviderCost now keeps the internal id alongside the display name. The
providers map keys stay lowercased display names (byte-identical, the
Swift menubar decodes them); new current.providerDetails exposes
{id,label,cost} so GUI clients can round-trip a valid --provider value.
New top-level currency {code,symbol,rate}: payload values remain raw
USD, clients convert at display time like the Swift menubar does.
2026-07-16 01:36:59 -07:00
iamtoruk
44d2449d30 fix(cli): compare cache-hit rate excludes cache writes
Compare defined cache-hit as read/(input+read+write) while menubar-json
and every other surface use read/(input+read), so the same model showed
two different numbers depending on screen. Standardize on the latter;
TUI compare output changes intentionally.
2026-07-16 01:14:41 -07:00
Andrew Lee
bdf4f3fe24 kiro: price v1 executions from credits; unify fallback across parsers
v1 modern execution files carry the same metered credits as v2
(usageSummary[].usage, unit "credit" — the predecessor of v2's
promptTurnSummaries), but the parser only harvested usedTools from
that array and priced executions from estimated tokens. Since v2 only
ships in brand-new IDE builds, v1 is the format nearly all Kiro IDE
users are on today. Sum usage across usageSummary entries and price at
the public overage rate.

Align all three credit parsers (CLI, v1, v2) on one fallback contract —
gate on summed credits > 0 and fall back to token-estimated pricing
with costIsEstimated: true:

- CLI turns without metering_usage were priced at a frozen $0
- CLI turns with an EMPTY metering_usage array (75 of 10,105 real turn
  metadatas — meta written before metering lands) passed the truthy
  presence check and froze $0 marked as real cost
- legacy .chat calls now carry costIsEstimated: true, which was always
  the reality but never stated

Validated against a real machine: 221 of 277 v1/legacy calls now carry
metered cost ($20.11) instead of token estimates that had overstated
them ~5x; verified usageSummary values are per-turn amounts, not
cumulative counters (sequences fluctuate). Call counts unchanged before
and after — nothing gained, lost, or double-counted.

Also document the companion-file fingerprint blind spot at
fingerprintFile: kiro CLI credits and v2 modelId live in companion
files the single-file fingerprint never sees, so a parse racing the
companion write can cache fallback values that only self-heal while
the transcript keeps changing.

AI-Origin: ai-generated
AI-Tool: kiro
2026-07-14 22:36:08 +00:00
Andrew Lee
7dcd618898 kiro: preserve credit-based cost through the session cache
providerCallToCachedCall drops provider-computed costUSD unless the
provider is allowlisted, and cachedCallToApiCall then re-prices from
token counts. Kiro's cost now comes from metered credits, which token
pricing cannot reproduce — reports were understating real kiro spend
~18x (e.g. $130.72 of metered CLI credits reported as $7.20 of token
estimates). Add kiro to the pass-through allowlist, same as the other
platform-billed providers (mistral-vibe, devin, hermes, ...).

Both persistence layers froze pre-fix dollar values and neither
self-invalidates (historical session files never change), so bump:

- session cache: CACHE_VERSION 4 -> 5 (cached kiro entries carry
  costUSD: undefined and would keep being token re-priced forever)
- daily cache: DAILY_CACHE_VERSION 10 -> 11 with MIN_SUPPORTED_VERSION
  raised (finalized days carry token-estimated kiro costs off by up to
  16x per model), same as the v10 cursor precedent

Test: end-to-end through parseAllSessions — a CLI fixture with 2.5
metered credits must report $0.10; fails with the token re-price
($0.000048) when the allowlist entry is removed.

Docs: document the pricing contract in docs/providers/kiro.md — metered
credit cost is frozen at parse time, so price-override / model-alias
do not affect kiro dollar amounts (shared tradeoff of all allowlisted
providers).

AI-Origin: ai-generated
AI-Tool: kiro
2026-07-14 22:35:51 +00:00
Andrew Lee
a1f8f4c4ff kiro: add IDE v2 session store support with credit-based pricing
Kiro's new IDE builds write sessions to ~/.kiro/sessions/<hash>/sess_<id>/
(session.json + messages.jsonl event log) instead of globalStorage. Add a
v2 parser and discovery, plus fixes surfaced while validating against a
real upgraded machine:

- Parse v2 event-sourced turns (user/turn_start/assistant/tool_call/
  tool_result/usage_summary/turn_end) with per-execution dedup keys
  (kiro-v2:<session>:<execId>) and defensive flushes for out-of-order
  events and in-progress sessions
- Price turns from real metered credits at the public overage rate
  (USD_PER_KIRO_CREDIT = $0.04/credit, individual plan: $20/mo for
  1,000 credits), falling back to token estimation only when a turn has
  no usage_summary (aborted/in-flight); costIsEstimated distinguishes
  the two
- Fix pre-existing CLI parser bug: metering_usage values are credits,
  not dollars — costs were overstated 25x at the real rate
- Count v2 tool_result content as input context (matching the CLI
  parser's ToolResults treatment); previously only the user prompt was
  counted, undercounting input ~25x on agentic turns
- Keep reasoningTokens disjoint from outputTokens: downstream
  aggregation (models-report, audit-report, parser) sums the two
  fields, so folding reasoning into output would double-count. Combine
  them only for the token-pricing fallback, matching codex/gemini
- Take the real modelId from session.json, so v2 sessions are not
  mislabeled kiro-auto
- Extract parseWorkspaceSession from the inline createParser block for
  parity with the other four format parsers
- Add v2SessionsRootOverride and stop deriving the v2 scan root from
  the cliDir parent when only agentDirOverride is set (tests were
  scanning the system tmpdir)
- Tests: v2 parsing/discovery/dedup/routing, credit vs fallback
  pricing, tool_result turn scoping, reasoning disjointness, cli-dir
  skip guard, and a mixed-format coexistence suite (legacy .chat + v1
  executions + workspace-sessions + CLI + v2 on one machine) asserting
  exact aggregate counts and no double counting
- Docs: v2 store layout, credit pricing, dedup namespaces, and the
  disjoint-store verification (v1->v2 is a clean cutover, no
  migration/dual-write observed)

AI-Origin: human
2026-07-14 22:35:51 +00:00
Andrew Lee
d8a7b2a95f fix(sync): address PR review — https enforcement, keychain test isolation, golden ID pins
Before-merge items:
1. HTTPS enforced on every remote endpoint (RFC 8252 §8.3): baseUrl,
   issuer, authorization/token/revocation endpoints, and the traces
   endpoint all reject non-https, with a loopback (127.0.0.1/::1/
   localhost) exception for offline tests and local dev. Enforcement is
   central (assertHttps) — the browser-open guard is no longer the only
   check whose failure was swallowed.
2. Credential store test isolation: CODEBURN_SYNC_TOKEN_STORE=file
   forces the file store (honors HOME) so the offline suite never
   touches the real macOS login keychain. Set in the e2e suite.
3. Golden pins for deriveSpanId/deriveTraceId/deriveDeviceId with fixed
   inputs and expected hex — the idempotency contract depends on these
   encodings being stable across releases; a green-tests encoding change
   would silently double-count history on span-ID-keyed backends.
   getDeviceId refactored over a pure deriveDeviceId(host, user).

Smaller review items:
- Callback server: ready promise resolves the actually-bound port from
  the listening event (kills the 100ms-sleep race after port fallback);
  Connection: close on all responses + closeAllConnections() on
  shutdown (pooled keep-alive sockets from a closed server could
  swallow requests aimed at a later server on the same port); error
  handler guarded so a post-bind error can never rebind to a different
  port than advertised; optional ports param ([0] = ephemeral) removes
  fixed-port contention between parallel test workers.
- fetchOidcConfig verifies the issuer claim matches the fetch origin
  (OIDC Discovery §4.3 mix-up defense).
- partialSuccess.rejectedSpans wrapped in Number() — proto3 int64 JSON
  mapping sends strings from strict protojson servers; += would
  concatenate.
- Ledger writes are atomic (temp + rename); corrupt-ledger recovery and
  no-tmp-left-behind tests added; XDG_CACHE_HOME honored (ledger is
  reconstructible state, not config).
- Mock IdP now implements /oauth2/authorize (registers PKCE challenge,
  302s to redirect_uri) and verifies S256 code_verifier + single-use
  codes at the token endpoint. The e2e drives the real redirect flow
  and asserts wrong-verifier and code-reuse are rejected — PKCE binding
  is now exercised end to end.
- sync reset calls clearLedger() instead of reimplementing the path.
- push sets exit code 1 on rate-limited/server-error outcomes so cron
  and script callers can detect incomplete pushes.

Deferred (noted for fast-follow): macOS 'security -i' stdin mode
(untestable on this Linux box), ai.cost_estimated as a real
ParsedApiCall flag (touches core parser types).

Sync suite: 81 passing (5x stable), 5 developer-only.

AI-Origin: human
2026-07-13 16:45:39 +00:00
Andrew Lee
163842edac test(sync): unit, mock-IdP e2e, and developer-only integration tests
Offline, CI-safe (70 tests):
- sync.test.ts (26): discovery parsing, PKCE, auth URL, scope
  negotiation, callback server (state mismatch, IdP error, timeout,
  port fallback), config round-trip
- sync-ledger-otlp.test.ts (23): deterministic ID derivation, OTLP
  structure/attributes, batching, ledger append/dedup/prune/clear
- sync-push.test.ts (15): pipeline against a scriptable mock OTLP
  server — success+ledger+cost, partial-success batch NOT ledgered,
  401 stop, 429 wait-and-retry / persistent-429 give-up / Retry-After
  parsing (delta-seconds, HTTP-date, garbage), 5xx deferral,
  idempotent failure recovery
- sync-e2e.test.ts (6) + fixtures/mock-idp.ts: full setup→refresh→
  rotate→logout round-trip against an in-process mock IdP

Developer-only (skip unless env vars set, never CI):
- sync-headless-e2e.test.ts (1): real browser PKCE flow via Playwright
- sync-infra-e2e.test.ts (4): push/auth-reject/batch/idempotent
  re-push against a deployed backend

AI-Origin: human
2026-07-12 16:05:34 +00:00
iamtoruk
ec0b80207b feat(cli): --format json list output for model-alias and proxy-path
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.
2026-07-11 17:17:04 -07:00
iamtoruk
fc1920d4f1 feat(cli): add compare and sessions JSON emitters
compare --format json: list mode (aggregateModelStats for pickers) +
full mode via --model-a/--model-b, emitting
{period, modelA, modelB, metrics, categories, workingStyle}.
Reuses computeComparison/computeCategoryComparison/computeWorkingStyle/
scanSelfCorrections; TUI path unchanged.

sessions --format json: new src/sessions-report.ts, uncapped flat
SessionRow[] (id, project, provider, models, cost, tokens, turns,
duration) via aggregateSessions; table + json formats.

Enables the desktop app's Sessions + Compare screens (Phase 3b).
2026-07-11 12:16:45 -07:00
iamtoruk
b95b62d8fa fix(cli): surface mDNS scan errors, robust spend-flow rollup + date label, harden tests
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).
2026-07-10 16:20:41 -07:00
iamtoruk
48d669bd23 feat(cli): spend flow-json (model x project) + devices/share/identity --format json
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).
2026-07-10 15:55:42 -07:00
Resham Joshi
c6f2073766
fix(bash): attribute wrapped commands to the real tool, not the wrapper (#658)
Some checks failed
CI / semgrep (push) Has been cancelled
extractBashCommands recorded the wrapper (sudo, npx, rtk, and friends)
instead of the command it delegates to, so any agent that prefixes its
shell calls collapsed its whole bash breakdown into one meaningless
bucket and the optimize detectors lost the actual tool.

Skip a known set of command wrappers when a real command follows, and
interleave that skip with the existing VAR=value env-assignment skip so
forms like 'sudo NODE_ENV=prod node x' resolve to the real tool. A
wrapper followed by a flag or a quoted token is kept as-is so we never
emit a garbage key.

Closes #657
2026-07-10 06:36:07 -07:00
AgentSeal
ebf8dd34a5 fix(codex): attribute CLI-wrapped MCP calls (mcp-cli), and re-parse cached sessions (#478)
Users who run MCP tools through a CLI wrapper (e.g. philschmid/mcp-cli)
instead of registering servers natively don't produce Codex
mcp_tool_call_end events; Codex logs a plain exec_command. So their MCP
usage showed only as a shell command and was absent from the MCP
breakdown, even after #513 fixed the native path.

- Recognize `mcp-cli [options] call <server> <tool>` in the exec command
  and also attribute it as mcp__<server>__<tool>. The exec still counts as
  Bash since it genuinely is a shell exec. Flag-tolerant, quote/path/bash-lc
  tolerant, and scoped to the mcp-cli binary (not foo-mcp-cli); only the
  `call` subcommand (a real execution) matches, not info/grep/listing.
- Register `codex` in PROVIDER_PARSE_VERSIONS. session-cache.json serves
  unchanged session files without invoking the provider parser, so the
  codex-cache version bump alone would never re-attribute already-cached
  sessions. This also retroactively repairs #513's native-MCP fix for
  users whose files were cached before it shipped.
- Bump CODEX_CACHE_VERSION 4->5 for the provider-internal layer.

Tested: parse cases (bash -lc wrapper, flags-before-call, argv array,
plus info/grep/foo-mcp-cli negatives), and a mutation-verified cache
regression test that fails without the PROVIDER_PARSE_VERSIONS entry.
ReDoS-checked. Reviewed with a Fable 5 adversarial pass (Codex is down);
its two must-fixes (the cache gap and the flags-before-call miss that
would have missed the reporter's own sessions) are folded in.

Fixes #478
2026-07-10 12:11:59 +02:00
Resham Joshi
3b8496017f
Merge pull request #624 from leonardo-claudio/feat/kiro-cli-tool-mappings
Some checks are pending
CI / semgrep (push) Waiting to run
feat(kiro): add CLI tool-name mappings and MCP passthrough
2026-07-10 01:56:40 -07:00
Resham Joshi
5093d232e1
Merge pull request #600 from getagentseal/feat/add-ron-currency
Add Romanian Leu (RON) currency to menubar and GNOME extension
2026-07-10 01:33:53 -07:00
AgentSeal
6b6b6b8df6 feat(menubar): Claude config selector, hardened (supersedes #635)
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.
2026-07-10 01:44:51 +02:00
Resham Joshi
3c6d98678c
Merge pull request #652 from getagentseal/merge/scan-progress-v2
Cold-scan progress indicator (hardened) — supersedes #637
2026-07-10 00:52:42 +02:00
AgentSeal
2914ea66a6 fix(parser): keep scan-progress out of the interactive dashboard and compare TUIs
The cold-scan progress line was gated only on stderr.isTTY, which is also
true when the interactive dashboard and `codeburn compare` render Ink to
the same terminal — so the \r progress line printed over their frame and
garbled the screen (confirmed under a PTY). isTTY alone can't tell an Ink
UI apart from a plain CLI command.

Gate on an explicit 'interactive Ink UI is live' flag instead: the two
interactive entrypoints call setInteractiveScanUI() before they render, so
their scans (and the dashboard's 30s auto-refresh, including the
getPlanUsages -> parseAllSessions path) stay silent, while plain CLI
commands (overview, export, status) still show progress. Verified under a
PTY: dashboard and compare silent, overview shows progress, piped/non-TTY
silent. Adds a gate unit test covering the interactive-active, plain-TTY,
non-TTY, threshold, and finish()-clear cases.
2026-07-10 00:50:35 +02:00
AgentSeal
5f14a1f5ee fix(providers): isolate a throwing provider so one bad file can't blank every scan
Both provider-discovery loops (discoverAllSessions, and the menubar
provider enumeration in usage-aggregator) called discoverSessions with no
try/catch, so any provider that threw — a crafted or corrupt file reaching
a string op — took down usage discovery for all 32 providers at once,
blanking the whole dashboard, CLI, and menubar.

Add safeDiscoverSessions: wrap discovery in try/catch, warn once per
provider per run to stderr (matching the per-file parse-failure isolation
already in parser.ts and the whole-data-loss notices in fs-utils), and
skip the provider instead of aborting. Both loop sites use it.
discoverAllSessions takes an injectable provider list so the isolation
loop itself is tested; mutation-verified that reverting either call site
fails the new loop-level test.

Follow-up (separate): surface a structured 'incomplete discovery' signal
in the JSON/MCP payload so resident consumers can distinguish a real $0
from a dropped provider, and scope the warn-dedup per request rather than
per process lifetime.
2026-07-10 00:00:22 +02:00
AgentSeal
308bec0ce6 fix(lingtai): harden manifest parsing against wrong-typed fields
A planted or corrupt .agent.json that is valid JSON but has a non-string
field (e.g. agent_name as an object) reached sanitizeProject().trim() and
threw. discoverAllSessions loops providers with no try/catch, so that one
file broke usage discovery for every provider, not just LingTai. Normalize
the manifest to string-or-undefined at read time; add a wrong-typed-field
regression test.
2026-07-09 23:48:46 +02:00
ZacharyHu0
a485087b5b Add LingTai TUI provider support 2026-07-09 23:47:59 +02:00
AgentSeal
bf6eee00f3 fix(report): only exact zero-rate overrides suppress the unpriced flag
getModelCosts resolves exact overrides before any table hit, so an exact
zero-rate override provably priced the model and means free. Prefix and
case-insensitive overrides resolve after table hits: a zero-rate stub
shadowed by one still reports $0 from the stub, so it stays flagged.

Refs #638
2026-07-09 21:22:23 +02:00
AgentSeal
e6162f6b50 fix(report): address unpriced-detector review findings
Adversarial review found two false-negative paths and two polish items:

- Drop the reverse-display-name suppression. Droid prices the lowercased
  display name and gets $0, so a $0 row keyed "Sonnet 4.6" is real
  uncounted spend; suppressing it because claude-sonnet-4-6 prices today
  hid exactly the failure this feature exists to expose.
- Treat zero-rate pricing hits as unpriced. LiteLLM ships [0,0] stubs
  for models it lists without a price; a stub hit means unknown price,
  not free. Explicit zero-rate user overrides still count as free.
- Make MenubarPayload.current.unpricedModels optional for source
  compatibility with payload producers that predate the field.
- Replace localeCompare with a byte comparison so tied rows sort
  identically on every host locale.

Refs #638
2026-07-09 21:17:47 +02:00
AgentSeal
231e7cc0d4 feat(report): surface unpriced models across overview, JSON, dashboard, and MCP
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
2026-07-09 21:04:56 +02:00
AgentSeal
f2fa575a52 Fix model pricing variant resolution 2026-07-08 22:55:44 +02:00
AgentSeal
75b6925749 Fix menubar install release lookup 2026-07-08 21:36:20 +02:00
Leonardo V M Claudio
cf07b6515a feat(kiro): add CLI tool-name mappings and MCP passthrough
- Map code → Read, subagent → Agent, web_fetch → WebFetch in toolNameMap
- Pass through mcp__* prefixed tools unchanged in toolDisplayName
- Add tests for CLI tool normalization and MCP passthrough
2026-07-06 10:14:44 -03:00
ozymandiashh
f8543ac8fc fix(currency): resolve RON symbol to "lei" across all surfaces
RON was added to the native macOS/GNOME pickers but not to the shared
SYMBOL_OVERRIDES map, so the CLI, CSV/JSON export, and web dashboard
resolved it through Intl to "RON" while native UI showed "lei" — a
cross-surface split for GNOME/CLI-set currency. Mirror the CNY precedent
(906c533): symbol override + fraction-digits test + README example.
2026-07-06 03:43:03 +03:00
Resham Joshi
d41ca116d0
Merge pull request #608 from NihalJain/feat/jetbrains-copilot-tracking
Some checks are pending
CI / semgrep (push) Waiting to run
feat(copilot): track GitHub Copilot JetBrains IDE usage
2026-07-05 15:38:14 +02:00
AgentSeal
d2edf8c9da fix(copilot): content-derived JetBrains dedup keys; isolate the new env var in tests
Maintainer follow-up:

- Derive JetBrains dedup keys from the reply content (sha256 prefix plus a
  per-hash occurrence counter) instead of the blob's scan position. Copilot
  is a durable provider: cached turns are never deleted and a re-parse
  appends any unseen key, while MVStore compaction can rewrite the store
  with blobs in a different byte order. With positional keys, a rewrite
  that moves a new blob ahead of an old one hands the new turn the old
  key (skipped as seen) and re-emits the old turn under a fresh index,
  double-billing it. Covered by a regression test that fails on the
  positional scheme.
- Add CODEBURN_COPILOT_JETBRAINS_DIR to the env-isolation cleared list so
  a developer's real JetBrains store never bleeds into fixture tests.
2026-07-05 15:36:49 +02:00
AgentSeal
926dcdb7fc fix(kiro): harden ws-session parsing and cover the stale-cache path (#619)
Maintainer follow-up on top of the parsing fix:

- Replace the placeholder cache tests with a regression test that runs the
  full parseAllSessions() pipeline against a seeded session-cache.json: a
  zero-turn entry at the current fingerprint is honored (control), and a
  pre-fix fingerprint forces the re-parse that recovers the missed calls.
- Treat history items carrying an executionId as execution-backed regardless
  of their text, so a stub-text change can never reintroduce double-counting;
  the 'On it.' check remains for stubs whose ref rides a separate item.
- Resolve the workspace-session timestamp before consuming the dedup key,
  and drop the call when stat fails instead of fabricating a current time.
- Read selectedModel through stringField instead of an unchecked cast.
- Import stat statically.
2026-07-05 15:02:25 +02:00
Andrew Lee
9c2ac6e22a fix(kiro): parse context.messages, add .kiro-server path, extract usageSummary tools, workspace-sessions
Four fixes for the Kiro IDE provider:

1. Add 'entries' to extractText() key list — Kiro IDE stores message
   content in context.messages[].entries (not .content), causing the
   parser to extract 0 chars from every execution file.

2. Check data.context[key] for conversation arrays in parseModernExecution
   — current Kiro builds store messages at data.context.messages, not at
   the top-level data.messages path the parser was checking.

3. Scan both ~/.kiro-server/data/... AND ~/.config/Kiro/... on Linux —
   remote dev boxes use .kiro-server while local installs use .config/Kiro.
   Both can have data simultaneously; the old code short-circuited on the
   first path found.

4. Discover and parse workspace-sessions/<base64>/*.json files — newer
   Kiro builds write session state here with history[].message format.
   Skips stub entries (executionId refs + 'On it.' only) to avoid
   double-counting with execution files parsed separately.

5. Add kiro: 'ide-parsing-v1' to PROVIDER_PARSE_VERSIONS for automatic
   cache invalidation — users upgrading from the broken parser will get
   a fresh re-parse without manually clearing session-cache.json.

Bonus: Extract tool names from usageSummary[].usedTools, add chatSessionId
to session ID resolution, add Kiro-specific tool name mappings.

AI-Origin: human
2026-07-05 01:28:33 +00:00
ozymandiashh
1678cbd883
Merge pull request #599 from Enclavet/fix-cursor-projects
Some checks failed
CI / semgrep (push) Has been cancelled
Fix cursor project parsing
2026-07-03 19:44:48 +03:00
Nihal Jain
cd07707363 fix(copilot): parse JetBrains agent sessions from old plugin format (≤1.5.x)
JetBrains Copilot plugin ≤1.5.x (e.g. 1.5.59-243) stores all session turns
inside ONE large binary-framed outer Nitrite document, rather than the
per-turn {"__first__":{"type":"Subgraph",...}} blobs introduced in later
plugins (≥1.12.x, e.g. 1.12.1-251).

In the old format each assistant turn is a UUID-keyed Value entry whose
value field contains a JSON-string-escaped AgentRound record:

  {"<uuid>":{"type":"Value","value":"{\"type\":\"AgentRound\",
    \"data\":\"{...reply...}\"}"}, ...}

The extractResponseText depth-unescape loop already handles this one extra
level of escaping; the only gap was that extractJetBrainsDbTurns never fed
it the outer document — it only scanned for __first__/Subgraph blobs, which
the old plugin never writes.

Add a fallback that activates when the Subgraph scan produces zero turns but
'AgentRound' text is present in the raw file (old-format signal). It locates
the binary-framed outer document (UUID-keyed Value entry, hex matched
case-insensitively so an uppercase UUID does not fall through to $0), extracts
it with matchJsonObject, and passes it to extractResponseText. Because the outer
document holds every turn in one blob, this emits ONE session-level call per
document (all rounds' replies joined): cost/tokens are correct, only the
per-turn call-count granularity is coarser — an accepted tradeoff for legacy
data. MVStore keeps two identical collection copies; seenReplies dedupes them.

The fallback is guarded by turns.length === 0 so new-format sessions (whose
Subgraph scan succeeds) are completely unaffected and never double-counted.

Tests: old-format doc with multiple AgentRound rounds → 1 call whose token
count equals the two non-empty replies joined (the empty tool-call round is
excluded); an uppercase-UUID variant (fails without the case-insensitive
match); and a guard that new-format Subgraph turns are not double-counted.
docs/providers/copilot.md documents the old format and the one-call-per-session
limitation.
2026-07-03 18:21:08 +05:30