tokenhud 0.1.0-rc.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zhuo Qiu
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 ADDED
@@ -0,0 +1,655 @@
1
+ # tokenhud
2
+
3
+ tokenhud is a terminal dashboard for your coding-agent usage. It reads the transcripts that
4
+ Claude Code and Codex write on your machine and shows, for every account, how close you are
5
+ to your subscription limits, what your usage would cost at API prices, and where it went:
6
+ by day, by model and by account. It keeps that history after the transcripts are deleted.
7
+ It also runs an MCP server, so a Claude Code agent can check its own account's limits and
8
+ wait for a reset instead of failing halfway through a task.
9
+
10
+ tokenhud succeeds [cc-usage](https://github.com/ZhuoQiuMcgill/cc-usage) and imports its
11
+ history on first run.
12
+
13
+ ![tokenhud's Overview on made-up accounts: a limits card per account, spend from the last hour to all time, a 24-hour cost chart, the top models and the week's limit events](docs-public/images/hero.png)
14
+
15
+ <sub>The Overview on made-up accounts. [Views](#views) explains every part of every screen.</sub>
16
+
17
+ ## Install
18
+
19
+ **With Bun** (recommended on Linux and macOS, once the packages are on npm):
20
+
21
+ ```sh
22
+ bun add -g tokenhud
23
+ ```
24
+
25
+ No Node is needed. Bun puts the `tokenhud` command in `~/.bun/bin`, which Bun's installer
26
+ adds to your PATH; `tokenhud doctor` says if it isn't there. Release candidates:
27
+ `bun add -g tokenhud@next`.
28
+
29
+ > **On Windows, Bun isn't supported yet.** `bun add -g tokenhud` installs a command that
30
+ > fails at once with `interpreter executable "/bin/sh" not found`. On Windows, use
31
+ > `npm i -g tokenhud` or the PowerShell installer (see **On Windows**, below).
32
+
33
+ **With npm:**
34
+
35
+ ```sh
36
+ npm install -g tokenhud # or run it without installing: npx tokenhud, or bunx tokenhud (not on Windows)
37
+ ```
38
+
39
+ Either way you get `tokenhud`, and the prebuilt binary for your machine in a platform
40
+ package (`@tokenhud/linux-x64` and so on), which the package manager installs as an optional
41
+ dependency: don't install with `--omit=optional`. On Linux and macOS the `tokenhud` command
42
+ is a small sh script that starts the binary: no JS runtime runs, so it needs neither Node
43
+ nor Bun, and nothing in the directory you run it in (a `.env`, a `bunfig.toml`) can reach
44
+ tokenhud.
45
+
46
+ **On Windows**, tokenhud is supported through npm (`npm i -g tokenhud`, Node 18 or later)
47
+ or the PowerShell installer, `install.ps1`, below; not through Bun. npm's install script
48
+ sets up a Node launcher for the command. If you installed with Bun, that command can't run,
49
+ so switch by hand: `bun remove -g tokenhud`, then `npm i -g tokenhud`. `tokenhud doctor`,
50
+ run from an npm or `install.ps1` copy, points out a bun install left behind.
51
+
52
+ On Windows the command needs that install script. Where scripts are off, the command fails
53
+ at once with `The system cannot find the path specified.` (`tokenhud update` says so too);
54
+ each of these puts it right:
55
+
56
+ | Scripts off by | Repair |
57
+ |---|---|
58
+ | `npm install -g --ignore-scripts`, or `ignore-scripts=true` in your `.npmrc` | `npm rebuild -g --ignore-scripts=false tokenhud` |
59
+ | `npx` with `ignore-scripts=true` | delete the `_npx` folder in npm's cache (`npm config get cache` shows where), then `npx --ignore-scripts=false tokenhud` |
60
+ | pnpm 10 or later, which runs no dependency's scripts unless approved | `pnpm approve-builds -g`, choose tokenhud, then `pnpm add -g tokenhud` |
61
+ | yarn 1, which writes the command before it runs install scripts | `yarn global remove tokenhud`, then install with npm |
62
+
63
+ **Without a package manager**, on Linux and macOS:
64
+
65
+ ```sh
66
+ curl -fsSL https://raw.githubusercontent.com/ZhuoQiuMcgill/tokenhud/main/install.sh | sh
67
+ ```
68
+
69
+ On **Windows** (PowerShell):
70
+
71
+ ```powershell
72
+ irm https://raw.githubusercontent.com/ZhuoQiuMcgill/tokenhud/main/install.ps1 | iex
73
+ ```
74
+
75
+ Both download the binary for your machine from
76
+ [GitHub Releases](https://github.com/ZhuoQiuMcgill/tokenhud/releases), check its SHA-256
77
+ against the release's `SHA256SUMS`, and install it without sudo or admin rights:
78
+ `install.sh` to `~/.local/bin/tokenhud` (it tells you if that isn't on your PATH),
79
+ `install.ps1` to `%LOCALAPPDATA%\tokenhud\bin\tokenhud.exe`, which it adds to your user
80
+ PATH. Running either again reinstalls, or updates to the newest release. Install one way
81
+ only: `tokenhud doctor` lists every tokenhud on your PATH (see [Update](#update-and-uninstall)
82
+ for switching).
83
+
84
+ | Variable | Effect |
85
+ |---|---|
86
+ | `TOKENHUD_VERSION` | Install this release, e.g. `0.1.0` or `0.1.0-rc.2`. Default: the latest stable release; until there is one, name a release candidate here |
87
+ | `TOKENHUD_INSTALL` | Install into this directory instead |
88
+ | `TOKENHUD_NO_MODIFY_PATH` | `1`: `install.ps1` leaves your PATH alone |
89
+
90
+ ```sh
91
+ curl -fsSL https://raw.githubusercontent.com/ZhuoQiuMcgill/tokenhud/main/install.sh | TOKENHUD_VERSION=0.1.0-rc.2 sh
92
+ ```
93
+
94
+ ```powershell
95
+ $env:TOKENHUD_VERSION = '0.1.0-rc.2'; irm https://raw.githubusercontent.com/ZhuoQiuMcgill/tokenhud/main/install.ps1 | iex
96
+ ```
97
+
98
+ **Alpine and other musl-based Linux** need the C++ runtime the binary links against:
99
+ `apk add libstdc++ libgcc`. With Bun or npm, name the musl binary's package as well, since
100
+ it is not an optional dependency (Bun would download it on every Linux machine), and do the
101
+ same to install another version by hand. `tokenhud update` updates both.
102
+
103
+ ```sh
104
+ bun add -g @tokenhud/linux-x64-musl tokenhud # on arm64: @tokenhud/linux-arm64-musl
105
+ npm install -g @tokenhud/linux-x64-musl tokenhud
106
+ ```
107
+
108
+ **By hand:** download `tokenhud-<os>-<arch>` (`linux-x64`, `linux-arm64`, `linux-x64-musl`,
109
+ `linux-arm64-musl`, `darwin-x64`, `darwin-arm64`, `windows-x64.exe`, `windows-arm64.exe`)
110
+ and `SHA256SUMS` from a release, check it with `sha256sum --check --ignore-missing
111
+ SHA256SUMS` (`shasum -a 256 --check --ignore-missing SHA256SUMS` on macOS), make it
112
+ executable and put it on your PATH. On macOS, a binary downloaded with a web browser is
113
+ quarantined, and macOS refuses to start it, because tokenhud's binaries are signed but not
114
+ notarized. Clear the flag with `xattr -d com.apple.quarantine tokenhud-darwin-arm64` (or
115
+ `-x64`). `install.sh` downloads with curl, which doesn't set the flag.
116
+
117
+ ## First run
118
+
119
+ Run `tokenhud`. It finds your Claude Code and Codex accounts (see [Accounts](#accounts)),
120
+ and on its first start:
121
+
122
+ - if you used cc-usage, it imports cc-usage's usage history and settings
123
+ ([what comes over](docs-public/MIGRATING-FROM-CC-USAGE.md));
124
+ - it reads every transcript once to build its store. That takes a few seconds; after that,
125
+ it reads only what's new as the agents write it.
126
+
127
+ `tokenhud --once` prints the Overview once and exits: with colours in a terminal, as plain
128
+ text when piped. `--width N` sets its width. `tokenhud doctor` reports what tokenhud found:
129
+ the store, the accounts it follows, unpriced models, anything still only in cc-usage, and
130
+ every tokenhud on your PATH.
131
+
132
+ ## Keys
133
+
134
+ Every view moves the same way, with the left hand on WASD or either hand on the arrows:
135
+
136
+ | Key | Does |
137
+ |---|---|
138
+ | `a/d` or `←/→` | Switch the tab: what the view shows |
139
+ | `w/s` or `↑/↓` | Move the selection: which row or card |
140
+ | `enter` | Open the selection |
141
+ | `esc` | Go back one step: close, un-drill, clear |
142
+
143
+ Letters work the same with Caps Lock on. In a text field (History's model filter, the time
144
+ zone filter, a new label) letters are just text.
145
+
146
+ | Key | Does, in every view |
147
+ |---|---|
148
+ | `1-4` | Switch view: Overview, History, Models, Accounts |
149
+ | `tab` or `shift-tab` | Next view, or the previous one |
150
+ | `c` | Cycle the account scope: all accounts, then each account, then all again |
151
+ | `x` | Open settings |
152
+ | `?` | Show the keys, the current view's included |
153
+ | `q` or `Ctrl-C` | Quit |
154
+
155
+ A view with tabs shows them as a strip, `◀ a … d ▶`, over what they switch. The footer
156
+ shows the keys of the view you are in.
157
+
158
+ ### Overview
159
+
160
+ | Key | Does |
161
+ |---|---|
162
+ | `a/d` | Switch the activity chart's window: the last 5 hours, 24 hours or 7 days |
163
+ | `w/s` | Select an account card (none is selected at first) |
164
+ | `enter` | Open the selected card's account (the first card's when none is selected) in Accounts |
165
+ | `esc` | Clear the card selection |
166
+ | `t` | Show cost or tokens in the activity chart and the top models |
167
+
168
+ ### History
169
+
170
+ | Key | Does |
171
+ |---|---|
172
+ | `a/d` | Switch the table: this week, this month, every day, the weeks or the months |
173
+ | `w/s` | Select a row; the heat map highlights its day, week or month |
174
+ | `pgup/pgdn`, `home/end` | Move ten rows, or to the first or last |
175
+ | `enter` | On a week or month, list its days; on a day, list its limit events |
176
+ | `esc` | Close the limit events, then the open week or month, then clear the filter |
177
+ | `f` or `/` | Filter every number by model: type part of its name or id, `enter` applies it |
178
+
179
+ ### Models
180
+
181
+ | Key | Does |
182
+ |---|---|
183
+ | `a/d` | Switch the window: today, this week, this month, all time, the last 1, 5 or 24 hours |
184
+ | `w/s` | Select a model |
185
+ | `enter` | Show or hide the selected model's rates and who used it |
186
+ | `r` | Sort by cost, tokens or name |
187
+
188
+ ### Accounts
189
+
190
+ | Key | Does |
191
+ |---|---|
192
+ | `w/s` | Select an account; the last entry is "add a root…" |
193
+ | `enter` | Open the account's action menu; on "add a root…", the settings account editor |
194
+
195
+ ### Account menu
196
+
197
+ `enter` on an account, in the Accounts view or in settings under Accounts, opens a small
198
+ menu: show only this account (again: all accounts), enable or disable it, rename it, mark
199
+ it history only or not, mark it as the same subscription account as another directory
200
+ ("Same account as…", picked from a list) and, once linked, unlink it.
201
+
202
+ | Key | Does |
203
+ |---|---|
204
+ | `w/s` or `↑/↓` | Select an action |
205
+ | `enter` | Run it |
206
+ | `esc` | Close the menu |
207
+
208
+ ### Help
209
+
210
+ | Key | Does |
211
+ |---|---|
212
+ | `esc`, `?`, `enter` or `q` | Close the help |
213
+
214
+ ### Settings
215
+
216
+ | Key | Does |
217
+ |---|---|
218
+ | `w/s` or `↑/↓` | Move; in the time zone list, only the arrows (letters filter it) |
219
+ | `a/d` or `←/→` | Step the selected setting's value in place |
220
+ | `pgup/pgdn`, `home/end` | Move ten rows, or to the first or last |
221
+ | `enter` | Open the selected setting's list, pick a value, or open an account's action menu |
222
+ | `esc`, `x` or `q` | Back; from the main list, back to the view (`x` only there) |
223
+ | `backspace` | Delete the last character of a filter or label |
224
+
225
+ Settings are the refresh interval, the default spend window, whether to show cost, the
226
+ theme (dark, light, high contrast), the time zone, whether to check for updates, and the
227
+ accounts. They are saved in `~/.config/tokenhud/config.json`.
228
+
229
+ ## Views
230
+
231
+ tokenhud has four views; the number keys switch between them (see [Keys](#keys)). The
232
+ screenshots below are tokenhud at 120 × 45, the top half of a portrait monitor, on made-up
233
+ accounts. In each legend, the numbers and colours match the boxes in the picture above it.
234
+
235
+ ### Overview screen
236
+
237
+ How close each account is to its limits, and what your usage is costing right now.
238
+
239
+ <!-- shots:overview -->
240
+ ![The Overview with seven numbered boxes: header, limits cards, MCP agents, spend, activity chart, top models, limit events](docs-public/images/overview.png)
241
+
242
+ 1. 🟥 **Header.** The four views, the one you are on highlighted. On the right, the account scope: `all accounts`, or the one account that every number in every view is narrowed to. Then the status dot. `● live · 5s` (teal): this tokenhud reads new transcript lines as Claude Code and Codex write them, and refreshes its clock-driven numbers every 5 seconds (the refresh interval setting). `● stale` (amber): it isn't reading transcripts itself, because another tokenhud is (this one shows what that one stores), or it is still starting, or reading failed. `● error` (red): the numbers have stopped updating while tokenhud restarts the part that computes them.
243
+ 2. 🟧 **Limits.** A card per account, Claude Code and Codex alike: its 5-hour and weekly subscription limits, and what its current spending means for them. The note on the right is a reminder of the two paces, `pace (30m)` over the last 30 minutes and `avg` over the week so far, and that every time on a card is an estimate. [Reading a limits card](#reading-a-limits-card) explains each reading.
244
+ 3. 🟨 **Agents.** Shown while tokenhud's MCP server runs for a Claude Code session (see [Use with Claude Code](#use-with-claude-code)). One line per agent session that called a tool in the last 10 minutes: its project (the name of the directory the session runs in, never its path; `claude session` when the server doesn't say), the account the call was about, the tool, and how long ago. A narrow card leaves out the account, then the project. With no recent calls, it says how many servers are running.
245
+ 4. 🟩 **Spend.** What your usage would cost at the providers' API prices (not what your subscription costs), and its tokens: input, output, cache reads and cache writes together. `1h` and `5h` are the last 60 minutes and the last 5 hours (shown on screens 120 columns wide or more). `today`, `this week` and `this month` start at local midnight, on Monday and on the 1st. `all-time` is everything tokenhud has stored, including usage whose transcripts are gone. For `*` and `≈`, see [Glyphs and colours](#glyphs-and-colours).
246
+ 5. 🟦 **Activity.** Cost over the last 24 hours, one bar per time slot. The tabs on the right switch to the last 5 hours or 7 days (`a`/`d`; the one shown is in brackets), and `t` switches to tokens. The title gives the slot's length, which grows until the chart fits the width: 30 minutes here. On the left, the tallest slot's cost, half of it, and zero; below, hours back from now.
247
+ 6. 🟪 **Top models.** The five models with the most cost in the last 24 hours: the cost, its share of the 24 hours' cost, and a bar of that share. A fast or priority tier gets its own row, marked `(fast)`. A model with no published price shows `unpriced` and comes last. Under the list, the chart's tallest slot and when it started.
248
+ 7. 🟫 **Limit events.** Limits hit in the last 7 days: day and time, account, and what happened. `5-hour limit reached` (red) is a window that reached 100 %; `weekly passed 80%` (amber) is a weekly window crossing 80 %. After a reached limit, `resumed 16:05` is when tokenhud first saw the window usable again after its reset; `resets` and a time, when a window that is still full will reset; `—`, that no fetch has seen it since. Limits reached come first, newest first, then the 80 % marks. Up to 8 lines; the rest are counted on the last one.
249
+ <!-- /shots:overview -->
250
+
251
+ #### Reading a limits card
252
+
253
+ <!-- shots:overview-card -->
254
+ ![Close-up of four limits cards, with the account, meters, percentages and countdowns, pace, verdict, stale age and a not-signed-in account boxed and numbered](docs-public/images/overview-card.png)
255
+
256
+ 1. 🟥 **Account.** The account's label and its provider, `claude` or `codex`. The label comes from the account's config directory (`~/.claude-work` is `work`) unless you renamed it.
257
+ 2. 🟧 **Meters.** The `5h` row is the 5-hour window, the `week` row the weekly one. Each bar is how much of that limit is used: blue below 50 %, amber from 50 %, red-orange from 80 %. A model's own weekly limit isn't on the card; the Accounts view lists it.
258
+ 3. 🟨 **Used, and time to reset.** `84%` is the share of the 5-hour limit used, as the provider reported it the last time tokenhud fetched the limits; `1h52m` is the time left until that window resets. On the weekly row, `41%` used and `5d21h` (5 days 21 hours) to go. Once a window has reset, the card reads 0 % and `—` until the next fetch.
259
+ 4. 🟩 **Pace.** The spend pace the verdict comes from, at API prices, as dollars per hour (tokens per hour with costs hidden). `pace` with `(30m)` is what the account spent in the last 30 minutes: `$6.8/h` here, and `$0/h` for nothing in that time. `avg` with `this week` is a weekly window's average since it began, idle time and nights included: the pace a weekly verdict comes from. Where a card is narrow, `(30m)` or `this week` goes first, then the word; the number stays.
260
+ 5. 🟦 **Verdict.** What that pace means for the two windows: an estimate (see [How the projection works](#how-the-projection-works)). The first of these that applies: `at 100% until …`, a window is full now, until the last full one resets; `100% at 12:13` (`hits 100% at …` on a wider card), the time the 5-hour window fills at its pace, with the day when it isn't today; `100% ~tonight`, the part of the day the weekly window fills at its average (or tomorrow morning, Sunday evening and so on; never to the minute, which a week's average can't tell); `wk ~94%` (`week ends ~…` on a wider card), the weekly window is on course to end its week at 80 % or more; `idle`, nothing spent in 30 minutes, or `idle · week N%` when a window is at 80 % or more all the same; `safe` (`safe until …` on a wider card), both windows last until they reset; `no estimate yet`, too little spending in a window to tell. Red means a window is or will be full, amber a high week.
261
+ 6. 🟪 **Stale limits.** The limits were fetched more than 15 minutes ago: `(52m old)` says how long ago, and the card shows them as they were then.
262
+ 7. 🟫 **Not signed in here.** A history-only account: one you marked history only, or one that isn't signed in on this machine (it runs on another computer now, say). Its limits can't be read here, so it has no meters, but its usage history stays. See [Accounts](#accounts).
263
+ <!-- /shots:overview-card -->
264
+
265
+ #### How the projection works
266
+
267
+ Each window is projected at a pace of its own, at API prices:
268
+
269
+ - **The 5-hour window:** what the account spent in the last 30 minutes, times two
270
+ (`pace … (30m)` on the card).
271
+ - **A weekly window:** its average since it began, what the account has spent in it so far
272
+ over the hours since it opened, nights and idle time included (`avg … this week`). Half an
273
+ hour is too little of a week: two agents working at once for 30 minutes would otherwise
274
+ say the week runs out tonight. In a weekly window's first 6 hours there is too little to
275
+ average, and the 30-minute pace stands in.
276
+
277
+ For each window, tokenhud estimates how much of the limit a dollar uses: the window's use
278
+ when the limits were last fetched, divided by what the account had spent in that window by
279
+ then (at least $0.50, or there is no estimate). It adds what was spent since that fetch, and
280
+ works out how long the rest of the window lasts at its pace. If that ends before the window
281
+ resets, the card says when: to the minute for the 5-hour window (`100% at 12:13`), and to a
282
+ part of the day for a weekly one (`100% ~tonight`; or tomorrow morning, Sunday evening and
283
+ so on), as precise as a week's average can be. If after, the window is safe. For the weekly window it
284
+ also works out where the week would end at its pace: that is the `week ends ~N%` figure,
285
+ shown from 80 %. Treat it as a rough guide:
286
+
287
+ - tokenhud sees only the transcripts on this machine. Use of the same subscription elsewhere
288
+ (another computer, claude.ai) fills the meter without showing up here, which makes the
289
+ estimate too early.
290
+ - API prices stand in for the provider's own accounting, which isn't published, so a change
291
+ in models or in cache use changes the estimate.
292
+ - A burst or a break dominates the 30-minute pace, and so the 5-hour estimate. The weekly
293
+ average is slow to follow a change of habit instead: a busy day after a quiet week shows
294
+ late.
295
+
296
+ ### History screen
297
+
298
+ Your usage day by day, week by week and month by month, and the days you hit a limit.
299
+
300
+ <!-- shots:history -->
301
+ ![The History view with six numbered boxes: tabs, heat map, day card, period table, totals, footer](docs-public/images/history.png)
302
+
303
+ 1. 🟥 **Tabs.** What the table lists, switched with `a`/`d`; the one shown is in brackets. `this week` and `this month` list the days so far of the current week or month (History opens on this week), `days` every day of the heat map, `weeks` (Monday to Sunday) and `months` one row each. On the right, what `*` and `≈` mean, and `f`, the model filter: with a filter, every number on the screen counts only the models whose name or id contains what you typed.
304
+ 2. 🟧 **Heat map.** One square per day for the last 26 weeks: a column per week, Monday at the top. The shade is the day's cost against the busiest day shown: the darkest square is a day without usage, and the four lighter shades are under a quarter of the busiest day, under a half, under three quarters, and the rest. The table's selected row is white: here a day; on the weeks or months tab, that week's or month's days. Days after today are blank.
305
+ 3. 🟨 **Day card.** The selected day. `cost`, and how it compares with your usual day: `2.4×` is the day's cost divided by your average daily cost over the 30 days before today (or fewer, if your usage started more recently). `tokens`, split into input, output and cache (reads and writes). `models`: its three most expensive models, with their share of the day's cost. `accounts`: each account's cost that day. `limits`: the day's limit events. `hit 100% at 14:10 (waited 1h55m)` is a window that reached its limit, and how long it was until tokenhud saw it usable again; a weekly window crossing 80 % reads passed 80%.
306
+ 4. 🟩 **Period table.** The rows of the tab shown, newest first, moved through with `w`/`s`. On the weeks or months tab, `enter` lists a row's days and `esc` goes back; on a day with limit events, `enter` lists them. `input`, `output` and `cache` (reads and writes) are tokens; `cost` is at API prices. `vs 30-day avg` compares the period with your average day times the period's days so far: the bar is full at 2.25 times, 1 times is a little under half of it, and it turns red-orange above 1.5 times; the ratio follows it. `top model` is the period's most expensive model, `accounts` those with usage, most cost first.
307
+ 5. 🟦 **Totals.** Every period listed, added up: here the `177 days` of the heat map's 26 weeks, today included.
308
+ 6. 🟪 **Footer.** Its first line is the keys of the view you are on: `a/d` its tabs, `w/s` the selection, `enter` to open it, then the view's own; `esc back` joins them while there is something to go back from. Its second line is the keys every view has (all of them are in [Keys](#keys)). On the right, `MCP ● 2 agents`: tokenhud's MCP server is running (teal dot) and two agent sessions called it in the last 10 minutes; `MCP ○` means no server is running. A newer release, or another tokenhud reading the transcripts, is noted here too.
309
+ <!-- /shots:history -->
310
+
311
+ ### Models screen
312
+
313
+ What each model cost you, the rates it was billed at, and who used it.
314
+
315
+ <!-- shots:models -->
316
+ ![The Models view with five numbered boxes: rate board, total, footnotes, rates card, who used it](docs-public/images/models.png)
317
+
318
+ 1. 🟥 **Rate board.** Every model used in the window, by cost (`r` sorts by tokens, then by name). The tabs on the right are the windows, switched with `a`/`d`; the one shown is in brackets. Today, this week, this month and all time are calendar periods, and 1h, 5h and 24h the last hours. One row per model and tier; a fast or priority tier is its own row, `(fast)`. For input, output and cache (reads and writes): the tokens used, and the `$/M` rate they are billed at today, in dollars per million tokens (`—`: no price). `cost` prices each request at the rate in effect on its date, so it can differ from tokens times today's rate. The bar and the percentage are the model's share of the window's cost. `*` after a name: some or all of its tokens have no published rate, so they are counted but not priced. The selected row (`w`/`s`) is highlighted, and `enter` shows or hides its cards below.
319
+ 2. 🟧 **Total.** All models together. Rates don't add up, so the total has none.
320
+ 3. 🟨 **Footnotes.** `$/M` is the base rate; cache writes, and requests over a model's long-context threshold, cost more. Then the share of the window's tokens that have a price, and which costs are estimates: `codex-auto-review` is priced as the model OpenAI said serves it.
321
+ 4. 🟩 **Rates.** The selected model's prices today, per million tokens: input and output; cache reads, and what fraction of the input rate they are; cache writes, for 5 minutes and for 1 hour (Anthropic) or one rate (OpenAI); the fast tier's prices; the long-context threshold and its multipliers, for models that have one; where the prices come from and when they were last checked; and any price change inside the window.
322
+ 5. 🟦 **Who used it.** Each account's share of the selected model's cost in the window, and that cost. Then the day the model was first used, and how many requests the window holds.
323
+ <!-- /shots:models -->
324
+
325
+ ### Accounts screen
326
+
327
+ Every account in detail: where its data comes from, its limits now and in past weeks, and
328
+ its last 30 days.
329
+
330
+ <!-- shots:accounts -->
331
+ ![The Accounts view with six numbered boxes: account list, selected account, where it comes from, limits, weekly history, last 30 days](docs-public/images/accounts.png)
332
+
333
+ 1. 🟥 **Accounts.** Every account tokenhud has usage for: those active on this machine first, then by all-time cost. The dot and the percentage show the account's most-used limit among its windows that haven't reset yet: blue below 50 %, amber from 50 %, red-orange from 80 %. `○` and a grey label: inactive here (history only, not signed in, turned off, or its config directory isn't on this machine). `+ add a root…` opens the account settings. Under the list, the keys: `w`/`s` select an account, and `enter` opens its menu: show only this account, enable or disable it, rename it, history only, and linking it to another directory on the same subscription account.
334
+ 2. 🟧 **Account.** The selected account, its provider, and when its limits were last fetched.
335
+ 3. 🟨 **Where it comes from.** `root` is the config directory its transcripts are read from, and how: `watched` (read as they are written), `polled` (checked at intervals, for a Windows drive under WSL) or `disabled`. `history`: how many requests tokenhud has stored for it, and the day of the first.
336
+ 4. 🟩 **Limits.** Every limit window of the account: 5-hour, weekly, then any of a model's own. The bar and the percentage are how much is used (colours as on the cards), then when the window resets: a time today, a weekday and a time within a week, else a date. A window that has reset since the last fetch shows an empty bar, `—`, and how long ago it reset. A failed fetch is noted under the meters.
337
+ 5. 🟦 **Weekly history.** The weekly window over the last 8 weeks, on a 0–100 % scale, each week labelled with the day it reset. The last bar (grey) is this week so far. Earlier weeks are known only from limit events: `100%` for a week that reached its limit, `≥80%` for one that passed 80 %, `—` for no record: under 80 %, or not seen by tokenhud.
338
+ 6. 🟪 **Last 30 days.** `spend 30d`: one bar per day for the last 30 days, today last, each against the busiest of them; then their total cost. `models`: the account's models over those 30 days, by share of cost. `agents`: its latest MCP tool call in the last 10 minutes, and how many calls it made.
339
+ <!-- /shots:accounts -->
340
+
341
+ ### Glyphs and colours
342
+
343
+ - `━` **bars**: a limit meter or a share. The coloured part is the share used, the dark
344
+ track the rest. Limit meters are blue below 50 %, amber from 50 % and red-orange from 80 %;
345
+ share bars are amber for cost and blue for tokens.
346
+ - `▁▂▃▄▅▆▇█` **columns**: the activity chart, the 30-day bars and the weekly history, in
347
+ eighths of a row.
348
+ - `■` **heat map**: the darkest square is a day without usage; the four lighter shades are
349
+ days under a quarter of the busiest day shown, under a half, under three quarters, and
350
+ the rest. White is the table's selected row: a day, or a week's or month's days.
351
+ - `*` after a cost: some of its tokens have no price, so the cost leaves them out and is a
352
+ lower bound. After a model's name: some or all of its tokens have no published rate.
353
+ `unpriced`: none of its tokens has a price. See [Pricing](#pricing).
354
+ - `≈` before a cost: part of it is priced from an estimate (`codex-auto-review`, priced as
355
+ the model OpenAI said serves it).
356
+ - `~` in `wk ~94%` or `100% ~tonight`: a projection, the second to a part of the day.
357
+ Every time on a limits card is an estimate too.
358
+ - **Grey (dim) text** is secondary: labels, notes, and what is old or inactive, such as a
359
+ limits card's `(52m old)`, an inactive account's `○`, or a window past its reset.
360
+ - **Dots**: teal `●` is live (the header) or an MCP server running (the footer); amber `●`
361
+ stale; red `●` an error. In Accounts, an account's dot takes its limit colour.
362
+ - **Highlights**: a lighter background marks the selected row, and the active view, tab or
363
+ window; a tab strip's active tab is in brackets too (`[24h]`), for plain text and for
364
+ anyone who can't tell the colours apart.
365
+
366
+ The layout adapts to the terminal: it is laid out for half of a 1080p screen (about 105 ×
367
+ 50), the top half of a portrait monitor (about 120 × 45), 80 × 24 and wider. On narrower
368
+ screens, columns and sections drop out, least important first, and the limits cards turn into
369
+ two lines each; a number is never cut short.
370
+
371
+ ## Accounts
372
+
373
+ An account is one Claude Code or Codex config directory. tokenhud finds:
374
+
375
+ - **Claude Code:** `~/.claude`, `$CLAUDE_CONFIG_DIR`, and every `~/.claude-*` directory
376
+ (`~/.claude-work` is labelled `work`);
377
+ - **Codex:** `~/.codex` and `$CODEX_HOME`;
378
+ - **under WSL**, the same directories on the Windows side (`/mnt/c/Users/*/.claude*`,
379
+ `/mnt/c/Users/*/.codex*`), labelled with a `-win` suffix. `TOKENHUD_WSL_USERS=` (empty)
380
+ skips them.
381
+
382
+ Directories elsewhere go in `config.json` as `claude_roots` or `codex_roots`, for example
383
+ `"claude_roots": [{"path": "/srv/claude-ci", "label": "ci"}]`. In the Accounts view, or in
384
+ settings under Accounts, `enter` opens an account's menu: turn it off, rename it, or mark it
385
+ history only. `c` narrows every view to one account.
386
+
387
+ **History-only accounts.** An account that isn't signed in on this machine any more (it now
388
+ runs on another computer, say) keeps all of its history. Its card says "not signed in
389
+ here" instead of showing an error, and tokenhud checks its limits only once a day, or never
390
+ once you mark it history only (in its menu).
391
+
392
+ **Several directories on one subscription account.** Two config dirs signed in to the same
393
+ Claude (or ChatGPT) account share one set of limits: under WSL, `~/.claude` and the
394
+ Windows-side `.claude` often are. tokenhud then shows them as one limits account: one
395
+ card titled with both labels, limits fetched once through whichever is signed in, and a
396
+ pace summed over both, since both spend from the same limit. History, spend and the
397
+ Accounts view stay per directory; there, each says which directories it shares its account
398
+ with, and the account's 30-day total.
399
+
400
+ tokenhud links them by itself when their limits reset at the same times (within 2 s) and
401
+ their use moves together: equal on two pairs of fetches seconds apart, at most ten minutes
402
+ apart, and changed in between. An account nobody is using proves nothing, so two
403
+ directories on an idle account show apart until it is used. Once they are linked, the
404
+ other directory is still fetched every 30 minutes, and once more right away when its
405
+ credential file changes. If a reset differs, the two show apart straight away, and a second
406
+ difference in a row unlinks them; a difference in use alone is checked again next round.
407
+ These checks ride on the account's own fetches, the TUI's and the MCP server's alike.
408
+ Finding two directories on one account in the first place is left to the fetches the TUI
409
+ makes every 5 minutes: the MCP server fetches only the account it is asked about, so an
410
+ agent asking often adds no fetches of a directory not linked to it.
411
+
412
+ An account's menu (`enter` on it, in the Accounts view or in settings under Accounts) links
413
+ it to another by hand ("Same account as…") and unlinks it ("Unlink"). These are saved as
414
+ `same_account` and `separate_accounts` in `config.json`, and win over what tokenhud finds.
415
+ A link you made is never undone: if the limits differ, Accounts and `tokenhud doctor` say
416
+ so. `tokenhud doctor` lists the linked directories, how each link was made, and the pairs
417
+ kept apart.
418
+
419
+ ## Use with Claude Code
420
+
421
+ tokenhud's MCP server lets a Claude Code agent check the limits of the account it runs
422
+ on, decide whether to pause, and wait for a reset. That matters most for sessions that
423
+ can't resume on their own: `claude -p`, background tasks and teammates. Interactive
424
+ Claude Code resumes by itself after a reset, so there an agent should tell you instead of
425
+ waiting.
426
+
427
+ ### Install
428
+
429
+ [Install tokenhud](#install) first, so `tokenhud` is on the PATH Claude Code starts with:
430
+ the plugin and the MCP server both run `tokenhud mcp`. Otherwise `/mcp` in Claude Code
431
+ shows the server as failed.
432
+
433
+ Plugins and user-scope MCP servers belong to one Claude config dir, so install once per
434
+ account: once for `~/.claude`, and once more for each `CLAUDE_CONFIG_DIR` you use.
435
+ `tokenhud doctor` shows which accounts have it, and whether `tokenhud` is on the PATH.
436
+
437
+ **The plugin** adds the MCP server and a skill that tells agents when to use it:
438
+
439
+ ```sh
440
+ claude plugin marketplace add ZhuoQiuMcgill/tokenhud
441
+ claude plugin install tokenhud@tokenhud
442
+
443
+ # another account
444
+ CLAUDE_CONFIG_DIR=~/.claude-work claude plugin marketplace add ZhuoQiuMcgill/tokenhud
445
+ CLAUDE_CONFIG_DIR=~/.claude-work claude plugin install tokenhud@tokenhud
446
+ ```
447
+
448
+ **The MCP server on its own:**
449
+
450
+ ```sh
451
+ claude mcp add -s user tokenhud -- tokenhud mcp
452
+ CLAUDE_CONFIG_DIR=~/.claude-work claude mcp add -s user tokenhud -- tokenhud mcp
453
+ ```
454
+
455
+ **Native Windows:** an npm install puts a `tokenhud.cmd` shim on the PATH, which Claude
456
+ Code can't start directly. Register the server through `cmd` instead of installing the
457
+ plugin: `claude mcp add -s user tokenhud -- cmd /c tokenhud mcp`. The standalone
458
+ `tokenhud.exe` (installed with `install.ps1`) works directly, plugin included.
459
+
460
+ ### Tools
461
+
462
+ | Tool | What it does |
463
+ |---|---|
464
+ | `limits` | The account's limit windows (5-hour, weekly, per model): utilization from 0 to 1, reset time, the spend pace each window is projected at (`pace_basis`: `30m`, the last 30 minutes, or for a weekly window `window_avg`, its average since it began), and when the window would run out at that pace (an estimate, a weekly window's good to a part of a day). Fetches fresh limits when the cached ones are over 60 s old. For a directory that shares its subscription account with others, the windows and pace are the account's, and `shared_with` names the others. |
465
+ | `should_wait` | `wait: true` when a window is at 90 % or more (`min_headroom`, default 0.1), when `estimated_cost` (USD) would take it there, or when it is projected to run out within 10 minutes, before its reset. The 5-hour and weekly windows always count; a per-model window (such as a model's weekly limit) counts only when `model` names that model, and is otherwise just mentioned. Returns a short reason, the window it is about with its pace and projection as `limits` gives them, and `wait_s`: until the reset, plus 30 s. |
466
+ | `wait_for_reset` | Waits until the window `should_wait` binds on (for the same `model`) resets, or its utilization drops under `until_utilization_below`, for at most `max_wait_s` (5 hours or less). Sends progress every 30 s, re-checks the limits every 5 minutes, and stops at once when the call is cancelled. |
467
+ | `usage` | Tokens and API-equivalent cost for a period, optionally by model, account, day, week or month (at most 500 groups per call), as `tokenhud json usage` prints them ([schema](docs-public/JSON.md)), plus `stale_s`, the age of the store's data. |
468
+ | `accounts` | The accounts on this machine, from cached data only: whether their limits can be read here (`signed_in`, null until first checked), their last usage, which one this session runs on, and `group`, the same id for every directory on one subscription account. |
469
+
470
+ Every tool answers for the account the session runs on: `CLAUDE_CONFIG_DIR`, else
471
+ `~/.claude`, confirmed by finding the session's transcript. `limits` reports how it was
472
+ found (`detected_via`). Pass `account` (a label from `accounts`) for another account, or
473
+ `provider: "codex"` for Codex. An account that isn't signed in on this machine reports
474
+ `signed_in: false`, and `should_wait` doesn't make agents wait on it.
475
+
476
+ ## Scripts: `tokenhud json`
477
+
478
+ ```sh
479
+ tokenhud json usage --period this_week --group-by day
480
+ tokenhud json models --period today
481
+ tokenhud json accounts
482
+ ```
483
+
484
+ Each prints one JSON document from the store, with the same queries the MCP server uses.
485
+ [docs-public/JSON.md](docs-public/JSON.md) documents the options, the output and the
486
+ schema-1 contract.
487
+
488
+ ## Pricing
489
+
490
+ Costs are API-equivalent: what the tokens would cost at the providers' published API
491
+ prices, not what your subscription costs. Each request is priced by its model, its date
492
+ (prices that changed apply from the day they changed), its tier (Claude fast mode, Codex
493
+ priority), long context, and cache reads and writes. The bundled table and where every price
494
+ comes from are in [src/pricing](src/pricing) (`pricing.json`, `SOURCES.md`).
495
+
496
+ - A model with no price is counted but not priced. Its cost shows as `unpriced`, totals that
497
+ leave tokens out are marked `*`, and `tokenhud doctor` lists the models and how much of
498
+ your usage is priced.
499
+ - `codex-auto-review` is priced as an estimate, at the model OpenAI said serves it.
500
+ - Costs are recomputed from the stored token counts every time, so a price correction
501
+ applies to all of your history.
502
+
503
+ **Your own prices** go in `~/.config/tokenhud/pricing.overrides.json`, which holds only your
504
+ entries; rates are USD per 1 million tokens:
505
+
506
+ ```json
507
+ {
508
+ "models": {
509
+ "my-local-model": { "input": 3, "output": 15, "cache_read": 0.3 },
510
+ "gpt-5.6-sol": {
511
+ "periods": [
512
+ { "from": null, "card": { "input": 5, "output": 30, "cache_read": 0.5 } },
513
+ { "from": "2026-08-21T07:00:00Z", "card": { "input": 4, "output": 20, "cache_read": 0.4 } }
514
+ ]
515
+ }
516
+ }
517
+ }
518
+ ```
519
+
520
+ `input` and `output` are required. `cache_read` defaults to a tenth of `input` and
521
+ `cache_write` to 1.25 × `input`; `fast` holds a fast or priority card in the same form. An
522
+ entry replaces everything the bundled table says about that model, its dated prices and
523
+ fast card included. Because the file holds only your entries, later corrections to the
524
+ bundled table still reach every other model. An entry tokenhud can't read is skipped, with
525
+ a warning in `tokenhud doctor`.
526
+
527
+ ## Data and privacy
528
+
529
+ - **Provider data is read-only.** tokenhud reads the transcripts under Claude Code's and
530
+ Codex's config directories, and never writes, moves or deletes anything there.
531
+ - **It stores token counts, not content:** per request, its time, model, token counts and
532
+ account. No prompts, responses or file contents. (`cache.db` remembers which transcript
533
+ files it has read, by path, so it can pick up where it left off.)
534
+ - **Credentials stay in memory.** To show a Claude account's limits, tokenhud reads that
535
+ account's OAuth token from its credentials file and calls Anthropic's usage endpoint; when
536
+ the token has expired, it lets the `claude` command refresh it. For Codex it asks the
537
+ installed `codex` app server. Tokens are never logged, cached or sent anywhere else.
538
+ - **Network:** the limit requests above, and GitHub's list of tokenhud releases: when you
539
+ run `tokenhud update`, and at most once a day while the TUI runs, to show when a newer
540
+ release is out (settings, "Check for updates", turns that off). No telemetry; prices are
541
+ bundled, never fetched.
542
+
543
+ tokenhud's own files are in `~/.config/tokenhud/` (or `$XDG_CONFIG_HOME/tokenhud/`):
544
+
545
+ ```
546
+ tokenhud.db your usage history: the only copy of usage whose transcripts are gone
547
+ tokenhud.db.bak a verified daily backup of it, and tokenhud.db.bak.prev before that
548
+ cache.db how far each transcript has been read (safe to delete: it is rebuilt)
549
+ config.json settings
550
+ pricing.overrides.json your prices, if any
551
+ limits.json the last limits fetched
552
+ update-check.json when GitHub was last asked about a newer release, and its answer
553
+ logs/tokenhud.log errors, for tokenhud doctor and bug reports
554
+ mcp/ which MCP servers are running, and their projects' names, for the Overview
555
+ ingest.lock.db which tokenhud process writes the store
556
+ ```
557
+
558
+ A store that can't be read is renamed `tokenhud.db.corrupt-<time>` and its history
559
+ recovered into a new one; it is never deleted.
560
+
561
+ ## Update and uninstall
562
+
563
+ ```sh
564
+ tokenhud update --check # is there a newer release?
565
+ tokenhud update # install it
566
+ ```
567
+
568
+ How `tokenhud update` updates depends on how tokenhud was installed:
569
+
570
+ - **With Bun or npm** (globally): it asks the package manager which version npm's `latest`
571
+ tag points at (`next` for a release candidate, as long as `next` is no older than it),
572
+ installs exactly that version (`bun add -g --no-cache tokenhud@0.2.0` or
573
+ `npm install -g tokenhud@0.2.0`), then checks that the `tokenhud` command runs it and says
574
+ which version it went from and to. It never installs an older version than the one you
575
+ have; `--allow-downgrade` does. `--no-cache` makes Bun ask the registry rather than reuse
576
+ an answer from a few minutes ago.
577
+ - **With `install.sh` or `install.ps1`**: the binary replaces itself. It downloads the new
578
+ release, checks its SHA-256 against the release's `SHA256SUMS`, checks that it starts, and
579
+ only then swaps it in.
580
+ - **Through npx or bunx, or as a project's dependency**: it prints the command that updates
581
+ it.
582
+
583
+ `--prerelease` includes release candidates (npm's `next` tag). `--print` prints the command
584
+ that updates your install, naming the tag rather than its version, and runs nothing. After updating, `tokenhud update` warns if
585
+ another tokenhud comes first on your PATH, since a shell would still run that one.
586
+
587
+ Nothing updates on its own. When a newer release is out, the TUI says so in its footer, in
588
+ dim text; it asks GitHub at most once a day, and the "Check for updates" setting turns that
589
+ off.
590
+
591
+ **Switching from `install.sh` to Bun:** run `bun add -g tokenhud`, then the new copy's
592
+ doctor, `~/.bun/bin/tokenhud doctor` (plain `tokenhud` may still start the old one). It
593
+ lists every tokenhud on your PATH, with its version and how it was installed, marks the one
594
+ a shell runs, and names the file the old install left, such as `~/.local/bin/tokenhud`.
595
+ Delete that file (`rm ~/.local/bin/tokenhud`): doctor never deletes anything. Until you do,
596
+ if `~/.local/bin` comes before `~/.bun/bin` on your PATH, typing `tokenhud` still runs the
597
+ old copy. On Windows the same goes for switching from `install.ps1` to npm: delete
598
+ `%LOCALAPPDATA%\tokenhud` and remove its `bin` folder from your user PATH.
599
+
600
+ To uninstall:
601
+
602
+ 1. Remove the program: `bun remove -g tokenhud` or `npm uninstall -g tokenhud`; for
603
+ `install.sh`'s binary, `rm ~/.local/bin/tokenhud`; on Windows, delete
604
+ `%LOCALAPPDATA%\tokenhud` and remove its `bin` folder from your user PATH (Settings ›
605
+ System › About › Advanced system settings › Environment Variables).
606
+ 2. Remove it from Claude Code, once per account: `claude plugin uninstall tokenhud@tokenhud`
607
+ or `claude mcp remove -s user tokenhud`.
608
+ 3. If you no longer want your usage history, delete `~/.config/tokenhud/`.
609
+
610
+ ## Development
611
+
612
+ Prerequisite: [Bun](https://bun.com) 1.4.2 or later. CI pins 1.4.2; Bun 1.3.12 and 1.4.0
613
+ produced macOS binaries with broken signatures.
614
+
615
+ ```sh
616
+ bun install # dependencies and dev tooling
617
+ bun run check # typecheck (tsc), lint and format check (Biome), tests (bun test)
618
+ bun run build # standalone binary for this machine at dist/tokenhud
619
+ bun src/cli.ts # run from source
620
+ ```
621
+
622
+ `bun run build --target=<bun target>` cross-compiles one binary, for example
623
+ `--target=bun-windows-x64` writes `dist/tokenhud.exe`. `bun run build --release` builds all
624
+ eight release binaries and `SHA256SUMS` (install with `bun install --os="*" --cpu="*"`
625
+ first), and `bun run build --smoke` runs the ones this machine can.
626
+ `test/release/npm-e2e.test.ts` installs and updates the npm packages with bun and with npm
627
+ from a registry on localhost, as CI does; its header says how to run it. `bun run format`
628
+ rewrites files in the project style. [VERSIONING.md](VERSIONING.md) describes releases.
629
+
630
+ Repository layout:
631
+
632
+ ```
633
+ src/cli.ts entry point: parses arguments and dispatches commands
634
+ src/commands/ one module per subcommand: json, mcp, doctor, import-cc-usage, update
635
+ src/tui/ the interactive views, --once, settings
636
+ src/ingest/ reading transcripts into the store, live
637
+ src/sources/ the Claude Code and Codex transcript parsers, and account discovery
638
+ src/limits/ subscription limits, pace and limit events
639
+ src/mcp/ the MCP server: account detection, the tools, waiting for a reset
640
+ src/query/ the query layer: periods, totals and groupings, priced to the cent
641
+ src/store/ the SQLite usage store, its hourly rollup, backups and recovery
642
+ src/pricing/ the dated price table and the cost engine
643
+ src/update.ts tokenhud update: install method, releases, verified replacement
644
+ src/installs.ts every tokenhud on PATH and how it was installed, for doctor and update
645
+ test/ bun test suites
646
+ scripts/build.ts builds, checksums and smoke-tests the binaries
647
+ install.sh the Linux and macOS installer; install.ps1 is the Windows one
648
+ npm/ the npm package's command (sh) and Windows launcher; scripts/stage-npm.ts
649
+ builds the npm packages
650
+ plugin/ the Claude Code plugin (listed by .claude-plugin/marketplace.json)
651
+ ```
652
+
653
+ ## License
654
+
655
+ [MIT](LICENSE)
package/bin/tokenhud ADDED
@@ -0,0 +1,163 @@
1
+ #!/bin/sh
2
+ # The `tokenhud` command of the npm package on Linux and macOS: the file bun, npm, npx,
3
+ # bunx, pnpm and yarn link the command to. It finds the tokenhud binary in the platform
4
+ # package installed beside it, @tokenhud/<os>-<arch>[-musl], and replaces itself with it
5
+ # (exec), so the binary gets the same arguments, exit code and signals.
6
+ #
7
+ # Plain POSIX sh (dash, bash, BusyBox ash, macOS sh), so no JS runtime starts: it needs
8
+ # neither Node nor Bun, and nothing in the working directory is read. Bun running a launcher
9
+ # there would load its .env and bunfig.toml, which the binary itself never does.
10
+ #
11
+ # On Windows, npm's install script puts lib/tokenhud.cjs, which Node runs, in place of this
12
+ # file (preinstall.cjs).
13
+
14
+ say() {
15
+ printf '%s\n' "$@" >&2
16
+ }
17
+
18
+ # Its own tools (uname, readlink, sed, getconf, ldd) are found even on a PATH without them;
19
+ # the binary is given PATH as it was, unset included.
20
+ if [ "${PATH+set}" = set ]; then
21
+ given_path=$PATH
22
+ else
23
+ unset given_path
24
+ fi
25
+ PATH=${PATH:+$PATH:}/usr/bin:/bin
26
+
27
+ # The C library this Linux runs on, as the system answers it: glibc knows its own version
28
+ # (getconf), and ldd names its libc. A musl loader on disk proves nothing by itself: glibc
29
+ # systems can have one for building musl programs. It decides only when neither answers.
30
+ host_libc() {
31
+ if getconf GNU_LIBC_VERSION >/dev/null 2>&1; then
32
+ echo glibc
33
+ return
34
+ fi
35
+ case "$(ldd --version 2>&1 || true)" in
36
+ *musl*)
37
+ echo musl
38
+ return
39
+ ;;
40
+ *GLIBC* | *glibc* | *"GNU C Library"* | *"GNU libc"*)
41
+ echo glibc
42
+ return
43
+ ;;
44
+ esac
45
+ for loader in /lib/ld-musl-*.so.1; do
46
+ if [ -e "$loader" ]; then
47
+ echo musl
48
+ return
49
+ fi
50
+ done
51
+ echo glibc
52
+ }
53
+
54
+ # This package's directory, links resolved: the command on PATH is a link to this file
55
+ # (relative or absolute, maybe through several), and readlink without -f is all macOS has.
56
+ self=$0
57
+ case $self in
58
+ */*) ;;
59
+ *) self=./$self ;;
60
+ esac
61
+ while [ -h "$self" ]; do
62
+ link=$(readlink "$self") || break
63
+ case $link in
64
+ /*) self=$link ;;
65
+ *) self=${self%/*}/$link ;;
66
+ esac
67
+ done
68
+ pkg=$(cd -P "${self%/*}/.." && pwd -P) || exit 1
69
+
70
+ case $(uname -s) in
71
+ Linux) os=linux ;;
72
+ Darwin) os=darwin ;;
73
+ *) os=$(uname -s) ;;
74
+ esac
75
+ case $(uname -m) in
76
+ x86_64 | amd64) arch=x64 ;;
77
+ aarch64 | arm64) arch=arm64 ;;
78
+ *) arch=$(uname -m) ;;
79
+ esac
80
+ id=$os-$arch
81
+ # A glibc binary can't start on musl, nor the other way round: only this libc's package.
82
+ if [ "$os" = linux ] && [ "$(host_libc)" = musl ]; then
83
+ id=$id-musl
84
+ fi
85
+ case $id in
86
+ linux-x64 | linux-arm64 | linux-x64-musl | linux-arm64-musl | darwin-x64 | darwin-arm64) ;;
87
+ *)
88
+ say "tokenhud: there is no tokenhud binary for $id. Supported: linux-x64, linux-arm64," \
89
+ "linux-x64-musl, linux-arm64-musl, darwin-x64, darwin-arm64, win32-x64, win32-arm64."
90
+ exit 1
91
+ ;;
92
+ esac
93
+ # A Mac runs either one: npm under an x64 Node (Rosetta) installs darwin-x64 on Apple Silicon.
94
+ ids=$id
95
+ case $id in
96
+ darwin-arm64) ids="$id darwin-x64" ;;
97
+ darwin-x64) ids="$id darwin-arm64" ;;
98
+ esac
99
+
100
+ # The platform package as Node finds a dependency: in node_modules beside this package, or in
101
+ # any directory above it (npm nests it, bun and npx hoist it, pnpm puts it beside this one).
102
+ binary=""
103
+ for want in $ids; do
104
+ dir=$pkg
105
+ while :; do
106
+ case $dir in
107
+ */node_modules) ;;
108
+ *)
109
+ if [ -f "$dir/node_modules/@tokenhud/$want/bin/tokenhud" ]; then
110
+ found=$dir/node_modules/@tokenhud/$want
111
+ binary=$found/bin/tokenhud
112
+ break 2
113
+ fi
114
+ ;;
115
+ esac
116
+ # The root's /node_modules was the last to look in.
117
+ [ -z "$dir" ] && break
118
+ dir=${dir%/*}
119
+ done
120
+ done
121
+
122
+ if [ -z "$binary" ]; then
123
+ case $id in
124
+ *-musl)
125
+ # Not an optional dependency: Bun ignores `libc`, and would install it on every Linux.
126
+ say "tokenhud: on musl Linux (Alpine), the tokenhud binary is a package of its own," \
127
+ "@tokenhud/$id, and it needs the C++ runtime. Install them beside tokenhud:" \
128
+ " apk add libstdc++ libgcc (as root)" \
129
+ " bun add -g @tokenhud/$id tokenhud" \
130
+ " npm install -g @tokenhud/$id tokenhud" \
131
+ "or install the binary directly: https://github.com/ZhuoQiuMcgill/tokenhud#install"
132
+ ;;
133
+ *)
134
+ say "tokenhud: the package with the tokenhud binary for this machine, @tokenhud/$id," \
135
+ "is not installed. It comes as an optional dependency, so it is missing when" \
136
+ "optional dependencies were skipped (--omit=optional, --no-optional, or a lockfile" \
137
+ "made on another platform). Reinstall tokenhud the same way, without --omit=optional:" \
138
+ " bun add -g tokenhud (a global install with bun)" \
139
+ " npm install -g tokenhud (a global install with npm)" \
140
+ " npm install tokenhud (in a project)" \
141
+ "or install the binary directly: https://github.com/ZhuoQiuMcgill/tokenhud#install"
142
+ ;;
143
+ esac
144
+ exit 1
145
+ fi
146
+
147
+ # Installed by name on musl, the binary's package can lag behind tokenhud's: say so.
148
+ version_of() {
149
+ sed -n 's/^ *"version": *"\([^"]*\)".*/\1/p' "$1/package.json" 2>/dev/null
150
+ }
151
+ mine=$(version_of "$pkg")
152
+ theirs=$(version_of "$found")
153
+ if [ -n "$mine" ] && [ -n "$theirs" ] && [ "$mine" != "$theirs" ]; then
154
+ say "tokenhud: warning: ${found##*/node_modules/} is $theirs, but tokenhud is $mine. Update it" \
155
+ "to match: bun add -g ${found##*/node_modules/}@$mine (or npm install -g ${found##*/node_modules/}@$mine)"
156
+ fi
157
+
158
+ if [ "${given_path+set}" = set ]; then
159
+ PATH=$given_path
160
+ else
161
+ unset PATH
162
+ fi
163
+ exec "$binary" "$@"
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ // The `tokenhud` command of the npm package on Windows: when npm installs the package, it
3
+ // puts this file in place of bin/tokenhud (preinstall.cjs) and runs it with Node. It finds
4
+ // the platform package installed as an optional dependency (@tokenhud/<platform>-<arch>)
5
+ // and runs its binary with the same arguments, exit code and signals.
6
+ //
7
+ // Node, not Bun: Bun running it would load the working directory's .env and bunfig.toml,
8
+ // which tokenhud itself never reads. On Linux and macOS, bin/tokenhud is a sh script that
9
+ // runs the binary with no JS runtime at all. This file still knows every platform, so its
10
+ // tests run anywhere.
11
+
12
+ const { spawn } = require("node:child_process");
13
+ const { readFileSync } = require("node:fs");
14
+ const { dirname, join } = require("node:path");
15
+
16
+ const SUPPORTED = [
17
+ "linux-x64",
18
+ "linux-arm64",
19
+ "linux-x64-musl",
20
+ "linux-arm64-musl",
21
+ "darwin-x64",
22
+ "darwin-arm64",
23
+ "win32-x64",
24
+ "win32-arm64",
25
+ ];
26
+
27
+ /** Whether this Linux runs on musl (Alpine), as npm decides it for the `libc` field. */
28
+ function isMusl() {
29
+ try {
30
+ const ldd = readFileSync("/usr/bin/ldd", "latin1");
31
+ if (ldd.includes("musl")) return true;
32
+ if (ldd.includes("GNU C Library") || ldd.includes("glibc")) return false;
33
+ } catch {}
34
+ try {
35
+ process.report.excludeNetwork = true;
36
+ return !process.report.getReport().header.glibcVersionRuntime;
37
+ } catch {
38
+ return false;
39
+ }
40
+ }
41
+
42
+ /**
43
+ * The one platform package that runs here. A glibc binary can't start on musl, nor the
44
+ * other way round, so there is no second choice: Bun ignores the `libc` field and installs
45
+ * a glibc package on Alpine too, and that one must not be run.
46
+ */
47
+ function platformId() {
48
+ const base = `${process.platform}-${process.arch}`;
49
+ return process.platform === "linux" && isMusl() ? `${base}-musl` : base;
50
+ }
51
+
52
+ function findBinary(id) {
53
+ const exe = process.platform === "win32" ? "tokenhud.exe" : "tokenhud";
54
+ try {
55
+ return join(dirname(require.resolve(`@tokenhud/${id}/package.json`)), "bin", exe);
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ const id = platformId();
62
+ if (!SUPPORTED.includes(id)) {
63
+ console.error(
64
+ `tokenhud: there is no tokenhud binary for ${id}. Supported: ${SUPPORTED.join(", ")}.`,
65
+ );
66
+ process.exit(1);
67
+ }
68
+
69
+ const bin = findBinary(id);
70
+ if (bin === null) {
71
+ console.error(
72
+ id.endsWith("-musl")
73
+ ? // Not an optional dependency: Bun would install it on every Linux (it ignores `libc`).
74
+ "tokenhud: on musl Linux (Alpine), the tokenhud binary is a package of its own,\n" +
75
+ `@tokenhud/${id}, and it needs the C++ runtime. Install them beside tokenhud:\n` +
76
+ " apk add libstdc++ libgcc (as root)\n" +
77
+ ` bun add -g @tokenhud/${id} tokenhud\n` +
78
+ ` npm install -g @tokenhud/${id} tokenhud\n` +
79
+ "or install the binary directly: https://github.com/ZhuoQiuMcgill/tokenhud#install"
80
+ : `tokenhud: the package with the tokenhud binary for this machine, @tokenhud/${id},\n` +
81
+ "is not installed. It comes as an optional dependency, so it is missing when\n" +
82
+ "optional dependencies were skipped (--omit=optional, --no-optional, or a lockfile\n" +
83
+ "made on another platform). Reinstall tokenhud the same way, without --omit=optional:\n" +
84
+ " npm install -g tokenhud (a global install)\n" +
85
+ " npm install tokenhud (in a project)\n" +
86
+ "or install the binary directly: https://github.com/ZhuoQiuMcgill/tokenhud#install",
87
+ );
88
+ process.exit(1);
89
+ }
90
+
91
+ // Signals that stop this launcher stop tokenhud: each is passed on to it. Windows sends
92
+ // Ctrl-C, Ctrl-Break and a closing console window as SIGINT, SIGBREAK and SIGHUP.
93
+ const FORWARD =
94
+ process.platform === "win32"
95
+ ? ["SIGINT", "SIGBREAK", "SIGHUP", "SIGTERM"]
96
+ : ["SIGINT", "SIGTERM", "SIGHUP"];
97
+
98
+ let child = null;
99
+ const running = () => child !== null && child.exitCode === null && child.signalCode === null;
100
+
101
+ // Registered before tokenhud starts. A runtime can hand a handler to the OS a moment after
102
+ // `process.on` returns (Bun does), and a signal landing in that moment, with tokenhud already
103
+ // running, would kill this launcher and leave tokenhud running on its own.
104
+ for (const signal of FORWARD) {
105
+ try {
106
+ process.on(signal, () => {
107
+ if (running()) child.kill(signal);
108
+ });
109
+ } catch {
110
+ // A signal this runtime can't listen for on this platform.
111
+ }
112
+ }
113
+ // However this launcher ends (an uncaught error, a forced exit), tokenhud doesn't outlive it.
114
+ process.on("exit", () => {
115
+ if (running()) child.kill("SIGTERM");
116
+ });
117
+
118
+ child = spawn(bin, process.argv.slice(2), { stdio: "inherit", windowsHide: false });
119
+
120
+ child.on("error", (error) => {
121
+ console.error(`tokenhud: couldn't start ${bin}: ${error.message}`);
122
+ process.exit(1);
123
+ });
124
+ child.on("exit", (code, signal) => {
125
+ if (signal !== null) {
126
+ // Die of the same signal, so whoever started this launcher sees what stopped tokenhud.
127
+ for (const s of FORWARD) process.removeAllListeners(s);
128
+ process.kill(process.pid, signal);
129
+ return;
130
+ }
131
+ process.exit(code ?? 1);
132
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "tokenhud",
3
+ "version": "0.1.0-rc.2",
4
+ "license": "MIT",
5
+ "homepage": "https://github.com/ZhuoQiuMcgill/tokenhud#readme",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ZhuoQiuMcgill/tokenhud.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/ZhuoQiuMcgill/tokenhud/issues"
12
+ },
13
+ "description": "Live terminal heads-up display for coding-agent usage, limits and cost",
14
+ "keywords": [
15
+ "claude",
16
+ "claude-code",
17
+ "codex",
18
+ "usage",
19
+ "rate-limits",
20
+ "tui",
21
+ "mcp"
22
+ ],
23
+ "bin": {
24
+ "tokenhud": "bin/tokenhud"
25
+ },
26
+ "files": [
27
+ "bin/tokenhud",
28
+ "lib/tokenhud.cjs",
29
+ "preinstall.cjs"
30
+ ],
31
+ "scripts": {
32
+ "preinstall": "node preinstall.cjs"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "optionalDependencies": {
38
+ "@tokenhud/linux-x64": "0.1.0-rc.2",
39
+ "@tokenhud/linux-arm64": "0.1.0-rc.2",
40
+ "@tokenhud/darwin-x64": "0.1.0-rc.2",
41
+ "@tokenhud/darwin-arm64": "0.1.0-rc.2",
42
+ "@tokenhud/win32-x64": "0.1.0-rc.2",
43
+ "@tokenhud/win32-arm64": "0.1.0-rc.2"
44
+ }
45
+ }
package/preinstall.cjs ADDED
@@ -0,0 +1,34 @@
1
+ // The tokenhud package's preinstall (package.json: "preinstall": "node preinstall.cjs").
2
+ //
3
+ // bin/tokenhud, the command, is a POSIX sh script: on Linux and macOS it runs the binary
4
+ // with no JS runtime, and needs no install script. Windows can't run it. npm, which runs
5
+ // this under Node, links the command only after preinstall, and on Windows writes a
6
+ // tokenhud.cmd that starts the program the file's first line names. So on Windows, under
7
+ // Node, this puts lib/tokenhud.cjs (`#!/usr/bin/env node`) in its place, and the command
8
+ // runs on Node, which reads no .env or bunfig.toml. Elsewhere, and under Bun, it does
9
+ // nothing: bun makes its Windows shim before it runs any install script.
10
+ //
11
+ // It never fails the install: if the file can't be replaced, it says how to repair it.
12
+ "use strict";
13
+
14
+ const { copyFileSync, renameSync, rmSync } = require("node:fs");
15
+ const { join } = require("node:path");
16
+
17
+ if (process.platform === "win32" && !process.versions.bun) {
18
+ const command = join(__dirname, "bin", "tokenhud");
19
+ const tmp = `${command}.${process.pid}.tmp`;
20
+ try {
21
+ // A rename, so the command is never half written.
22
+ copyFileSync(join(__dirname, "lib", "tokenhud.cjs"), tmp);
23
+ renameSync(tmp, command);
24
+ } catch (error) {
25
+ try {
26
+ rmSync(tmp, { force: true });
27
+ } catch {}
28
+ console.warn(
29
+ `tokenhud: couldn't set up the Windows command (${error.message}).\n` +
30
+ "Reinstall tokenhud, or install it with install.ps1: " +
31
+ "https://github.com/ZhuoQiuMcgill/tokenhud#install",
32
+ );
33
+ }
34
+ }