nilometer 0.0.0-stage → 0.1.0
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.
- package/CHANGELOG.md +63 -0
- package/LICENSE +21 -0
- package/README.md +462 -2
- package/core/pricing/prices.json +348 -0
- package/core/schema/001_ingestion.sql +57 -0
- package/core/schema/002_line_count.sql +7 -0
- package/core/schema/003_derived.sql +70 -0
- package/core/schema/004_requests_dedup.sql +50 -0
- package/core/schema/005_repositories.sql +25 -0
- package/core/schema/006_status_readings.sql +33 -0
- package/core/schema/007_pricing.sql +95 -0
- package/core/schema/008_interruptions.sql +263 -0
- package/core/schema/009_headroom.sql +84 -0
- package/core/schema/010_attribution.sql +106 -0
- package/core/schema/011_projections.sql +83 -0
- package/core/schema/012_unsplit_cache_writes.sql +49 -0
- package/core/schema/013_repository_revalidation.sql +104 -0
- package/core/schema/014_observed_readings.sql +204 -0
- package/core/schema/015_response_observations.sql +127 -0
- package/core/schema/016_windows_repository_paths.sql +41 -0
- package/core/schema/017_unattributed_usage_once.sql +47 -0
- package/core/schema/018_request_time_index.sql +21 -0
- package/core/schema/019_dedup_earliest_of_equals.sql +61 -0
- package/core/schema/020_limit_quota_fields.sql +131 -0
- package/core/schema/021_plan_periods.sql +121 -0
- package/core/schema/022_attachment_walk.sql +223 -0
- package/core/schema/023_auto_continuations.sql +129 -0
- package/core/schema/024_counted_limit_groups.sql +29 -0
- package/core/schema/025_model_fallback.sql +77 -0
- package/core/schema/026_changed_readings.sql +106 -0
- package/core/schema/027_weekly_crossings.sql +32 -0
- package/core/schema/028_stale_readings.sql +196 -0
- package/core/schema/029_lockout_sessions.sql +68 -0
- package/core/schema/030_repository_aliases.sql +81 -0
- package/core/schema/031_cte_cursor_budget.sql +153 -0
- package/dist/cli/main.cli.js +39 -0
- package/dist/cli/program.js +801 -0
- package/dist/core/db/database.js +182 -0
- package/dist/core/db/fingerprint.js +71 -0
- package/dist/core/ingest/attribution.js +131 -0
- package/dist/core/ingest/classify.js +382 -0
- package/dist/core/ingest/command.js +95 -0
- package/dist/core/ingest/derive.js +212 -0
- package/dist/core/ingest/discover.js +126 -0
- package/dist/core/ingest/ingest.js +160 -0
- package/dist/core/ingest/reader.js +89 -0
- package/dist/core/ingest/reset-time.js +97 -0
- package/dist/core/ingest/results.js +201 -0
- package/dist/core/ingest/spool.js +165 -0
- package/dist/core/ingest/store.js +99 -0
- package/dist/core/install/install.js +185 -0
- package/dist/core/install/locations.js +89 -0
- package/dist/core/install/private-files.js +259 -0
- package/dist/core/install/record.js +103 -0
- package/dist/core/plans/plan-prices.js +128 -0
- package/dist/core/plans/repository-aliases.js +136 -0
- package/dist/core/pricing/prices.js +147 -0
- package/dist/core/settings/settings-file.js +215 -0
- package/dist/core/settings/statusline.js +240 -0
- package/dist/core/util/commands.js +91 -0
- package/dist/core/util/node-version.js +58 -0
- package/dist/core/util/open-page.js +69 -0
- package/dist/core/util/paths.js +36 -0
- package/dist/core/verify/compare.js +77 -0
- package/dist/core/verify/verify.js +220 -0
- package/dist/viewer/explain.js +574 -0
- package/dist/viewer/format.js +150 -0
- package/dist/viewer/html-charts.js +1292 -0
- package/dist/viewer/html-contract.js +2 -0
- package/dist/viewer/html-data.js +718 -0
- package/dist/viewer/html.js +1654 -0
- package/dist/viewer/queries.js +128 -0
- package/dist/viewer/render.js +602 -0
- package/dist/viewer/report.js +120 -0
- package/dist/viewer/save.js +110 -0
- package/docs/requirements.md +113 -0
- package/hooks/statusline.sh +104 -0
- package/package.json +61 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (unreleased)
|
|
4
|
+
|
|
5
|
+
First public version.
|
|
6
|
+
|
|
7
|
+
- **Status line collector:** a POSIX shell hook that records Claude Code's `rate_limits` payload after every terminal reply, wrapping any status line you already have. `init` installs it with a settings backup, and `uninstall` restores the previous setting.
|
|
8
|
+
- **Ingestion:** session logs and status line readings copied into a local SQLite database, with backfill from every log still on disk, deduplication of streamed responses, and a report of anything malformed.
|
|
9
|
+
- **Report:** observed interruptions (limit hits, mid-task hits, elapsed lockout time, sessions not resumed), usage windows (last observed usage, peak), unattributed usage, and usage by model and repository. Projections are kept in a separate, labeled section: burn rate, and observed tokens at API list price beside your plan price.
|
|
10
|
+
- **`explain`:** the events behind any number, with file, line, and byte, added back up.
|
|
11
|
+
- **`report --save`:** dated text and JSON copies.
|
|
12
|
+
- **`report --save --html`:** also saves the report as one HTML page you open in a browser, beside the text and JSON copies. It draws the same numbers as charts, and clicking a bar, column, or point lists the events behind it. Everything is inside the file, so it works offline and makes no network requests. Like `explain` output, it includes session IDs.
|
|
13
|
+
- **`verify`:** runs the same comparison the audit makes against [ccusage](https://github.com/ryoppippi/ccusage), but on your own logs, and prints a verdict and any differing day, model and field. Nothing it prints is a path, a repository name, a session id, or prompt text, so the result can be shared without the data behind it. The one command that uses the network.
|
|
14
|
+
- **`uninstall --delete-data`:** removes the recorded data too, after saying what it held and that it cannot be undone. It removes only the files Nilometer wrote, and names anything it left alone.
|
|
15
|
+
- **Platforms:** macOS; Windows with Git for Windows; Linux, tested in CI.
|
|
16
|
+
- **Prices:** every model on Anthropic's pricing page has a dated, sourced row, including Opus 5.5 and Mythos 5.1, whose cache reads are priced at their own rates rather than their family's. A model that isn't on the page stays unpriced and is reported as such, never priced as $0.
|
|
17
|
+
- **Published to npm:** `npm install -g nilometer`, then `nilometer init`; or `npx nilometer <command>` without installing. `nilometer --version` prints the installed version. Every push now installs the packed package in an empty home on Linux, macOS and Windows, under Node.js 24 and 26: `init`, a status line reading, the package removed (the hook still records), ingest, report, and an `uninstall` that leaves `settings.json` byte-identical.
|
|
18
|
+
- **Nothing to compile:** the database is Node's built-in SQLite (`node:sqlite`) instead of the `better-sqlite3` native module, so installing downloads no binary and needs no compiler. Reports are unchanged: on the same database, both produce the same JSON. They take about 3% longer.
|
|
19
|
+
- **Node.js 26:** tested beside Node.js 24, on Linux, macOS and Windows. The SQLite inside Node.js 26 refused two of the interruption views ("nested too deep"). They now read each underlying view once, which gives the same numbers and halves the time those views take.
|
|
20
|
+
- **Node.js check:** every command checks the Node.js version first. On a version older than 24 it stops with a message saying what to install, where it used to crash with no explanation; on a version other than 24 or 26 it warns that the version isn't tested and runs.
|
|
21
|
+
- **Limit hits read structured fields:** on current Claude Code versions, the window and reset of a limit hit come from the structured `quotaLimits` fields in the log, with the message text as the fallback for older versions. Each hit records which source it used, and any disagreement between the two is reported.
|
|
22
|
+
- **Usage above 100% is kept:** a usage window reading above 100%, which Claude Code reports once a limit is passed, is recorded as a reading instead of being discarded as invalid, so peak and last observed usage show it.
|
|
23
|
+
- **Concurrent sessions no longer corrupt status line readings:** the hook appends each reading in a single write. Before, a reading written while other sessions' hooks ran could interleave with theirs, and both were lost (reported as malformed). Run `nilometer init` after updating to install the new hook.
|
|
24
|
+
- **The HTML page has tabs:**
|
|
25
|
+
- **Overview first:** a card for each observed chart, with its key numbers and a small picture of the chart.
|
|
26
|
+
- **Then one tab per chart:** usage windows, interruptions, by model, and by repository.
|
|
27
|
+
- **Projections in their own labelled group:** burn rate and observed tokens at API list price.
|
|
28
|
+
|
|
29
|
+
Usage by repository and burn rate are new to the page; before, only the text report showed them. Every number on a card still opens its events. Printing shows every tab, and without JavaScript the page reads top to bottom as before.
|
|
30
|
+
- **Interruptions per plan:**
|
|
31
|
+
- **Per plan period:** once you've entered a plan price with `plan-price set`, the report splits limit hits, lockouts, and elapsed lockout time by the plan in effect, so a count no longer mixes your old plan with your new one.
|
|
32
|
+
- **The HTML page** shows the split as a table on the Interruptions tab and as one lane per plan on the lockout chart.
|
|
33
|
+
- **`nilometer explain by-plan`** lists the events behind each row.
|
|
34
|
+
- **Mid-task interruptions count again:** current Claude Code versions write context lines (token reminders, memory, hook output) between a request and the tool result or prompt it answered. A limit hit's position now looks past them, so most hits that showed as "position unknown" are now counted as mid-task or at the start of a turn. A line of a kind Nilometer hasn't classified stops the lookup and leaves the position unknown. `nilometer explain mid-task` says how many lines each lookup passed.
|
|
35
|
+
- **Claude Code's own resumption is shown:**
|
|
36
|
+
- **What changes:** where Claude Code logged that it resumed a session by itself after a limit reset (an `auto-continuation` line), the report says so beside the lockout it followed. The page draws a ring at the end of that lockout's bar, and `nilometer explain lockout` lists the logged lines.
|
|
37
|
+
- **What stays:** nothing is labeled automatic from timing alone.
|
|
38
|
+
- **"Open" means not yet reset:** a usage window counts as open only until its reset time, as of when you run the report. Before, a window stayed "open" if no status line reading had come in since its reset, for example after a stretch of work in the VS Code extension, and burn rate kept projecting it. A note under the table says an open window's usage is as of its last reading.
|
|
39
|
+
- **No more 0-second lockouts from late readings:** a limit seen only in the status line is now counted only from readings taken before the window's reset. Before, a reading that arrived after the reset could add a lockout of 0 s.
|
|
40
|
+
- **Unattributed usage is no longer reported:** the section that looked for usage outside Claude Code (a rise between status line readings with no Claude Code request in between) was zero in every window of a month of real use. Each reading follows a request in its own session, so it can't see that usage separately. `nilometer explain unattributed` still lists it.
|
|
41
|
+
- **Refused requests are recorded:**
|
|
42
|
+
- **What changes:** when Claude Code logs that one model refused a request and another model retried it, that line is now its own event, keeping both models and the refused request's id, and `ingest` counts it. Before, it was filed with the lines Nilometer ignores.
|
|
43
|
+
- **What doesn't:** the refused request leaves no token count in the log, so nothing is added to tokens or cost.
|
|
44
|
+
- **When it takes effect:** the next `ingest` re-reads stored lines once to apply this.
|
|
45
|
+
- **A missing status line hook is reported:** when your status line names a Nilometer hook file that doesn't exist, for example after moving to a new machine, `report` warns above the numbers and on the page, and says to run `nilometer init`. Until then no usage readings are recorded, and nothing else shows it: the hook can't log an error if it never starts. `report` reads `settings.json` and never writes it.
|
|
46
|
+
- **`report --save --html --open`:** opens the saved page in your browser. If it can't, for example over SSH, it says where the page is and the command still succeeds.
|
|
47
|
+
- **`verify` names UTC days:** its days are UTC dates, as ccusage counts them. The output now says so, so in the evening the newest day no longer reads as "today" when it's tomorrow's local date.
|
|
48
|
+
- **Readings that change between requests are dated correctly:** newer Claude Code versions sometimes update an idle session's status line with the account's later usage. Such a value used to be dated back to that session's last request, which could draw a late-week number days early on the chart, and it replaced the request's own value. It's now its own reading, dated when it was captured.
|
|
49
|
+
- **Weekly windows at 75% and 90%:** the report and the page now list, for each weekly window, the first reading at or above 75% and 90%, and how long was left before the reset. A week that came close to the limit without hitting it used to show no interruptions and nothing else. `nilometer explain crossings` lists the readings.
|
|
50
|
+
- **Stale readings set aside:** about 1% of status line readings come in lower than an earlier reading in the same window, which can't be true, since usage within a window only rises. Such a reading could become a window's "last observed usage" (a window locked at 100% showed 73%), and it made the usage lines dip. It's now marked stale: counted under the windows table, listed in `nilometer explain headroom`, drawn as a hollow point off the line, and never used as the last observed value.
|
|
51
|
+
- **Lockouts first:** the interruptions section now leads with lockouts, and each says how many requests it refused and from how many sessions, for example "13 refused requests in 3 sessions". Before, those 13 read as "13 hits", which looked like 13 separate limits. The total reads "Limit hits: 35 in 8 lockouts".
|
|
52
|
+
- **Page layout fixes:**
|
|
53
|
+
- Lockout labels no longer run off the chart; where they don't fit beside the bars, they go under them.
|
|
54
|
+
- A time axis uses one style ("1 h 30 m", never "90 m" beside "2 h").
|
|
55
|
+
- A window with a lockout gets a plain marker instead of a number too small to read.
|
|
56
|
+
- Number columns line up under their headers, and tables scroll in their box on a narrow screen instead of stacking their text.
|
|
57
|
+
- The Overview's small charts are clean pictures without unreadable text.
|
|
58
|
+
- Labels no longer repeat, and lines that only say "0" are left off the page; the text report keeps them.
|
|
59
|
+
- The repository chart names the top 20 instead of 10.
|
|
60
|
+
- The first section is now "Where the numbers come from", with a sentence on why usage percentages start later than the session logs.
|
|
61
|
+
- **Less text on the page:** each section's explanations now sit behind a small "i" beside its heading, which opens on hover, keyboard focus or a tap, opens along the bottom on a narrow screen, and prints in full. Caveats that change what a number means stay beside it. The page's visible paragraph text is about 30% shorter.
|
|
62
|
+
- **Repository aliases:** `nilometer alias set <old-path> <new-path>` counts a renamed or moved project's old path under its new one, so it shows as one repository. The report lists each alias you entered under the repository table, and `nilometer explain by-repo` marks each request an alias moved. `alias list` and `alias remove` show and undo them.
|
|
63
|
+
- **Install guide:** [`docs/requirements.md`](docs/requirements.md) explains how to check for and install everything Nilometer needs, including how to get Node.js 24 and not a newer release.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Fariñas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,463 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Nilometer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**See what your Claude subscription is actually costing you: in interruptions when it's too small, in unused capacity when it's too big.**
|
|
4
|
+
|
|
5
|
+
*Named after the ancient Egyptian nilometers: columns that recorded how high the Nile actually rose each year, a record of what happened that was then used to judge what it cost.*
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What this is
|
|
10
|
+
|
|
11
|
+
A local tool that reads Claude Code's subscription telemetry and session logs. It reports how often usage limits interrupt your work, how much of each usage window you actually use, where your tokens go (by repository and by model), and what those tokens would cost at API list price.
|
|
12
|
+
|
|
13
|
+
Every observed number comes from an event that actually happened. Estimates are allowed only when derived from those observations, and they are always labeled. The tool does not tell you which plan to buy, and it does not score your productivity.
|
|
14
|
+
|
|
15
|
+
Nilometer is an independent tool for Claude Code. It isn't made, endorsed, or supported by Anthropic.
|
|
16
|
+
|
|
17
|
+
## Why
|
|
18
|
+
|
|
19
|
+
Claude subscriptions come in tiers, and you can pick wrong in either direction.
|
|
20
|
+
|
|
21
|
+
- **Too small:** long Claude Code sessions hit the usage limit, and work stops until the window resets.
|
|
22
|
+
- **Too big:** you pay every month for capacity you never touch.
|
|
23
|
+
|
|
24
|
+
People usually make this decision on vibes, because nothing shows how much interruption they're absorbing or how much headroom they're paying for.
|
|
25
|
+
|
|
26
|
+
This tool doesn't answer *"which plan should I be on?"* It shows the observed cost of your current plan in both directions and lets you judge it yourself.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Measurement Principles
|
|
31
|
+
|
|
32
|
+
These limit what the tool is allowed to display. If a proposed metric breaks them, it gets reworked, labeled as a projection, or dropped.
|
|
33
|
+
|
|
34
|
+
1. **Every metric states exactly what was observed, and nothing more.** Elapsed lockout time is elapsed lockout time, not "time lost." What you did during the lockout is unknown, so the tool does not claim to know it.
|
|
35
|
+
2. **Observations and projections are visually and structurally separate.** Projections are allowed, but they must be derived from observations, labeled as estimates, and kept out of the headline.
|
|
36
|
+
3. **No counterfactuals.** The tool will not report what would have happened under a different plan, session structure, or prompting style. *Repricing is not a counterfactual:* showing your observed tokens at API list price keeps the usage fixed and changes only the price, so it's allowed as a labeled projection. Claiming you *would have spent* that amount on the API is not allowed, because paying per token changes how people work.
|
|
37
|
+
4. **Every number is traceable.** Any metric can show the underlying events it was computed from (in the CLI: `nilometer explain <metric>`).
|
|
38
|
+
5. **Every metric shows its date range.** Data sources start at different dates (see [History](#history)), so each number states the time span it covers.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## How it works
|
|
43
|
+
|
|
44
|
+
Two local sources feed one SQLite database.
|
|
45
|
+
|
|
46
|
+
### 1. Status line payload (subscription state)
|
|
47
|
+
|
|
48
|
+
Claude Code runs a status line script after every assistant turn and passes it a JSON payload on stdin. For Claude.ai Pro/Max subscribers, that payload includes a `rate_limits` object:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{ "session_id": "abc123...",
|
|
52
|
+
"transcript_path": "/path/to/transcript.jsonl",
|
|
53
|
+
"rate_limits": {
|
|
54
|
+
"five_hour": { "used_percentage": 42, "resets_at": 1774036800 },
|
|
55
|
+
"seven_day": { "used_percentage": 86, "resets_at": 1774580400 } } }
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This is exact, first-party subscription state: no scraping of output, no estimating tokens, no reverse-engineered limits. **Claude Code runs the script itself, so there is no daemon and no background process to keep alive.** The status line script is the collector.
|
|
59
|
+
|
|
60
|
+
Things about the payload that matter here:
|
|
61
|
+
|
|
62
|
+
- **`session_id` and `transcript_path`** point each rate-limit reading at the matching session log, so readings don't have to be matched to sessions by timestamp.
|
|
63
|
+
- **A window disappears once its `resets_at` passes.** A missing window means "reset," not "no data."
|
|
64
|
+
- **The numbers are from the session's last API response.** *Observed (2026-09-14/15):* re-rendering the status line (a refresh timer, a window reset) repeats them unchanged until that session makes another request, even while other sessions use the account. Nilometer dates each reading by that response, not by when it was captured, and counts repeats once ([D-044](decisions.md)). A terminal left open on a refresh timer therefore usually can't record usage from the VS Code extension. *Since Claude Code 2.1.281* an idle session sometimes does show the account's later total; a value that changed without a new request is dated when it was captured, as its own reading ([D-086](decisions.md)).
|
|
65
|
+
- **`used_percentage` arrives in whole-number steps** (sometimes with float noise, e.g. `7.000000000000001`), so small amounts of usage show as 0%.
|
|
66
|
+
- **`rate_limits` only appears after the first API response in a session.**
|
|
67
|
+
- **The payload also carries `cost.total_cost_usd`**, Claude Code's own list-price estimate for the session. It's stored with each reading as a possible cross-check on the tool's cost math, never as a source. No check uses it yet.
|
|
68
|
+
- **A third window, `spend_limit`,** only appears for organizations behind a Claude apps gateway with spend limits. It's ignored for personal subscriptions.
|
|
69
|
+
|
|
70
|
+
### 2. Session logs (tokens, model, attribution)
|
|
71
|
+
|
|
72
|
+
Claude Code writes one JSONL file per session under `~/.claude/projects/`. For each request, the log records:
|
|
73
|
+
|
|
74
|
+
- **Request details:** the model and a `requestId`.
|
|
75
|
+
- **Full token breakdown:** input, output, cache writes (5-minute and 1-hour), and cache reads.
|
|
76
|
+
- **Context:** working directory, git branch, and timestamp.
|
|
77
|
+
|
|
78
|
+
The payload carries none of this.
|
|
79
|
+
|
|
80
|
+
When a usage limit stops a turn, Claude Code also writes a synthetic log line with `error: "rate_limit"`. That makes limit hits observable in logs written before this tool was installed.
|
|
81
|
+
|
|
82
|
+
Two ingestion rules:
|
|
83
|
+
|
|
84
|
+
- **Deduplicate by session and message ID** ([D-001](decisions.md)). One API response is written as several cumulative log lines. Summing them overcounts tokens, and keeping the first undercounts; the largest record wins.
|
|
85
|
+
- **Copy logs before Claude Code deletes them.** Logs are deleted after 30 days by default. The database is the long-term record; the logs are not.
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Claude Code ──(stdin JSON, every turn)──▶ statusline script ──▶ SQLite ◀── pricing table
|
|
89
|
+
│ ▲ (dated, per model)
|
|
90
|
+
└──(session logs on disk)──▶ ingestion (backfill at init) ───┘
|
|
91
|
+
│
|
|
92
|
+
viewer (on demand)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Chaining, not clobbering.** Many developers already run a custom status line. `init` detects an existing `statusLine` entry, wraps it, passes its output through unchanged, and logs the payload on the side. The tool is a passive tap on a hook you were already running. If you don't have one, you get a minimal default.
|
|
96
|
+
|
|
97
|
+
### History
|
|
98
|
+
|
|
99
|
+
`init` backfills from every session log still on disk. Token, model, repository, and cost metrics start as far back as those logs go; Claude Code keeps 30 days by default. So do interruption metrics, from the logged limit-hit lines ([D-004](decisions.md)). Elapsed lockout time is backfilled only where the logged reset time can be parsed.
|
|
100
|
+
|
|
101
|
+
Usage percentages exist only from the moment the collector is installed, and only from Claude Code sessions in a terminal ([D-029](decisions.md)). Claude Code doesn't write them anywhere else, so they can't be backfilled. Headroom, peak usage, and burn rate start at `init`. Principle 5 keeps those differing start dates visible.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## MVP scope
|
|
106
|
+
|
|
107
|
+
One screen. No daemon, no desktop app, no live updating. Every metric below is retrospective.
|
|
108
|
+
|
|
109
|
+
### Observed: "Is my plan too small?"
|
|
110
|
+
|
|
111
|
+
| Metric | Source |
|
|
112
|
+
|---|---|
|
|
113
|
+
| Lockouts and limit hits | Logged `error: "rate_limit"` lines; `used_percentage` reaching 100 after `init`. A hit seen in both counts once ([D-023](decisions.md)). Lockouts lead: a lockout refuses every request until its reset, so one can hold many hits, each logged hit a refused request, and each lockout says how many requests it refused and from how many sessions ([D-090](decisions.md)). Once a plan price is entered, hits, lockouts, and elapsed lockout time are also shown per plan period, so a count never mixes two plans without saying so ([D-075](decisions.md)) |
|
|
114
|
+
| Mid-task interruptions | The stopped request was answering a tool result, so the model was mid-agentic-loop rather than at a turn boundary. Hits whose position can't be determined are counted separately ([D-021](decisions.md)). Context lines Claude Code inserts between a request and what it answered (token reminders, memory, hook output) are looked past; a line of an unknown kind stops the lookup, leaving the position unknown ([D-076](decisions.md)) |
|
|
115
|
+
| Elapsed lockout time | Limit hit → reset time, with overlapping hits counted once. **Not** a claim about lost productivity. Shown beside it: reset → next Claude Code request ([D-023](decisions.md)), and, where Claude Code logged that it resumed a session by itself after the reset, how many sessions it resumed ([D-077](decisions.md)) |
|
|
116
|
+
| Weekly windows at 75% and 90% | The first status line reading at or above each level in each weekly window, and the time from it to the reset. A week that came close without a hit shows here and nowhere else. It's a reading, not a reason: why usage changed afterwards isn't inferred. The level may have been passed earlier, between readings, so the time left is a lower bound ([D-087](decisions.md)) |
|
|
117
|
+
| Sessions not resumed | No further request in the session after a limit was hit. May be abandonment; may be a deliberate stop. Reported without interpretation; sessions whose reset came after the logs end are counted separately ([D-022](decisions.md)) |
|
|
118
|
+
|
|
119
|
+
### Observed: "Am I paying for more than I use?"
|
|
120
|
+
|
|
121
|
+
| Metric | Source |
|
|
122
|
+
|---|---|
|
|
123
|
+
| Window headroom | Last observed `used_percentage` before each window's `resets_at`, shown with the time of that reading. Budget used after the last Claude Code turn isn't seen, so this is a lower bound ([D-024](decisions.md)). A reading more than 1 point below an earlier one in the same window is stale, since usage within a window only rises: it's counted and listed, but never the last observed value, and it's drawn off the line ([D-089](decisions.md)) |
|
|
124
|
+
| Peak usage per window | Highest observed `used_percentage` in each 5-hour and 7-day window ([D-024](decisions.md)) |
|
|
125
|
+
|
|
126
|
+
### Observed: "Where does it go?"
|
|
127
|
+
|
|
128
|
+
| Metric | Source |
|
|
129
|
+
|---|---|
|
|
130
|
+
| Usage by repository | Session logs, grouped by git repository: worktrees and subfolders count as their repository, and a folder that no longer exists keeps its last known repository ([D-010](decisions.md), [D-028](decisions.md)) |
|
|
131
|
+
| Usage by model | Session logs, grouped by model (e.g. Opus vs. Sonnet), by token type |
|
|
132
|
+
|
|
133
|
+
### Projected (labeled as such)
|
|
134
|
+
|
|
135
|
+
- **Burn rate** → estimated time until the current window's limit is reached, at the window's average rate since it began. The premise, that the window started at 0% and the rate continues, is shown beside the number ([D-026](decisions.md)).
|
|
136
|
+
- **API-equivalent cost** → your observed tokens priced at Anthropic API list price, compared against what the plan cost you over the same period.
|
|
137
|
+
- **How each request is priced:** per model and per token type (input, output, 5-minute and 1-hour cache writes, cache reads), at the price in effect *on that request's date*. The pricing table is dated because prices change.
|
|
138
|
+
- **Display label:** *"Your observed tokens at API list price."* Never *"what you would have spent on the API"* (principle 3).
|
|
139
|
+
- **Scope:** covers Claude Code usage only. Web, mobile, and Claude Design usage produces no token records.
|
|
140
|
+
- **Currency:** USD, Anthropic's billing currency. Plan price is entered as the plan's USD list price, not a local-currency charge, so both sides of the comparison use the same currency.
|
|
141
|
+
- **Period:** calendar months in the machine's timezone. The plan price is entered with `plan-price set <YYYY-MM> <usd> --name <plan>` and applies from that month on ([D-027](decisions.md)).
|
|
142
|
+
|
|
143
|
+
### Explicitly out of scope
|
|
144
|
+
|
|
145
|
+
Auto-resume (now built into Claude Code) · multi-assistant support · prompt-quality or "efficiency" scoring · flow-interruption metrics · plugin architecture · cloud sync · recommendations engine
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## What this cannot see
|
|
150
|
+
|
|
151
|
+
`rate_limits` is account-wide, so usage from the Claude web app, mobile app, and Claude Design shows up in the totals. But those surfaces leave nothing on disk to read.
|
|
152
|
+
|
|
153
|
+
The tool can't separate that usage out. The status line is read only when Claude Code makes a request, and each reading already includes whatever was used elsewhere, so the two arrive mixed in the same percentages ([D-079](decisions.md)). It can't report what that usage was, and it can't price it. Any breakdown or cost of non-CLI usage would break principle 1.
|
|
154
|
+
|
|
155
|
+
The same goes for Claude Code on your other computers. Their session logs stay on those machines, so their tokens and limit hits aren't read here. Their usage still counts toward the account-wide percentages, so the headroom and peaks shown here include it.
|
|
156
|
+
|
|
157
|
+
Rate-limit state is only sampled when Claude Code runs a turn. Between turns, the tool is blind. This is why headroom is a lower bound.
|
|
158
|
+
|
|
159
|
+
The status line only runs when Claude Code runs in a terminal. *Observed (2026-09-13):* the VS Code extension doesn't run it, so turns made there leave no usage-percentage readings. Their tokens, costs, and limit hits are still read from the session logs. Headroom, peak, and burn rate cover terminal sessions only ([D-029](decisions.md)).
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Auto-resume
|
|
164
|
+
|
|
165
|
+
Built into Claude Code since v2.1.234 (August 2026). Claude Code continues automatically when a session limit resets. It's on by default and set in `/config`, and the Desktop app has its own checkbox. This tool doesn't do auto-resume and doesn't need to.
|
|
166
|
+
|
|
167
|
+
It does change what two metrics mean. A session that continues right after a reset may have been resumed automatically, not by you. **Sessions not resumed** and **elapsed lockout time** therefore report *activity*, not whether you personally came back.
|
|
168
|
+
|
|
169
|
+
Claude Code marks its own continuation in the log: a line with `origin.kind: "auto-continuation"` (observed from v2.1.281, [D-022](decisions.md)). Where that line exists, the report says the session was **resumed by Claude Code (logged auto-continuation)**, beside the lockout it followed ([D-077](decisions.md)). Nothing is labeled automatic from timing alone: a workflow that restarts its subagents writes no such line, so its sessions aren't labeled.
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Tech stack
|
|
174
|
+
|
|
175
|
+
- TypeScript on Node.js 24 LTS, also tested on 26 ([D-015](decisions.md), [D-094](decisions.md))
|
|
176
|
+
- SQLite via Node's built-in `node:sqlite`, so installing compiles nothing ([D-093](decisions.md))
|
|
177
|
+
- Tests, lint, and docstring rules run by one `npm run audit`, locally and in GitHub Actions ([D-016](decisions.md))
|
|
178
|
+
|
|
179
|
+
**Analysis logic lives in SQL, not in application code.** Views and queries do the work, not rows fetched and filtered in JS. This keeps the core portable if a Rust-backed desktop client ever happens (see below).
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
core/ schema (numbered SQL migrations), ingestion, pricing table, plan prices. No UI.
|
|
183
|
+
hooks/ status line collector script (POSIX sh).
|
|
184
|
+
viewer/ thin: formats the SQL views as the report, JSON, and explain output.
|
|
185
|
+
cli/ command wiring.
|
|
186
|
+
fixtures/ synthetic session logs with hand-computed expected results.
|
|
187
|
+
scripts/ audit runner, fixture generator, Python reference implementation, ccusage comparison.
|
|
188
|
+
tests/ unit, integration, and wording tests.
|
|
189
|
+
docs/ development standards, the audit, and the build history.
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Install and use
|
|
195
|
+
|
|
196
|
+
There's no GUI, installer, or auto-updater. Configuration was always the hard part, not packaging, and a desktop app writes to `settings.json` no better than a CLI does.
|
|
197
|
+
|
|
198
|
+
**Requirements:** Node.js 24 or 26, and Claude Code signed in with a Pro or Max subscription. On Windows, also Git for Windows. **If you don't have these yet, or aren't sure, start with [`docs/requirements.md`](docs/requirements.md)**: how to check each one and how to install it, including how to get Node.js 24 or 26 and not another version. Nilometer stops with a message on a Node.js older than 24, and warns on any other untested one ([D-066](decisions.md), [D-094](decisions.md)).
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
npm install -g nilometer
|
|
202
|
+
nilometer init
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Installing compiles nothing: the database is the SQLite built into Node.js ([D-093](decisions.md)). To try a command without installing, use `npx nilometer <command>`.
|
|
206
|
+
|
|
207
|
+
- **macOS:** tested.
|
|
208
|
+
- **Linux:** the test suite and an install from the package pass in CI.
|
|
209
|
+
- **Windows:** tested with **Git for Windows** installed ([D-049](decisions.md)). Claude Code runs the status line through Git Bash when Git for Windows is present, so the shell hook works unchanged; without it, Claude Code uses PowerShell and the hook can't run. Run the commands below in Git Bash. On Windows the hook adds roughly 110 ms per reply at p95, against about 35 ms on macOS (2026-09-17).
|
|
210
|
+
|
|
211
|
+
**Updating:** `npm install -g nilometer@latest`, then `nilometer init` again. It's safe to repeat, and it refreshes the hook when a release changes it.
|
|
212
|
+
|
|
213
|
+
**From a clone,** to work on Nilometer or run its audit, which also needs git, Python 3, gitleaks, and shellcheck:
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
git clone https://github.com/MVFarinas/Nilometer.git nilometer
|
|
217
|
+
cd nilometer
|
|
218
|
+
npm ci
|
|
219
|
+
npm run build
|
|
220
|
+
npm link # optional: puts the `nilometer` command on your PATH
|
|
221
|
+
nilometer init # or, without npm link: npm start -- init
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
On Node 24 before 24.15, `npm ci` prints `EBADENGINE` warnings for three lint plugins that ask for a newer Node. They're development dependencies, they don't affect Nilometer, and the install still succeeds.
|
|
225
|
+
|
|
226
|
+
`init` registers the status line hook in `~/.claude/settings.json`. If you already have a status line, it keeps running unchanged. It backs up the settings file, then backfills from every session log still on disk.
|
|
227
|
+
|
|
228
|
+
| Command | What it does |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `nilometer init` | Install the hook (wrapping any existing status line), then backfill from session logs |
|
|
231
|
+
| `nilometer ingest` | Read new session log lines and status line readings into the database. `--full` rereads everything; `--json` prints the outcome as JSON |
|
|
232
|
+
| `nilometer report` | Print what was observed, then projections in a separate section. `--json` prints unrounded rows and every label. `--save` also writes a dated copy (`report_YYYY-MM-DD_HHMMSS.txt` and `.json`) to `~/.local/share/nilometer/reports/`; `--save --html` adds a self-contained `.html` page with the same name, which works offline and makes no network requests. The page opens on an overview, has one tab per chart, and keeps the projections in their own labelled tabs. `--save --html --open` also opens the saved page in your browser; if it can't, the command says where the page is and still succeeds |
|
|
233
|
+
| `nilometer explain <metric>` | List the events behind a number in the report, with the file, line, and byte each came from, and add them back up. Metrics: `limit-hits`, `mid-task`, `lockout`, `by-plan`, `not-resumed`, `headroom`, `peak`, `crossings`, `unattributed`, `by-model`, `by-repo`, `burn-rate`, `api-list-price` |
|
|
234
|
+
| `nilometer plan-price set <YYYY-MM> <usd> --name <plan>` | Record your plan's USD list price per month from that month on, shown beside API list price. `plan-price list` shows what's entered |
|
|
235
|
+
| `nilometer alias set <old-path> <new-path>` | Count a renamed or moved project's old path under its new one, so it shows as one repository. The report lists every alias you entered under the repository table. `alias list` shows them; `alias remove <old-path>` undoes one ([D-092](decisions.md)) |
|
|
236
|
+
| `nilometer uninstall` | Remove the hook and restore the status line setting it replaced. Recorded data is kept |
|
|
237
|
+
| `nilometer --version` | Print the installed version |
|
|
238
|
+
|
|
239
|
+
**Removing it:** `nilometer uninstall` restores your previous status line and keeps the recorded data; then `npm uninstall -g nilometer` removes the command. To delete the data too, remove the data directory below. It may hold session history that Claude Code has already deleted, so that history is then gone for good.
|
|
240
|
+
|
|
241
|
+
**Where data lives:**
|
|
242
|
+
- **Database and status line readings:** `~/.local/share/nilometer`. Override it with `NILOMETER_HOME` or `--data-dir`. Everything there is readable only by your account, since the database holds copies of your session logs ([D-043](decisions.md)). On Windows, file modes don't exist: the folder, under your user profile, relies on that profile's permissions instead ([D-049](decisions.md)).
|
|
243
|
+
- **Session logs:** read from `~/.claude`, or from every entry of `CLAUDE_CONFIG_DIR`.
|
|
244
|
+
|
|
245
|
+
### Privacy
|
|
246
|
+
|
|
247
|
+
- **Everything stays on your computer.** Nilometer reads Claude Code's session logs and status line payload, and copies them into a local SQLite database. It makes no network requests when it runs, and it sends no telemetry.
|
|
248
|
+
- **The database holds copies of your session logs:** prompts, code, and file paths. That's why the data directory is readable only by your account.
|
|
249
|
+
- **Reports contain spend figures and project names.** Think before sharing one. `nilometer explain` output also names session files and IDs.
|
|
250
|
+
|
|
251
|
+
### Checking the numbers against another tool
|
|
252
|
+
|
|
253
|
+
[ccusage](https://github.com/ryoppippi/ccusage) reads the same session logs and is the reference this
|
|
254
|
+
project checks its cost math against. The audit runs that comparison over committed fixtures; this
|
|
255
|
+
runs it over **your** logs:
|
|
256
|
+
|
|
257
|
+
```sh
|
|
258
|
+
nilometer verify
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
It prints how many days were compared and any day, model and field where the two disagree — and
|
|
262
|
+
nothing else. Days are UTC dates on both sides, so in the evening the newest one can be tomorrow's
|
|
263
|
+
local date; the current UTC day is never compared, because both tools are still reading it. No paths, no repository names, no session ids, no prompt text, so the result can be
|
|
264
|
+
sent to someone without sending your data.
|
|
265
|
+
|
|
266
|
+
Two things it will tell you about rather than call a difference. Requests whose log Claude Code has
|
|
267
|
+
since deleted are counted and left out, because ccusage reads the files that are there now and this
|
|
268
|
+
tool keeps what it read ([D-002](decisions.md)). And only token counts are compared, not cost: cost
|
|
269
|
+
depends on both tools' price tables agreeing, which is a separate question from whether the logs
|
|
270
|
+
were read the same way.
|
|
271
|
+
|
|
272
|
+
**This is the one command that uses the network.** It fetches a pinned ccusage through `npx`.
|
|
273
|
+
Nothing is uploaded, and nothing it downloads is used by the rest of the tool.
|
|
274
|
+
|
|
275
|
+
### Removing it, and deleting the data
|
|
276
|
+
|
|
277
|
+
`nilometer uninstall` takes the hook out of `settings.json` and puts back the status line command it
|
|
278
|
+
replaced, backing the file up first. **It keeps your recorded data**, because that data outlives the
|
|
279
|
+
logs it came from.
|
|
280
|
+
|
|
281
|
+
To remove the data too:
|
|
282
|
+
|
|
283
|
+
```sh
|
|
284
|
+
nilometer uninstall --delete-data
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
That deletes the database, the status line spool, the hook's error log, the install record, and any
|
|
288
|
+
saved reports, then prints what it held — how many requests and readings, and the span they covered —
|
|
289
|
+
because **this can't be undone.** Your database holds copies of session logs Claude Code has already
|
|
290
|
+
deleted under its own 30-day cleanup, and nothing else has them.
|
|
291
|
+
|
|
292
|
+
It removes only the files Nilometer writes. If you pointed `--data-dir` at a folder holding anything
|
|
293
|
+
else, those files stay and are named in the output, and the folder stays with them.
|
|
294
|
+
|
|
295
|
+
### Keeping it current
|
|
296
|
+
|
|
297
|
+
Status line readings are recorded on every terminal turn, whether or not you ingest, so nothing is lost between runs. The one deadline is Claude Code deleting session logs after 30 days by default. **Ingest at least weekly,** or schedule it. With cron, every 6 hours:
|
|
298
|
+
|
|
299
|
+
```sh
|
|
300
|
+
crontab -e
|
|
301
|
+
# add, with your own paths:
|
|
302
|
+
0 */6 * * * <node 24 binary> <nilometer checkout>/dist/cli/main.cli.js ingest >/dev/null 2>&1
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
cron doesn't load your shell's Node version manager, so give the full path to a Node 24 binary. With fnm, that's `~/.local/share/fnm/node-versions/v24.<x>/installation/bin/node`, and it changes when you install a new Node 24 release. `nilometer` on your PATH may resolve to a different Node inside cron.
|
|
306
|
+
|
|
307
|
+
Run `nilometer ingest` before `nilometer report` to include the latest turns. Status line metrics start filling in once Claude Code has run a few turns **in a terminal** with the hook installed; the VS Code extension doesn't run the status line. Ingest at least weekly, since Claude Code deletes session logs after 30 days by default.
|
|
308
|
+
|
|
309
|
+
If your status line still names a Nilometer hook file that no longer exists, for example after moving to a new machine, `report` prints a warning above the numbers and on the page, because no readings are being recorded. Run `nilometer init` again to fix it. `report` only reads `settings.json`; it never changes it.
|
|
310
|
+
|
|
311
|
+
### Sample output
|
|
312
|
+
|
|
313
|
+
From the synthetic test fixture (`tests/unit/viewer/fixture.ts`), not real usage:
|
|
314
|
+
|
|
315
|
+
```text
|
|
316
|
+
Claude subscription usage
|
|
317
|
+
Times are in America/Chicago. Last ingest: 2026-09-12 19:00. Database: ~/.local/share/nilometer/usage.db
|
|
318
|
+
|
|
319
|
+
OBSERVED
|
|
320
|
+
|
|
321
|
+
Interruptions
|
|
322
|
+
Covers: session logs 2026-08-20 10:00 to 2026-09-02 15:00 (America/Chicago); status line 2026-09-02 09:00–14:00 (America/Chicago)
|
|
323
|
+
Elapsed lockout time (first limit hit to the window's reset, overlapping lockouts counted once): 1 h 59 m across 1 lockout
|
|
324
|
+
Limit hits with an unknown reset time, in no lockout: 1
|
|
325
|
+
2026-09-01 09:00 to 2026-09-01 11:00: 1 h 59 m, 1 refused request in 1 session; reset to next Claude Code request: 30 m 5 s
|
|
326
|
+
Limit hits: 2 in 1 lockout
|
|
327
|
+
Each logged hit is a request the limit refused. A lockout refuses every request until its reset, so one lockout can hold several hits.
|
|
328
|
+
5-hour window: 1 · weekly window: 1 · window unknown: 0
|
|
329
|
+
From the session logs: 2 · seen only in the status line: 0
|
|
330
|
+
Mid-task interruptions (the refused request was answering a tool result): 1
|
|
331
|
+
At the start of a turn: 1 · position unknown: 0
|
|
332
|
+
Sessions with no further request after a limit hit: 1 of 2 sessions with a hit
|
|
333
|
+
Reset came after the logs end: 0 · reset time unknown: 1
|
|
334
|
+
A session that continued after its reset may have been resumed automatically; these counts are requests, not who made them.
|
|
335
|
+
|
|
336
|
+
Usage windows
|
|
337
|
+
Covers: status line 2026-09-02 09:00–14:00 (America/Chicago). Readings start when the collector is installed.
|
|
338
|
+
Window Resets Last observed usage Reading at Peak Readings Status
|
|
339
|
+
------ ---------------- ------------------- ---------------- ----- -------- ------
|
|
340
|
+
weekly 2026-09-06 19:00 15% 2026-09-02 14:00 15% 3 open
|
|
341
|
+
5-hour 2026-09-02 17:00 40% 2026-09-02 13:00 40% 1 open
|
|
342
|
+
5-hour 2026-09-02 12:00 35.5% 2026-09-02 10:00 35.5% 2 reset
|
|
343
|
+
Last observed usage is a lower bound: usage after the last Claude Code turn isn't seen.
|
|
344
|
+
No weekly window has a reading at or above 75%.
|
|
345
|
+
|
|
346
|
+
…
|
|
347
|
+
|
|
348
|
+
PROJECTED: estimates derived from the observations above, not observations
|
|
349
|
+
|
|
350
|
+
Burn rate
|
|
351
|
+
Covers: status line 2026-09-02 09:00–14:00 (America/Chicago)
|
|
352
|
+
Premise: each window's usage started at 0% when it began, and its average rate since then continues.
|
|
353
|
+
weekly window resetting 2026-09-06 19:00: 15% at 2026-09-02 14:00; projected 0.2 percentage points per hour; limit projected for 2026-09-18 09:40, after the reset.
|
|
354
|
+
5-hour window resetting 2026-09-02 17:00: 40% at 2026-09-02 13:00; projected 40 percentage points per hour; limit projected for 2026-09-02 14:30, before the reset.
|
|
355
|
+
|
|
356
|
+
Observed tokens at API list price, by month
|
|
357
|
+
Months in America/Chicago. Covers Claude Code only: web, mobile, and Claude Design usage leaves no token records.
|
|
358
|
+
Month At API list price Plan price entered Priced requests Unpriced requests Covers
|
|
359
|
+
------- ----------------- ------------------------------- --------------- ----------------- ------------------------------------
|
|
360
|
+
2026-08 less than $0.01 none entered 1 0 2026-08-20 10:00 to 2026-08-20 10:00
|
|
361
|
+
2026-09 $0.22 Example plan, $100.00 per month 2 1 2026-09-01 09:00 to 2026-09-02 10:00
|
|
362
|
+
Rates were read from Anthropic's pricing page on 2026-09-13; 3 requests dated before that are priced at those rates.
|
|
363
|
+
Requests with cache writes of unrecorded duration, priced at the 5-minute rate (their cost is a lower bound): 1
|
|
364
|
+
Unpriced requests have no verified rate and are left out of the amounts, not counted as $0.
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## Prior art
|
|
370
|
+
|
|
371
|
+
Token and cost tracking for Claude Code is well covered. This tool exists for what those tools don't do.
|
|
372
|
+
|
|
373
|
+
| Tool | Covers |
|
|
374
|
+
|---|---|
|
|
375
|
+
| [ccusage](https://github.com/ccusage/ccusage) | API-equivalent cost from session logs by day, session, and 5-hour window; per-model and per-project breakdowns. Keeps no history of its own |
|
|
376
|
+
| [claude-usage](https://github.com/phuryn/claude-usage) | Local SQLite dashboard of tokens and API-equivalent cost by model and project |
|
|
377
|
+
| [Claude-Code-Usage-Monitor](https://github.com/Maciek-roboblog/Claude-Code-Usage-Monitor) | Live burn rate, limit-hit detection, forecasts; optional history that outlives the 30-day cleanup |
|
|
378
|
+
| [claude-usage-tracker](https://github.com/jimdawdy-hub/claude-usage-tracker) | Plan-vs-API cost comparison; reads usage % from claude.ai via browser cookies |
|
|
379
|
+
| [claude-usage-tracker](https://github.com/haasonsaas/claude-usage-tracker) (haasonsaas) | Usage and cost tracking with plan limits estimated from hour ranges, model suggestions, and efficiency scores |
|
|
380
|
+
|
|
381
|
+
**What's different here:**
|
|
382
|
+
|
|
383
|
+
- **Interruption history:** lockout durations, mid-task interruptions, sessions not resumed.
|
|
384
|
+
- **Headroom:** how much of each window you actually use, as the downgrade signal.
|
|
385
|
+
- **The measurement principles:** observed and projected numbers kept separate, and every number traceable to its events.
|
|
386
|
+
|
|
387
|
+
ccusage is the reference implementation for cost math. This tool's numbers should match it, apart from documented differences such as dated prices ([D-011](decisions.md)).
|
|
388
|
+
|
|
389
|
+
What these tools got wrong is recorded in [`decisions.md`](decisions.md), so it isn't repeated here.
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Roadmap
|
|
394
|
+
|
|
395
|
+
Nothing here is a commitment to a date. Each item needs its own decision record and an audit before
|
|
396
|
+
it ships, and some of these will be answered with "no" — the scope stays retrospective, one screen,
|
|
397
|
+
no daemon.
|
|
398
|
+
|
|
399
|
+
**v1: the one screen.** Status line collector, session log ingestion with backfill, SQLite store,
|
|
400
|
+
repository and model attribution, the observed metrics, burn-rate and API-equivalent-cost
|
|
401
|
+
projections. *Built and audited 2026-09-13, and in daily use since.* Windows followed on 2026-09-17
|
|
402
|
+
([D-049](decisions.md)), and a security and privacy review on 2026-09-18 ([D-050](decisions.md)).
|
|
403
|
+
|
|
404
|
+
**Next:**
|
|
405
|
+
|
|
406
|
+
- **Logs and status line readings counted separately in the ingest summary.** "This run: 2 files"
|
|
407
|
+
counts the spool of status line readings as one of the files, which reads as a miscount when you
|
|
408
|
+
have a single session log.
|
|
409
|
+
- **Install without cloning.** Installing means cloning this repository today. Publishing to npm is
|
|
410
|
+
planned, not scheduled; the data directory, database schema, and hook marker stay compatible
|
|
411
|
+
either way, so an existing install keeps its history.
|
|
412
|
+
- **Repository aliases**, so a project that was renamed or moved counts as one instead of two.
|
|
413
|
+
Nothing in the logs links an old path to a new one ([D-028](decisions.md)), so the alias has to
|
|
414
|
+
come from you.
|
|
415
|
+
- **Linux beyond CI.** The test suite passes on Linux in CI, but nobody has yet run the hook on a
|
|
416
|
+
Linux desktop for a week and compared the result.
|
|
417
|
+
- **A limit hit recorded while the tool is running.** The interruption metrics — the count, the
|
|
418
|
+
elapsed lockout time, the reset wording — have exactly one real event behind them: a single
|
|
419
|
+
5-hour limit hit from July 2026, backfilled from a session log, on a Claude Code version that
|
|
420
|
+
predates automatic resumption. The metric re-sums to it correctly, and the resumption after it was
|
|
421
|
+
a person, hours after the window had already reset. What hasn't been seen is a hit recorded while
|
|
422
|
+
the status line hook is running, or a plan that reaches its limits regularly; until then
|
|
423
|
+
[D-022](decisions.md) reports the logged fields as written and labels nothing as automatic. If you
|
|
424
|
+
run into limits and try this, the ingest summary counts and the report headings are the useful
|
|
425
|
+
thing to share; the reports themselves are yours and stay local.
|
|
426
|
+
|
|
427
|
+
**Under review once v1 has a month of real use:**
|
|
428
|
+
|
|
429
|
+
- **Unattributed usage was dropped from the report** at the Phase 8 review ([D-079](decisions.md)). It was
|
|
430
|
+
zero in every one of 49 windows, because every fresh status line reading follows a request in its own session
|
|
431
|
+
([D-044](decisions.md)). `nilometer explain unattributed` still lists its view.
|
|
432
|
+
- **Time-of-day interruption patterns**, and **notifications** — both only if a month of real
|
|
433
|
+
readings shows they'd answer something the one screen doesn't.
|
|
434
|
+
- **How the database grows** over a year of daily use, and whether it needs a size ceiling or
|
|
435
|
+
pruning. Nothing is deleted today, on purpose: session logs Claude Code has since removed survive
|
|
436
|
+
only in it.
|
|
437
|
+
|
|
438
|
+
**Considered and on hold (2026-09-13):** live usage in the status bar, a `watch` view, a tray app,
|
|
439
|
+
and multi-device sync. The scope stays retrospective until real use shows what to change
|
|
440
|
+
([D-031](decisions.md)).
|
|
441
|
+
|
|
442
|
+
**v2: Tauri, if and only if the tray icon justifies it.** The one feature a CLI genuinely cannot
|
|
443
|
+
offer is a menu-bar item showing live 5-hour usage. That, and nothing else, is the case for a
|
|
444
|
+
desktop client. With the schema settled and the queries already written in SQL, porting to
|
|
445
|
+
`rusqlite` is mechanical.
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## License
|
|
450
|
+
|
|
451
|
+
[MIT](LICENSE). Why that one, and what was rejected: [D-053](decisions.md).
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
## Non-goals
|
|
456
|
+
|
|
457
|
+
This is not Grafana for Claude, and it is not a productivity score. It reports what happened. Conclusions about plans, workflows, and habits are yours to draw.
|
|
458
|
+
|
|
459
|
+
---
|
|
460
|
+
|
|
461
|
+
## Development
|
|
462
|
+
|
|
463
|
+
The standards every change meets, the audit that checks them (`npm run audit`), and the build history are in [`docs/development.md`](docs/development.md). Why things are built the way they are, with the options rejected, is in [`decisions.md`](decisions.md).
|