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 usenode:sqliteto read them. - Test:
tests/providers/crush.test.ts(10 tests, fixture-based)
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.
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,
...
);None at the provider level.
Per crush:<sessionId> (crush.ts).
| 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.
- Timestamps are seconds, not milliseconds. The Crush schema comments in the upstream migration claim millisecond timestamps, but every actual
INSERT/UPDATEininternal/db/sql/{sessions,messages}.sqlusesstrftime('%s', 'now'), which returns Unix seconds. The parser multiplies by 1000 before constructing aDate. 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 NULLare 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. detectLowWorthSessionsmay 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.
- 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 tosessions/messages, never removed any of the ones we read. - 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. - If the bug is "Crush model column shows
unknown", the session has no messages with a non-nullmodel. Some early Crush builds did not record provider on every message; addLIKEmatching againstproviderif you want a stronger fallback. - 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. - New fixtures go under the inline schema in
tests/providers/crush.test.ts; keep theCREATE TABLEliteral and synchronized with the upstream migration.