mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-19 18:23:41 +00:00
chore: remove Roo Code support (#1551)
Roo Code was discontinued in May 2026 (its repository is archived). Cached Roo sessions and daily history slices are dropped on load.
This commit is contained in:
parent
4e22a8e09f
commit
fe560f7f4d
38 changed files with 193 additions and 384 deletions
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Removed
|
||||
- Roo Code support. Roo Code was discontinued in May 2026 (its repository is archived).
|
||||
|
||||
### Fixed
|
||||
- **Cursor Agent counts every assistant message instead of one turn per user message.** Agentic loops emit dozens of assistant messages per user message and the parser kept only the first (on one real corpus: 247 of 4,946 messages, ~4% of assistant text). Each message now carries the last user message forward, tool_use inputs join the output text, and input tokens use the full user text, billed once per user message, while the display stays truncated at 500 chars. The parse version bump re-parses cached sessions, and the daily cache moves to v36 so settled days re-derive.
|
||||
- **Windows Store installs: no update offer the Store cannot deliver, and `codeburn menubar` opens the Store copy.** The desktop app read GitHub releases for its update banner, so a Store copy was told about a version still waiting on Store certification and sent to a Store page that did not have it. Store installs now skip the GitHub check and the About update button, like the tray app already did; the Store updates them. `codeburn menubar` spawned the tray exe inside `WindowsApps`, which Windows refuses from outside the package (`spawn EPERM`); it now opens the Store app by its app id, and the desktop app starts the tray. Reported in #1520, thanks @Brandin.
|
||||
|
|
@ -59,7 +62,7 @@
|
|||
### Changed
|
||||
- **The resident `codeburn serve` process gives its memory back once the cold start is over.** On a first run the desktop's serve child parses the whole corpus to seal the daily summaries, and then kept every byte of what it read: an unscoped session cache the load memo served to every later request whatever range it asked for, and the lifetime parse trees, with nothing to retire them once the polls stopped. On a 170 MB shard set that was 2,198 MB resident, unchanged across ten minutes of idle, against 364 MB of live heap. The cold start now releases what it can re-read from disk the moment its summaries are written, so the poll that follows loads only the month shards its own range covers (141,154 cached calls resident, down to 90,094; five lifetime parse trees, down to one day's), and then collects, because V8 hands the pages back only on a later collection and returns none of them on its own. Steady resident after a cold start falls from 2,198 MB to 700 MB, and a warm start, which has no fill to release after but the same opening burst behind it, from 900 MB to 781 MB. Nothing is dropped that the cache is the only record of: a held shard-publish window is published before the release, not discarded, and a durable provider's section never waits in that window at all. Answers are unchanged, every period is leaf-identical to before, cold and warm, and a poll costs the same; the whole cost is a one-off 1.6 CPU-seconds of collection after a cold start. (#1511)
|
||||
- **The resident `codeburn serve` process stops re-reading the whole corpus, and re-writing a whole month of cache, on every poll.** While the desktop app is open its serve child answers a status poll every 30-60 seconds, and as long as any agent is writing a transcript each of those polls re-walked every project directory and re-stat'd every session file (17,611 stats and 2,460 readdirs on a 4,530-transcript corpus) and then republished the current month's cache shard: 49 MB rewritten, and read back on the next poll, because one file gained a line. Both are now driven by the root watchers that already exist. The discovery sweep reuses the directory listing and the dev/ino/mtime/size of any path no filesystem event has named, re-reading only the paths the watcher named and the directories those sit in. Nothing derived is reused (every file is still reconciled against its cached fingerprint by the same code as before), and the sweep falls back to reading everything whenever the watcher cannot vouch for the answer: no watcher at all (every one-shot CLI run), a watcher that failed to arm or errored, an event that arrived unscoped or overflowed the tracked-path bound, a path outside every armed root, a WSL/UNC path, a SQLite-backed or virtual-suffix source whose fingerprint is not the path an event would name, the first parse after start, anything remembered longer than four and a half minutes, and a wall-clock jump the monotonic clock did not see. Shard publication is coalesced to at most once every ninety seconds in the resident process (longer than the slowest poll, or the window would expire before every poll and coalesce nothing), and is always flushed when the app closes its stdin. One-shot CLI runs keep writing immediately. A publish the window swallows writes nothing at all, so the on-disk cache is never newer-looking than its content: the cost of losing a window is that the next start re-parses those appends, and a held publish is dropped rather than written if another process has published since. Measured on a 4,530-transcript corpus with one session appended every three seconds, polling as the desktop does: 0.71 to 0.33 CPU-seconds per poll at a 30-second cadence, 17,611 to 11,592 stats, 2,460 to 324 readdirs, three shard publishes down to one. A quiet corpus was already free and still is. Time-to-reflect is unchanged at 1.23 s before and 1.19 s after, including for a new project directory, a new session file, a deleted file, a file replaced by rename, an append landing mid-poll, a change burst past the watcher's tracking bound, and a corpus changed while the watcher is unhealthy. (#1511)
|
||||
- **Vercel AI Gateway spend is no longer added to your totals, because it was being counted twice.** The gateway's reporting API returns one row per day per model: a cost, a token count and a `request_count`, with no request ids, no timestamps and no attribution. The requests behind those rows came from the local tools you pointed at the gateway (Claude Code via `ANTHROPIC_BASE_URL`, Codex, OpenCode, Cline/Roo/Kilo, Cursor), and every one of those tools writes its own session file that CodeBurn already parses. Nothing in a gateway row can be matched against those sessions, so there is no way to reconcile the two, which meant every headline, model row, daily row and history point carried the same spend twice for anyone with a gateway credential. **If you use the gateway, your headline totals will go down.** The gateway is still shown, always: its own provider row carries the full amount (marked "not in total" in the desktop app), the JSON report gains `overview.excludedGatewayCost` when something is excluded, and the terminal report and dashboard print a one-line footnote. `--provider vercel-gateway` is unaffected and reports the full amount. The rule lives in one place, the parse that every surface reads, so `models`, `sessions`, `export`, `compare`, `compare-periods`, `spend`, `yield`, `audit`, `budget`, the Ink dashboard's panels and the Teams sync push all agree with the headline instead of each report deciding for itself, and no detail panel (By tool, Top models, Top projects, By activity, the JSON `projects[]`) lists spend the total beside it is not counting. The gateway still appears once, clearly labelled: its own `By tool` row with no share, its own provider row in the payload and in `--format json` (`overview.excludedProviders`), and a one-line footnote under any total that left it out. The daily cache and the session cache are the deliberate exception: they keep storing the gateway slice, because a past day's aggregate row can never be fetched again. To count it in totals anyway, run `codeburn gateway-totals include` (`exclude` to undo, no argument to see the current setting). The exclusion is read-side only (the daily cache still seals the gateway slice on every day), so turning it on applies retroactively to sealed days without re-fetching anything, which matters because a gateway row cannot be re-derived from disk. Also fixed alongside it: `request_count` was parsed and then ignored, so a day+model row with 300 requests counted as one call; the gateway's call count now reflects the real figure on every surface. (#1511)
|
||||
- **Vercel AI Gateway spend is no longer added to your totals, because it was being counted twice.** The gateway's reporting API returns one row per day per model: a cost, a token count and a `request_count`, with no request ids, no timestamps and no attribution. The requests behind those rows came from the local tools you pointed at the gateway (Claude Code via `ANTHROPIC_BASE_URL`, Codex, OpenCode, Cline/Kilo, Cursor), and every one of those tools writes its own session file that CodeBurn already parses. Nothing in a gateway row can be matched against those sessions, so there is no way to reconcile the two, which meant every headline, model row, daily row and history point carried the same spend twice for anyone with a gateway credential. **If you use the gateway, your headline totals will go down.** The gateway is still shown, always: its own provider row carries the full amount (marked "not in total" in the desktop app), the JSON report gains `overview.excludedGatewayCost` when something is excluded, and the terminal report and dashboard print a one-line footnote. `--provider vercel-gateway` is unaffected and reports the full amount. The rule lives in one place, the parse that every surface reads, so `models`, `sessions`, `export`, `compare`, `compare-periods`, `spend`, `yield`, `audit`, `budget`, the Ink dashboard's panels and the Teams sync push all agree with the headline instead of each report deciding for itself, and no detail panel (By tool, Top models, Top projects, By activity, the JSON `projects[]`) lists spend the total beside it is not counting. The gateway still appears once, clearly labelled: its own `By tool` row with no share, its own provider row in the payload and in `--format json` (`overview.excludedProviders`), and a one-line footnote under any total that left it out. The daily cache and the session cache are the deliberate exception: they keep storing the gateway slice, because a past day's aggregate row can never be fetched again. To count it in totals anyway, run `codeburn gateway-totals include` (`exclude` to undo, no argument to see the current setting). The exclusion is read-side only (the daily cache still seals the gateway slice on every day), so turning it on applies retroactively to sealed days without re-fetching anything, which matters because a gateway row cannot be re-derived from disk. Also fixed alongside it: `request_count` was parsed and then ignored, so a day+model row with 300 requests counted as one call; the gateway's call count now reflects the real figure on every surface. (#1511)
|
||||
- **A Hermes mid-session price change lands on the day it is observed, and sealed days are never rewritten.** This was already the behaviour; it is now the stated rule, documented where the observation is stamped (`src/providers/hermes.ts`). (#1511)
|
||||
- **An open desktop window only refreshes the headline at the live cadence; the slower sections moved to slower tiers.** Every refresh used to spawn a fresh `act report` process, run a yield pass and run a full optimize scan. The headline poll now asks for neither optimize findings nor a fresh act report; `act report` refreshes on mount, on a manual refresh and every 10 minutes, yield every 5, and the optimize scan runs at most once a day per query scope, stored with the time it was computed, reused only on the same local day, and re-asked for on the first render of a new local day so a window left open across midnight does not keep yesterday's savings under Today. A figure that is not live says how old it is, a failed scan shows its error instead of scanning forever, and no stored scan means no savings clause rather than a zero. On battery the live interval doubles, and arming a new cadence no longer refetches everything. The window also stops polling and animating while it is hidden and catches up the moment it comes back. Measured for an open window: 65 to 70 percent less CPU while a session is being written, 84 to 91 percent less when idle, with every CodeBurn process at 0 percent while hidden. (#1496, #1511)
|
||||
- **Both apps retire the resident parser after 15 idle minutes and start a new one on demand.** The renderer stops polling while the window is hidden, but the `serve` child stayed resident with its parsed cache, one to two GB on a large corpus. After 15 minutes without a request it is stopped the way a settings restart stops it, without spending the unexpected-death budget, and the next request starts a new resident through the existing lazy path rather than a one-shot, whose snapshot settle window would leave the figure a whole tick behind. A retired parser closes its stdin and gets a bounded grace to publish any held cache window before it is signalled, so retiring no longer discards a coalesced write: flushed 0 of 5 before, 5 of 5 after, for both clients. `CODEBURN_SERVE_IDLE_MS` and `CodeBurnServeIdleSeconds` tune or disable it. (#1511)
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ That registers a local MCP server over stdio. Your agent can then answer "where
|
|||
|
||||
It reads the same files on disk that the CLI reads. The server answers from that local data and makes no network call of its own, and project names are pseudonymized unless the agent asks for them.
|
||||
|
||||
## Works with 41 tools
|
||||
## Works with 40 tools
|
||||
|
||||
CodeBurn detects the tools you already use. There is nothing to configure and no folder to point it at. If a tool is installed and has sessions on disk, it shows up. Each logo links to that tool's page.
|
||||
|
||||
|
|
@ -179,7 +179,6 @@ CodeBurn detects the tools you already use. There is nothing to configure and no
|
|||
<a href="docs/providers/pi.md" title="Pi"><img src="https://raw.githubusercontent.com/getagentseal/codeburn/main/assets/providers/pi.png" alt="Pi" height="34" /></a>
|
||||
<a href="docs/providers/omp.md" title="OMP (Oh My Pi)"><img src="https://raw.githubusercontent.com/getagentseal/codeburn/main/assets/providers/omp.svg" alt="OMP (Oh My Pi)" height="34" /></a>
|
||||
<a href="docs/providers/droid.md" title="Droid"><img src="https://raw.githubusercontent.com/getagentseal/codeburn/main/assets/providers/droid.png" alt="Droid" height="34" /></a>
|
||||
<a href="docs/providers/roo-code.md" title="Roo Code"><img src="https://raw.githubusercontent.com/getagentseal/codeburn/main/assets/providers/roo-code.png" alt="Roo Code" height="34" /></a>
|
||||
<a href="docs/providers/kilo-code.md" title="KiloCode"><img src="https://raw.githubusercontent.com/getagentseal/codeburn/main/assets/providers/kilo-code.png" alt="KiloCode" height="34" /></a>
|
||||
<a href="docs/providers/qwen.md" title="Qwen"><img src="https://raw.githubusercontent.com/getagentseal/codeburn/main/assets/providers/qwen.png" alt="Qwen" height="34" /></a>
|
||||
<a href="docs/providers/kimi.md" title="Kimi Code CLI"><img src="https://raw.githubusercontent.com/getagentseal/codeburn/main/assets/providers/kimi.svg" alt="Kimi Code CLI" height="34" /></a>
|
||||
|
|
@ -199,7 +198,7 @@ CodeBurn detects the tools you already use. There is nothing to configure and no
|
|||
</p>
|
||||
|
||||
<details>
|
||||
<summary><strong>All 41 tools</strong></summary>
|
||||
<summary><strong>All 40 tools</strong></summary>
|
||||
|
||||
Each page lists where that tool keeps its data, the format it uses, and the quirks CodeBurn works around.
|
||||
|
||||
|
|
@ -209,7 +208,7 @@ Each page lists where that tool keeps its data, the format it uses, and the quir
|
|||
[Grok Build](docs/providers/grok.md) · [Hermes Agent](docs/providers/hermes.md) · [IBM Bob](docs/providers/ibm-bob.md) · [KiloCode](docs/providers/kilo-code.md) · [Kimi](docs/providers/kimi.md) · [Kimi Code](docs/providers/kimicode.md) ·
|
||||
[Kiro](docs/providers/kiro.md) · [LingTai TUI](docs/providers/lingtai-tui.md) · [Mistral Vibe](docs/providers/mistral-vibe.md) · [Mux](docs/providers/mux.md) · [OMP](docs/providers/omp.md) · [Open Design](docs/providers/open-design.md) ·
|
||||
[OpenClaude](docs/providers/openclaude.md) · [OpenClaw](docs/providers/openclaw.md) · [OpenCode](docs/providers/opencode.md) · [Pi](docs/providers/pi.md) · [Qwen](docs/providers/qwen.md) · [Quick Desktop](docs/providers/quickdesk.md) ·
|
||||
[Roo Code](docs/providers/roo-code.md) · [Warp](docs/providers/warp.md) · [ZCode](docs/providers/zcode.md) · [Zed](docs/providers/zed.md) · [Zerostack](docs/providers/zerostack.md)
|
||||
[Warp](docs/providers/warp.md) · [ZCode](docs/providers/zcode.md) · [Zed](docs/providers/zed.md) · [Zerostack](docs/providers/zerostack.md)
|
||||
|
||||
CodeBurn also reads the [Vercel AI Gateway](docs/providers/vercel-gateway.md) reporting API, which is a gateway rather than a tool, so its spend is shown on its own row and left out of your totals by default.
|
||||
|
||||
|
|
|
|||
|
|
@ -245,19 +245,16 @@
|
|||
"$HOME/.zcode/cli/db/db.sqlite",
|
||||
"$HOME/.config/Claude/local-agent-mode-sessions",
|
||||
"$HOME/.config/Code/User/globalStorage/saoudrizwan.claude-dev/tasks",
|
||||
"$HOME/.config/Code/User/globalStorage/rooveterinaryinc.roo-cline/tasks",
|
||||
"$HOME/.config/Code/User/globalStorage/kilocode.kilo-code/tasks",
|
||||
"$HOME/.config/Code/User/globalStorage/github.copilot-chat",
|
||||
"$HOME/.config/Code/User/globalStorage/emptyWindowChatSessions",
|
||||
"$HOME/.config/Code/User/workspaceStorage",
|
||||
"$HOME/.config/Code - Insiders/User/globalStorage/saoudrizwan.claude-dev/tasks",
|
||||
"$HOME/.config/Code - Insiders/User/globalStorage/rooveterinaryinc.roo-cline/tasks",
|
||||
"$HOME/.config/Code - Insiders/User/globalStorage/kilocode.kilo-code/tasks",
|
||||
"$HOME/.config/Code - Insiders/User/globalStorage/github.copilot-chat",
|
||||
"$HOME/.config/Code - Insiders/User/globalStorage/emptyWindowChatSessions",
|
||||
"$HOME/.config/Code - Insiders/User/workspaceStorage",
|
||||
"$HOME/.config/VSCodium/User/globalStorage/saoudrizwan.claude-dev/tasks",
|
||||
"$HOME/.config/VSCodium/User/globalStorage/rooveterinaryinc.roo-cline/tasks",
|
||||
"$HOME/.config/VSCodium/User/globalStorage/kilocode.kilo-code/tasks",
|
||||
"$HOME/.config/VSCodium/User/globalStorage/github.copilot-chat",
|
||||
"$HOME/.config/VSCodium/User/globalStorage/emptyWindowChatSessions",
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -30,7 +30,6 @@ import openaiLight from '../assets/providers/openai-light.svg'
|
|||
import pi from '../assets/providers/pi.png'
|
||||
import qwenDark from '../assets/providers/qwen-dark.svg'
|
||||
import qwenLight from '../assets/providers/qwen-light.svg'
|
||||
import rooCode from '../assets/providers/roo-code.png'
|
||||
import vercelGateway from '../assets/providers/vercel-gateway.png'
|
||||
import warp from '../assets/providers/warp.jpg'
|
||||
import zcode from '../assets/providers/zcode.jpg'
|
||||
|
|
@ -64,7 +63,6 @@ const SINGLE_LOGOS: Record<string, string> = {
|
|||
mux,
|
||||
openclaw,
|
||||
pi,
|
||||
'roo-code': rooCode,
|
||||
'vercel-gateway': vercelGateway,
|
||||
warp,
|
||||
zcode,
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -69,7 +69,6 @@ One line per file in this directory, recursively.
|
|||
- [providers/pi.md](providers/pi.md) — Pi agent CLI.
|
||||
- [providers/quickdesk.md](providers/quickdesk.md) — Amazon Quick Desktop local usage and session history.
|
||||
- [providers/qwen.md](providers/qwen.md) — Qwen Code CLI.
|
||||
- [providers/roo-code.md](providers/roo-code.md) — Roo Code VS Code extension.
|
||||
- [providers/vercel-gateway.md](providers/vercel-gateway.md) — Cloud usage for [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) via the reporting API.
|
||||
- [providers/vscode-cline-parser.md](providers/vscode-cline-parser.md) — Shared discovery and parsing for Cline and VS Code extensions descended from Cline.
|
||||
- [providers/warp.md](providers/warp.md) — Warp Oz agent sessions from Warp's local SQLite database.
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ type Provider = {
|
|||
|
||||
`src/providers/index.ts` registers providers across two tiers:
|
||||
|
||||
- **Eager**: `claude`, `cline`, `cline-cli`, `codewhale`, `codebuff`, `codex`, `copilot`, `devin`, `droid`, `dsh`, `gemini`, `hermes`, `ibm-bob`, `kilo-code`, `kiro`, `kimi`, `kimicode`, `lingtai-tui`, `mistral-vibe`, `mux`, `openclaw`, `openclaude`, `open-design`, `pi`, `omp`, `qwen`, `quickdesk`, `roo-code`, `zerostack`, `grok`. Imported at module load.
|
||||
- **Eager**: `claude`, `cline`, `cline-cli`, `codewhale`, `codebuff`, `codex`, `copilot`, `devin`, `droid`, `dsh`, `gemini`, `hermes`, `ibm-bob`, `kilo-code`, `kiro`, `kimi`, `kimicode`, `lingtai-tui`, `mistral-vibe`, `mux`, `openclaw`, `openclaude`, `open-design`, `pi`, `omp`, `qwen`, `quickdesk`, `zerostack`, `grok`. Imported at module load.
|
||||
- **Lazy**: `antigravity`, `forge`, `goose`, `cursor`, `opencode`, `cursor-agent`, `crush`, `warp`, `vercel-gateway`, `zcode`, `zed`. Imported via dynamic `import()` so the heavy dependencies (SQLite, protobuf, network clients) do not touch users who do not have those tools installed.
|
||||
|
||||
Both lists hit the same `getAllProviders()` aggregator. A failed lazy import is silent and excludes that provider from the run.
|
||||
|
|
@ -212,7 +212,7 @@ Two knock-on rules:
|
|||
|
||||
Session `cwd` values recorded inside WSL are Linux paths (`/home/me/proj`) that name nothing on the Windows filesystem. `resolveCanonicalProjectPath` (`src/parser.ts`) already refuses to walk a path that is not absolute *on the current platform*, so those are attributed to the recorded `cwd` verbatim instead of being walked or canonicalized.
|
||||
|
||||
`src/providers/vscode-cline-parser.ts` is a shared helper consumed by `cline`, `ibm-bob`, `kilo-code`, and `roo-code`. It is not registered as a provider on its own.
|
||||
`src/providers/vscode-cline-parser.ts` is a shared helper consumed by `cline`, `ibm-bob`, and `kilo-code`. It is not registered as a provider on its own.
|
||||
|
||||
For the per-provider data location, storage format, parser quirks, and test coverage, see `docs/providers/`.
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ For categories that involve code edits, CodeBurn tracks file-aware retry cycles.
|
|||
| **Forge** | SQLite `~/.forge/.forge.db` | Queries `conversations` read-only and parses `context.messages`. Assistant usage entries provide prompt, completion, and cached counts; CodeBurn subtracts cached from prompt for input pricing, emits one call per assistant message, and extracts tool calls plus shell commands. |
|
||||
| **Pi / OMP** | `~/.pi/agent/sessions/<sanitized-cwd>/*.jsonl` (Pi), `~/.omp/agent/sessions/<sanitized-cwd>/*.jsonl` (OMP) | Each assistant message carries usage (input, output, cacheRead, cacheWrite) plus inline `toolCall` blocks. Tool names normalize to the standard set (`bash` → `Bash`, `dispatch_agent` → `Agent`); bash commands come from `toolCall.arguments.command`. |
|
||||
| **Codebuff** (formerly Manicode) | `~/.config/manicode/projects/<project>/chats/<chatId>/chat-messages.json` (honors `CODEBUFF_DATA_DIR`; walks `manicode-dev` / `manicode-staging`) | Bills in credits, so each completed assistant message is costed at the public rate of $0.01/credit via `msg.credits`. When an upstream provider's stashed RunState records token-level usage (`message.metadata.runState.sessionState.mainAgentState.messageHistory[*].providerOptions`), the real tokens and LiteLLM cost take precedence. Native tool names (`read_files`, `str_replace`, `run_terminal_command`, `spawn_agents`) normalize to `Read`, `Edit`, `Bash`, `Agent`. |
|
||||
| **Cline / Roo Code / KiloCode** | VS Code `globalStorage` across VS Code, VS Code Insiders, and VSCodium (Cline at `saoudrizwan.claude-dev`, plus `~/.cline/data`) | Cline-family agents. CodeBurn reads `ui_messages.json` from each task directory, extracting token counts from `type: "say"` entries with `say: "api_req_started"`. |
|
||||
| **Cline / KiloCode** | VS Code `globalStorage` across VS Code, VS Code Insiders, and VSCodium (Cline at `saoudrizwan.claude-dev`, plus `~/.cline/data`) | Cline-family agents. CodeBurn reads `ui_messages.json` from each task directory, extracting token counts from `type: "say"` entries with `say: "api_req_started"`. |
|
||||
| **Cline CLI** | `~/.cline/data/sessions/<session-id>/` (honors `CLINE_SESSION_DATA_DIR`, `CLINE_DATA_DIR`, `CLINE_DIR`) | The Cline command-line agent, whose layout is unrelated to the VS Code extension's. Reads `<session-id>.json` for session metadata and the rolled-up `usage`, and `<session-id>.messages.json` for the per-message `metrics` block (input, output, cacheRead, cacheWrite, cost) that becomes one call each. |
|
||||
| **CodeWhale** | `~/.codewhale/sessions/*.json` plus unmigrated legacy `~/.deepseek/sessions/*.json`; `$CODEWHALE_HOME/sessions` is an exact override | Emits one cumulative record per saved session. CodeWhale exposes only `total_tokens`, so CodeBurn preserves that aggregate in the input column rather than inventing an input/output split. Cost is the exact stored parent-session plus subagent USD total; model pricing is used only when the cost snapshot is absent. Tool blocks, shell commands, skills, and subagent types are retained. |
|
||||
| **DeepSeek Harness** (`dsh`) | `~/.dsh/sessions/--<slug>--/<session-id>/session.jsonl.zstd` (or `session.jsonl` when compression is off); `DSH_HOME` relocates the root | DeepSeek's open-source agent harness, unrelated to the CodeWhale desktop app. The `.zstd` log is a concatenation of independent zstd frames (one per write batch), decoded frame by frame; needs Node 22.15+. One call per `(turn, step)`, with usage from the step's `assistant/message` (the streamed `assistant/chunk` sample is a draft of the same call, never a second one). DSH records tokens but no cost, so calls are priced from the shared tables with reasoning billed at the output rate. |
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ For the architectural picture, see `../architecture.md`.
|
|||
| [OMP](omp.md) | JSONL | `src/providers/pi.ts` | `tests/providers/omp.test.ts` |
|
||||
| [Qwen](qwen.md) | JSONL | `src/providers/qwen.ts` | none |
|
||||
| [Quick Desktop](quickdesk.md) | EMF JSONL + SQLite | `src/providers/quickdesk.ts` | `tests/providers/quickdesk.test.ts` |
|
||||
| [Roo Code](roo-code.md) | JSON | `src/providers/roo-code.ts` | `tests/providers/roo-code.test.ts` |
|
||||
| [Zerostack](zerostack.md) | JSON | `src/providers/zerostack.ts` | `tests/providers/zerostack.test.ts` |
|
||||
| [Grok Build](grok.md) | JSON/JSONL | `src/providers/grok.ts` | `tests/providers/grok.test.ts` |
|
||||
| [Grok Bot](grokbot.md) | JSON | `src/providers/grokbot.ts` | `tests/providers/grokbot.test.ts` |
|
||||
|
|
@ -62,7 +61,7 @@ For the architectural picture, see `../architecture.md`.
|
|||
|
||||
| Helper | Used by | Source |
|
||||
|---|---|---|
|
||||
| [vscode-cline-parser](vscode-cline-parser.md) | `cline`, `ibm-bob`, `kilo-code`, `roo-code` | `src/providers/vscode-cline-parser.ts` |
|
||||
| [vscode-cline-parser](vscode-cline-parser.md) | `cline`, `ibm-bob`, `kilo-code` | `src/providers/vscode-cline-parser.ts` |
|
||||
|
||||
## File Format
|
||||
|
||||
|
|
|
|||
|
|
@ -55,4 +55,4 @@ None at the provider level; the metadata file is the cached source path and the
|
|||
|
||||
1. Reproduce with a minimal session directory: `<id>.json` plus `<id>.messages.json`.
|
||||
2. Run `tests/providers/cline-cli.test.ts`.
|
||||
3. This provider shares no code with `vscode-cline-parser.ts` — changes here cannot affect Cline, Roo Code, KiloCode, or IBM Bob.
|
||||
3. This provider shares no code with `vscode-cline-parser.ts` — changes here cannot affect Cline, KiloCode, or IBM Bob.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ Sessions from the Cline **command-line** agent use an unrelated layout and are h
|
|||
|
||||
These task roots are scanned:
|
||||
|
||||
1. VS Code extension globalStorage for `saoudrizwan.claude-dev`, in every supported VS Code variant: stable (`Code`), Insiders (`Code - Insiders`), and VSCodium. The per-platform paths come from `getVSCodeGlobalStoragePaths` in `src/providers/vscode-cline-parser.ts`, the same helper Roo Code and KiloCode use.
|
||||
1. VS Code extension globalStorage for `saoudrizwan.claude-dev`, in every supported VS Code variant: stable (`Code`), Insiders (`Code - Insiders`), and VSCodium. The per-platform paths come from `getVSCodeGlobalStoragePaths` in `src/providers/vscode-cline-parser.ts`, the same helper KiloCode uses.
|
||||
2. Cline's home-data root at `~/.cline/data`.
|
||||
|
||||
Every root is expected to contain a `tasks/` child directory. Discovery is delegated to `discoverClineTasks` in `src/providers/vscode-cline-parser.ts`, so a task is only included when it has a `ui_messages.json` file.
|
||||
|
|
@ -44,10 +44,10 @@ Discovery deduplicates by task id across all Cline roots so a task that exists i
|
|||
- This provider is intentionally a thin wrapper over the shared Cline-family parser.
|
||||
- Cline can keep data in both VS Code globalStorage and `~/.cline/data`, depending on version and workflow.
|
||||
- A user can run Cline in VS Code stable, Insiders, and VSCodium at the same time; each variant has its own globalStorage tree, so all of them must be scanned.
|
||||
- If Cline changes the JSON shape, fix `vscode-cline-parser.ts` only if Roo Code and KiloCode still pass. Branch provider-specific parsing rather than duplicating the whole parser.
|
||||
- If Cline changes the JSON shape, fix `vscode-cline-parser.ts` only if KiloCode and IBM Bob still pass. Branch provider-specific parsing rather than duplicating the whole parser.
|
||||
|
||||
## When fixing a bug here
|
||||
|
||||
1. Reproduce with a minimal task directory containing `ui_messages.json` and `api_conversation_history.json`.
|
||||
2. Run `tests/providers/cline.test.ts`, plus `tests/providers/roo-code.test.ts` and `tests/providers/kilo-code.test.ts` if the shared parser changes.
|
||||
2. Run `tests/providers/cline.test.ts`, plus `tests/providers/kilo-code.test.ts` and `tests/providers/ibm-bob.test.ts` if the shared parser changes.
|
||||
3. Keep the provider name `cline`; downstream filters and dedup keys depend on it.
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ The `Bob-IDE` paths cover the preview-era app name that some installs used befor
|
|||
|
||||
Each task is a directory under `tasks/<task-id>/` and must contain `ui_messages.json`.
|
||||
|
||||
CodeBurn parses the same Cline-family UI event format used by Roo Code and KiloCode:
|
||||
CodeBurn parses the same Cline-family UI event format used by Cline and KiloCode:
|
||||
|
||||
- `ui_messages.json` entries with `type: "say"` and `say: "api_req_started"` contain serialized token/cost metrics.
|
||||
- `ui_messages.json` user text entries seed the turn's first user message.
|
||||
|
|
@ -45,11 +45,11 @@ Per `<providerName>:<taskId>:<apiRequestIndex>` via `vscode-cline-parser.ts`.
|
|||
|
||||
- IBM Bob has shipped under both `IBM Bob` and `Bob-IDE` application data folder names.
|
||||
- This provider intentionally covers the IDE task-history format. Bob Shell's `~/.bob` checkpoint data is a separate storage surface and is not parsed until we have a stable usage schema fixture.
|
||||
- The shared Cline parser does not currently extract individual tool names from UI messages, so tool breakdowns are empty for IBM Bob just like Roo Code and KiloCode.
|
||||
- The shared Cline parser does not currently extract individual tool names from UI messages, so tool breakdowns are empty for IBM Bob just like Cline and KiloCode.
|
||||
|
||||
## When Fixing A Bug Here
|
||||
|
||||
1. Check whether the install uses `IBM Bob` or `Bob-IDE` as the application data directory.
|
||||
2. Confirm the task folder still contains `ui_messages.json` and `api_conversation_history.json`.
|
||||
3. If the UI message schema changed, add a focused fixture to `tests/providers/ibm-bob.test.ts`.
|
||||
4. If the change also affects Roo Code or KiloCode, update `src/providers/vscode-cline-parser.ts` and run all three provider test files.
|
||||
4. If the change also affects Cline or KiloCode, update `src/providers/vscode-cline-parser.ts` and run all three provider test files.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,6 @@ Delegated. Per `<providerName>:<taskId>:<index>` (handled in `vscode-cline-parse
|
|||
|
||||
## When fixing a bug here
|
||||
|
||||
1. If the bug is "Cline, KiloCode, and Roo Code all broken in the same way", fix it in `vscode-cline-parser.ts`.
|
||||
2. If the bug is "KiloCode broken, Roo Code fine", the difference is upstream (KiloCode's emitted JSON differs slightly). Reproduce with a fixture and consider whether the cline parser needs to branch on extension ID.
|
||||
1. If the bug is "Cline and KiloCode both broken in the same way", fix it in `vscode-cline-parser.ts`.
|
||||
2. If the bug is "KiloCode broken, Cline fine", the difference is upstream (KiloCode's emitted JSON differs slightly). Reproduce with a fixture and consider whether the cline parser needs to branch on extension ID.
|
||||
3. Read [`vscode-cline-parser.md`](vscode-cline-parser.md) before editing.
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
# Roo Code
|
||||
|
||||
Roo Code VS Code extension.
|
||||
|
||||
- **Source:** `src/providers/roo-code.ts`
|
||||
- **Loading:** eager (`src/providers/index.ts:11`)
|
||||
- **Test:** `tests/providers/roo-code.test.ts` (247 lines)
|
||||
|
||||
## Where it reads from
|
||||
|
||||
VS Code extension globalStorage for `rooveterinaryinc.roo-cline` (extension ID set at `roo-code.ts:4`). The actual walk is delegated to `discoverClineTasks` in `src/providers/vscode-cline-parser.ts`.
|
||||
|
||||
## Storage format
|
||||
|
||||
Per-task directories with `ui_messages.json` and `api_conversation_history.json`. See [`vscode-cline-parser`](vscode-cline-parser.md) for the schema.
|
||||
|
||||
## Caching
|
||||
|
||||
None at the provider level; delegates to the shared helper.
|
||||
|
||||
## Deduplication
|
||||
|
||||
Delegated. Per `<providerName>:<taskId>:<index>` (in `vscode-cline-parser.ts:109`).
|
||||
|
||||
## Quirks
|
||||
|
||||
- Thin wrapper. Almost every Roo Code bug actually lives in `vscode-cline-parser.ts`.
|
||||
- The VS Code extension wrappers using the Cline-family parser differ **only** by extension ID.
|
||||
|
||||
## When fixing a bug here
|
||||
|
||||
1. If the bug also reproduces against Cline or KiloCode, fix it in `vscode-cline-parser.ts`.
|
||||
2. If the bug is Roo Code-specific, the difference is upstream JSON shape. Reproduce with a fixture and consider whether the cline parser needs to branch on extension ID.
|
||||
3. Read [`vscode-cline-parser.md`](vscode-cline-parser.md) before editing.
|
||||
|
|
@ -37,7 +37,7 @@ A report row is a **daily aggregate per model**: one cost, one token count and
|
|||
one `request_count` for a whole day, with no request ids, timestamps or
|
||||
attribution. Nothing in it can be matched against the local tools you pointed at
|
||||
the gateway — Claude Code (`ANTHROPIC_BASE_URL`), Codex, OpenCode,
|
||||
Cline/Roo/Kilo, Cursor — which already record those same requests from their own
|
||||
Cline/Kilo, Cursor — which already record those same requests from their own
|
||||
session files. Counting both double counts the same spend.
|
||||
|
||||
So gateway spend is:
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
Shared discovery and parsing for Cline and VS Code extensions descended from Cline.
|
||||
|
||||
- **Source:** `src/providers/vscode-cline-parser.ts`
|
||||
- **Loading:** not a provider; imported by `cline.ts`, `ibm-bob.ts`, `kilo-code.ts`, and `roo-code.ts`.
|
||||
- **Test:** none directly. Coverage comes from `tests/providers/cline.test.ts`, `tests/providers/ibm-bob.test.ts`, `tests/providers/kilo-code.test.ts`, and `tests/providers/roo-code.test.ts`.
|
||||
- **Loading:** not a provider; imported by `cline.ts`, `ibm-bob.ts`, and `kilo-code.ts`.
|
||||
- **Test:** none directly. Coverage comes from `tests/providers/cline.test.ts`, `tests/providers/ibm-bob.test.ts`, and `tests/providers/kilo-code.test.ts`.
|
||||
|
||||
## What it does
|
||||
|
||||
|
|
@ -45,6 +45,6 @@ Per `<providerName>:<taskId>:<index>` where `index` is the position of the `api_
|
|||
|
||||
## When fixing a bug here
|
||||
|
||||
1. A change here ripples to Cline, IBM Bob, KiloCode, and Roo Code. Run all four provider test files before opening a PR.
|
||||
1. A change here ripples to Cline, IBM Bob, and KiloCode. Run all three provider test files before opening a PR.
|
||||
2. If you find that one of the extensions emits a different shape, branch on the extension ID parameter that the discovery function already takes; do not duplicate the parser.
|
||||
3. If you add support for another Cline-family task store, register it as a thin wrapper file in the same shape as `cline.ts`, `ibm-bob.ts`, `kilo-code.ts`, and `roo-code.ts`.
|
||||
3. If you add support for another Cline-family task store, register it as a thin wrapper file in the same shape as `cline.ts`, `ibm-bob.ts`, and `kilo-code.ts`.
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ const PROVIDERS = [
|
|||
{ id: 'kilo-code', label: 'Kilo Code' },
|
||||
{ id: 'kiro', label: 'Kiro' },
|
||||
{ id: 'kimi', label: 'Kimi' },
|
||||
{ id: 'roo-code', label: 'Roo Code' },
|
||||
];
|
||||
|
||||
const CURRENCIES = [
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ const PROVIDERS = [
|
|||
{ id: 'opencode', label: 'OpenCode' },
|
||||
{ id: 'pi', label: 'Pi' },
|
||||
{ id: 'qwen', label: 'Qwen' },
|
||||
{ id: 'roo-code', label: 'Roo Code' },
|
||||
{ id: 'antigravity', label: 'Antigravity' },
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -3084,7 +3084,6 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
|
|||
case pi = "Pi"
|
||||
case qwen = "Qwen"
|
||||
case omp = "OMP"
|
||||
case rooCode = "Roo Code"
|
||||
case crush = "Crush"
|
||||
case antigravity = "Antigravity"
|
||||
case goose = "Goose"
|
||||
|
|
@ -3106,7 +3105,6 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
|
|||
case .cursorAgent: ["cursor-agent", "cursor agent"]
|
||||
case .cline: ["cline"]
|
||||
case .codewhale: ["codewhale"]
|
||||
case .rooCode: ["roo-code", "roo code"]
|
||||
case .kiloCode: ["kilo-code", "kilocode"]
|
||||
case .ibmBob: ["ibm-bob", "ibm bob"]
|
||||
case .openclaw: ["openclaw"]
|
||||
|
|
@ -3145,7 +3143,6 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
|
|||
case .pi: "pi"
|
||||
case .qwen: "qwen"
|
||||
case .omp: "omp"
|
||||
case .rooCode: "roo-code"
|
||||
case .crush: "crush"
|
||||
case .antigravity: "antigravity"
|
||||
case .goose: "goose"
|
||||
|
|
|
|||
|
|
@ -514,7 +514,6 @@ extension ProviderFilter {
|
|||
case .pi: return Color(red: 0xB2/255.0, green: 0x6B/255.0, blue: 0x3D/255.0)
|
||||
case .qwen: return Color(red: 0x61/255.0, green: 0x5E/255.0, blue: 0xEB/255.0)
|
||||
case .omp: return Color(red: 0x8B/255.0, green: 0x5C/255.0, blue: 0xB0/255.0)
|
||||
case .rooCode: return Color(red: 0x4C/255.0, green: 0xAF/255.0, blue: 0x50/255.0)
|
||||
case .crush: return Color(red: 0xE0/255.0, green: 0x6C/255.0, blue: 0x9F/255.0)
|
||||
case .antigravity: return Color(red: 0xFF/255.0, green: 0x7A/255.0, blue: 0x45/255.0)
|
||||
case .goose: return Color(red: 0xB7/255.0, green: 0x8D/255.0, blue: 0x52/255.0)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ export function getCodeburnCacheDir(): string {
|
|||
return override?.trim() ? override : join(homedir(), '.cache', 'codeburn')
|
||||
}
|
||||
|
||||
/** Providers CodeBurn no longer reads. Their session cache sections and daily
|
||||
* history slices are dropped on load, so a removed tool leaves nothing behind. */
|
||||
export const RETIRED_PROVIDER_NAMES: ReadonlySet<string> = new Set(['roo-code'])
|
||||
|
||||
/** A versioned cache file is the only source when it exists. Legacy adoption is ENOENT-only. */
|
||||
export type ExistingTextFile =
|
||||
| { status: 'absent' }
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ export type CodeburnConfig = {
|
|||
// Vercel AI Gateway rows are DAILY AGGREGATES per model with no request id,
|
||||
// timestamp or attribution (see src/providers/vercel-gateway.ts), so they
|
||||
// cannot be matched against the local tools that were pointed at the gateway
|
||||
// (Claude Code via ANTHROPIC_BASE_URL, Codex, OpenCode, Cline/Roo/Kilo,
|
||||
// (Claude Code via ANTHROPIC_BASE_URL, Codex, OpenCode, Cline/Kilo,
|
||||
// Cursor) — counting both double counts the same spend. Gateway rows are
|
||||
// therefore always shown as their own labelled row but kept OUT of every
|
||||
// headline total unless this is true. Read-side only: the daily cache always
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { existsSync } from 'fs'
|
|||
import { mkdir, open, readdir, readFile, rename, stat, unlink } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
|
||||
import { getCodeburnCacheDir } from './cache-dir.js'
|
||||
import { getCodeburnCacheDir, RETIRED_PROVIDER_NAMES } from './cache-dir.js'
|
||||
import { sweepSupersededCacheFiles } from './cache-sweep.js'
|
||||
import type { ProjectFilterTarget } from './parser.js'
|
||||
import type { DateRange, ProjectSummary } from './types.js'
|
||||
|
|
@ -534,6 +534,15 @@ function migrateDays(days: Record<string, unknown>[]): DailyEntry[] {
|
|||
...(sanitizeProjects(d.projects)),
|
||||
...(d.carried === true ? { carried: true as const } : {}),
|
||||
}))
|
||||
.filter(day => {
|
||||
let retired = false
|
||||
for (const provider of RETIRED_PROVIDER_NAMES) {
|
||||
if (!Object.hasOwn(day.providers, provider)) continue
|
||||
subtractSliceFromDay(day, provider, day.providers[provider]!)
|
||||
retired = true
|
||||
}
|
||||
return !retired || hasPositiveDayContent(day)
|
||||
})
|
||||
// Day and slices are summed independently: a day can hold the full model
|
||||
// split while one of its slices was written before slices carried one (or
|
||||
// the reverse), and a provider-scoped view reads the slice's map alone.
|
||||
|
|
|
|||
|
|
@ -1960,7 +1960,7 @@ program
|
|||
|
||||
program
|
||||
.command('gateway-totals [mode]')
|
||||
.description('Include or exclude Vercel AI Gateway spend in headline totals. Gateway reports are daily per-model aggregates with no request identity, so the same spend is usually already counted by the local tools you pointed at the gateway (Claude Code, Codex, OpenCode, Cline/Roo/Kilo, Cursor). Excluded by default; the gateway is always shown as its own row either way. Modes: include, exclude.')
|
||||
.description('Include or exclude Vercel AI Gateway spend in headline totals. Gateway reports are daily per-model aggregates with no request identity, so the same spend is usually already counted by the local tools you pointed at the gateway (Claude Code, Codex, OpenCode, Cline/Kilo, Cursor). Excluded by default; the gateway is always shown as its own row either way. Modes: include, exclude.')
|
||||
.option('--format <format>', 'Output format: text, json', 'text')
|
||||
.action(async (mode?: string, opts?: { format?: string }) => {
|
||||
const format = opts?.format ?? 'text'
|
||||
|
|
|
|||
|
|
@ -5812,7 +5812,7 @@ function deferToBackgroundFill(path: string, fp: { mtimeMs: number }, cached: un
|
|||
/// Vercel AI Gateway's `/v1/report` returns one cost/token/request_count per
|
||||
/// day per model, with no request id, timestamp or attribution. The local
|
||||
/// tools that were pointed at the gateway (Claude Code via
|
||||
/// ANTHROPIC_BASE_URL, Codex, OpenCode, Cline/Roo/Kilo, Cursor) record those
|
||||
/// ANTHROPIC_BASE_URL, Codex, OpenCode, Cline/Kilo, Cursor) record those
|
||||
/// same requests in their own session files, and nothing in an aggregate row
|
||||
/// can be matched against them — so a corpus holding both counts the same
|
||||
/// spend twice.
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import type { ParsedProviderCall, ProbeRoot, Provider, SessionParser, SessionSou
|
|||
// <sessions>/<sessionId>/<sessionId>.messages.json per-message metrics
|
||||
//
|
||||
// Kept as its own provider rather than a third root on `cline` so the shared
|
||||
// Cline-family parser (also serving Roo Code and KiloCode) stays untouched.
|
||||
// Cline-family parser (also serving KiloCode and IBM Bob) stays untouched.
|
||||
|
||||
const PROVIDER_NAME = 'cline-cli'
|
||||
const DISPLAY_NAME = 'Cline CLI'
|
||||
|
|
|
|||
|
|
@ -39,8 +39,8 @@ async function dedupeTaskSources(sources: SessionSource[]): Promise<SessionSourc
|
|||
export function createClineProvider(overrideDirs?: string | string[]): Provider {
|
||||
const configuredDirs = normalizeOverrideDirs(overrideDirs)
|
||||
// Cline may be installed in any VS Code variant (stable, Insiders, VSCodium),
|
||||
// so every globalStorage root is scanned - same as the Roo Code and KiloCode
|
||||
// siblings - plus Cline's own home-data root. Shared by discovery and
|
||||
// so every globalStorage root is scanned - same as the KiloCode
|
||||
// sibling - plus Cline's own home-data root. Shared by discovery and
|
||||
// probeRoots so doctor can never report a root discovery does not read.
|
||||
const taskRoots = (): string[] => configuredDirs ?? [
|
||||
...clineTaskRoots(EXTENSION_ID),
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ import { openDesign } from './open-design.js'
|
|||
import { pi, omp } from './pi.js'
|
||||
import { qwen } from './qwen.js'
|
||||
import { quickdesk } from './quickdesk.js'
|
||||
import { rooCode } from './roo-code.js'
|
||||
import { zerostack } from './zerostack.js'
|
||||
import { grok } from './grok.js'
|
||||
import { grokbot } from './grokbot.js'
|
||||
|
|
@ -195,7 +194,7 @@ async function loadZed(): Promise<Provider | null> {
|
|||
}
|
||||
}
|
||||
|
||||
const coreProviders: Provider[] = [claude, cline, clineCli, codewhale, codebuff, codex, copilot, devin, droid, dsh, gemini, hermes, ibmBob, kiloCode, kiro, kimi, kimicode, lingtaiTui, mistralVibe, mux, openclaw, openclaude, openDesign, pi, omp, qwen, quickdesk, rooCode, zerostack, grok, grokbot]
|
||||
const coreProviders: Provider[] = [claude, cline, clineCli, codewhale, codebuff, codex, copilot, devin, droid, dsh, gemini, hermes, ibmBob, kiloCode, kiro, kimi, kimicode, lingtaiTui, mistralVibe, mux, openclaw, openclaude, openDesign, pi, omp, qwen, quickdesk, zerostack, grok, grokbot]
|
||||
|
||||
// Lazily loaded providers, listed by name so --provider validation works even
|
||||
// when an optional module fails to load. Must stay in sync with getAllProviders.
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
import { discoverClineTasks, createClineParser, clineTaskRoots } from './vscode-cline-parser.js'
|
||||
import type { ProbeRoot, Provider, SessionSource, SessionParser } from './types.js'
|
||||
|
||||
const EXTENSION_ID = 'rooveterinaryinc.roo-cline'
|
||||
|
||||
export function createRooCodeProvider(overrideDir?: string | string[]): Provider {
|
||||
return {
|
||||
name: 'roo-code',
|
||||
displayName: 'Roo Code',
|
||||
|
||||
modelDisplayName(model: string): string {
|
||||
return model
|
||||
},
|
||||
|
||||
toolDisplayName(rawTool: string): string {
|
||||
return rawTool
|
||||
},
|
||||
|
||||
async probeRoots(): Promise<ProbeRoot[]> {
|
||||
return clineTaskRoots(EXTENSION_ID, overrideDir).map(path => ({ path, label: 'tasks' }))
|
||||
},
|
||||
|
||||
async discoverSessions(): Promise<SessionSource[]> {
|
||||
return discoverClineTasks(EXTENSION_ID, 'roo-code', 'Roo Code', overrideDir)
|
||||
},
|
||||
|
||||
createSessionParser(source: SessionSource, seenKeys: Set<string>): SessionParser {
|
||||
return createClineParser(source, seenKeys, 'roo-code')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const rooCode = createRooCodeProvider()
|
||||
|
|
@ -6,7 +6,7 @@ import { fetchWithTimeout } from '../fetch-utils.js'
|
|||
// The report is a DAILY AGGREGATE per model: no request ids, no timestamps, no
|
||||
// attribution. Nothing in a row can be matched against the local tools that
|
||||
// were pointed at the gateway (Claude Code via ANTHROPIC_BASE_URL, Codex,
|
||||
// OpenCode, Cline/Roo/Kilo, Cursor), which record the same requests
|
||||
// OpenCode, Cline/Kilo, Cursor), which record the same requests
|
||||
// themselves. Gateway spend is therefore shown as its own row and kept out of
|
||||
// headline totals unless `includeGatewayInTotals` is set — see
|
||||
// `excludeProviderFromDay` in src/usage-aggregator.ts.
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { createHash, randomBytes } from 'crypto'
|
|||
import { join } from 'path'
|
||||
import { StringDecoder } from 'string_decoder'
|
||||
|
||||
import { getCodeburnCacheDir } from './cache-dir.js'
|
||||
import { getCodeburnCacheDir, RETIRED_PROVIDER_NAMES } from './cache-dir.js'
|
||||
import { acquireCacheRefreshLock, releaseOwnedRefreshLocksForExit } from './cache-refresh-lock.js'
|
||||
import { parseBillingMode, type BillingMode } from './models.js'
|
||||
import type { ToolCall } from './types.js'
|
||||
|
|
@ -522,7 +522,6 @@ export const PROVIDER_PARSE_VERSIONS: Record<string, string> = {
|
|||
// archived-subtree-v1: KiloCode shares the SQLite parser and the same schema.
|
||||
// billing-routes-v2: its warm cache must move with both shared route fields.
|
||||
'kilo-code': 'worktree-project-grouping-v1-session-model-v1-archived-subtree-v1-billing-routes-v2-v2-legacy-union-v1-unknown-usage-v1',
|
||||
'roo-code': 'worktree-project-grouping-v1',
|
||||
// billing-cost-v1: Warp's own billing record (total_provider_cost_in_cents,
|
||||
// total_charged_usage, credits_spent) now rides on each call as
|
||||
// `costFromBilling` and is preserved by providerCallToCachedCall. Entries
|
||||
|
|
@ -1732,6 +1731,13 @@ export async function loadCache(scope?: CacheLoadScope): Promise<SessionCache> {
|
|||
: null
|
||||
const reads: Promise<void>[] = []
|
||||
for (const [provider, meta] of Object.entries(envelope.providers)) {
|
||||
if (RETIRED_PROVIDER_NAMES.has(provider)) {
|
||||
// Kept only in the base, so the next save leaves it out of the envelope
|
||||
// and deletes its index and pieces.
|
||||
state.base.set(provider, await readIndex(dir, meta.index) ?? { ...emptyBase(), index: meta.index })
|
||||
state.dirty = true
|
||||
continue
|
||||
}
|
||||
const section: ProviderSection = {
|
||||
envFingerprint: meta.envFingerprint,
|
||||
files: {},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { isAbsolute, join } from 'path'
|
|||
import { homedir } from 'os'
|
||||
|
||||
import { createClineProvider, getClineDataPath } from '../src/providers/cline.js'
|
||||
import { createRooCodeProvider } from '../src/providers/roo-code.js'
|
||||
import { createKiloCodeProvider } from '../src/providers/kilo-code.js'
|
||||
import { createGrokProvider } from '../src/providers/grok.js'
|
||||
import { createPiProvider, createOmpProvider } from '../src/providers/pi.js'
|
||||
|
|
@ -28,7 +27,7 @@ import { createMistralVibeProvider, getMistralVibeSessionsDir } from '../src/pro
|
|||
// that one and is still open; fold the two together once it lands.
|
||||
|
||||
const CLINE_EXTENSION = 'saoudrizwan.claude-dev'
|
||||
const ROO_EXTENSION = 'rooveterinaryinc.roo-cline'
|
||||
const KILO_EXTENSION = 'kilocode.kilo-code'
|
||||
|
||||
describe('probeRoots mirrors discovery resolution (Tier 2, batch 1)', () => {
|
||||
it('cline reports exactly the roots discovery scans', async () => {
|
||||
|
|
@ -50,26 +49,18 @@ describe('probeRoots mirrors discovery resolution (Tier 2, batch 1)', () => {
|
|||
])
|
||||
})
|
||||
|
||||
it('roo-code reports the override, or exactly the VS Code variant roots', async () => {
|
||||
expect(await createRooCodeProvider('/tmp/roo-a').probeRoots!()).toEqual([
|
||||
{ path: '/tmp/roo-a', label: 'tasks' },
|
||||
])
|
||||
expect(await createRooCodeProvider().probeRoots!()).toEqual(
|
||||
getVSCodeGlobalStoragePaths(ROO_EXTENSION).map(path => ({ path, label: 'tasks' })),
|
||||
)
|
||||
})
|
||||
|
||||
// Regression: an earlier draft mirrored the resolution in a local helper that
|
||||
// detected "no override" with `=== undefined`, while discoverClineTasks uses
|
||||
// truthiness. An empty-string override made doctor report [""] while
|
||||
// discovery scanned the three default roots. Both now call one resolver.
|
||||
it('an empty-string override resolves the same for probeRoots and discovery', async () => {
|
||||
const probed = (await createRooCodeProvider('').probeRoots!()).map(r => r.path)
|
||||
expect(probed).toEqual(clineTaskRoots(ROO_EXTENSION, ''))
|
||||
expect(probed).toEqual(getVSCodeGlobalStoragePaths(ROO_EXTENSION))
|
||||
const probed = (await createKiloCodeProvider('').probeRoots!()).filter(r => r.label === 'tasks').map(r => r.path)
|
||||
expect(probed).toEqual(clineTaskRoots(KILO_EXTENSION, ''))
|
||||
expect(probed).toEqual(getVSCodeGlobalStoragePaths(KILO_EXTENSION))
|
||||
// discoverClineTasks resolves through the same function, so an empty
|
||||
// override cannot send discovery somewhere probeRoots did not report.
|
||||
expect(await discoverClineTasks(ROO_EXTENSION, 'roo-code', 'Roo Code', '')).toEqual([])
|
||||
const discovered = await discoverClineTasks(KILO_EXTENSION, 'kilo-code', 'KiloCode', '')
|
||||
for (const source of discovered) expect(probed.some(root => source.path.startsWith(root))).toBe(true)
|
||||
})
|
||||
|
||||
it('kilo-code reports both halves of its discovery: tasks and the sqlite store', async () => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ function fakeProvider(name: string, discover: Provider['discoverSessions']): Pro
|
|||
|
||||
describe('provider registry', () => {
|
||||
it('has core providers registered synchronously', () => {
|
||||
expect(providers.map(p => p.name)).toEqual(['claude', 'cline', 'cline-cli', 'codewhale', 'codebuff', 'codex', 'copilot', 'devin', 'droid', 'dsh', 'gemini', 'hermes', 'ibm-bob', 'kilo-code', 'kiro', 'kimi', 'kimicode', 'lingtai-tui', 'mistral-vibe', 'mux', 'openclaw', 'openclaude', 'open-design', 'pi', 'omp', 'qwen', 'quickdesk', 'roo-code', 'zerostack', 'grok', 'grokbot'])
|
||||
expect(providers.map(p => p.name)).toEqual(['claude', 'cline', 'cline-cli', 'codewhale', 'codebuff', 'codex', 'copilot', 'devin', 'droid', 'dsh', 'gemini', 'hermes', 'ibm-bob', 'kilo-code', 'kiro', 'kimi', 'kimicode', 'lingtai-tui', 'mistral-vibe', 'mux', 'openclaw', 'openclaude', 'open-design', 'pi', 'omp', 'qwen', 'quickdesk', 'zerostack', 'grok', 'grokbot'])
|
||||
})
|
||||
|
||||
it('codebuff tool display names normalize codebuff-native names to canonical set', () => {
|
||||
|
|
|
|||
|
|
@ -54,9 +54,4 @@ describe('kilo-code provider - metadata', () => {
|
|||
expect(kiloCode.name).toBe('kilo-code')
|
||||
expect(kiloCode.displayName).toBe('KiloCode')
|
||||
})
|
||||
|
||||
it('uses different extension ID than roo-code', () => {
|
||||
expect(kiloCode.name).toBe('kilo-code')
|
||||
expect(kiloCode.name).not.toBe('roo-code')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,247 +0,0 @@
|
|||
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
|
||||
import { rooCode, createRooCodeProvider } from '../../src/providers/roo-code.js'
|
||||
import type { ParsedProviderCall } from '../../src/providers/types.js'
|
||||
|
||||
let tmpDir: string
|
||||
|
||||
function makeUiMessages(opts: {
|
||||
tokensIn?: number
|
||||
tokensOut?: number
|
||||
cacheReads?: number
|
||||
cacheWrites?: number
|
||||
cost?: number
|
||||
userMessage?: string
|
||||
ts?: number
|
||||
}): string {
|
||||
const messages: unknown[] = []
|
||||
|
||||
if (opts.userMessage) {
|
||||
messages.push({ type: 'say', say: 'user_feedback', text: opts.userMessage, ts: 1700000000000 })
|
||||
}
|
||||
|
||||
const apiData: Record<string, unknown> = {
|
||||
tokensIn: opts.tokensIn ?? 100,
|
||||
tokensOut: opts.tokensOut ?? 50,
|
||||
cacheReads: opts.cacheReads ?? 0,
|
||||
cacheWrites: opts.cacheWrites ?? 0,
|
||||
}
|
||||
if (opts.cost !== undefined) apiData.cost = opts.cost
|
||||
|
||||
messages.push({
|
||||
type: 'say',
|
||||
say: 'api_req_started',
|
||||
text: JSON.stringify(apiData),
|
||||
ts: opts.ts ?? 1700000001000,
|
||||
})
|
||||
|
||||
return JSON.stringify(messages)
|
||||
}
|
||||
|
||||
function makeApiHistory(opts?: { model?: string }): string {
|
||||
const modelTag = opts?.model ? `<model>${opts.model}</model>` : ''
|
||||
const messages = [
|
||||
{ role: 'user', content: [{ type: 'text', text: `hello\n<environment_details>\n${modelTag}\n</environment_details>` }] },
|
||||
{ role: 'assistant', content: [{ type: 'text', text: 'response' }] },
|
||||
]
|
||||
return JSON.stringify(messages)
|
||||
}
|
||||
|
||||
describe('roo-code provider - parsing', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await mkdtemp(join(tmpdir(), 'roo-code-test-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('parses tokens and cost from ui_messages.json', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-001')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({
|
||||
tokensIn: 200,
|
||||
tokensOut: 100,
|
||||
cacheReads: 50,
|
||||
cacheWrites: 30,
|
||||
cost: 0.05,
|
||||
userMessage: 'fix the bug',
|
||||
}))
|
||||
await writeFile(join(taskDir, 'api_conversation_history.json'), makeApiHistory())
|
||||
|
||||
const source = { path: taskDir, project: 'task-001', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
const call = calls[0]!
|
||||
expect(call.provider).toBe('roo-code')
|
||||
expect(call.inputTokens).toBe(200)
|
||||
expect(call.outputTokens).toBe(100)
|
||||
expect(call.cacheReadInputTokens).toBe(50)
|
||||
expect(call.cacheCreationInputTokens).toBe(30)
|
||||
expect(call.costUSD).toBe(0.05)
|
||||
expect(call.userMessage).toBe('fix the bug')
|
||||
expect(call.sessionId).toBe('task-001')
|
||||
})
|
||||
|
||||
it('extracts model from api_conversation_history.json', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-002')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ tokensIn: 100, tokensOut: 50 }))
|
||||
await writeFile(join(taskDir, 'api_conversation_history.json'), makeApiHistory({ model: 'claude-sonnet-4-5' }))
|
||||
|
||||
const source = { path: taskDir, project: 'task-002', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.model).toBe('claude-sonnet-4-5')
|
||||
})
|
||||
|
||||
it('falls back to cline-auto when no model indicators', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-003')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ tokensIn: 100, tokensOut: 50 }))
|
||||
await writeFile(join(taskDir, 'api_conversation_history.json'), JSON.stringify([
|
||||
{ role: 'user', content: [{ type: 'text', text: 'hello' }] },
|
||||
{ role: 'assistant', content: [{ type: 'text', text: 'hi' }] },
|
||||
]))
|
||||
|
||||
const source = { path: taskDir, project: 'task-003', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.model).toBe('cline-auto')
|
||||
})
|
||||
|
||||
it('deduplicates across parser runs', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-004')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ tokensIn: 100, tokensOut: 50 }))
|
||||
|
||||
const source = { path: taskDir, project: 'task-004', provider: 'roo-code' }
|
||||
const seenKeys = new Set<string>()
|
||||
|
||||
const calls1: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, seenKeys).parse()) calls1.push(call)
|
||||
|
||||
const calls2: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, seenKeys).parse()) calls2.push(call)
|
||||
|
||||
expect(calls1).toHaveLength(1)
|
||||
expect(calls2).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles missing ui_messages.json gracefully', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-005')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
|
||||
const source = { path: taskDir, project: 'task-005', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('handles invalid JSON gracefully', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-006')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), 'not valid json')
|
||||
|
||||
const source = { path: taskDir, project: 'task-006', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('skips entries with zero tokens', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-007')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), JSON.stringify([
|
||||
{ type: 'say', say: 'api_req_started', text: JSON.stringify({ tokensIn: 0, tokensOut: 0 }), ts: 1700000000000 },
|
||||
]))
|
||||
|
||||
const source = { path: taskDir, project: 'task-007', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('calculates cost from model when cost field missing', async () => {
|
||||
const taskDir = join(tmpDir, 'tasks', 'task-008')
|
||||
await mkdir(taskDir, { recursive: true })
|
||||
await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ tokensIn: 1000, tokensOut: 500 }))
|
||||
await writeFile(join(taskDir, 'api_conversation_history.json'), makeApiHistory())
|
||||
|
||||
const source = { path: taskDir, project: 'task-008', provider: 'roo-code' }
|
||||
const calls: ParsedProviderCall[] = []
|
||||
for await (const call of rooCode.createSessionParser(source, new Set()).parse()) calls.push(call)
|
||||
|
||||
expect(calls).toHaveLength(1)
|
||||
expect(calls[0]!.costUSD).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('roo-code provider - discovery', () => {
|
||||
beforeEach(async () => {
|
||||
tmpDir = await mkdtemp(join(tmpdir(), 'roo-code-test-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it('discovers task directories with ui_messages.json', async () => {
|
||||
const task1 = join(tmpDir, 'tasks', 'task-a')
|
||||
const task2 = join(tmpDir, 'tasks', 'task-b')
|
||||
await mkdir(task1, { recursive: true })
|
||||
await mkdir(task2, { recursive: true })
|
||||
await writeFile(join(task1, 'ui_messages.json'), '[]')
|
||||
await writeFile(join(task2, 'ui_messages.json'), '[]')
|
||||
|
||||
const provider = createRooCodeProvider(tmpDir)
|
||||
const sessions = await provider.discoverSessions()
|
||||
|
||||
expect(sessions).toHaveLength(2)
|
||||
expect(sessions.every(s => s.provider === 'roo-code')).toBe(true)
|
||||
})
|
||||
|
||||
it('skips tasks without ui_messages.json', async () => {
|
||||
const task = join(tmpDir, 'tasks', 'task-no-ui')
|
||||
await mkdir(task, { recursive: true })
|
||||
await writeFile(join(task, 'api_conversation_history.json'), '[]')
|
||||
|
||||
const provider = createRooCodeProvider(tmpDir)
|
||||
const sessions = await provider.discoverSessions()
|
||||
|
||||
expect(sessions).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('returns empty for nonexistent directory', async () => {
|
||||
const provider = createRooCodeProvider('/nonexistent/path')
|
||||
const sessions = await provider.discoverSessions()
|
||||
expect(sessions).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('roo-code provider - metadata', () => {
|
||||
it('has correct name and displayName', () => {
|
||||
expect(rooCode.name).toBe('roo-code')
|
||||
expect(rooCode.displayName).toBe('Roo Code')
|
||||
})
|
||||
|
||||
it('passes through model display names', () => {
|
||||
expect(rooCode.modelDisplayName('claude-sonnet-4-5')).toBe('claude-sonnet-4-5')
|
||||
})
|
||||
|
||||
it('passes through tool display names', () => {
|
||||
expect(rooCode.toolDisplayName('read_file')).toBe('read_file')
|
||||
})
|
||||
})
|
||||
131
tests/retired-providers.test.ts
Normal file
131
tests/retired-providers.test.ts
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import { mkdir, readdir, readFile, rm, writeFile } from 'fs/promises'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
|
||||
import { RETIRED_PROVIDER_NAMES } from '../src/cache-dir.js'
|
||||
import { DAILY_CACHE_VERSION, dailyCachePath, loadDailyCache, type DailyEntry, type ModelDayStats } from '../src/daily-cache.js'
|
||||
import {
|
||||
CACHE_VERSION,
|
||||
clearLoadCacheMemo,
|
||||
computeEnvFingerprint,
|
||||
isCacheDirty,
|
||||
loadCache,
|
||||
markCacheDirty,
|
||||
saveCache,
|
||||
sessionCacheDir,
|
||||
type CachedFile,
|
||||
type SessionCache,
|
||||
} from '../src/session-cache.js'
|
||||
|
||||
const RETIRED = [...RETIRED_PROVIDER_NAMES][0]!
|
||||
let TMP_DIR: string
|
||||
|
||||
beforeEach(async () => {
|
||||
TMP_DIR = join(tmpdir(), `codeburn-retired-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`)
|
||||
process.env['CODEBURN_CACHE_DIR'] = TMP_DIR
|
||||
await mkdir(TMP_DIR, { recursive: true })
|
||||
clearLoadCacheMemo()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
clearLoadCacheMemo()
|
||||
await rm(TMP_DIR, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function file(provider: string, key: string): CachedFile {
|
||||
const timestamp = '2026-05-10T10:00:00.000Z'
|
||||
return {
|
||||
fingerprint: { dev: 1, ino: key.length, mtimeMs: 3, sizeBytes: 4 },
|
||||
lastCompleteLineOffset: 9,
|
||||
mcpInventory: [],
|
||||
turns: [{
|
||||
timestamp,
|
||||
sessionId: key,
|
||||
userMessage: 'go',
|
||||
calls: [{
|
||||
provider,
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
usage: { inputTokens: 10, outputTokens: 5, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, cachedInputTokens: 0, reasoningTokens: 0, webSearchRequests: 0, cacheCreationOneHourTokens: 0 },
|
||||
costUSD: 0.01,
|
||||
speed: 'standard',
|
||||
timestamp,
|
||||
tools: [],
|
||||
bashCommands: [],
|
||||
skills: [],
|
||||
subagentTypes: [],
|
||||
deduplicationKey: `${key}-0`,
|
||||
}],
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
describe('retired providers', () => {
|
||||
it('drops a retired provider from the session cache and deletes its files on the next save', async () => {
|
||||
const cache: SessionCache = {
|
||||
version: CACHE_VERSION,
|
||||
complete: true,
|
||||
providers: {
|
||||
claude: { envFingerprint: computeEnvFingerprint('claude'), files: { '/a.jsonl': file('claude', 'a') } },
|
||||
[RETIRED]: { envFingerprint: 'old', files: { '/r.json': file(RETIRED, 'r') } },
|
||||
},
|
||||
}
|
||||
markCacheDirty(cache, 'claude')
|
||||
markCacheDirty(cache, RETIRED)
|
||||
expect(await saveCache(cache)).toBe(true)
|
||||
clearLoadCacheMemo()
|
||||
const dir = sessionCacheDir()
|
||||
const before = await readdir(dir)
|
||||
|
||||
const loaded = await loadCache()
|
||||
expect(Object.keys(loaded.providers)).toEqual(['claude'])
|
||||
expect(isCacheDirty(loaded)).toBe(true)
|
||||
expect(await saveCache(loaded)).toBe(true)
|
||||
|
||||
const envelope = JSON.parse(await readFile(join(dir, 'envelope.json'), 'utf-8')) as { providers: Record<string, unknown> }
|
||||
expect(Object.keys(envelope.providers)).toEqual(['claude'])
|
||||
const after = await readdir(dir)
|
||||
expect(after.length).toBeLessThan(before.length)
|
||||
for (const name of after) expect(name).not.toContain(RETIRED)
|
||||
clearLoadCacheMemo()
|
||||
expect(Object.keys((await loadCache()).providers.claude!.files)).toEqual(['/a.jsonl'])
|
||||
})
|
||||
|
||||
it('drops retired slices from daily history and the day totals they fed', async () => {
|
||||
const model = (cost: number, calls: number): ModelDayStats => ({ calls, cost, savingsUSD: 0, inputTokens: 10 * calls, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 })
|
||||
const base = { savingsUSD: 0, sessions: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, editTurns: 0, oneShotTurns: 0, categories: {} }
|
||||
const days: DailyEntry[] = [
|
||||
{
|
||||
...base,
|
||||
date: '2026-05-10',
|
||||
cost: 2.5,
|
||||
calls: 3,
|
||||
inputTokens: 30,
|
||||
models: { Sonnet: model(2, 2), Other: model(0.5, 1) },
|
||||
providers: {
|
||||
claude: { cost: 2, calls: 2, savingsUSD: 0, inputTokens: 20, models: { Sonnet: model(2, 2) } },
|
||||
[RETIRED]: { cost: 0.5, calls: 1, savingsUSD: 0, inputTokens: 10, models: { Other: model(0.5, 1) } },
|
||||
},
|
||||
},
|
||||
{
|
||||
...base,
|
||||
date: '2026-05-11',
|
||||
cost: 0.3,
|
||||
calls: 1,
|
||||
inputTokens: 10,
|
||||
models: { Other: model(0.3, 1) },
|
||||
providers: { [RETIRED]: { cost: 0.3, calls: 1, savingsUSD: 0, inputTokens: 10, models: { Other: model(0.3, 1) } } },
|
||||
},
|
||||
]
|
||||
await writeFile(dailyCachePath(), JSON.stringify({ version: DAILY_CACHE_VERSION, savingsConfigHash: '', lastComputedDate: '2026-05-11', days, complete: true }))
|
||||
|
||||
const loaded = await loadDailyCache()
|
||||
expect(loaded.days.map(d => d.date)).toEqual(['2026-05-10'])
|
||||
const day = loaded.days[0]!
|
||||
expect(Object.keys(day.providers)).toEqual(['claude'])
|
||||
expect(day.cost).toBe(2)
|
||||
expect(day.calls).toBe(2)
|
||||
expect(day.inputTokens).toBe(20)
|
||||
expect(Object.keys(day.models)).toEqual(['Sonnet'])
|
||||
})
|
||||
})
|
||||
|
|
@ -63,7 +63,6 @@ const PROVIDER_COLORS: Record<string, string> = {
|
|||
pi: '#B26B3D',
|
||||
qwen: '#615EEB',
|
||||
omp: '#8B5CB0',
|
||||
'roo-code': '#4CAF50',
|
||||
crush: '#E06C9F',
|
||||
antigravity: '#FF7A45',
|
||||
goose: '#B78D52',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue