Phased build order. Each phase produces something runnable and useful on its own; later phases enrich earlier ones rather than depending on a big-bang integration.
Per AGENTS.md / CLAUDE.md, this project follows TDD. Every phase below is "done"
only when:
- Unit tests cover the logic introduced in that phase, written before or alongside the implementation.
- Integration tests exercise any new HTTP endpoint end-to-end (handler → DB → response).
- The full suite (
go test ./...) passes locally and in CI. - New schema migrations are tested both forward and on a populated DB.
Per-phase notes below call out the specific things to cover; assume the general expectation above also applies even when not repeated.
go.mod, basic directory layout perREADME.md.- A no-op
cmd/trayappthat opens a tray icon and exits cleanly. - A no-op
cmd/clusage-clithat prints version. - CI:
go vet,go test, cross-compile both binaries.
Exit criteria: trayapp.exe runs, shows a tray icon, quits cleanly. clusage-cli ping
returns connection refused against an unreachable host (no panics).
- SQLite schema + migrations for
usage_eventsandparse_errors. POST /logHTTP handler with validation.POST /parse_errorHTTP handler.clusage-cli logMode A only (explicit flags).- Manual smoke test: from a container, post fake events to the host; verify rows in DB.
Exit criteria: a container can register a usage event and the row appears in
usage.db on the host.
A shared JSONL parser feeds both the host tailer and the container Stop hook. Both
land in usage_events and dedup against (session_id, message_id).
- Internal
transcript-parserpackage: takes a path or stream, yields events. - Host tailer:
fsnotifywatcher on~/.claude/projects/, offset persistence. - Container path:
clusage-cli log --from-hookreads stdin JSON, locatestranscript_path, runs the same parser, POSTs each event to/log. - Stop-hook example documented for
~/.claude/settings.json.
Test focus: a corpus of recorded transcript JSONL files (committed under
testdata/) exercises the parser against representative shapes and known schema
quirks; offset persistence is tested by simulating restart mid-file; the dedup
constraint is verified by replaying the same events twice and asserting one row.
Exit criteria: (a) a host Claude Code session produces usage_events rows with no
manual posting; (b) a container with the Stop hook installed produces usage_events
rows on the host with no ~/.claude mount.
POST /snapshotHTTP handler.quota_snapshotstable.windowstable population: detect session window starts from first event after a gap, set baselines from the closest snapshot.- Userscript v1: read DOM, post snapshot, debounce.
Exit criteria: opening claude.ai with the userscript installed creates quota_snapshots
rows; windows rows correctly bracket recent activity.
- Static HTML/JS served from trayapp.
- Two burn-down charts (session and weekly).
GET /consumptionendpoint and the consumption widget that consumes it (USD + percent-of-session + percent-of-weekly over a chosen period).- Health/status panel: last snapshot age, parse error count.
Exit criteria: opening http://localhost:27812 shows charts that match observed reality.
GET /slackperdocs/slack-indicator.md.POST /slack/releasefor queues to record consumed slack.clusage-cli slacksubcommand.- Optional
slack_samplestime-series;slack_releasesalways on. - Document the queue contract in
docs/slack-indicator.md(already drafted).
Test focus: table-driven tests for the slack math (clamp boundaries, both windows,
combined min); each gate (session/weekly headroom, baseline freshness, not-paused)
tested in isolation and in combination; window-not-started returns null fraction,
not 0.
Exit criteria: a queue script can poll slack --format release-bool and get sane
release decisions in synthetic and real tests.
- Tray icon color states.
install.ps1for autostart (price table is embedded, no config bootstrap needed).- Userscript auto-update headers.
- README updated with screenshots.
Exit criteria: a fresh user can install in <10 minutes from the README alone.
- Tier 3 headless scrape. Only if Tier 1 + Tier 2 prove insufficient in real use.
- Multi-host / Cloudflare tunnel. Mostly landed:
uplink.urlforwards one trayapp's events to another (seedocs/configuration.md), which covers the same-account VM case, and every non-loopback caller must present the receiver's access token (seedocs/architecture.md, "Network and security"). Still deferred is transport security — cloudflared or TLS — for anything beyond a trusted local network, since the token travels as plain HTTP. - Forecasting. Replace uniform
E(t)with a learned curve from trailing windows. - Job runner. A built-in queue that consumes the slack signal directly. The current design intentionally exposes only the signal; a runner is a separate project.
- Slack release reconciliation.
POST /slack/release/<id>/completewithactual_cost, plus aslack_releases.actual_costcolumn, so we can compare estimates to actuals and tune the per-job budget cap. - CLI offset file.
clusage-cli log --from-hookwould consult an offset file in$XDG_STATE_HOME/clusageinstead of re-POSTing the full transcript every turn. Saves bandwidth in long sessions; not needed for v1 since the server dedupes. - Per-project breakdown. Use
project_pathon events to chart cost per project. - Export. CSV/Parquet dump of
usage_eventsfor external analysis. - Calibrated quota-cost model. Regress
quota_snapshotspercent-deltas against theusage_eventstokens (by model) that fell between each snapshot pair, to solve for "% of session/weekly quota per token, per model." Both data streams already exist; this finally bridges the two unconnected currencies the dashboard tracks (Anthropic's opaque "% used" and our owncost_usd_equivalent). Outputs: (a) a concrete API-rate-equivalent value vs subscription cost ratio, (b) snapshot-free burn-down forecasts, (c) automatic price-table drift detection when Anthropic changes rates, (d) per-project quota attribution without a published price. Tradeoff: needs enough snapshot density to be well-conditioned, and the regression has to be defended against the userscript's integer-percent rounding (~1% resolution per snapshot — fine across many pairs, lossy on any single one). - Local MCP server. Expose the trayapp's signals (
get_slack,get_burn_rate,get_consumption) as an MCP server bound to127.0.0.1, so Claude Code itself can query its own remaining quota mid-session and self-throttle or defer expensive sub-tasks. Closes the measurement loop the rest of the project opens. More speculative than the calibration model — value depends on users wiring agent behaviors around the signal — but architecturally cheap (the data is already served over HTTP; an MCP wrapper is a thin adapter). Auth/trust story is the same as the rest of the API: localhost-only, no secrets.
- A hosted multi-tenant version.
- Authentication on the local HTTP API. (Trust boundary is the host.)
- Replacing
clusage. Different goals;clusagestays useful for ad-hoc reads. - Reconciling against Anthropic invoices. Out of scope.