codeburn/docs/providers/crush.md
Resham Joshi 4c29f6b880
Add Crush provider plus per-provider icon column in README (#286)
Closes #278.

Adds Charmbracelet Crush as a lazy-loaded provider:
- src/providers/crush.ts: walks ~/.local/share/crush/projects.json
  (XDG_DATA_HOME and CRUSH_GLOBAL_DATA aware), opens each project's
  crush.db read-only, queries root sessions where parent_session_id
  IS NULL. Emits one ParsedProviderCall per session with real
  prompt_tokens, completion_tokens, cost (dollars), and the
  dominant model resolved from messages.model.
- src/providers/index.ts: register crush alongside cursor, goose,
  opencode, antigravity, cursor-agent in the lazy import path.
- tests/providers/crush.test.ts: 10 fixture-based tests covering
  discovery, parsing, missing-registry, malformed JSON, missing db,
  child session exclusion, dominant model selection, dedup, and
  array-shaped legacy registry.

Schema source: charmbracelet/crush@v0.66.1
internal/db/migrations/20250424200609_initial.sql, verified by
spawning a research agent against upstream. The schema *comments*
in that migration claim millisecond timestamps but every actual
INSERT/UPDATE uses strftime('%s', 'now') which returns Unix
seconds; the parser treats values as seconds. Tokscale's
parser (junhoyeo/tokscale#346) gets this wrong and is off by
1000x, plus its parser misses the prompt_tokens/completion_tokens
columns that exist in Crush's schema. Our integration uses both,
so Crush sessions get real per-model attribution.

Menubar:
- mac/Sources/CodeBurnMenubar/AppStore.swift: add .crush case to
  ProviderFilter and its cliArg switch.
- mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift: add
  Crush color to the per-tab color extension. The visibleFilters
  computed property already filters by detected providers, so the
  Crush tab appears automatically when a user has Crush data.

README:
- Replace the provider table with an icon-led layout. Icons live
  under assets/providers/<name>.<ext>. 14 icons sourced from
  junhoyeo/tokscale (MIT) under nominative fair use, 4 sourced
  separately: codex (OpenAI org avatar), cursor-agent (reuses the
  Cursor icon), kiro (kiro.dev favicon, ico->png via sips), omp
  (can1357/oh-my-pi icon.svg, MIT). Attribution line added.
- Add Crush row.

Docs:
- docs/providers/crush.md: full per-provider doc with verified
  schema excerpt, the seconds-vs-milliseconds quirk, and a
  "when fixing a bug here" checklist.
- docs/architecture.md: provider count 17 -> 18, test count
  41 -> 42, and crush in the lazy list.
- docs/providers/README.md: add Crush row to the lazy index.
- CONTRIBUTING.md: bump test count to 568 (was 558).

All 568 tests pass locally; swift build clean.
2026-05-09 20:47:56 -07:00

4.7 KiB

Crush

Charmbracelet's Crush TUI coding agent.

  • Source: src/providers/crush.ts
  • Loading: lazy (src/providers/index.ts). Lazy because Crush ships per-project SQLite databases and we use node:sqlite to read them.
  • Test: tests/providers/crush.test.ts (10 tests, fixture-based)

Where it reads from

Crush keeps a global registry that lists every project it has touched, and a separate SQLite database per project.

File Path
Registry (project list) $CRUSH_GLOBAL_DATA/projects.json, otherwise $XDG_DATA_HOME/crush/projects.json, otherwise ~/.local/share/crush/projects.json (Linux/macOS) or %LOCALAPPDATA%/crush/projects.json (Windows).
Per-project db <project.path>/<project.data_dir>/crush.db where data_dir defaults to .crush.

The registry shape is an object keyed by project id (modern Crush) or an array (older builds and tokscale's sample fixtures). The parser accepts both.

Storage format

SQLite. Schema verified against charmbracelet/crush v0.66.1 (internal/db/migrations/20250424200609_initial.sql plus subsequent additive migrations).

Two tables matter for codeburn:

CREATE TABLE sessions (
  id TEXT PRIMARY KEY,
  parent_session_id TEXT,
  title TEXT NOT NULL,
  message_count INTEGER NOT NULL DEFAULT 0,
  prompt_tokens INTEGER NOT NULL DEFAULT 0,
  completion_tokens INTEGER NOT NULL DEFAULT 0,
  cost REAL NOT NULL DEFAULT 0.0,
  updated_at INTEGER NOT NULL,
  created_at INTEGER NOT NULL,
  ...
);

CREATE TABLE messages (
  id TEXT PRIMARY KEY,
  session_id TEXT NOT NULL,
  role TEXT NOT NULL,
  parts TEXT NOT NULL DEFAULT '[]',
  model TEXT,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL,
  ...
);

Caching

None at the provider level.

Deduplication

Per crush:<sessionId> (crush.ts).

What we extract

codeburn field Crush source
inputTokens sessions.prompt_tokens
outputTokens sessions.completion_tokens
costUSD sessions.cost (already in dollars)
model dominant value of messages.model for the session, picked by GROUP BY model ORDER BY COUNT(*) DESC LIMIT 1. Falls back to unknown.
timestamp sessions.updated_at if set, otherwise created_at

Cache tokens, reasoning tokens, web-search counts, tools, and bash commands are all left as zero / empty. Crush does not record per-message token data, so per-turn attribution is not available.

Quirks worth knowing

  • Timestamps are seconds, not milliseconds. The Crush schema comments in the upstream migration claim millisecond timestamps, but every actual INSERT/UPDATE in internal/db/sql/{sessions,messages}.sql uses strftime('%s', 'now'), which returns Unix seconds. The parser multiplies by 1000 before constructing a Date. Tokscale's parser (junhoyeo/tokscale#346) gets this wrong and is off by 1000x. Confirmed against Crush v0.66.1.
  • Cost is stored in dollars as a REAL. No conversion needed.
  • Child sessions are skipped. Only rows with parent_session_id IS NULL are surfaced. Crush sub-agents inherit cost into the parent.
  • Zero-spend rows are filtered. Discovery skips sessions with cost = 0 AND prompt_tokens = 0 AND completion_tokens = 0.
  • Optimize detectors that depend on tools (detectJunkReads, detectDuplicateReads, detectLowReadEditRatio) will not flag Crush sessions. That is correct: Crush does not log per-tool calls in a way we can read today.
  • detectLowWorthSessions may flag Crush sessions because it looks for cost without edits. That is a known false positive; if it becomes noisy, we can branch the detector on provider.

When fixing a bug here

  1. Confirm the issue against a real Crush install (brew install charmbracelet/tap/crush) before assuming the schema has changed. Migrations in the last six months have only added columns to sessions/messages, never removed any of the ones we read.
  2. If the bug is "Crush sessions show timestamps from 1970-something", check whether someone "fixed" the seconds-vs-milliseconds handling by removing the * 1000. The schema comment is wrong; the data is in seconds.
  3. If the bug is "Crush model column shows unknown", the session has no messages with a non-null model. Some early Crush builds did not record provider on every message; add LIKE matching against provider if you want a stronger fallback.
  4. If the bug is "no sessions discovered", the registry path probably has not been verified for the user's setup. Print getRegistryPath() and have them confirm the file exists at that location.
  5. New fixtures go under the inline schema in tests/providers/crush.test.ts; keep the CREATE TABLE literal and synchronized with the upstream migration.