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 +21 -0
- package/README.md +655 -0
- package/bin/tokenhud +163 -0
- package/lib/tokenhud.cjs +132 -0
- package/package.json +45 -0
- package/preinstall.cjs +34 -0
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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" "$@"
|
package/lib/tokenhud.cjs
ADDED
|
@@ -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
|
+
}
|