{"_id":"@benvargas/tokdash","_rev":"4-a8b9ae8c377241e088301f71b045231a","name":"@benvargas/tokdash","dist-tags":{"latest":"0.2.2"},"versions":{"0.1.0":{"name":"@benvargas/tokdash","version":"0.1.0","keywords":["ccusage","claude-code","ai-usage","dashboard","cost-tracking","ssh","bun"],"license":"MIT","_id":"@benvargas/tokdash@0.1.0","maintainers":[{"name":"benvargas","email":"ben@vargas.com"}],"homepage":"https://github.com/ben-vargas/tokdash#readme","bugs":{"url":"https://github.com/ben-vargas/tokdash/issues"},"bin":{"tokdash":"bin/tokdash.js"},"dist":{"shasum":"ecffcdfefc6995370698558a95ae64f71505c0b1","tarball":"https://registry.npmjs.org/@benvargas/tokdash/-/tokdash-0.1.0.tgz","fileCount":24,"integrity":"sha512-jAAqUVn04UN83Jigw/6iev/wp3v28kD2koO4vLae6qIDr+S5QvC22aaEuEZZ9hF3wV6hgTTo9oRXwoKWnhuzKQ==","signatures":[{"sig":"MEYCIQDQkRGw3lJdiKWt+F84J2U7A/HhMqh0eh+Gv5VYvvSGnQIhAOfMeXLhF+AJlSBfrvr6n7TUT7Cfyhuqtou7Adeqs/VE","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":898496},"type":"module","engines":{"bun":">=1.3.0"},"gitHead":"94fec3731357cffda9b0018a125635b86ecbc5f6","scripts":{"dev":"bun scripts/dev.ts","test":"bun test","build":"vite build","start":"bun src/server/index.ts","prepublishOnly":"bun run build && bun test"},"_npmUser":{"name":"benvargas","email":"ben@vargas.com"},"repository":{"url":"git+https://github.com/ben-vargas/tokdash.git","type":"git"},"_npmVersion":"11.9.0","description":"Multi-host ccusage dashboard — aggregate AI coding-agent usage and cost across machines over SSH","directories":{},"_nodeVersion":"24.14.0","dependencies":{"zod":"^4.4.3","hono":"^4.12.27"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.1.3","react":"^19.2.7","recharts":"^3.9.1","react-dom":"^19.2.7","@types/bun":"^1.3.14","typescript":"^6.0.3","tailwindcss":"^4.3.2","@types/react":"^19.2.17","@types/react-dom":"^19.2.3","@tailwindcss/vite":"^4.3.2","@vitejs/plugin-react":"^6.0.3","@tanstack/react-query":"^5.101.2"},"_npmOperationalInternal":{"tmp":"tmp/tokdash_0.1.0_1783719372747_0.7373667686459116","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@benvargas/tokdash","version":"0.2.0","keywords":["ccusage","claude-code","ai-usage","dashboard","cost-tracking","ssh","bun"],"license":"MIT","_id":"@benvargas/tokdash@0.2.0","maintainers":[{"name":"benvargas","email":"ben@vargas.com"}],"homepage":"https://github.com/ben-vargas/tokdash#readme","bugs":{"url":"https://github.com/ben-vargas/tokdash/issues"},"bin":{"tokdash":"bin/tokdash.js"},"dist":{"shasum":"cfe8a8d0c7d51ad5afe8c003b1061bfdfaf61682","tarball":"https://registry.npmjs.org/@benvargas/tokdash/-/tokdash-0.2.0.tgz","fileCount":24,"integrity":"sha512-7bdiKaJreiz4dULhNyfYcqutlgKjTnZbR7merRXnrBwu1Mxw1zUzZGrBdbEUrn/OeKeQ3Sy0xgTZ9uRYdg7d+Q==","signatures":[{"sig":"MEUCIQDzvV7jCnsz8ci6ohSqmGnyrVb4fFHEzBwb6r9UrT+A5gIgJCox4YeQNp63bQE0+NDcpZ5ODqPdyK80vuo4TJGpJcE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":899879},"type":"module","engines":{"bun":">=1.3.0"},"gitHead":"73d7b98dc005474a5322d9a505c24149e11a46df","scripts":{"dev":"bun scripts/dev.ts","test":"bun test","build":"vite build","start":"bun src/server/index.ts","prepublishOnly":"bun run build && bun test"},"_npmUser":{"name":"benvargas","email":"ben@vargas.com"},"repository":{"url":"git+https://github.com/ben-vargas/tokdash.git","type":"git"},"_npmVersion":"11.9.0","description":"Multi-host ccusage dashboard — aggregate AI coding-agent usage and cost across machines over SSH","directories":{},"_nodeVersion":"24.14.0","dependencies":{"zod":"^4.4.3","hono":"^4.12.27"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.1.3","react":"^19.2.7","recharts":"^3.9.1","react-dom":"^19.2.7","@types/bun":"^1.3.14","typescript":"^6.0.3","tailwindcss":"^4.3.2","@types/react":"^19.2.17","@types/react-dom":"^19.2.3","@tailwindcss/vite":"^4.3.2","@vitejs/plugin-react":"^6.0.3","@tanstack/react-query":"^5.101.2"},"_npmOperationalInternal":{"tmp":"tmp/tokdash_0.2.0_1783720710845_0.3525562772321815","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@benvargas/tokdash","version":"0.2.1","keywords":["ccusage","claude-code","ai-usage","dashboard","cost-tracking","ssh","bun"],"license":"MIT","_id":"@benvargas/tokdash@0.2.1","maintainers":[{"name":"benvargas","email":"ben@vargas.com"}],"homepage":"https://github.com/ben-vargas/tokdash#readme","bugs":{"url":"https://github.com/ben-vargas/tokdash/issues"},"bin":{"tokdash":"bin/tokdash.js"},"dist":{"shasum":"30a1c584f523037e6836809b7ea738a767c790e4","tarball":"https://registry.npmjs.org/@benvargas/tokdash/-/tokdash-0.2.1.tgz","fileCount":24,"integrity":"sha512-YTojJan5dNAnn7udJ/DBTpaG9sfBWNix7hoXYFgWBYSTPHto4sjVCyywgIZIak7CyNe9ogatDp1D8ovwrcl8eA==","signatures":[{"sig":"MEUCIQDZgML4KH7/MPHvBna+BDy2e5Rua7szLaK2O7Rehqw8RQIgI3WcohQoASzRR0HRPB17qxISg9oS4UqX1wf/LV1rGEs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":900307},"type":"module","engines":{"bun":">=1.3.0"},"gitHead":"5a324d9130a5ab56b6317ebceaac9744a6dea132","scripts":{"dev":"bun scripts/dev.ts","test":"bun test","build":"vite build","start":"bun src/server/index.ts","prepublishOnly":"bun run build && bun test"},"_npmUser":{"name":"benvargas","email":"ben@vargas.com"},"repository":{"url":"git+https://github.com/ben-vargas/tokdash.git","type":"git"},"_npmVersion":"11.9.0","description":"Multi-host ccusage dashboard — aggregate AI coding-agent usage and cost across machines over SSH","directories":{},"_nodeVersion":"24.14.0","dependencies":{"zod":"^4.4.3","hono":"^4.12.27"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^8.1.3","react":"^19.2.7","recharts":"^3.9.1","react-dom":"^19.2.7","@types/bun":"^1.3.14","typescript":"^6.0.3","tailwindcss":"^4.3.2","@types/react":"^19.2.17","@types/react-dom":"^19.2.3","@tailwindcss/vite":"^4.3.2","@vitejs/plugin-react":"^6.0.3","@tanstack/react-query":"^5.101.2"},"_npmOperationalInternal":{"tmp":"tmp/tokdash_0.2.1_1783798699159_0.13178056212573952","host":"s3://npm-registry-packages-npm-production"}},"0.2.2":{"name":"@benvargas/tokdash","version":"0.2.2","description":"Multi-host ccusage dashboard — aggregate AI coding-agent usage and cost across machines over SSH","license":"MIT","repository":{"type":"git","url":"git+https://github.com/ben-vargas/tokdash.git"},"homepage":"https://github.com/ben-vargas/tokdash#readme","bugs":{"url":"https://github.com/ben-vargas/tokdash/issues"},"keywords":["ccusage","claude-code","ai-usage","dashboard","cost-tracking","ssh","bun"],"bin":{"tokdash":"bin/tokdash.js"},"type":"module","scripts":{"dev":"bun scripts/dev.ts","build":"vite build","start":"bun src/server/index.ts","test":"bun test","prepublishOnly":"bun run build && bun test"},"engines":{"bun":">=1.3.0"},"dependencies":{"hono":"^4.12.27","zod":"^4.4.3"},"devDependencies":{"@tanstack/react-query":"^5.101.2","@tailwindcss/vite":"^4.3.2","@types/bun":"^1.3.14","@types/react":"^19.2.17","@types/react-dom":"^19.2.3","@vitejs/plugin-react":"^6.0.3","react":"^19.2.7","react-dom":"^19.2.7","recharts":"^3.9.1","tailwindcss":"^4.3.2","typescript":"^6.0.3","vite":"^8.1.3"},"gitHead":"81f2fbd725d42dbd6c5936cc3284b90ea270e3fa","_id":"@benvargas/tokdash@0.2.2","_nodeVersion":"24.14.0","_npmVersion":"11.9.0","dist":{"integrity":"sha512-SbN7wKp5M6ihoz+UvCH7S933mCy9wvnPOJSPfQSWw7Np7zJEUENbBWu4IZLAx9aTSyNwJOeXZMmZr1ekcV/E/Q==","shasum":"a7bd7daf841186dabc355d255a69fe765b045a94","tarball":"https://registry.npmjs.org/@benvargas/tokdash/-/tokdash-0.2.2.tgz","fileCount":25,"unpackedSize":901031,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDf5Kw73ijgPWZXXkkP1Bntb47a3A/qo9fx8B3r/GCjEAiEAqho6yMxXvrLfPEHSRfeGWAcjvdvPwLqMrcPRiVL5Mto="}]},"_npmUser":{"name":"benvargas","email":"ben@vargas.com"},"directories":{},"maintainers":[{"name":"benvargas","email":"ben@vargas.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/tokdash_0.2.2_1785868555038_0.9762979264590244"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-10T21:36:12.575Z","modified":"2026-08-04T18:35:55.605Z","0.1.0":"2026-07-10T21:36:12.937Z","0.2.0":"2026-07-10T21:58:31.128Z","0.2.1":"2026-07-11T19:38:19.339Z","0.2.2":"2026-08-04T18:35:55.227Z"},"bugs":{"url":"https://github.com/ben-vargas/tokdash/issues"},"license":"MIT","homepage":"https://github.com/ben-vargas/tokdash#readme","keywords":["ccusage","claude-code","ai-usage","dashboard","cost-tracking","ssh","bun"],"repository":{"type":"git","url":"git+https://github.com/ben-vargas/tokdash.git"},"description":"Multi-host ccusage dashboard — aggregate AI coding-agent usage and cost across machines over SSH","maintainers":[{"name":"benvargas","email":"ben@vargas.com"}],"readme":"# TokDash\n\nA local dashboard that aggregates [`ccusage`](https://github.com/ryoppippi/ccusage) v20 coding-agent\nusage and cost across all of your machines — with first-class dark and light themes. Instead of manually running\n`bunx ccusage@latest` on each host and combining the numbers in your head, TokDash runs one unified\n`ccusage --json` fetch per host (locally or over SSH), normalizes the output, merges every host into a\nsingle model, and renders combined and per-host statistics with instant date-range / host / harness\nfiltering. It is a single-user, localhost-only tool — no auth, no database, no deploy.\n\n![TokDash dashboard, dark theme](https://raw.githubusercontent.com/ben-vargas/tokdash/main/screenshots/desktop-dark.png)\n\nCost is the hero everywhere; tokens are the supporting act. Each host and harness keeps one consistent\ncolor across every chart, chip, table, and status dot.\n\n## Features\n\n- **Multi-host aggregation.** One dashboard for as many machines as you like — local and remote — with\n  cross-host totals and per-host breakdowns.\n- **One fetch per host.** A single `ccusage daily --json --sections daily,monthly,session --by-agent`\n  invocation per refresh yields totals, monthly history, sessions, and exact per-harness/per-model\n  slices in one round trip.\n- **Runs anywhere ccusage runs.** Point each host at `bunx`, a full-path `bunx`, or nvm-managed\n  `npx` — whatever that machine has. Remote hosts are reached over plain SSH.\n- **Stale-while-revalidate.** Cached snapshots render instantly on startup; refreshes happen in the\n  background, on demand or on an interval, and a failing host degrades to its last good snapshot\n  instead of blanking the page.\n- **Instant filtering.** Date range, host, and harness filters re-aggregate server-side from cache —\n  no SSH round trip per filter change.\n- **In-app configuration.** Add and edit hosts, test connections, and change settings from the UI; the\n  config file is also plain JSON you can hand-edit.\n- **Extra sources.** Fold a pi-format harness's session store (e.g. oh-my-pi) into a host's report\n  alongside its native agents.\n- **Themed and responsive.** Dark and light themes, and a layout that collapses cleanly down to phone\n  widths.\n\n## Requirements\n\n- [Bun](https://bun.sh) (the app runs on Bun; no separate Node install is needed for TokDash itself).\n- `ccusage` reachable on each host — via `bunx`/`npx`, so nothing to pre-install globally.\n- For remote hosts: SSH with **key-based auth** configured as an alias in `~/.ssh/config`.\n- macOS or Linux hosts. (Windows hosts are a non-goal.)\n\n## Quick start\n\n### Install\n\nThe installed CLI keeps its config in `~/.config/tokdash/config.json` and snapshots in\n`~/.cache/tokdash/snapshots`. The config is created by onboarding when you first save from the\nsettings dialog or API.\n\n```bash\nbunx @benvargas/tokdash\n```\n\nOr install it globally with npm:\n\n```bash\nnpm install -g @benvargas/tokdash\ntokdash\n```\n\nBun must be installed for either method. The npm package includes a small Node-compatible shim that\nspawns the Bun server. MOCK demo mode requires a full git clone because its dev-only `fixtures/`\ndirectory is not included in the published npm package.\n\n### From source\n\n```bash\ngit clone <repo-url> tokdash\ncd tokdash\nbun install\n\ncp tokdash.config.example.json tokdash.config.json\n# edit tokdash.config.json — set your timezone and hosts (see Configuration below)\n\nbun run dev\n```\n\n`bun run dev` starts Vite on **http://localhost:5173** (proxying `/api` to the API server) and the API\nserver on **http://127.0.0.1:4114**, in one command.\n\n**Zero-config start.** You do not actually need a config file to begin. Start the app with no\n`tokdash.config.json` at all and add your first host entirely through the settings UI (gear icon →\n**Add host**), testing the connection before you save. Copying the example is just a faster way to get\na couple of hosts in place.\n\n```bash\n# Production: build the frontend, then serve it + the API from one Bun server\nbun run build\nbun start                 # http://127.0.0.1:4114\n\n# Demo / offline mode: replay committed fixtures — no config of your own, no SSH, near-zero latency\nMOCK=1 TOKDASH_CONFIG=tokdash.config.example.json bun start\n\n# Tests\nbun test\n```\n\n`bun start` serves the built `dist/` and the API together at **http://127.0.0.1:4114**. The server\nbinds to `127.0.0.1` only.\n\nOn first start the server serves whatever is in the snapshot cache immediately, then kicks a background\nrefresh if any enabled host has no snapshot or a stale one (stale-while-revalidate). It never blocks\nstartup on the network.\n\n## Screenshots\n\n| | |\n|---|---|\n| ![Light theme](https://raw.githubusercontent.com/ben-vargas/tokdash/main/screenshots/desktop-light.png) | ![Mobile, 390px](https://raw.githubusercontent.com/ben-vargas/tokdash/main/screenshots/mobile-dark.png) |\n| Light theme (warm paper, same tokens) | Responsive collapse at 390px |\n\n![Host error state](https://raw.githubusercontent.com/ben-vargas/tokdash/main/screenshots/host-error-state.png)\n\nA host that fails to refresh (here an unreachable SSH alias) degrades to its last cached snapshot with\na visible error dot and a tooltip carrying the real reason and exit code — the rest of the dashboard\nrenders normally.\n\n![Hermes-only, custom range](https://raw.githubusercontent.com/ben-vargas/tokdash/main/screenshots/filters-hermes.png)\n\nFiltering to a single harness over a custom range: a harness that lives on only one host shows `$0.00`\nfor the others; unified agent slices provide its real per-model costs.\n\n## Configuration\n\nThe config path resolves in order from `TOKDASH_CONFIG`, then `./tokdash.config.json` when that file\nexists, then `$XDG_CONFIG_HOME/tokdash/config.json` (`~/.config/tokdash/config.json` when\n`XDG_CONFIG_HOME` is unset). Config is validated with zod. Everything below is also editable from the\nsettings UI (gear icon) — the file and the UI are two views of the same document. Start from\n`tokdash.config.example.json`, which demonstrates the three common host shapes:\n\n```json\n{\n  \"timezone\": \"America/New_York\",\n  \"fetchWindowDays\": 90,\n  \"refreshIntervalMinutes\": 5,\n  \"hosts\": [\n    {\n      \"id\": \"laptop\",\n      \"label\": \"Laptop (local)\",\n      \"color\": \"#7c8cf8\",\n      \"enabled\": true,\n      \"ssh\": null,\n      \"ccusageCmd\": \"bunx ccusage@latest\",\n      \"extraSources\": [\n        {\n          \"type\": \"pi-jsonl\",\n          \"agent\": \"omp\",\n          \"path\": \"~/.omp/agent/sessions\"\n        }\n      ]\n    },\n    {\n      \"id\": \"workstation\",\n      \"label\": \"Workstation (bun, via SSH)\",\n      \"color\": \"#4ec9b0\",\n      \"enabled\": true,\n      \"ssh\": \"workstation\",\n      \"ccusageCmd\": \"~/.bun/bin/bunx ccusage@latest\"\n    },\n    {\n      \"id\": \"buildbox\",\n      \"label\": \"Build box (nvm node, via SSH)\",\n      \"color\": \"#e8a951\",\n      \"enabled\": true,\n      \"ssh\": \"buildbox\",\n      \"ccusageCmd\": \"PATH=\\\"$HOME/.nvm/versions/node/$(ls $HOME/.nvm/versions/node | sort -V | tail -1)/bin:$PATH\\\" npx -y ccusage@latest\"\n    }\n  ]\n}\n```\n\n### Top-level fields\n\n| Field | Type | Constraints | Meaning |\n|---|---|---|---|\n| `timezone` | IANA string | must be a real IANA zone (validated against `Intl`, not just a regex) | Report timezone. Passed as `-z <tz>` to every `ccusage` call so day boundaries agree across hosts. |\n| `fetchWindowDays` | integer | 1–3660 | Trailing window fetched per refresh (`--since today − N`, `--until today`). |\n| `refreshIntervalMinutes` | integer | 1–1440 | Auto-refresh interval, and the age past which a snapshot is considered `stale`. |\n| `hosts[]` | array | — | One entry per machine. |\n\n### Host fields\n\n| Field | Type | Constraints | Meaning |\n|---|---|---|---|\n| `id` | string | 1–64 chars, **unique** across hosts | Stable identifier; used in `.cache/snapshots/<id>.json`, API params, and URLs. |\n| `label` | string | non-empty | Display name in chips, tables, and legends. |\n| `color` | hex string | `#rrggbb` | This host's series color everywhere (chips, bars, lines, table swatches, freshness dots). Used verbatim in both themes. |\n| `enabled` | boolean | — | Disabled hosts are skipped by refresh; their chip renders greyed and non-interactive. |\n| `ssh` | string \\| null | non-empty when set | SSH alias from `~/.ssh/config`. `null` runs the command locally with no SSH. |\n| `ccusageCmd` | string | non-empty | Shell prefix used to invoke ccusage on that host (see [Remote hosts & PATH](#remote-hosts--path)). |\n| `extraSources[]` | array | optional | Named pi-format stores folded into that host's one unified report (see below). |\n\n### `extraSources` entries\n\nEach entry adds a pi-format harness's session store to a host's report:\n\n| Field | Type | Constraints |\n|---|---|---|\n| `type` | string | must be the literal `\"pi-jsonl\"` |\n| `agent` | string | ccusage store-name grammar; **not** a reserved name (a built-in harness, `all`, or `pi`); unique per host |\n| `path` | string | non-empty, no control characters; unique and non-overlapping with other sources on the host; `~` and `$HOME` are fine (they expand on the host) |\n\nTokDash writes all of a host's `extraSources` into a temporary ccusage config and passes `--config` on\nthat host's single invocation. A missing path is silently absent. Store prefixes such as `[pi] ` and\n`[omp] ` are stripped from model labels.\n\n### Environment variables\n\n| Var | Default | Effect |\n|---|---|---|\n| `TOKDASH_CONFIG` | Cwd file if present, otherwise `$XDG_CONFIG_HOME/tokdash/config.json` | Path to the config file. |\n| `PORT` | `4114` | API / production server port (always bound to `127.0.0.1`). |\n| `TOKDASH_CACHE_DIR` | Cwd `.cache/snapshots` for config overrides; otherwise `$XDG_CACHE_HOME/tokdash/snapshots` | Where per-host snapshots are read/written (`snapshots-mock` under `MOCK=1`). |\n| `TOKDASH_AUTOREFRESH` | (unset) | Set to `0` to pause the periodic auto-refresh **scheduler** (a one-time startup refresh may still fire for a stale cache). |\n| `MOCK` | (unset) | Set to `1` to replay `fixtures/real/<hostId>/` instead of shelling out. |\n\n`XDG_CONFIG_HOME` and `XDG_CACHE_HOME` default to `~/.config` and `~/.cache`, respectively.\n\nThe config file is **re-read fresh on every API request** — it is tiny, so hand edits (add a host, flip\n`enabled`, change a color) take effect on the next request with no restart. `PUT /api/config` validates\nthe whole document, then rewrites it atomically (temp file + rename).\n\n## Adding a host\n\n**From the UI:** open Settings (gear, top-right) → **Add host**. Fill in label, id, color, SSH alias\n(leave empty for a local host), and the ccusage command. Click **Test connection** to verify before\nsaving. Save writes the config through `PUT /api/config`; no restart is needed.\n\n**By hand:** add an object to `hosts[]` in `tokdash.config.json` and save. The next request picks it\nup; the next refresh fetches it.\n\n**What \"Test connection\" reports.** It runs `<ccusageCmd> --version` on the host and reports:\n\n- **Success:** the ccusage version (e.g. `ccusage 20.0.16`), the round-trip in ms, and the agents\n  detected on that host (from its most recent snapshot).\n- **Failure:** the exit code mapped to a human message, plus the verbatim `stderr` tail so you can see\n  exactly what the remote shell said. Exit-code mapping: `255` → SSH connection failed, `127` →\n  command not found (check PATH), `2` → ccusage rejected arguments, timeout → timed out.\n\n## Remote hosts & PATH\n\nThe single hardest part of multi-host ccusage is that **non-interactive SSH does not load your login\nshell environment** — it never sources `~/.zshrc` / `~/.bash_profile`. A command that works when you\nSSH in interactively can exit **127 (command not found)** when TokDash runs it non-interactively, and\n`zsh -lc` does **not** reliably fix it. The remedy is to make each host's `ccusageCmd` fully explicit\nabout where its runtime lives. Two tested recipes cover almost every host:\n\n- **Hosts with bun** — use the **full path** to `bunx` instead of a bare `bunx`:\n  ```\n  ~/.bun/bin/bunx ccusage@latest\n  ```\n  A plain `bunx ccusage@latest` over SSH fails with exit 127 because bun's bin dir is only added to\n  `PATH` in your interactive shell rc.\n\n- **Hosts without bun (nvm-managed node)** — run `npx -y` from the nvm node bin dir with an explicit\n  `PATH` prepend:\n  ```\n  PATH=\"$HOME/.nvm/versions/node/$(ls $HOME/.nvm/versions/node | sort -V | tail -1)/bin:$PATH\" npx -y ccusage@latest\n  ```\n  Keep this **single-quoted** in the JSON string / SSH invocation so `$HOME` and the `$(...)` subshell\n  expand on the **remote** machine, not locally. The `ls … | sort -V | tail -1` form picks the newest\n  installed node version, so it survives nvm upgrades.\n\n**Debugging a host.** Test the exact command as SSH will run it:\n\n```bash\nssh <alias> '<ccusageCmd> --version'\n```\n\nIf that prints a ccusage version, TokDash will work; if it exits 127, it is a PATH problem — make the\ncommand more explicit as above. The in-app **Test connection** button does exactly this round trip and\nsurfaces the exit code and stderr tail for you.\n\n**SSH setup.** The fetcher always calls SSH with `-o BatchMode=yes` (never prompt for a password — a\nprompt would hang) and `-o ConnectTimeout=10`, so set up key-based auth for each alias in\n`~/.ssh/config` first.\n\n**Clean streams.** `bunx` prints \"Resolving dependencies…\" and npm prints \"npm notice\" nags to\n**stderr** on every run. TokDash captures stdout and stderr **separately** (it never uses `2>&1`, which\nwould corrupt `JSON.parse`); the stderr tail is kept for diagnostics and shown in Test connection and\nerror tooltips.\n\nA host failure never takes down the dashboard or discards that host's last good snapshot: the host\ndegrades to its cached data with a visible error state, and everything else renders.\n\n## Security model\n\n- **Localhost only.** The server binds `127.0.0.1` exclusively. It has no authentication and is not\n  designed for exposure to untrusted networks. Do not put it behind a public reverse proxy or bind it\n  to `0.0.0.0`.\n- **The config file is a trust boundary.** `ccusageCmd` is executed **as a shell command** on the\n  local machine or the remote host, so `tokdash.config.json` is as sensitive as a shell script —\n  anyone who can edit it (or reach the localhost API that writes it) can run commands as you. Treat it\n  accordingly.\n- **No free-text interpolation into commands.** TokDash never splices arbitrary strings into the\n  ccusage invocation: beyond your `ccusageCmd`, it appends only a fixed set of subcommands, dates\n  matching `^\\d{4}-\\d{2}-\\d{2}$`, and a timezone matching a strict character class.\n\n## How it works\n\n**Fetch strategy (per refresh, hosts in parallel).** Over the config's trailing window\n(`--since today − fetchWindowDays --until today -z <tz>`), for each enabled host:\n\n1. TokDash runs exactly one command:\n   `ccusage daily --json --sections daily,monthly,session --by-agent -s <from> -u <until> -z <tz>`.\n   Hosts with `extraSources` get one temporary config file and the same invocation adds `--config`.\n2. The envelope's top-level daily/monthly/session rows become host totals, monthly data, and sessions;\n   each daily row's `agents` slices become the exact per-harness daily series and model breakdowns.\n   Malformed JSON or an invalid envelope degrades the whole host to its last good snapshot.\n3. The raw envelope + normalized snapshot is written atomically to `.cache/snapshots/<hostId>.json`\n   with `fetchedAt`, per-command durations, and a stderr tail.\n\n**Normalization.** The unified envelope and every row/slice are validated with permissive zod schemas.\nBad individual rows are skipped with warnings; session metadata may be null. Named-store model prefixes\nare stripped, and collisions after stripping are merged by summing their costs/tokens.\n\n**Cache & refresh.** Snapshots render instantly on startup (stale-while-revalidate); the UI shows\nper-host freshness (`fresh` / `stale (2h ago)` / `error: <reason>`) and refreshes in the background —\non demand via the Refresh button, and periodically at `refreshIntervalMinutes`. Refreshes are\n**single-flight**: a refresh requested while one is running joins the in-flight one instead of starting\na second.\n\n**One aggregation path.** Filter changes never trigger SSH. On every filter change the UI re-queries\n`GET /api/usage`, which the server answers from cached snapshots via a single server-side aggregation\npath (`mergeHosts → resolveUsageFilter → computeUsage`, all pure functions in `src/shared`). The\nbrowser renders only what that response contains — it does not re-aggregate. The one exception: a\ncustom date range older than the cached window kicks a background refetch with a wider `--since` while\nstill serving what the cache has.\n\n**MOCK mode.** `MOCK=1` swaps the command executor for one that replays `fixtures/real/<hostId>/` with\nnear-zero latency. Cache, refresh, routes, and aggregation run identically — only the shell-out is\nreplaced. Used by tests, the aggregation gate, and offline UI development.\n\n## API reference\n\nAll responses are JSON and zod-validated before they are sent. Wire conventions: `hosts` / `agents`\nare comma-separated ids (omitted = all); `from` / `to` are inclusive `YYYY-MM-DD` in the config\ntimezone (omitted `to` = today); an \"*N*d\" preset means today plus the N−1 preceding days.\n\n| Method | Path | Params | Purpose |\n|---|---|---|---|\n| `GET`  | `/api/config` | — | Current config document. |\n| `PUT`  | `/api/config` | body: full config | Validate then atomically rewrite the config. |\n| `GET`  | `/api/usage` | `from`, `to`, `hosts`, `agents` | Merged, filtered, zero-filled usage — the single source of truth the UI renders. |\n| `POST` | `/api/refresh` | — | Kick a background refresh (single-flight); responds `202` immediately. |\n| `GET`  | `/api/status` | — | Per-host freshness, durations, detected agents, and errors; plus a global `refreshing` flag. |\n| `POST` | `/api/hosts/:id/test` | — | Test connection to one host (`--version`); reports round-trip, version, agents, exit code, stderr tail. |\n\n(There is also an unlisted `GET /api/health` returning `{ ok: true }`.)\n\n## Development\n\n```\nsrc/shared/   types, zod schemas, and pure aggregation logic (no I/O) — the one\n              aggregation path the API and the gate both exercise\nsrc/server/   executor (local + SSH, timeouts, stderr separation, unified replay),\n              snapshot cache, refresh manager, config I/O, Hono routes\nsrc/web/      React 19 app — App, components/ (incl. charts/), hooks/\nfixtures/     real/       committed verbatim ccusage output per host, for MOCK + tests\n              synthetic/  hand-authored edge cases (empty, gaps, unknown agents, …)\nscripts/      dev.ts (dev orchestrator), verify-aggregation.ts (aggregation gate)\ntest/         bun:test suites\n```\n\n- **Stack:** Bun + TypeScript (strict). Server: Hono on `Bun.serve`. Frontend: Vite + React 19 +\n  Tailwind CSS v4 + Recharts + TanStack Query v5. Validation: zod at every external boundary.\n- **Tests:** `bun test` covers unified-envelope normalization, cross-host merge, zero-fill,\n  date-boundary inclusivity, harness filtering, previous-period comparison, month-end projection,\n  number/currency formatting, cache, unified executor replay, refresh, and routes.\n- **Aggregation gate:** `scripts/verify-aggregation.ts` boots `MOCK=1` and independently recomputes\n  expected `/api/usage` totals straight from the fixture JSON with plain arithmetic (it imports nothing\n  from `src/`, so the check isn't circular), then asserts equality to the cent:\n  ```bash\n  MOCK=1 TOKDASH_CONFIG=tokdash.config.example.json PORT=4114 bun start &  # background\n  PORT=4114 bun scripts/verify-aggregation.ts\n  ```\n\n## Non-goals\n\nAuth, multi-user, and cloud deployment; a historical database beyond the snapshot cache; cost budgets\nor alerting; editing ccusage's underlying data; Windows hosts; and ccusage's Claude-only\n`blocks` / `statusline` / live burn-rate features.\n\n## Contributing\n\nIssues and pull requests are welcome.\n\n## License\n\nMIT — see [LICENSE](LICENSE).\n","readmeFilename":"README.md"}