Project-scoped guidance for any Claude agent working in this repo. Read this first.
A local FastAPI + SQLite + HTMX webapp that gives Jon visibility into his Claude Code sessions:
- Workspace audit (P1) — scans
~/.claude/, classifies entries, allows safe deletion of cruft - Session memory (P2) — FTS5 search over JSONL session files at
~/.claude/projects/, family auto-detection, transcript browsing - Cost watcher — Node ccsniff sidecar tails JSONL → POSTs token usage to
/ingest/usage→/costspage renders live USD totals, model breakdown, day-grouped session cards, Gemini Q&A box
UI is Game Boy DMG-themed (4-shade LCD green, scanlines, "Press Start 2P" + "VT323"). Repo lives at ~/dev/claude-workspace-tool. GitHub: https://github.com/jon-devlapaz/gbc.
gbc # FastAPI on http://127.0.0.1:7878 with --reload
gbc-watch # Node ccsniff sidecar (separate terminal)
pytest # Python tests (from repo root)
cd watcher && npm test # Node testsBoth gbc and gbc-watch are zsh functions in ~/.zshrc. Order doesn't matter — the watcher's retry queue absorbs ECONNREFUSED while FastAPI starts.
~/.claude/projects/*.jsonl ← Claude Code writes these
│
▼ (Node sidecar)
watcher/index.js ──ccsniff──► watcher/parser.js ──HTTP──► /ingest/usage
│
▼
data/workspace.db
▲
│
FastAPI routes
/costs, /sessions, /
Two processes, decoupled by HTTP. Pricing lives only in app/pricing.py. Idempotency via cost_events.message_uuid UNIQUE. Subagent attribution via parent_session_id (path-derived: <projects>/<dir>/<parent>/subagents/agent-*.jsonl).
app/
main.py FastAPI factory; all route definitions
db.py SQLite schema + idempotent migrations
pricing.py RATES dict ($/M tokens) + resolve(model, tier)
cost_ingest.py POST /ingest/usage; UsageEvent pydantic model
cost_query.py Aggregations: today_total, range_total, by_model,
by_session, by_cwd, by_day (subagent rollup)
cost_recompute.py `python -m app.cost_recompute` backfills unknown_pricing rows
cost_qa.py build_snapshot() + ask() — Gemini-powered Q&A over snapshot
sessions_query.py by_day() for session list day-grouping
session_index.py Reindex JSONL → sessions table + prompts_fts
session_reader.py Stream JSONL events for transcript view
fts.py FTS5 search over user prompts
llm.py select_provider() → (name, call_fn). Anthropic + Gemini.
reasoner.py Per-entry purpose-guess workflow
classifier.py Entry classification (kill_candidate, keep, unknown, etc.)
scanner.py Walk ~/.claude/ for the audit
inspector.py Live dir-tree inspector
files.py Safe file read/write/duplicate with whitelist
executor.py Armed-only delete executor
formatting.py Jinja filters: size, age, local_time, local_datetime
templates/ Jinja templates extending base.html
static/style.css DMG palette + all styling
watcher/ Node ESM sidecar
index.js Orchestrator — backfill + ccsniff-driven live loop
parser.js JSONL → usage records (with cwd + path-based parent)
poster.js Batched POST + retry queue + queueCap
state.js Per-file byte offsets at data/.watcher-state.json
tests/ pytest; 33 test files
docs/superpowers/ specs/ + plans/ (design docs)
data/workspace.db Live SQLite (gitignored)
- Game Boy DMG palette in
:rootofstatic/style.css. Use these tokens — never hardcode hex except as a fallback invar(--name, #fallback). - Variables:
--lcd-0(#0f380f darkest "ink"),--lcd-1,--lcd-2,--lcd-3(#9bbc0f screen bg),--bezel(#2a3028),--shell(beige),--led-red(danger). - Fonts:
--font-pixel('Press Start 2P', for headings/labels) and--font-lcd('VT323', for body / numbers). <section>elements containing a<form>get a global:has(form)red treatment ("kill_candidate"). If you add a form to a non-destructive section, override with explicitborder-color/box-shadow.- Cache-busting:
base.htmlreferencesstyle.css?v={{ static_version }}(mtime-based). No action needed unless you serve other static assets.
- Routes use
templates.TemplateResponse(request, "name.html", ctx)— modern Starlette signature, NOT the legacy(name, ctx). - All routes inside
create_app()as closures (so they captureget_db,reasoner_call_fn,templates, etc.). _base_ctx()providesreasoner_enabled,reasoner_provider,static_version— merge with page-specific dicts via_base_ctx() | page_ctx.- Display timezone is America/Chicago by default. Set
os.environ['TZ']at top ofmain.pybefore anydatetimeimport — affects SQLitedate(ts, 'localtime')AND Pythondatetime.now(). Override viaCLAUDE_TOOL_DISPLAY_TZenv var.
- Schema bootstrap in
app/db.pySCHEMAconstant viaexecutescript(CREATE TABLE IF NOT EXISTS only). - Additive migrations beyond CREATE: put in
migrate(conn)function, called fromconnect()afterinit_schema(). - Foreign keys enabled (
PRAGMA foreign_keys = ON). - Cost data:
cost_eventsis loose-joined tosessionsvia textsession_id(no FK — sidecar may ingest before session is indexed). - Timestamps: ISO-8601 UTC with
Zsuffix in storage. Display vialocal_time/local_datetimeJinja filters.
- pytest with
dbfixture intests/conftest.py— fresh in-memory schema for each test. - Test files mirror module names (
test_<module>.py). - Integration tests use
TestClient(create_app())withmonkeypatch.setenv("CLAUDE_TOOL_CLAUDE_ROOT", ...)and("CLAUDE_TOOL_DATA_DIR", ...)so each test gets isolated fake roots. - Real SQLite, not mocks. This is a deliberate choice (mocks lie about migrations).
- Pre-existing
tests/test_wire_nav.pyerrors are Playwright fixture issues unrelated to this codebase — ignore.
- ESM, Node 18+, zero deps except
ccsniff. - ccsniff event payload uses
conversation.file(not.path). The 30s safety re-scan is intentional defense-in-depth in case events are missed. - State file at
data/.watcher-state.jsonis gitignored. Idempotent — safe to delete and rebuild offsets from scratch (backfill will replay;INSERT OR IGNOREonmessage_uuiddrops dupes).
- Read-only over JSONL corpus. Code under
app/session_*.pyandwatcher/never writes to / moves / deletes anything under~/.claude/projects/. Parse + project into SQLite only. - Prompt content stays local.
prompts_ftsis never sent to an LLM. Reasoner only sees entry-purpose metadata, never user prompt text. - Cost Q&A snapshot is bounded.
build_snapshot()returns ~1-2 KB of stats — totals, top-N sessions, model/cwd breakdowns. Never raw prompt content. Don't expand. - Deny-by-default deletion. The audit's
executor.pyonly runs delete whenarmed=trueis explicitly checked. Mock/dry-run mode is the default. - No prompt-text leak into git. Logs, error messages, debug output: never include
first_promptor message content.
- README.md — user-facing run instructions
- docs/superpowers/specs/ — design docs (cost watcher:
2026-04-25-cost-watcher-design.md) - docs/superpowers/plans/ — implementation plans (cost watcher:
2026-04-25-cost-watcher.md)
Add a new model to pricing:
- Add
(model_name, "standard"): _OPUS|_SONNET|_HAIKU(or new template) inapp/pricing.pyRATES python -m app.cost_recomputeto backfillunknown_pricing=1rows- Refresh
/costs— the ⚠ warning disappears
Reset cost data:
sqlite3 data/workspace.db "DELETE FROM cost_events; VACUUM;"
rm data/.watcher-state.json # only if you want to re-backfill old JSONLRe-run watcher backfill from scratch:
rm data/.watcher-state.json
gbc-watchIdempotent — message_uuid UNIQUE drops duplicates.
Add a new aggregation query:
- Stays in
app/cost_query.py - Pure-SQL where possible; CTE rollup pattern for subagent attribution
- Add a test in
tests/test_cost_query.pyusing thedbfixture and synthetic INSERTs
- Zen of Python — explicit > implicit
- One responsibility per module
- Don't add comments that explain WHAT (names should do that). Add a comment for non-obvious WHY.
- Don't write multi-paragraph docstrings; one short summary line is enough.
- Match existing patterns in this repo over generic "best practices."
Read the spec at docs/superpowers/specs/2026-04-25-cost-watcher-design.md before changing cost-tracking surface area. Read app/db.py SCHEMA before changing data shape. Run the full pytest + npm test suite before claiming a feature is done.