# toknmtr Self-hosted **Claude Code usage & analytics dashboard**. An **agent** on each of your machines parses Claude Code's JSONL transcripts into a full event log and pushes it to a **server** (one SvelteKit + SQLite container). The server stores everything — usage, tool calls, commands, raw prompts/responses — and serves a dashboard plus a full-text-searchable session archive. It's two halves: - **Server** (`src/`, shipped as a Docker image) — the dashboard, the ingest API, the DB. - **Agent** (`agent/` + `ops/`, runs on each machine) — parses `~/.claude/projects/**/*.jsonl` and POSTs to the server. Not part of the image; you run it wherever you use Claude Code. The server image contains **no data** — the SQLite DB is created empty in a mounted volume on first run. All conversation content only ever comes from *your* agent pushing *your* transcripts to *your* server. > **New here? Follow [DEPLOY.md](DEPLOY.md)** — a full step-by-step walkthrough (prerequisites, > server, agent, security, updating). The sections below are the quick reference. --- ## 1. Run the server (Docker) Requires Docker with the Compose plugin. From the repo root: ```sh cp .env.example .env # edit .env: set API_TOKEN to a long random secret # openssl rand -hex 32 docker compose up -d --build ``` The dashboard is now at **http://localhost:3001**. The DB lives in the `toknmtr-data` Docker volume; the container restarts unless stopped. **Privacy note — the dashboard has no auth.** By default the two surfaces that expose verbatim prompt/response text (the per-session **transcript view** and full-text **search**) are **hidden**; charts, KPIs, and session metadata stay visible. Only set `SHOW_TRANSCRIPTS=true` in `.env` once you've put the dashboard behind auth (reverse proxy, VPN, etc.). Don't expose it to the public internet as-is. ### Config (`.env`) | Var | Purpose | | ------------------ | --------------------------------------------------------------- | | `API_TOKEN` | **Required.** Bearer token the agent must send to `/api/ingest`. | | `SHOW_TRANSCRIPTS` | `true` reveals transcript view + search. Unset = hidden (safe). | | `PORT` | Host port is set in `docker-compose.yml` (`3001:3000`). | --- ## 2. Feeding it data (the agent) The server starts empty. To populate it, run the agent on each machine where you use Claude Code (needs Node 24+ for `--experimental-strip-types`). One-time setup per machine: ```sh mkdir -p ~/.toknmtr cat > ~/.toknmtr/env <<'EOF' TOKNMTR_URL=http://:3001 TOKNMTR_TOKEN= EOF chmod 600 ~/.toknmtr/env ``` Then, from a clone of this repo on that machine: ```sh # one-time: ingest all transcripts already on disk node --experimental-strip-types agent/run.ts --backfill # ongoing capture — register the Stop hook so every turn pushes incrementally ops/install-hook.sh ``` `ops/install-hook.sh` adds a near-zero-cost Claude Code `Stop` hook (additive + reversible with `--remove`). An optional `ops/install-cron.sh` adds a reconcile sweep as a safety net. Full agent/capture docs — the `~/.toknmtr/env` format, the hook-vs-cron tradeoff, backfill, and troubleshooting — are in **[`ops/README.md`](ops/README.md)**. Ingest is an idempotent upsert (keyed on `host + session_id + uuid`), so re-running the backfill or overlapping hook/cron sweeps never duplicates data. --- ## 3. Development Node 24, SvelteKit 2 (Svelte 5), TypeScript, `better-sqlite3`. ```sh npm install npm run dev # dev server npm run check # typecheck npm run build # production build → build/ (run with `node build`) npm run lint # prettier + eslint ``` Pricing lives server-side in `src/lib/server/pricing.ts` — adding a model is a one-line update. Because the subscription is flat-rate, all `$` figures are *notional* (API-equivalent).