Commit graph

263 commits

Author SHA1 Message Date
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
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
AgentSeal
fea66a80d3 fix(act): report scale, corrupt-journal safety, and under-claim gaps (#606)
Bug-hunt follow-ups on the realized-savings report:

- Scale the displayed estimate to the measured window so the Estimated and
  Realized columns are comparable: mcp/archive use the per-session baseline
  times the post-window session count, read-edit uses deficitThen times the
  same edits denominator as realized (so realized never exceeds it). The
  at-apply estimate stays in --json as estimatedAtApply next to
  estimatedForWindow; the footer states the scaling and that mcp/archive
  realized figures are derived from session counts, not independently
  measured.
- Never crash on a corrupt journal: records without a string status or a
  parseable string `at` are skipped and surfaced ("N malformed records
  skipped"); the optimize header computation is additionally wrapped so any
  error just drops the header.
- Zero post-window sessions now reads "not measurable: no sessions in the
  window yet" instead of measured-zero.
- The optimize header sums only normal-confidence measured rows (under-claim);
  low-confidence rows stay visible in act report only.
- Tests: floor discipline on non-integer mcp and read-edit products (a ceil
  mutation fails), the project-scope keeper subtraction, the archive
  estimate==realized tautology, malformed-journal robustness, and the
  low-confidence header exclusion.
2026-07-03 13:36:36 +02:00
Nihal Jain
2916cd988e fix(copilot): read JetBrains agent-mode replies from AgentRound records
JetBrains Copilot has two turn shapes in the Nitrite .db:

- ask mode — the reply is a `Markdown` record's `text`;
- agent / plan mode (e.g. PyCharm agent sessions, `/plan …`) — the reply is the
  `reply` field of an `AgentRound` record, and the `Markdown` record instead
  holds the USER's prompt.

extractResponseText only read Markdown, so agent-mode turns yielded no reply
text: they were discovered (session/turn counts showed up) but priced at $0
because output tokens came out zero. On this machine that silently
under-counted a PyCharm session ($0 → $0.35) and several IntelliJ agent turns.

Determine the mode by the PRESENCE of an `AgentRound` record and read only that
record's `reply` (collecting every non-empty round in a multi-round blob).
Crucially, an agent blob whose reply is empty — a failed turn or a pure
tool-call round — does NOT fall back to the Markdown record, so a user prompt
is never mistaken for the assistant's output; such turns bill $0 as before.
Ask-mode blobs (no AgentRound) keep reading Markdown. Plan mode's sidecar
records — Thinking, PendingChanges (proposed diff, under `content`), AskQuestion,
Notification, SubTurn, and file-read `text` results — are never read as output.
Verified across all local stores: the two reply shapes never coexist in one
blob, so the split is unambiguous.

Tests: agent-mode reply extraction (ignoring the prompt Markdown), pure
tool-call rounds → $0, multi-round collection, and a failed agent turn → $0.
docs/providers/copilot.md documents both turn shapes and the ignored sidecar
records.
2026-07-03 17:06:14 +05:30
AgentSeal
ad471f3d8c feat(act): realized savings measurement (codeburn act report) (#606)
Capture a trailing-14-day before-baseline when a fix is applied and
re-measure it against the post-apply window so optimize can show realized
numbers next to estimates.

- ActionBaseline (windowDays, capturedAt, estimatedTokens, sessions, metrics)
  persisted by runAction; captured in the optimize --apply flow and at guard
  install time.
- codeburn act report [--json]: applied, not-undone actions older than 3 days,
  re-running the detectors over apply-date-to-now (capped 30 days). Per-kind
  realized deltas: MCP/archive tokens-per-session times saved sessions with
  reverted-by-user detection; read-edit deficit reduction; guard yield split
  labeled correlation. Bash cap is marked not measurable (result sizes are not
  retained). Low confidence under 20 post-window sessions or past a 2x volume
  shift. Realized numbers rounded down, estimate kept visible.
- optimize gains one header line only when a measured action exists, and
  appends "(previously applied <date>, re-flagged)" to re-triggered findings.
  No change for users with no applied actions.

Reuses scanAndDetect helpers over a date-bounded range; exports the token
constants and read/edit tool sets rather than duplicating the math.
2026-07-03 13:04:37 +02:00
Nihal Jain
bae016c050 feat(copilot): track GitHub Copilot JetBrains IDE usage
## What & why

The JetBrains Copilot plugin (IntelliJ, PyCharm, RubyMine, …) stores its
chat/agent sessions under `~/.config/github-copilot/<ide>/<kind>/<storeId>/` —
a location none of the existing Copilot sources (CLI JSONL, VS Code chat
sessions/transcripts, OTel SQLite) read. As a result all JetBrains Copilot
usage was silently uncounted in every CodeBurn report. This adds a reader for
that store so those sessions are discovered, priced, and attributed to the
right project.

## How it works

- **Reader.** The store's session content is a Nitrite `.db` — an H2 MVStore of
  Java-serialized documents. It is scanned as `latin1` for byte-offset
  stability: no Java deserializer, no new dependency, and it is not SQLite so
  `node:sqlite` is not involved.
- **Reply text.** Assistant replies live in nested-escaped
  `{"__first__":{"type":"Subgraph"…}}` blobs. The text is recovered by
  unescaping one level at a time and, at the depth where the Markdown record's
  `data` field is a well-formed one-level-escaped JSON document, reading it
  structurally — so a reply containing its own quotes is never truncated or
  duplicated (which would otherwise inflate the estimate).
- **Tokens/cost.** The store records no token counts, so output tokens are
  estimated from the reply text (`CHARS_PER_TOKEN = 4`, re-decoded
  latin1→utf8 so multibyte replies count by codepoint) and every call is marked
  `costIsEstimated`. Failed generations (error status, no reply) are billed $0.
- **Sessions.** One `.db` holds many chat tabs; turns are grouped back to their
  conversation GUID so the UI shows one session per tab, deduped by reply
  content per conversation.
- **Project attribution**, most authoritative first:
  1. the plugin-recorded `projectName` field (JetBrains Copilot 1.12+), joined
     across kind dirs by store id — the billable turns live in
     `chat-agent-sessions`, but the label is usually written into the sibling
     `chat-sessions`/`chat-edit-sessions` store. Read length-delimited and
     re-decoded latin1→utf8 so non-ASCII repo names round-trip.
  2. the `.git` repo root of a referenced `file://` path.
  3. a generic `copilot-jetbrains` bucket when neither signal exists.
  The conversation title is a chat-thread name, not a project, so it is kept
  out of the project field and surfaced as the session label instead.

Override the JetBrains github-copilot root with
`CODEBURN_COPILOT_JETBRAINS_DIR`.

## Docs

- `docs/providers/copilot.md` — full JetBrains section (store layout, latin1
  scan, reply extraction, projectName precedence + cross-kind join).
- `docs/providers/README.md` — Copilot storage updated to note the Nitrite .db.

## How to verify

- `npm test -- copilot` and `npx tsc --noEmit` (fixtures reproduce the real
  nested-escaped .db framing, including quote- and multibyte-bearing replies).
- End to end against a real install:
  `CODEBURN_CACHE_DIR=$(mktemp -d) node dist/cli.js status --provider copilot \
     --period all --format menubar-json`
  — JetBrains sessions appear By-Project under their real repo names.
- Set `CODEBURN_COPILOT_JETBRAINS_DIR` to a fixture root to parse a controlled
  store without touching the real config dir.
2026-07-03 16:11:27 +05:30
AgentSeal
be2470d0e5 fix(guard): dedupe streaming message copies in the incremental cost fold
Claude Code rewrites each assistant message several times as it streams,
every copy carrying the full final usage; the shipped parser dedupes these
last-wins (dedupeStreamingMessageIds) but the guard fold summed every line,
measuring real sessions at 2.5-2.8x their true cost and false-blocking the
hard cap at roughly 40% of the configured spend.

- The session cache now maps message id -> that id's cost contribution and
  each id-carrying line replaces its previous contribution; id-less lines
  keep plain adds. Validated against two real transcripts (90MB and 116MB):
  guard totals now equal the shipped deduped totals exactly.
- Replace semantics also self-heal the trailing-line case: a complete final
  line without its newline is folded but byteOffset stops before it, so the
  next invocation re-reads it as a replace, not a double add.
- editCount becomes a set-once sawEdit boolean so duplicate copies of an
  edit tool_use cannot inflate it; cache schema bumped to v2 (old caches
  cold-reparse once).
- Per-session state moves to guard/sessions/ so a session id can never
  collide with the shared flags.json, dropping the doAllow special case.
- The git-commit detector now requires commit as the git subcommand at a
  command boundary (start of string or line, or ; & |), with intra-command
  gaps that never cross newlines: 'git log --grep commit' and
  'git diff && echo commit' no longer match, while newline-separated
  'git add ...\ngit commit' in multi-line Bash calls now does (verified as
  a real false negative on a live transcript).
- Corrected the statusline protocol note: each stdout line renders as its
  own row; we emit exactly one.
- New tests: streaming-duplicate fixtures (3x identical, growing last-wins,
  incremental replace) asserted equal to a cold shipped-parser computation,
  the trailing-partial-line scenario, the commit-detector matrix, and a
  stale-plan test proving guard-install plans carry expectedHash (a
  concurrent settings edit aborts the apply and survives). The act list CLI
  spawn test now anchors to the repo root from the test file location.
2026-07-03 12:32:46 +02:00
AgentSeal
dc182dc275 feat(guard): opt-in session-time hook pack for Claude Code (#605)
codeburn guard install|uninstall|status|refresh|allow plus the internal
hook/statusline handlers. Off by default, fully local, cleanly removable.

- Settings edits go through the action journal (guard-install / guard-uninstall)
  with expectedHash, appending our entries and removing exactly ours by command
  prefix; a byte-identical uninstall is asserted by test.
- PreToolUse budget cap (soft warn once, hard block with a per-session allow
  override), Stop yield checkpoint (expensive with no edits and no commit, once),
  and a flagged-project SessionStart opener built from the optimize detectors.
- Incremental per-session cache keyed by session id: resumes the transcript
  parse from the last complete-line byte offset via readSessionLines, folding
  only the tail into running totals. Warm invocation ~0.28s against a 90MB
  transcript, dominated by CLI startup; the tail parse itself is negligible.
- All handlers fail open: any error, malformed stdin, or missing transcript
  exits 0 with no output so a broken guard can never block a session.
- Hook protocol verified against the live docs (dated block at the top of
  hooks.ts); zero new dependencies.
2026-07-03 11:54:04 +02:00
AgentSeal
dcd47e65f5 fix(act): refuse to apply a plan whose target changed since it was built
Plans serialize the full post-edit file content at build time, so a
target edited between the preview and the interactive confirm would be
silently overwritten with stale content (recoverable via backup, but a
silent violation of the dry-run-shows-exactly-what-changes principle).

- PlannedChange edit/create variants gain optional expectedHash: sha256
  of the raw on-disk bytes the plan was built from (hashed before the
  BOM strip), null when the plan expects the file to be absent,
  undefined to skip validation (framework back-compat).
- plans.ts sets it everywhere a target is read: ConfigDocs hashes the
  raw buffer it parses, and the marker builders hash the file behind a
  shared markerChange helper.
- runAction validates all expected hashes after snapshotting and before
  the first mutation; a mismatch throws "<path> changed since the plan
  was built; re-run codeburn optimize --apply", removes the backup dir,
  and journals nothing. No rollback is involved since nothing mutated.

Tests: stale edit target rejected with no journal record and no backup
dir left, expects-absent plan rejected when the file appeared, matching
hash still applies, and a hash-less change still applies unchecked.
2026-07-03 11:20:59 +02:00
AgentSeal
92d970196e fix(act): scope project-scope removals, --yes safety, --only validation (#604)
Review fixes plus one coordinator amendment on top of the initial
optimize --apply implementation.

- mcp-project-scope no longer strips a server from every projects[*]
  container in ~/.claude.json: only the top-level entry and the
  finding's cold projects lose it, and the cwd's own config files count
  as cold only when the cwd is in the cold list. The plan preview
  annotates ~/.claude.json with the project entries that lose the
  server.
- unused-mcp findings are now appliable (remove-everywhere mcp-remove
  plans, same as low-coverage).
- Notes are rendered under manual findings too, so an all-unparseable
  config surfaces its parse error instead of a bare "manual".
- ConfigDocs strips a leading UTF-8 BOM before JSON.parse.
- claude-md plans are excluded from --apply --yes (they write to
  cwd/CLAUDE.md); they apply via the interactive picker or an explicit
  --only selection, and --yes prints them as skipped with the reason.
- --only with an unknown or not-appliable id errors to stderr with the
  run's valid appliable ids and exit code 2.
- EOF at the interactive prompt prints "Nothing applied." and exits 0;
  an answer that arrives together with EOF is still honored.

Adds end-to-end tests driving runOptimizeApply over injected stdio and
a fixture home: --yes output with journal ids and undo hints, picker
parsing, --only filtering and validation, EOF, claude-md skip, the
projects[*] over-deletion regression, manual-note rendering, and BOM
configs.
2026-07-03 11:14:06 +02:00
AgentSeal
0dd54366b7 feat(act): optimize --apply for config-class fixes (#604)
Add stable kebab-case finding ids to every optimize detector and to the
JSON report, then route the config-class findings through the action
journal so they can be applied and undone.

- optimize.ts: id on every WasteFinding and OptimizeJsonReport entry;
  appliable findings also carry a machine-readable apply payload (mcp
  server list, project-scope keepers, archive names).
- act/plans.ts: planFor(finding) builds concrete, journaled file
  mutations for mcp-remove, mcp-project-scope, skill/agent/command
  archives, CLAUDE.md rule blocks, and the bash output cap. JSON edits
  preserve the rest of the document (2-space indent, trailing newline);
  unparseable config files are reported and skipped, not fatal.
- act/optimize-apply.ts: codeburn optimize --apply with a plain-readline
  confirm, plus --yes, --dry-run, and --only; prints each journal id and
  the undo hint. --apply with --json exits 2.

Tests cover mcp remove/undo, project-scope global-to-project move,
unparseable-file skip, archive collision suffixing, CLAUDE.md marker
idempotency, a byte-identical dry-run tree hash, and a finding-id guard.
2026-07-03 10:41:29 +02:00
AgentSeal
2eec2fdc31 fix(act): support directory moves for archive actions
The archive actions in the acting epic move whole skill/agent directories,
which the framework could not handle: snapshotFile used copyFile (EISDIR on
a directory) and the afterHash pass used readFile. Snapshots now branch on
lstat, copying directory trees with fs.cp recursive. Directories get an
empty afterHash ('' means no content hash) and drift detection skips the
hash comparison for them; the occupied-original-path check still applies and
a missing movedTo still falls back to the backup. Backup restore likewise
branches: a directory snapshot replaces the target (rm then cp recursive).

Apply-side rename cannot replace a directory destination (ENOTEMPTY), so a
move retries once after clearing the already-snapshotted destination,
rethrowing other codes before any destination damage.

Tests: archive a directory tree and restore it byte-identical, move a
directory onto an existing destination directory (destBackup taken, both
trees restored), and dir-move undo with an occupied original path refusing
without --force and overwriting with it.
2026-07-03 10:14:48 +02:00
AgentSeal
d756eacae5 fix(act): harden undo, apply, and locking against review findings
Undo no longer clobbers files it did not create: an occupied original path
counts as drift for moves (--force removes then renames back), a move
destination that already exists is snapshotted (destBackup) and restored
after the file moves back, and a missing moved file falls back to the source
snapshot so forced undo cannot die mid-loop. Non-move reverts now key on
backup presence instead of the op label, which also restores files that a
create overwrote.

Apply snapshots once per unique path, hashes after all mutations so
overlapping changes carry the final state, and journals inside the rollback
region so a failed append reverts the mutations. The lock is taken with a
single wx write and goes stale by mtime only, so a fresh lock can never be
stolen while empty. Drift reads treat any unreadable target as drift with
its error code, ambiguous id prefixes report the match count, undo --last
skips already-undone records ("Nothing to undo."), and readRecords only
swallows ENOENT.

Tests cover each new behavior plus two mutation probes (forward-order revert
and removed locking both fail the suite), and a CLI-level check of
`act list --json` output shape and ordering.
2026-07-03 10:07:22 +02:00
AgentSeal
883580e6b9 feat(act): action journal, backups, and undo core
Add src/act, a dependency-free framework for journaling and reverting any
file CodeBurn modifies. runAction is the single mutation path: it snapshots
every target, applies the changes, then appends a JSONL record, rolling back
completed steps and journaling nothing if a mutation throws midway. Undo
checks each file against its post-apply sha256 and refuses on drift unless
forced, restoring edits, deletes (created files), and moves. A pid plus
timestamp lockfile (stale after 60s) guards apply and undo, and the journal
reader tolerates corrupt lines with last-line-wins status updates.

Wire up `codeburn act list` (table or --json) and
`codeburn act undo <id|--last> [--force]`. Storage lives under the existing
config home via the config.ts resolver. Tests cover apply with backups and
afterHash, byte-identical undo per op type, drift refusal and --force,
mid-apply rollback, and corrupt-journal tolerance.
2026-07-03 09:39:33 +02:00
Andrew Lee
400ca083a2 fix(cursor): map composers to workspace via composer.composerHeaders
Recent Cursor builds renamed the per-workspace composer list from
ItemTable['composer.composerData'] to 'composer.composerHeaders' (identical
{ allComposers: [{ composerId }] } shape). loadWorkspaceMap only read the old
key, so on these builds the composer->workspace map came back empty and every
composer fell through to the 'cursor' orphan bucket, losing per-project
attribution.

Read both keys and merge their allComposers lists (backward compatible with
older installs). Add a regression test covering the new composer.composerHeaders
key and the legacy composer.composerData key.

Verified against a real workspace state.vscdb: composer.composerData absent,
composer.composerHeaders present with composerIds matching the bubbleId/
composerData rows; 21/22 calls now attribute to the real workspace instead of
the 'cursor' orphan bucket.

AI-Origin: human
2026-07-02 17:51:23 +00:00
AgentSeal
de57bccda3 chore(pricing): refresh LiteLLM snapshot; MiniMax-M3 standard tier
MiniMax moved M3 to tiered pricing: the official standard tier (inputs
up to 512K) is $0.30/$1.20 per M with $0.06 cache reads, doubling above
512K. Upstream LiteLLM now carries the standard tier, so the pinned
expectation follows it; the old $0.60/$2.40 figure is the long-context
tier, not the base price anymore.
2026-07-02 06:17:01 +02:00
AgentSeal
ab89765afc fix(zed): top threads up to the cumulative counter
Validated against a real thread: request_token_usage is keyed by user
message and covered only part of the requests (cumulative was ~3x the
map sum), so per-request calls are now supplemented by one remainder
entry that brings each thread exactly to cumulative_token_usage. Live
run matches the store token-for-token on all four fields.
2026-07-02 05:53:40 +02:00
AgentSeal
a936e28595 fix(zed): read legacy uncompressed json thread rows
Zed's DataType enum carries both zstd (the current save path) and json;
verified against crates/agent/src/db.rs, along with the exact TokenUsage
field names, the SerializedLanguageModel {provider, model} shape, and
RFC3339 updated_at serialization.
2026-07-02 05:46:59 +02:00
AgentSeal
1f45c1541b feat(providers): add Zed agent provider (#480)
Reads Zed's agent threads from the single threads.db SQLite store: each
row's zstd-compressed JSON carries the thread model and per-request
Anthropic-shaped token usage, so calls are emitted per request with
exact input/output/cache fields and priced through the existing engine.
In-progress threads with an empty per-request map fall back to the
cumulative counter. zstd comes from node:zlib, which ships it from
22.15; on older Nodes the provider skips with a notice instead of
failing. On-disk format documented by @chatzinikolakisk in #480 and the
schema verified against a real Zed install.
2026-07-02 05:41:16 +02:00
Resham Joshi
f1a4e8cc4f
fix(cursor): use Cursor's real context tokens for input (#574) (#575)
* fix(cursor): use Cursor's real context tokens for input

Current Cursor builds leave the per-bubble tokenCount at {0,0}, so the provider
fell back to estimating input from visible text plus a second agentKv
content-char pass that double-counted the same conversation. Cursor records its
own tokenizer-accurate context size per conversation in
composerData.promptTokenBreakdown (the number behind the in-app context-window
bar); read that and credit it once per conversation for input instead.

Measured on a real local DB: today's Cursor input went 44,873 -> 168,486 tokens,
matching the sum of per-conversation context. The admin portal still counts
cumulative-per-turn plus cache, which are server-side only, so an opt-in Cursor
API stays the path to exact parity.

Output is a reply-text estimate; agentKv is retained for a tools/bash breakdown
in a follow-up.

* feat(cursor): add tools and bash-command breakdown from agentKv

Cursor logs the agent's tool calls (Read, Grep, Glob, Shell, ...) in agentKv
blobs. Join them to conversations via the turn requestId (carried on the bubble's
$.requestId and inherited positionally by the turn's agentKv rows) and attach each
conversation's tool list and Shell commands to the call that carries its input.

Measured on a real DB: Cursor now reports tools {Read, Grep, Glob, Shell,
SemanticSearch} and the executed shell commands, which were previously empty.

Removes the now-superseded parseAgentKv content-char estimate.

* fix(cursor): price composer-2.5 as Sonnet 4.6

composer-2.5 was missing from the built-in Cursor model aliases, so its usage
showed $0. Map it to claude-sonnet-4-6 like composer-2 (per cursor.com/blog).

* fix(cursor): cache version, model attribution, user message join, tool classification

- Bump CURSOR_CACHE_VERSION to 5: parser semantics changed (parseAgentKv
  removed, real context tokens from composerData.promptTokenBreakdown),
  stale v4 caches would show double-counted agentKv calls.
- Fix model attribution: real input tokens are credited on user bubbles
  (type=1) which carry no modelInfo. Add a pre-pass building composerId ->
  model from assistant bubbles so pricing/display uses the conversation's
  actual model instead of the default cursor-auto/sonnet-4.5.
- Fix buildUserMessageMap: was keying by JSON conversationId (empty in
  current Cursor builds). Now extracts composerId from the bubble key,
  matching parseBubbles.
- Add 'Shell' to BASH_TOOLS in classifier: Cursor's agent uses 'Shell'
  as the tool name, but it was missing from the bash tool set so Cursor
  agent turns with shell commands wouldn't classify as bash/build/test.
- Fix null coalescing in loadComposerInputTokens: r.used ?? r.ctx would
  fall through on a valid totalUsedTokens of 0. Use explicit null check.
- Decouple agentTools attachment from input credit: tools/bash were only
  attached on the first credited turn (creditedHere), silently dropping
  tool usage from subsequent turns in multi-turn conversations.
- Update stale comment about parseAgentKv being kept for a follow-up.
- Add tests for real token crediting, once-per-conversation, fallback,
  contextTokensUsed, tool/bash attribution, and model attribution.

* fix(cursor): avoid duplicating aggregated agent tools

* fix(cursor): price house composer models from Cursor's published rates

composer-1/1.5/2/2.5 were proxied to Claude Sonnet, overcounting cost
(~6x for composer-2/2.5). Use Cursor's published per-model rates instead,
and note in the parser why local reads undercount the admin console.

Co-authored-by: AgentSeal <hello@agentseal.org>

* fix(cursor): estimate non-Composer turn input from the agent stream

Non-Composer sessions (e.g. GPT) record no context-window meter and keep
the prompt in the agent stream, so the user bubble's own text is empty.
Those turns hit the 0/0-token fallback with text_length 0 and were dropped
entirely, so that model's traffic never appeared in the report.

loadAgentToolsByComposer now also sums the user-role stream text length per
conversation, and the meterless fallback estimates input from it (chars/4),
credited once per conversation, when the bubble text is empty. Turns with no
stream text are left untouched, so no phantom tokens are invented.

* fix(cursor): stable conversation crediting, restored stream coverage, and cache invalidation

Review fixes for the real-token accounting:

Conversation input now lands on one composer-anchored record
(cursor:composer-input:<id>) timestamped at composerData.createdAt, so
the credited day no longer depends on the parse window or cache floors,
daily-cache gap fills dedupe instead of multiplying, and each
conversation picks exactly one input source (real bubble tokenCounts,
the context meter, the agent stream, or visible text) so sources can
never stack or double count. A zero totalUsedTokens no longer shadows
contextTokensUsed.

The agent stream regained what the parseAgentKv removal dropped: tool
and system rows count as context, stream-only replies count as output,
and sessions with no bubble join are emitted again (DB mtime timestamp,
as before). Block-array content is measured by its text, not its JSON
envelope. Rows written before their requestId appears buffer forward
instead of inheriting the previous conversation, and a system row closes
the boundary. Tool names canonicalize to Bash and commands go through
extractBashCommands so cross-provider breakdowns merge; the classifier
no longer special-cases Shell (which also reclassified Copilot turns).
User bubbles consume their own queue entry so assistant replies pair
with the right question, and every cursor call is flagged
costIsEstimated.

The requestId and model joins ride the existing budgeted bubble scan
instead of two new unbounded full-table decodes, and the composerData
read seeks the key range. SQLITE_BUSY now propagates to the parser's
retry path instead of caching a silently degraded parse.

Upgrades actually take effect: the session cache gets a cursor parse
version, DAILY_CACHE_VERSION bumps to 10 so finalized days re-hydrate
under the new accounting, the cursor results cache bumps to v6, and the
builtin composer rates participate in the price config hash (rates now
cite cursor.com/docs/models).

Verified against a real Cursor store: all metered conversations match
the on-disk meter exactly, narrow and wide parse windows anchor
identically, repeat runs are byte-identical, and agentKv-only sessions
reappear.

---------

Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
2026-07-02 04:53:07 +02:00
ozymandiashh
2fc4d32e66
fix(devin): report friendly GPT model names with effort tier (#585)
Some checks are pending
CI / semgrep (push) Waiting to run
* fix(devin): report friendly GPT model names with effort tier (#487)

Devin transcripts label each step with a generation_model in dash form with
an effort suffix (gpt-5-3-codex-xhigh). getShortModelName is keyed in dot form
(gpt-5.3-codex), and its version-boundary check matches the dash id against the
base gpt-5 entry, collapsing every Devin GPT variant to GPT-5.

Normalize the dash form to canonical dot form, map through the short-name table,
and surface the effort tier, e.g. GPT-5.3 Codex (xhigh). Fall back to the
friendly model_name when generation_model is an opaque MODEL_* id, and read
extra.generation_model so ATIF v1.7 transcripts behave like v1.4. Devin rows
in the JSON report and model-efficiency now key on the friendly name.

* fix(devin): stop dated GPT snapshots being mislabeled as versions

The friendly-name path treated any two-number dash id (gpt-4-1106-preview) as a
dotted minor version, producing corrupt labels like 'GPT-4.1106 Preview'.

- Restrict the dash-to-dot rewrite to a single-digit minor at a token boundary,
  matching Devin's real ids (gpt-5-3-codex) while leaving dated snapshots alone.
- In getFriendlyGptName, defer to getShortModelName when any suffix token is
  purely numeric, so unknown snapshot ids pass through raw instead of being
  fabricated into a fake friendly name.

Adds a regression case (gpt-4-1106-preview) to the Devin variant matrix.

* chore: remove accidental node_modules symlink

An absolute-path node_modules symlink was committed by mistake. It leaked a
local username, was a dangling symlink for everyone else, and broke git/npm on
checkout (git won't replace a real node_modules/ with the symlink). It slipped
past .gitignore because the ignore rule is 'node_modules/' with a trailing
slash, which matches a directory but not a symlink.
2026-07-01 16:01:49 +02:00
ozymandiashh
d869ad86e2
fix(pi): classify native skill loads as Skill, not Read (#588) (#590)
Pi and OMP have no dedicated skill tool: a native skill load is emitted as
an ordinary `read` tool call whose path points at the skill's SKILL.md (or a
`skill://<name>` URI in newer OMP builds). The parser mapped every read to the
Read tool and never populated `skills`, so Pi/OMP sessions over-counted Reads
AND always showed an empty "Skills & Agents" breakdown.

Detect these reads (basename === 'SKILL.md', or a skill:// URI), extract the
skill name (parent directory, or the URI segment), and surface the call as the
`Skill` tool with the name recorded in `skills` -- exactly how the Claude
parser represents a skill invocation. That both removes the Read over-count and
lets the shared classifier tag the turn `general` so the Skills & Agents
breakdown picks it up (populating a field the dashboard never received before).
The path is read from arguments.path with a defensive arguments.file_path
fallback.

Tests cover SKILL.md / skill:// / file_path detection, a non-skill read staying
a Read, and an end-to-end check that a parsed skill load reaches the classifier
subCategory that feeds skillBreakdown.
2026-07-01 15:57:31 +02:00
ozymandiashh
61eb5fbca5
fix: scope cache read/write to the selected period (web + devices CLI) (#583) (#586)
The dashboard Cache read/write cards and the `codeburn devices` summarizer
summed the full 365-day history.daily backfill instead of the selected period,
so shorter windows over-counted (today ~197x on real data). Every other metric
already reads the period-scoped `current` block.

buildMenubarPayload now emits period-scoped cacheReadTokens/cacheWriteTokens on
`current` (from PeriodData, alongside inputTokens/outputTokens); the web cards
(single + combined views) and summarizeOneDevice read those instead of reducing
history.daily. The trend chart still uses history.daily. Older peers that omit
the fields fall back to 0 (web) or the windowed daily sum (CLI).

Adds regression tests for both surfaces.
2026-07-01 15:53:44 +02:00
Resham Joshi
2b0f781977
feat: add codeburn audit token-source view (#578)
Adds a per-(provider, model) audit table showing the raw token fields each
provider records (input, output, reasoning, cache write/read) alongside the
totals codeburn displays and prices. It makes the normalizations explicit:
reasoning folds into output, the Anthropic cacheReadInput / OpenAI cached
vocabularies collapse to a per-call max, and cache write/read use the 1.25x /
0.1x input multipliers when a model omits explicit cache rates.

--format json adds per-component cost, the applied rates, both raw cache-read
fields, and recomputed vs attributed cost, so any token-to-cost drift is
visible in one command. Same options as `codeburn models`.
2026-07-01 13:02:29 +02:00