tokenol 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. tokenol-0.1.0/.claude/settings.local.json +35 -0
  2. tokenol-0.1.0/.claudesignore +12 -0
  3. tokenol-0.1.0/.github/workflows/ci.yml +24 -0
  4. tokenol-0.1.0/.gitignore +16 -0
  5. tokenol-0.1.0/CHANGELOG.md +40 -0
  6. tokenol-0.1.0/PKG-INFO +253 -0
  7. tokenol-0.1.0/README.md +220 -0
  8. tokenol-0.1.0/docs/ASSUMPTIONS.md +34 -0
  9. tokenol-0.1.0/docs/METRICS.md +148 -0
  10. tokenol-0.1.0/docs/screenshots/main.jpg +0 -0
  11. tokenol-0.1.0/docs/screenshots/project.jpg +0 -0
  12. tokenol-0.1.0/docs/screenshots/session_bottom.jpg +0 -0
  13. tokenol-0.1.0/docs/screenshots/session_top.jpg +0 -0
  14. tokenol-0.1.0/pyproject.toml +64 -0
  15. tokenol-0.1.0/src/tokenol/__init__.py +3 -0
  16. tokenol-0.1.0/src/tokenol/assumptions.py +51 -0
  17. tokenol-0.1.0/src/tokenol/cli.py +408 -0
  18. tokenol-0.1.0/src/tokenol/enums.py +26 -0
  19. tokenol-0.1.0/src/tokenol/ingest/__init__.py +0 -0
  20. tokenol-0.1.0/src/tokenol/ingest/builder.py +88 -0
  21. tokenol-0.1.0/src/tokenol/ingest/discovery.py +43 -0
  22. tokenol-0.1.0/src/tokenol/ingest/parser.py +162 -0
  23. tokenol-0.1.0/src/tokenol/ingest/schema.py +10 -0
  24. tokenol-0.1.0/src/tokenol/metrics/__init__.py +0 -0
  25. tokenol-0.1.0/src/tokenol/metrics/context.py +99 -0
  26. tokenol-0.1.0/src/tokenol/metrics/cost.py +145 -0
  27. tokenol-0.1.0/src/tokenol/metrics/history.py +80 -0
  28. tokenol-0.1.0/src/tokenol/metrics/patterns.py +246 -0
  29. tokenol-0.1.0/src/tokenol/metrics/rollups.py +342 -0
  30. tokenol-0.1.0/src/tokenol/metrics/thresholds.py +87 -0
  31. tokenol-0.1.0/src/tokenol/metrics/verdicts.py +37 -0
  32. tokenol-0.1.0/src/tokenol/metrics/windows.py +103 -0
  33. tokenol-0.1.0/src/tokenol/model/__init__.py +0 -0
  34. tokenol-0.1.0/src/tokenol/model/events.py +116 -0
  35. tokenol-0.1.0/src/tokenol/model/pricing.py +112 -0
  36. tokenol-0.1.0/src/tokenol/model/registry.py +57 -0
  37. tokenol-0.1.0/src/tokenol/report/__init__.py +0 -0
  38. tokenol-0.1.0/src/tokenol/report/text.py +352 -0
  39. tokenol-0.1.0/src/tokenol/serve/__init__.py +0 -0
  40. tokenol-0.1.0/src/tokenol/serve/app.py +312 -0
  41. tokenol-0.1.0/src/tokenol/serve/prefs.py +68 -0
  42. tokenol-0.1.0/src/tokenol/serve/session_detail.py +222 -0
  43. tokenol-0.1.0/src/tokenol/serve/state.py +1370 -0
  44. tokenol-0.1.0/src/tokenol/serve/static/app.js +1205 -0
  45. tokenol-0.1.0/src/tokenol/serve/static/chart.js +230 -0
  46. tokenol-0.1.0/src/tokenol/serve/static/components.js +159 -0
  47. tokenol-0.1.0/src/tokenol/serve/static/day.html +102 -0
  48. tokenol-0.1.0/src/tokenol/serve/static/day.js +108 -0
  49. tokenol-0.1.0/src/tokenol/serve/static/index.html +332 -0
  50. tokenol-0.1.0/src/tokenol/serve/static/model.html +51 -0
  51. tokenol-0.1.0/src/tokenol/serve/static/model.js +57 -0
  52. tokenol-0.1.0/src/tokenol/serve/static/project.html +137 -0
  53. tokenol-0.1.0/src/tokenol/serve/static/project.js +240 -0
  54. tokenol-0.1.0/src/tokenol/serve/static/session.html +297 -0
  55. tokenol-0.1.0/src/tokenol/serve/static/session.js +848 -0
  56. tokenol-0.1.0/src/tokenol/serve/static/styles.css +797 -0
  57. tokenol-0.1.0/src/tokenol/serve/streaming.py +63 -0
  58. tokenol-0.1.0/tests/conftest.py +3 -0
  59. tokenol-0.1.0/tests/fixtures/basic.jsonl +2 -0
  60. tokenol-0.1.0/tests/fixtures/dedup.jsonl +2 -0
  61. tokenol-0.1.0/tests/fixtures/gemini.jsonl +1 -0
  62. tokenol-0.1.0/tests/fixtures/interrupted.jsonl +1 -0
  63. tokenol-0.1.0/tests/fixtures/missing_ids.jsonl +1 -0
  64. tokenol-0.1.0/tests/fixtures/sidechain.jsonl +1 -0
  65. tokenol-0.1.0/tests/test_cli.py +161 -0
  66. tokenol-0.1.0/tests/test_context_metrics.py +139 -0
  67. tokenol-0.1.0/tests/test_dedup.py +42 -0
  68. tokenol-0.1.0/tests/test_history_and_thresholds.py +132 -0
  69. tokenol-0.1.0/tests/test_metrics.py +72 -0
  70. tokenol-0.1.0/tests/test_parser.py +122 -0
  71. tokenol-0.1.0/tests/test_patterns.py +255 -0
  72. tokenol-0.1.0/tests/test_serve_app.py +736 -0
  73. tokenol-0.1.0/tests/test_serve_prefs.py +63 -0
  74. tokenol-0.1.0/tests/test_serve_state.py +490 -0
  75. tokenol-0.1.0/tests/test_serve_streaming.py +188 -0
  76. tokenol-0.1.0/tests/test_session_detail.py +147 -0
  77. tokenol-0.1.0/tests/test_verdicts.py +96 -0
  78. tokenol-0.1.0/tests/test_windows.py +161 -0
  79. tokenol-0.1.0/uv.lock +1112 -0
@@ -0,0 +1,35 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(find /home/ff235/.claude-* -name \".credentials.json\" -exec ls -la {} \\\\;)",
5
+ "Bash(mount)",
6
+ "Bash(xargs -I{} head -c 2000 \"{}\")",
7
+ "Bash(xargs -I{} grep -o '\"thinking\"[^}]*' \"{}\")",
8
+ "Bash(grep -o '\"type\":\"thinking\",\"thinking\":\"[^\"]\\\\{0,200\\\\}' /home/ff235/.claude/projects/-home-ff235-dev-ReviewSense/3f7581b5-b523-4b68-be8f-c570ec2e30cb/subagents/agent-a35dd56.jsonl)",
9
+ "Bash(python3 *)",
10
+ "WebFetch(domain:efficienist.com)",
11
+ "Bash(node -e \" const fs = require\\('fs'\\); const src = fs.readFileSync\\('/home/ff235/.npm-global/lib/node_modules/ccusage/dist/data-loader-9ESMosno.js', 'utf8'\\); // Search for dedup pattern const idx = src.indexOf\\('seenRequestId'\\) > 0 ? src.indexOf\\('seenRequestId'\\) : src.indexOf\\('Set\\('\\) ; console.log\\('idx:', idx\\); console.log\\(src.substring\\(Math.max\\(0,idx-100\\), idx+600\\)\\); \")",
12
+ "Bash(node -e \" const fs = require\\('fs'\\); const src = fs.readFileSync\\('/home/ff235/.npm-global/lib/node_modules/ccusage/dist/data-loader-9ESMosno.js', 'utf8'\\); // Find where requestId dedup happens near token counting let idx = 0; while \\(true\\) { idx = src.indexOf\\('requestId', idx+1\\); if \\(idx < 0\\) break; const chunk = src.substring\\(Math.max\\(0,idx-50\\), idx+200\\); if \\(chunk.includes\\('has\\('\\) || chunk.includes\\('add\\('\\) || chunk.includes\\('seen'\\) || chunk.includes\\('skip'\\) || chunk.includes\\('continue'\\)\\) { console.log\\('=== at', idx, '==='\\); console.log\\(chunk\\); console.log\\(\\); } } \")",
13
+ "Bash(node -e \" const fs = require\\('fs'\\); // Look in the other bundle file const src = fs.readFileSync\\('/home/ff235/.npm-global/lib/node_modules/ccusage/dist/calculate-cost-DWGfKMSD.js', 'utf8'\\); console.log\\('File size:', src.length\\); // Find key patterns for \\(const t of ['requestId', 'seenReq', 'dedup', 'has\\(', 'skip', 'cache_read', 'output_tokens']\\) { const idx = src.indexOf\\(t\\); if \\(idx >= 0\\) { console.log\\('--- found', t, 'at', idx\\); console.log\\(src.substring\\(Math.max\\(0,idx-50\\), idx+200\\)\\); console.log\\(\\); } } \")",
14
+ "Bash(node *)",
15
+ "Bash(mkdir -p /tmp/cc-leak-probe)",
16
+ "Read(//tmp/**)",
17
+ "Bash(npm pack *)",
18
+ "Bash(pip install *)",
19
+ "Bash(python *)",
20
+ "Bash(tokenol daily *)",
21
+ "Bash(ruff check *)",
22
+ "Bash(tokenol sessions *)",
23
+ "Bash(tokenol live *)",
24
+ "Bash(git add *)",
25
+ "Bash(git commit -m ' *)",
26
+ "Bash(uv run *)",
27
+ "Bash(uv build *)",
28
+ "Bash(git commit *)",
29
+ "Bash(git push *)",
30
+ "Bash(env *)",
31
+ "Bash(find ~/.claude* -type d -name \"frontend*\")",
32
+ "Bash(uv sync *)"
33
+ ]
34
+ }
35
+ }
@@ -0,0 +1,12 @@
1
+ # Auto-generated by Claude Wrapper to save Rate Limits
2
+ node_modules/
3
+ .venv/
4
+ venv/
5
+ __pycache__/
6
+ dist/
7
+ *.tar.gz
8
+ *.sqlite
9
+ *.db
10
+ .pytest_cache/
11
+ .cursor/
12
+ .windsurf/
@@ -0,0 +1,24 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.11", "3.12"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - name: Install
20
+ run: pip install -e ".[dev,serve]"
21
+ - name: Lint
22
+ run: ruff check src tests
23
+ - name: Test
24
+ run: pytest --cov=tokenol --cov-report=term-missing
@@ -0,0 +1,16 @@
1
+ _local/
2
+ *_PLAN.md
3
+ *.plan.md
4
+ RESUME.md
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ dist/
11
+ build/
12
+ *.egg-info/
13
+ .venv/
14
+ venv/
15
+ .coverage
16
+ htmlcov/
@@ -0,0 +1,40 @@
1
+ # Changelog
2
+
3
+ All notable changes to tokenol are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.0] — 2026-04-23
8
+
9
+ Initial public release.
10
+
11
+ ### Added
12
+ - **CLI** (`tokenol`): `today`, `week`, `window`, `verdicts`, `models`, `projects`,
13
+ `assumptions`, `doctor`, `watch`, `serve`.
14
+ - **Ingestion**: discovery across `~/.claude*` dirs (honours `CLAUDE_CONFIG_DIR`),
15
+ JSONL parsing with per-file mtime cache, compound-key deduplication,
16
+ Windows cwd normalization.
17
+ - **Metrics**: cost rollups with full 4-component billing (input, output,
18
+ cache_read, cache_creation); 5-hour rolling-window cost; context growth,
19
+ cache hit rate, cache reuse, cost-per-kW output; session verdicts
20
+ (`OK`, `CONTEXT_CREEP`, `RUNAWAY_WINDOW`, `TOOL_ERROR_STORM`,
21
+ `SIDECHAIN_HEAVY`, `DUAL_SESSION_CONFLICT`).
22
+ - **Pattern detection** on session drill-down: `idle_expiry`,
23
+ `compaction_reinflation`, `context_ceiling_plateau`, `sidechain_explosion`,
24
+ `tool_error_storm` with severity escalation.
25
+ - **Live dashboard** (`tokenol serve`): SSE-streamed main view with headline
26
+ tiles (+ last-hour trajectory), hourly / daily charts (linear ↔ log
27
+ toggle), model and project rollups, recent-activity table.
28
+ - **Drill-down pages**: `/session/<id>` (patterns, cost-per-turn small
29
+ multiples, per-turn modal), `/project/<cwd>` (cache trend with auto
30
+ hourly/daily bucketing, verdict distribution with tooltips),
31
+ `/day/<date>`, `/model/<name>`.
32
+ - **Preferences**: user-editable thresholds and ranges persisted via
33
+ `XDG_CONFIG_HOME`.
34
+ - **Project grouping**: shortest-proper-ancestor cwd rule generalizes across
35
+ nested repos without per-user configuration.
36
+
37
+ ### Tested
38
+ - 184 unit + integration tests on Python 3.10 / 3.11 / 3.12.
39
+
40
+ [0.1.0]: https://github.com/yourorg/tokenol/releases/tag/v0.1.0
tokenol-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.4
2
+ Name: tokenol
3
+ Version: 0.1.0
4
+ Summary: Audit Claude Code JSONL session logs: track cost, cache health, context blow-ups, and 5h-window pressure.
5
+ Project-URL: Homepage, https://github.com/farhanferoz/tokenol
6
+ Project-URL: Issues, https://github.com/farhanferoz/tokenol/issues
7
+ License: MIT
8
+ Keywords: anthropic,audit,claude,cli,cost,tokens
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.10
18
+ Requires-Dist: duckdb>=0.10
19
+ Requires-Dist: rich>=13
20
+ Requires-Dist: typer>=0.12
21
+ Provides-Extra: dev
22
+ Requires-Dist: httpx>=0.27; extra == 'dev'
23
+ Requires-Dist: hypothesis>=6; extra == 'dev'
24
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
25
+ Requires-Dist: pytest-cov; extra == 'dev'
26
+ Requires-Dist: pytest>=8; extra == 'dev'
27
+ Requires-Dist: ruff>=0.4; extra == 'dev'
28
+ Provides-Extra: serve
29
+ Requires-Dist: fastapi>=0.111; extra == 'serve'
30
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'serve'
31
+ Requires-Dist: watchfiles>=0.21; extra == 'serve'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # tokenol
35
+
36
+ Audit [Claude Code](https://claude.com/claude-code) JSONL session logs for cost, cache health, context blow-ups, and 5-hour rate-limit pressure.
37
+
38
+ `tokenol` parses the session transcripts that Claude Code writes to `~/.claude*/projects/**/*.jsonl` and produces per-day, per-session, per-project, and per-model rollups — plus a live burn-rate view for the active 5-hour window.
39
+
40
+ ## Why tokenol
41
+
42
+ Claude Code bills you for everything the model reads — input, output, **and** cache creation/reads. When the prompt cache is working, 95%+ of your context tokens cost a tenth of full input price. When it isn't — idle gaps past the 5-minute TTL, context compaction, two sessions in the same repo thrashing each other — the same conversation can cost 10× more without looking any different.
43
+
44
+ `tokenol` tells you which sessions, projects, and hours did that, and usually why. You run it locally over the JSONL logs Claude Code already writes; nothing is uploaded anywhere.
45
+
46
+ ## Dashboard
47
+
48
+ ![Main dashboard](docs/screenshots/main.jpg)
49
+
50
+ Session drill-down — pattern detection + cost-per-turn small multiples:
51
+
52
+ ![Session drill-down, top](docs/screenshots/session_top.jpg)
53
+ ![Session drill-down, lower panels](docs/screenshots/session_bottom.jpg)
54
+
55
+ Project page — cache efficiency trend, verdict distribution, top turns:
56
+
57
+ ![Project page](docs/screenshots/project.jpg)
58
+
59
+ ## Install
60
+
61
+ ```bash
62
+ pipx install tokenol
63
+ ```
64
+
65
+ Requires Python 3.10+.
66
+
67
+ ## Quick start
68
+
69
+ ```bash
70
+ # Daily token / cost aggregates over the last 14 days
71
+ tokenol daily
72
+
73
+ # Hourly breakdown for today
74
+ tokenol hourly
75
+
76
+ # Top 10 most expensive sessions in the last 30 days
77
+ tokenol sessions --since 30d --top 10 --sort cost
78
+
79
+ # Per-project rollup
80
+ tokenol projects
81
+
82
+ # Live view: burn rate + projected end-of-window cost
83
+ tokenol live --last 20m
84
+ ```
85
+
86
+ All commands scan every JSONL file under `$CLAUDE_CONFIG_DIR` (falling back to the standard `~/.claude*` locations) and deduplicate turns using the same `message.id:requestId` compound key that [ccusage](https://github.com/ryoppippi/ccusage) uses.
87
+
88
+ ### Scanning multiple projects
89
+
90
+ If you use workspace isolation (one `~/.claude-<project>` directory per repo, pointed at via `CLAUDE_CONFIG_DIR`), `tokenol` by default only sees the currently-active project. Pass **`--all-projects`** (or `-A`) to any command to scan every `~/.claude*` directory and get a cross-project view:
91
+
92
+ ```bash
93
+ # Total spend across every project in the last 14 days
94
+ tokenol daily --since 14d --all-projects
95
+
96
+ # Which sessions cost the most, globally
97
+ tokenol sessions --since 30d --top 10 -A
98
+ ```
99
+
100
+ You can also set `CLAUDE_CONFIG_DIR` to a colon- or comma-separated list of paths to scan a specific subset.
101
+
102
+ ## Commands
103
+
104
+ | Command | What it shows |
105
+ | ---------- | --------------------------------------------------------------------------- |
106
+ | `daily` | Per-day tokens (input, output, cache read/creation), cost, turn count |
107
+ | `hourly` | Per-hour breakdown for a single day (defaults to today) |
108
+ | `live` | Active 5-hour window burn rate, recent-activity rate, projected final cost |
109
+ | `sessions` | Per-session detail table with blow-up verdict (RUNAWAY, CONTEXT_CREEP, …) |
110
+ | `projects` | Per-project rollup grouped by `cwd` |
111
+ | `models` | Per-model rollup with tool-use counts and error rates |
112
+ | `verify` | Cross-check tokenol totals against `ccusage --json` (if installed) |
113
+ | `serve` | Launch a local browser dashboard with live burn-rate gauge and all panels |
114
+
115
+ Every command accepts:
116
+
117
+ - `--since 14d` — lookback window (e.g. `7d`, `30d`, or an ISO date)
118
+ - `--all-projects` / `-A` — scan every `~/.claude*` directory (ignores `CLAUDE_CONFIG_DIR`)
119
+ - `--strict` — exit non-zero if any cost-computation assumption fired
120
+ - `--show-assumptions` — always print the assumption footer
121
+ - `--log-level debug|info|warning`
122
+
123
+ `tokenol sessions` additionally takes `--sort` (`cost`, `input`, `output`, `cache_read`, `turns`, `max_input`, `duration`) and `--top`.
124
+
125
+ `tokenol live` takes `--last 20m|2h|30s` and exits non-zero if the projected window cost exceeds the configured reference.
126
+
127
+ ## Live dashboard
128
+
129
+ ```bash
130
+ # Install with dashboard dependencies
131
+ pipx install 'tokenol[serve]'
132
+
133
+ # Start the dashboard (binds to http://127.0.0.1:8787)
134
+ tokenol serve
135
+
136
+ # Cross-project view, faster tick, custom reference threshold
137
+ tokenol serve --all-projects --tick 2s --reference 25
138
+
139
+ # Open browser automatically
140
+ tokenol serve --open
141
+ ```
142
+
143
+ The dashboard updates via SSE as Claude Code writes events to disk. Main page layout (top to bottom):
144
+
145
+ | Panel | What it shows |
146
+ |---|---|
147
+ | **Topbar** | Today's cost · sessions · output · last-active time; global period selector (Today / 7D / 30D / All) |
148
+ | **Efficiency tiles** | Hit% · $/kW · Ctx · Cache reuse — each with a delta chip vs 7-day median and colour-coded threshold |
149
+ | **Hour By Hour** | Hourly metric timeline with day-picker, metric pills, project/model filters, and click-to-drilldown |
150
+ | **Daily History** | 30-day metric history with 7-day moving average overlay; range pills (7D / 30D / 90D / All) |
151
+ | **Models** | Per-model cost, turns, output, and efficiency metrics; local range override; click row → `/model/<name>` |
152
+ | **Recent Activity** | Active projects in the last 60 min with Ctx used, $/kW, hit%, verdict; sortable; click row → `/project/<cwd>` |
153
+
154
+ Keyboard shortcuts: `?` Glossary · `/` Find · `,` Settings · `Esc` close/back · `g t` scroll to top · `↑↓ Enter` table row navigation · `← →` chart cursor.
155
+
156
+ ### Efficiency metric glossary
157
+
158
+ | Metric | Definition | Target |
159
+ |---|---|---|
160
+ | **Hit%** | `cache_read / (cache_read + cache_creation + input)` | ≥ 95% |
161
+ | **$/kW** | `cost × 1000 / output_tokens` — dollars per 1k output tokens | < $0.20 |
162
+ | **Ctx** | `cache_read / output` as N:1 — context tokens read per output token | < 400:1 |
163
+ | **Cache reuse** | `cache_read / cache_creation` as N:1 — low = cache thrashing | > 50:1 |
164
+ | **Ctx used** | Latest turn's visible context ÷ model context window | < 85% |
165
+
166
+ ### Preferences
167
+
168
+ User preferences (tick cadence and threshold overrides) are saved to:
169
+
170
+ ```
171
+ $XDG_CONFIG_HOME/tokenol/prefs.json # default: ~/.config/tokenol/prefs.json
172
+ ```
173
+
174
+ Shape:
175
+
176
+ ```json
177
+ {
178
+ "tick_seconds": 300,
179
+ "reference_usd": 50.0,
180
+ "thresholds": {
181
+ "hit_rate_good_pct": 95,
182
+ "hit_rate_red_pct": 85,
183
+ "cost_per_kw_good": 0.20,
184
+ "cost_per_kw_red": 0.40,
185
+ "ctx_ratio_red": 400.0,
186
+ "cache_reuse_good": 50.0,
187
+ "cache_reuse_red": 20.0
188
+ }
189
+ }
190
+ ```
191
+
192
+ Reset to defaults via the Settings modal (`POST /api/prefs {"thresholds": "reset"}`).
193
+
194
+ ### Session drill-down
195
+
196
+ Click any session to open the drill-down page (`/session/<id>`). It shows:
197
+
198
+ - **What likely went wrong** — automated pattern cards at the top of the page, each with a headline, the measurable signal that triggered it, and a suggested fix. Five patterns are detected:
199
+
200
+ | Pattern | Signal |
201
+ |---|---|
202
+ | **Idle expiry** | Gap ≥ 1 h between turns + next turn was ≥ 80% cache_creation — the 5-minute prompt-cache TTL expired |
203
+ | **Compaction re-inflation** | Visible-token count dropped then climbed back to ≥ 80% of the previous peak — compacting but immediately refilling the context |
204
+ | **Context ceiling plateau** | ≥ 20 consecutive turns at ≥ 90% of the model's context window — paying near-full-context input rates throughout |
205
+ | **Sidechain explosion** | Sidechain/task-agent work accounts for > 40% of session cost |
206
+ | **Tool error storm** | > 20% error rate across any 10-turn window |
207
+
208
+ - **Cost per turn** — stacked bar chart (input / output / cache_read / cache_creation). Toggle "All" or "Top 30" to focus on the most expensive turns. Click any bar to open the per-turn detail modal.
209
+
210
+ - **Per-turn modal** — cost component breakdown, token counts, tool call results (✓/✗), first 500 chars of the user prompt and assistant preview. Navigate with ← / → or close with Esc.
211
+
212
+ ## What it detects
213
+
214
+ For every session, `tokenol` computes a blow-up verdict against spec-defined thresholds:
215
+
216
+ | Verdict (table label) | Trigger |
217
+ | ----------------------------- | ------------------------------------------------------ |
218
+ | `RUNAWAY_WINDOW` (`runaway`) | Any 5-hour window costs ≥ \$50 |
219
+ | `CONTEXT_CREEP` (`ctx-creep`) | Max single-turn input ≥ 500k **and** growth ≥ 2k/turn |
220
+ | `TOOL_ERROR_STORM` (`tool-errs`) | ≥ 10 tool uses with > 30% error rate |
221
+ | `SIDECHAIN_HEAVY` (`sidechain`) | Sidechain session costing > \$5 |
222
+ | `OK` (`ok`) | Everything else |
223
+
224
+ ### Daily efficiency columns
225
+
226
+ The `tokenol daily` report shows these cost/cache efficiency ratios:
227
+
228
+ | Column | Meaning | Target |
229
+ |---|---|---|
230
+ | `$/kW` | USD per 1,000 output tokens | `< $0.20` |
231
+ | `Ctx` | Context tokens read per output token (N:1) | lower is better |
232
+ | `Cache reuse` | Cache reads per cache-creation token (N:1) | `> 50:1` |
233
+ | `Hit%` | % of context served from prompt cache | `≥ 95%` |
234
+
235
+ ## Pricing
236
+
237
+ Flat per-model rates (no 1M-token tier surcharge — matches ccusage's default behaviour). The current registry lives in `src/tokenol/metrics/cost.py`. When a turn's model isn't in the registry, `tokenol` records an `UNKNOWN_MODEL_FALLBACK` assumption tag and uses a conservative default; run with `--show-assumptions` or `--strict` to surface these.
238
+
239
+ See [`docs/METRICS.md`](docs/METRICS.md) for metric definitions and [`docs/ASSUMPTIONS.md`](docs/ASSUMPTIONS.md) for the full list of assumption tags.
240
+
241
+ ## Development
242
+
243
+ ```bash
244
+ git clone https://github.com/farhanferoz/tokenol
245
+ cd tokenol
246
+ uv sync --extra dev
247
+ uv run pytest
248
+ uv run ruff check
249
+ ```
250
+
251
+ ## Licence
252
+
253
+ MIT
@@ -0,0 +1,220 @@
1
+ # tokenol
2
+
3
+ Audit [Claude Code](https://claude.com/claude-code) JSONL session logs for cost, cache health, context blow-ups, and 5-hour rate-limit pressure.
4
+
5
+ `tokenol` parses the session transcripts that Claude Code writes to `~/.claude*/projects/**/*.jsonl` and produces per-day, per-session, per-project, and per-model rollups — plus a live burn-rate view for the active 5-hour window.
6
+
7
+ ## Why tokenol
8
+
9
+ Claude Code bills you for everything the model reads — input, output, **and** cache creation/reads. When the prompt cache is working, 95%+ of your context tokens cost a tenth of full input price. When it isn't — idle gaps past the 5-minute TTL, context compaction, two sessions in the same repo thrashing each other — the same conversation can cost 10× more without looking any different.
10
+
11
+ `tokenol` tells you which sessions, projects, and hours did that, and usually why. You run it locally over the JSONL logs Claude Code already writes; nothing is uploaded anywhere.
12
+
13
+ ## Dashboard
14
+
15
+ ![Main dashboard](docs/screenshots/main.jpg)
16
+
17
+ Session drill-down — pattern detection + cost-per-turn small multiples:
18
+
19
+ ![Session drill-down, top](docs/screenshots/session_top.jpg)
20
+ ![Session drill-down, lower panels](docs/screenshots/session_bottom.jpg)
21
+
22
+ Project page — cache efficiency trend, verdict distribution, top turns:
23
+
24
+ ![Project page](docs/screenshots/project.jpg)
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pipx install tokenol
30
+ ```
31
+
32
+ Requires Python 3.10+.
33
+
34
+ ## Quick start
35
+
36
+ ```bash
37
+ # Daily token / cost aggregates over the last 14 days
38
+ tokenol daily
39
+
40
+ # Hourly breakdown for today
41
+ tokenol hourly
42
+
43
+ # Top 10 most expensive sessions in the last 30 days
44
+ tokenol sessions --since 30d --top 10 --sort cost
45
+
46
+ # Per-project rollup
47
+ tokenol projects
48
+
49
+ # Live view: burn rate + projected end-of-window cost
50
+ tokenol live --last 20m
51
+ ```
52
+
53
+ All commands scan every JSONL file under `$CLAUDE_CONFIG_DIR` (falling back to the standard `~/.claude*` locations) and deduplicate turns using the same `message.id:requestId` compound key that [ccusage](https://github.com/ryoppippi/ccusage) uses.
54
+
55
+ ### Scanning multiple projects
56
+
57
+ If you use workspace isolation (one `~/.claude-<project>` directory per repo, pointed at via `CLAUDE_CONFIG_DIR`), `tokenol` by default only sees the currently-active project. Pass **`--all-projects`** (or `-A`) to any command to scan every `~/.claude*` directory and get a cross-project view:
58
+
59
+ ```bash
60
+ # Total spend across every project in the last 14 days
61
+ tokenol daily --since 14d --all-projects
62
+
63
+ # Which sessions cost the most, globally
64
+ tokenol sessions --since 30d --top 10 -A
65
+ ```
66
+
67
+ You can also set `CLAUDE_CONFIG_DIR` to a colon- or comma-separated list of paths to scan a specific subset.
68
+
69
+ ## Commands
70
+
71
+ | Command | What it shows |
72
+ | ---------- | --------------------------------------------------------------------------- |
73
+ | `daily` | Per-day tokens (input, output, cache read/creation), cost, turn count |
74
+ | `hourly` | Per-hour breakdown for a single day (defaults to today) |
75
+ | `live` | Active 5-hour window burn rate, recent-activity rate, projected final cost |
76
+ | `sessions` | Per-session detail table with blow-up verdict (RUNAWAY, CONTEXT_CREEP, …) |
77
+ | `projects` | Per-project rollup grouped by `cwd` |
78
+ | `models` | Per-model rollup with tool-use counts and error rates |
79
+ | `verify` | Cross-check tokenol totals against `ccusage --json` (if installed) |
80
+ | `serve` | Launch a local browser dashboard with live burn-rate gauge and all panels |
81
+
82
+ Every command accepts:
83
+
84
+ - `--since 14d` — lookback window (e.g. `7d`, `30d`, or an ISO date)
85
+ - `--all-projects` / `-A` — scan every `~/.claude*` directory (ignores `CLAUDE_CONFIG_DIR`)
86
+ - `--strict` — exit non-zero if any cost-computation assumption fired
87
+ - `--show-assumptions` — always print the assumption footer
88
+ - `--log-level debug|info|warning`
89
+
90
+ `tokenol sessions` additionally takes `--sort` (`cost`, `input`, `output`, `cache_read`, `turns`, `max_input`, `duration`) and `--top`.
91
+
92
+ `tokenol live` takes `--last 20m|2h|30s` and exits non-zero if the projected window cost exceeds the configured reference.
93
+
94
+ ## Live dashboard
95
+
96
+ ```bash
97
+ # Install with dashboard dependencies
98
+ pipx install 'tokenol[serve]'
99
+
100
+ # Start the dashboard (binds to http://127.0.0.1:8787)
101
+ tokenol serve
102
+
103
+ # Cross-project view, faster tick, custom reference threshold
104
+ tokenol serve --all-projects --tick 2s --reference 25
105
+
106
+ # Open browser automatically
107
+ tokenol serve --open
108
+ ```
109
+
110
+ The dashboard updates via SSE as Claude Code writes events to disk. Main page layout (top to bottom):
111
+
112
+ | Panel | What it shows |
113
+ |---|---|
114
+ | **Topbar** | Today's cost · sessions · output · last-active time; global period selector (Today / 7D / 30D / All) |
115
+ | **Efficiency tiles** | Hit% · $/kW · Ctx · Cache reuse — each with a delta chip vs 7-day median and colour-coded threshold |
116
+ | **Hour By Hour** | Hourly metric timeline with day-picker, metric pills, project/model filters, and click-to-drilldown |
117
+ | **Daily History** | 30-day metric history with 7-day moving average overlay; range pills (7D / 30D / 90D / All) |
118
+ | **Models** | Per-model cost, turns, output, and efficiency metrics; local range override; click row → `/model/<name>` |
119
+ | **Recent Activity** | Active projects in the last 60 min with Ctx used, $/kW, hit%, verdict; sortable; click row → `/project/<cwd>` |
120
+
121
+ Keyboard shortcuts: `?` Glossary · `/` Find · `,` Settings · `Esc` close/back · `g t` scroll to top · `↑↓ Enter` table row navigation · `← →` chart cursor.
122
+
123
+ ### Efficiency metric glossary
124
+
125
+ | Metric | Definition | Target |
126
+ |---|---|---|
127
+ | **Hit%** | `cache_read / (cache_read + cache_creation + input)` | ≥ 95% |
128
+ | **$/kW** | `cost × 1000 / output_tokens` — dollars per 1k output tokens | < $0.20 |
129
+ | **Ctx** | `cache_read / output` as N:1 — context tokens read per output token | < 400:1 |
130
+ | **Cache reuse** | `cache_read / cache_creation` as N:1 — low = cache thrashing | > 50:1 |
131
+ | **Ctx used** | Latest turn's visible context ÷ model context window | < 85% |
132
+
133
+ ### Preferences
134
+
135
+ User preferences (tick cadence and threshold overrides) are saved to:
136
+
137
+ ```
138
+ $XDG_CONFIG_HOME/tokenol/prefs.json # default: ~/.config/tokenol/prefs.json
139
+ ```
140
+
141
+ Shape:
142
+
143
+ ```json
144
+ {
145
+ "tick_seconds": 300,
146
+ "reference_usd": 50.0,
147
+ "thresholds": {
148
+ "hit_rate_good_pct": 95,
149
+ "hit_rate_red_pct": 85,
150
+ "cost_per_kw_good": 0.20,
151
+ "cost_per_kw_red": 0.40,
152
+ "ctx_ratio_red": 400.0,
153
+ "cache_reuse_good": 50.0,
154
+ "cache_reuse_red": 20.0
155
+ }
156
+ }
157
+ ```
158
+
159
+ Reset to defaults via the Settings modal (`POST /api/prefs {"thresholds": "reset"}`).
160
+
161
+ ### Session drill-down
162
+
163
+ Click any session to open the drill-down page (`/session/<id>`). It shows:
164
+
165
+ - **What likely went wrong** — automated pattern cards at the top of the page, each with a headline, the measurable signal that triggered it, and a suggested fix. Five patterns are detected:
166
+
167
+ | Pattern | Signal |
168
+ |---|---|
169
+ | **Idle expiry** | Gap ≥ 1 h between turns + next turn was ≥ 80% cache_creation — the 5-minute prompt-cache TTL expired |
170
+ | **Compaction re-inflation** | Visible-token count dropped then climbed back to ≥ 80% of the previous peak — compacting but immediately refilling the context |
171
+ | **Context ceiling plateau** | ≥ 20 consecutive turns at ≥ 90% of the model's context window — paying near-full-context input rates throughout |
172
+ | **Sidechain explosion** | Sidechain/task-agent work accounts for > 40% of session cost |
173
+ | **Tool error storm** | > 20% error rate across any 10-turn window |
174
+
175
+ - **Cost per turn** — stacked bar chart (input / output / cache_read / cache_creation). Toggle "All" or "Top 30" to focus on the most expensive turns. Click any bar to open the per-turn detail modal.
176
+
177
+ - **Per-turn modal** — cost component breakdown, token counts, tool call results (✓/✗), first 500 chars of the user prompt and assistant preview. Navigate with ← / → or close with Esc.
178
+
179
+ ## What it detects
180
+
181
+ For every session, `tokenol` computes a blow-up verdict against spec-defined thresholds:
182
+
183
+ | Verdict (table label) | Trigger |
184
+ | ----------------------------- | ------------------------------------------------------ |
185
+ | `RUNAWAY_WINDOW` (`runaway`) | Any 5-hour window costs ≥ \$50 |
186
+ | `CONTEXT_CREEP` (`ctx-creep`) | Max single-turn input ≥ 500k **and** growth ≥ 2k/turn |
187
+ | `TOOL_ERROR_STORM` (`tool-errs`) | ≥ 10 tool uses with > 30% error rate |
188
+ | `SIDECHAIN_HEAVY` (`sidechain`) | Sidechain session costing > \$5 |
189
+ | `OK` (`ok`) | Everything else |
190
+
191
+ ### Daily efficiency columns
192
+
193
+ The `tokenol daily` report shows these cost/cache efficiency ratios:
194
+
195
+ | Column | Meaning | Target |
196
+ |---|---|---|
197
+ | `$/kW` | USD per 1,000 output tokens | `< $0.20` |
198
+ | `Ctx` | Context tokens read per output token (N:1) | lower is better |
199
+ | `Cache reuse` | Cache reads per cache-creation token (N:1) | `> 50:1` |
200
+ | `Hit%` | % of context served from prompt cache | `≥ 95%` |
201
+
202
+ ## Pricing
203
+
204
+ Flat per-model rates (no 1M-token tier surcharge — matches ccusage's default behaviour). The current registry lives in `src/tokenol/metrics/cost.py`. When a turn's model isn't in the registry, `tokenol` records an `UNKNOWN_MODEL_FALLBACK` assumption tag and uses a conservative default; run with `--show-assumptions` or `--strict` to surface these.
205
+
206
+ See [`docs/METRICS.md`](docs/METRICS.md) for metric definitions and [`docs/ASSUMPTIONS.md`](docs/ASSUMPTIONS.md) for the full list of assumption tags.
207
+
208
+ ## Development
209
+
210
+ ```bash
211
+ git clone https://github.com/farhanferoz/tokenol
212
+ cd tokenol
213
+ uv sync --extra dev
214
+ uv run pytest
215
+ uv run ruff check
216
+ ```
217
+
218
+ ## Licence
219
+
220
+ MIT
@@ -0,0 +1,34 @@
1
+ # tokenol Assumptions Catalog
2
+
3
+ Every heuristic or fallback that tokenol applies is recorded as an `AssumptionTag`.
4
+ These surface in three places:
5
+ - Report footer summary (printed automatically when any fire)
6
+ - `--show-assumptions` flag forces the footer to print even when empty
7
+ - `--log-level debug` emits stderr JSON lines per decision
8
+ - `--strict` mode treats any fired assumption as a fatal error
9
+
10
+ ---
11
+
12
+ ## Assumption Tags
13
+
14
+ | Tag | Heuristic | Why needed | Error mode |
15
+ |---|---|---|---|
16
+ | `WINDOW_BOUNDARY_HEURISTIC` | 5h window starts at first billable event; runs 5h wall-clock; next event after expiry starts a new window | Anthropic's exact server-side rule isn't published; matches community reverse-engineering | Drifts if Anthropic uses fixed-UTC or overlapping-rolling windows |
17
+ | `UNKNOWN_MODEL_FALLBACK` | Unknown Claude model inherits pricing + context from nearest-known family sibling | No machine-readable pricing feed; new models appear before we update | Warned per model; slight mispricing until registry updated |
18
+ | `DEDUP_PASSTHROUGH` | Events with `null` `message.id` or `null` `requestId` pass through dedup (matches ccusage behavior) | Cannot form the compound hash key | Rare; logged per event |
19
+ | `INTERRUPTED_TURN_SKIPPED` | Assistant messages with no `usage` fields (stop_reason=NONE) excluded from cost | Request never completed; no billing data | None — correctly excludes |
20
+ | `GEMINI_UNPRICED` | Non-Claude models (gemini-*) parsed but not priced | Multi-provider pricing is post-v1 | Cost rows show `—` for these models |
21
+
22
+ ---
23
+
24
+ ## New tags added in Phase 2
25
+
26
+ `WINDOW_BOUNDARY_HEURISTIC` was defined in Phase 1 but first fired in Phase 2 when `align_windows()` is called. It fires once per `align_windows()` call, not once per turn.
27
+
28
+ ---
29
+
30
+ ## CLI Flags
31
+
32
+ - `--strict` — refuse any assumption fallback; error out with non-zero exit code.
33
+ - `--show-assumptions` — force the footer to print even if no assumptions fired. Useful for CI output.
34
+ - `--log-level debug` — set Python's root logging level to DEBUG; enables verbose per-decision output.