@usagefleet/cli 1.2.69 → 1.2.71

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/README.md CHANGED
@@ -1,199 +1,85 @@
1
1
  # @usagefleet/cli
2
2
 
3
- Tails the local JSONL logs of Claude Code, Claude Desktop's agent-mode
4
- (Cowork) sessions, **and** the [pi](https://github.com/badlogic/pi-mono) coding
5
- agent (`~/.pi/agent/sessions`; only its Anthropic-provider records usage via
6
- other providers doesn't touch Claude limits), reporting token usage to a
7
- UsageFleet server. Read-only on the log files. Zero runtime dependencies
8
- (Node ≥ 20).
3
+ The UsageFleet collector. It tails the local logs of your Claude agents and
4
+ reports usage to [usagefleet.com](https://usagefleet.com), where it's split
5
+ across your device groups. Read-only on the log files, zero runtime
6
+ dependencies, Node 20.
9
7
 
10
8
  ## Install
11
9
 
12
- Two commands, identical on macOS, Linux and Windows. You only need a device
13
- **token** from the server's Devices page (endpoint defaults to
14
- `https://usagefleet.com`):
10
+ Get a device **token** from the [Devices](https://usagefleet.com/devices) page,
11
+ then, on that machine:
15
12
 
16
13
  ```bash
17
14
  npm i -g @usagefleet/cli
18
- usagefleet install --token uf_xxx # autostart at login; --endpoint <url> when self-hosting
15
+ usagefleet install --token uf_xxx
19
16
  ```
20
17
 
21
- In PowerShell chain them with `;` instead of `&&` Windows PowerShell 5.1 has
22
- no `&&`.
18
+ Same two commands on macOS, Linux and Windows (in PowerShell chain them with
19
+ `;` — 5.1 has no `&&`). `install` sets the collector to start at login and
20
+ writes `~/.config/usagefleet/config.json` (mode `600`). The dashboard fills in
21
+ within a minute.
23
22
 
24
- Autostart uses launchd (macOS), systemd `--user` (Linux) and Task Scheduler
25
- (Windows). If `npm i -g` fails with EACCES your global prefix is root-owned:
26
- either use a Node version manager (nvm, fnm, volta) or
27
- `npm config set prefix ~/.local` and put `~/.local/bin` on your PATH. Installing
28
- the collector under `sudo` would run it as the wrong user.
23
+ Running your own server? Add `--endpoint https://track.example.com`.
29
24
 
30
- **Update** happens on its own (see below) or on demand with
31
- `usagefleet update`. `npm i -g @usagefleet/cli` does the same thing.
25
+ If `npm i -g` fails with EACCES your global prefix is root-owned: use a Node
26
+ version manager (nvm, fnm, volta) or `npm config set prefix ~/.local` with
27
+ `~/.local/bin` on your PATH. Don't install under `sudo` — the collector would
28
+ run as the wrong user.
32
29
 
33
- ### Run from source
34
-
35
- ```bash
36
- cd apps/cli
37
- bun install
38
- bun run src/index.ts status # or: npm run build && node dist/index.js status
39
- ```
40
-
41
- ## Configure
42
-
43
- Get a device **token** from the server's Devices page, then either:
44
-
45
- ```bash
46
- usagefleet install --endpoint https://track.example.com --token uf_xxx
47
- # writes ~/.config/usagefleet/config.json
48
- ```
49
-
50
- That one file (mode `600`) holds everything the CLI persists — your settings
51
- plus the two machine-managed sections, `state` (per-log tail offsets) and
52
- `notify` (which alert thresholds already fired). Delete it to start clean.
53
- `XDG_CONFIG_HOME` is honoured. Installs predating the consolidation are folded
54
- in automatically from `~/.usagefleet.json`, `~/.usagefleet-state.json` and
55
- `~/.usagefleet-notify.json` on first write; those files are then unused and
56
- safe to delete.
57
-
58
- Or set env vars (they override the config file):
59
-
60
- | Variable | Meaning |
61
- |----------|---------|
62
- | `USAGEFLEET_ENDPOINT` | server base URL. Must be `https://` (loopback may be `http://`): it carries the device token on every request |
63
- | `USAGEFLEET_TOKEN` | device token |
64
- | `USAGEFLEET_PROJECTS` | override `~/.claude/projects` (Claude Code logs) |
65
- | `USAGEFLEET_DESKTOP` | override the Claude Desktop agent-mode sessions dir (auto-detected per-OS); set `off`/`0` to skip desktop collection |
66
- | `USAGEFLEET_PI` | override the pi agent sessions dirs, comma-separated (default `~/.pi/agent/sessions` plus whatever `PI_CODING_AGENT_DIR`/`PI_CODING_AGENT_SESSION_DIR` point at — a service inherits neither, so set this if you relocated pi's agent dir); set `off`/`0` to skip pi collection |
67
- | `USAGEFLEET_INTERVAL` | watch poll seconds (default 15) |
68
- | `USAGEFLEET_LIMITS_INTERVAL` | how often to ping for real 5h/weekly limits, seconds (default 300; decoupled from the faster usage poll so the 1-token ping doesn't run every cycle) |
69
- | `USAGEFLEET_NOTIFY` | desktop notifications on/off (default **on**; set `0`/`false`/`off` to disable) |
70
- | `USAGEFLEET_NOTIFY_THRESHOLDS` | comma list of utilization % that trigger an alert (default `80,95`) |
71
- | `USAGEFLEET_BATCH` | records per upload request (default 100, capped at the server's limit of 1000) |
72
- | `USAGEFLEET_CONFIG` | override the whole config file path (default `~/.config/usagefleet/config.json`) |
73
- | `USAGEFLEET_UPDATE` | set `0` to turn the self-update check off |
74
- | `USAGEFLEET_UPDATE_INTERVAL` | seconds between self-update checks (default `21600` = 6h, i.e. 4×/day) |
75
- | `USAGEFLEET_HOOK` | set `0` to keep the prompt-blocking hook out of `~/.claude/settings.json` |
76
-
77
- ### Setting the token per shell
78
-
79
- **bash / zsh** (macOS, Linux) — one-off for the current session:
80
-
81
- ```bash
82
- export USAGEFLEET_ENDPOINT="https://track.example.com"
83
- export USAGEFLEET_TOKEN="uf_xxx"
84
- ```
85
-
86
- Persist it by appending those lines to `~/.bashrc` / `~/.zshrc`, then
87
- `source ~/.zshrc`. Or set it inline for a single command:
88
-
89
- ```bash
90
- USAGEFLEET_ENDPOINT=https://track.example.com USAGEFLEET_TOKEN=uf_xxx usagefleet run
91
- ```
92
-
93
- **fish**:
94
-
95
- ```fish
96
- set -x USAGEFLEET_ENDPOINT "https://track.example.com"
97
- set -x USAGEFLEET_TOKEN "uf_xxx"
98
- # persist (writes to universal vars, survives restarts):
99
- set -Ux USAGEFLEET_TOKEN "uf_xxx"
100
- set -Ux USAGEFLEET_ENDPOINT "https://track.example.com"
101
- ```
102
-
103
- **PowerShell** (Windows) — current session:
104
-
105
- ```powershell
106
- $env:USAGEFLEET_ENDPOINT = "https://track.example.com"
107
- $env:USAGEFLEET_TOKEN = "uf_xxx"
108
- # persist for your user (new shells only):
109
- setx USAGEFLEET_TOKEN "uf_xxx"
110
- setx USAGEFLEET_ENDPOINT "https://track.example.com"
111
- ```
112
-
113
- **cmd.exe** (Windows):
114
-
115
- ```cmd
116
- set USAGEFLEET_ENDPOINT=https://track.example.com
117
- set USAGEFLEET_TOKEN=uf_xxx
118
- :: persist: setx USAGEFLEET_TOKEN "uf_xxx"
119
- ```
120
-
121
- > Prefer not to put a long-lived token in shell history/rc files? Use
122
- > `usagefleet install --endpoint <url> --token <t>` instead — it writes
123
- > `~/.config/usagefleet/config.json` (mode `600`), which the collector reads
124
- > automatically. The write merges, so re-running it rotates the token without
125
- > resetting your tail offsets.
126
- > When run as a service, `usagefleet install` bakes every `USAGEFLEET_*` value
127
- > that is currently set (plus `ANTHROPIC_API_KEY`) into the launchd/systemd unit.
128
- > The unit is written mode `600`, since it holds those secrets.
129
-
130
- ## Run
30
+ ## Commands
131
31
 
132
32
  ```bash
33
+ usagefleet status # service health, last limits reading, resolved config
133
34
  usagefleet run # one scan: upload usage + report limits
134
- usagefleet watch # poll continuously
35
+ usagefleet watch # poll continuously (what the service runs)
135
36
  usagefleet limits # report ONLY your real 5h/weekly limit usage
136
37
  usagefleet guard # exit 2 if this device's group is over a blocking limit
137
- usagefleet update # upgrade to the latest published version now
138
- usagefleet status # service health, last limits reading, resolved config
139
- usagefleet version # bare release version
38
+ usagefleet notify-test # fire a sample desktop notification
39
+ usagefleet update # upgrade now (it also self-updates every 6h)
40
+ usagefleet config # config file location + every env override
41
+ usagefleet install # (re)install the background service, idempotent
42
+ usagefleet uninstall # remove it
140
43
  ```
141
44
 
142
- ### Updates
143
-
144
- `watch` asks the npm registry for the published version at startup and then
145
- every 6 hours (`USAGEFLEET_UPDATE_INTERVAL`, in seconds); when it differs from
146
- the one baked into this build it runs `npm install -g @usagefleet/cli@<version>`
147
- and re-runs `install` to restart the service on it. `usagefleet update` does the
148
- same on demand.
45
+ ## What it collects
149
46
 
150
- npm is called through the absolute path next to the `node` running the
151
- collector, because a launchd/systemd service gets a minimal PATH that rarely has
152
- your version manager on it.
47
+ - **Token usage** Claude Code (`~/.claude/projects/**/*.jsonl`), Claude
48
+ Desktop agent-mode (Cowork) sessions, and the
49
+ [pi](https://github.com/badlogic/pi-mono) agent (`~/.pi/agent/sessions`,
50
+ Anthropic-provider records only; other providers don't touch Claude limits).
51
+ A per-file byte offset in the config file means each line is sent once;
52
+ rotation and truncation are handled and partial lines are never sent. Delivery
53
+ is at-least-once — the server dedups on `uuid`.
54
+ - **Your real limit %** — the collector uses the Claude login already on the
55
+ machine (subscription OAuth from `claude`: macOS login Keychain, elsewhere
56
+ `~/.claude/.credentials.json`; falling back to `ANTHROPIC_API_KEY`), sends a
57
+ 1-token ping to the Messages API, and reads Anthropic's
58
+ `anthropic-ratelimit-unified-5h/7d-utilization` headers. Credentials never
59
+ leave the machine — only the percentages do. `usagefleet status` shows which
60
+ login was found.
153
61
 
154
- Every failure is a no-op: registry unreachable, a version string that isn't
155
- plain semver, npm missing, npm exiting non-zero (a root-owned global prefix is
156
- the usual cause) and locally-built (`dev`) builds all leave the install as it
157
- was — the service is only restarted after npm reports success. Set
158
- `USAGEFLEET_UPDATE=0` to turn the check off.
62
+ Uploaded per record: token counts, model, session id, hostname, working
63
+ directory, git branch. Prompts, responses and file contents are never read.
159
64
 
160
- The collector tracks a per-file byte offset in the config file's `state`
161
- section, so each line is sent once; it handles rotation/truncation and never
162
- sends a partial line. Delivery is at-least-once — the server dedups on `uuid`.
65
+ ## Desktop notifications
163
66
 
164
- ### Real limit % (auto-detected)
67
+ The first time a window crosses a threshold (default `80` and `95`) you get a
68
+ desktop notification — once per threshold per window, re-armed when the window
69
+ resets. macOS uses `osascript`, Linux `notify-send` (falling back to
70
+ `kdialog --passivepopup`; install `libnotify-bin` if neither exists), Windows a
71
+ WinRT toast via `powershell.exe`.
165
72
 
166
- On `run`/`watch`/`limits`, the collector reads your **local Claude login** on
167
- this machine and reports your true utilization — no keys pasted anywhere:
73
+ `usagefleet notify-test` confirms it works. Tune with
74
+ `USAGEFLEET_NOTIFY_THRESHOLDS="50,80,95"`, disable with `USAGEFLEET_NOTIFY=0`.
168
75
 
169
- 1. **Subscription** the OAuth token from `claude` (Claude Code). On macOS it's
170
- read from the login Keychain (`Claude Code-credentials`); on Linux/Windows from
171
- `~/.claude/.credentials.json`. Sign in once with `claude` and it's detected.
172
- 2. **API key** — falls back to `ANTHROPIC_API_KEY` if no subscription login.
173
-
174
- It sends a 1-token ping to the Messages API and reads Anthropic's
175
- `anthropic-ratelimit-unified-5h/7d-utilization` (and `-reset`) headers, then POSTs
176
- the percentages to the server. `usagefleet status` shows which login was found.
177
- The token/credentials never leave your machine — only the resulting percentages
178
- are uploaded.
179
-
180
- ### Blocking prompts over the limit
76
+ ## Blocking prompts over the limit
181
77
 
182
78
  A group can be set to **refuse new prompts** once it has burned its budget slice
183
- (1/group count of the account limit) for a window. Two switches per group, both
184
- off by default flip them in the database:
185
-
186
- ```sql
187
- UPDATE groups SET block_on_session_limit = true WHERE name = 'Backend'; -- 5h window
188
- UPDATE groups SET block_on_weekly_limit = true WHERE name = 'Backend'; -- 7d window
189
- ```
190
-
191
- Enforcement is a Claude Code `UserPromptSubmit` hook, registered in
192
- `~/.claude/settings.json` automatically by `usagefleet install` (and removed
193
- by `usagefleet uninstall`). Re-running install refreshes the path instead of
194
- stacking a second hook; a settings file that doesn't parse is left untouched.
195
- Set `USAGEFLEET_HOOK=0` to keep your settings file out of it and add the hook
196
- yourself:
79
+ (1/N of the account limit) for a window a switch per window on the Groups
80
+ page, both off by default. `usagefleet install` registers a Claude Code `UserPromptSubmit` hook in
81
+ `~/.claude/settings.json` (removed by `uninstall`, refreshed rather than stacked
82
+ on re-install, and skipped entirely with `USAGEFLEET_HOOK=0`):
197
83
 
198
84
  ```json
199
85
  {
@@ -205,79 +91,81 @@ yourself:
205
91
  }
206
92
  ```
207
93
 
208
- `usagefleet guard` asks the server whether the calling device's group is over
209
- a window it blocks on; exit code 2 refuses the prompt and shows the reason.
210
- It **fails open** everywhere else no config, server down, timeout (5s), old
211
- server, 429 — because a tracker problem must never stop you from working.
212
- Only whole prompts are blocked, never tool calls mid-turn, so the current turn
213
- always finishes.
94
+ `guard` **fails open** everywhere else no config, server down, timeout (5s),
95
+ old server, 429 because a tracker problem must never stop you working. Only
96
+ whole prompts are blocked, never tool calls mid-turn, so the current turn always
97
+ finishes.
214
98
 
215
- ### Desktop notifications
99
+ ## Configuration
216
100
 
217
- When the collector reads your real 5h/weekly utilization, it raises a **desktop
218
- notification** the first time each window crosses a threshold (default `80%` and
219
- `95%`). It fires at most once per threshold per window and re-arms when the
220
- window resets, so it never spams.
101
+ `~/.config/usagefleet/config.json` (honours `XDG_CONFIG_HOME`) holds everything
102
+ the CLI persists: your settings plus two machine-managed sections, `state` (tail
103
+ offsets) and `notify` (which thresholds already fired). Delete it to start
104
+ clean. Re-running `install` merges, so rotating a token doesn't reset offsets.
221
105
 
222
- ```bash
223
- usagefleet notify-test # fire a sample notification to confirm it works
224
- ```
106
+ Env vars override the file:
107
+
108
+ | Variable | Meaning |
109
+ |----------|---------|
110
+ | `USAGEFLEET_ENDPOINT` | server base URL (default `https://usagefleet.com`). Must be `https://`, loopback may be `http://` |
111
+ | `USAGEFLEET_TOKEN` | device token |
112
+ | `USAGEFLEET_PROJECTS` | override `~/.claude/projects` |
113
+ | `USAGEFLEET_DESKTOP` | override the Claude Desktop sessions dir; `off` to skip it |
114
+ | `USAGEFLEET_PI` | override the pi sessions dirs, comma-separated; `off` to skip |
115
+ | `USAGEFLEET_INTERVAL` | watch poll seconds (default 15) |
116
+ | `USAGEFLEET_LIMITS_INTERVAL` | seconds between limit pings (default 300, so the 1-token ping isn't every cycle) |
117
+ | `USAGEFLEET_NOTIFY` | desktop notifications, on by default |
118
+ | `USAGEFLEET_NOTIFY_THRESHOLDS` | utilization % that trigger an alert (default `80,95`) |
119
+ | `USAGEFLEET_BATCH` | records per upload (default 100, server caps at 1000) |
120
+ | `USAGEFLEET_CONFIG` | override the config file path |
121
+ | `USAGEFLEET_UPDATE` | `0` turns the self-update check off |
122
+ | `USAGEFLEET_UPDATE_INTERVAL` | seconds between update checks (default `21600` = 6h) |
123
+ | `USAGEFLEET_HOOK` | `0` keeps the prompt-blocking hook out of `~/.claude/settings.json` |
225
124
 
226
- - **macOS** uses `osascript` Notification Center (no extra install).
227
- - **Linux (KDE Plasma / freedesktop)** uses `notify-send`; if that's missing it
228
- falls back to KDE's `kdialog --passivepopup`. Install `notify-send` via
229
- `libnotify` (e.g. `apt install libnotify-bin`) if neither is present.
230
- - **Windows** — a WinRT toast via built-in `powershell.exe` → Action Center (no
231
- extra install; it appears under "Windows PowerShell"). Check Settings →
232
- Notifications if nothing shows up.
125
+ When run as a service, `install` bakes every `USAGEFLEET_*` value currently set
126
+ (plus `ANTHROPIC_API_KEY`) into the launchd/systemd unit, written mode `600`.
233
127
 
234
- Tune or disable:
128
+ ## Background service
235
129
 
236
- ```bash
237
- export USAGEFLEET_NOTIFY_THRESHOLDS="50,80,95" # alert at 50/80/95%
238
- export USAGEFLEET_NOTIFY=0 # turn notifications off
239
- ```
130
+ `install` is idempotent and reload-safe: re-running it rewrites the service
131
+ definition and restarts it, so it doubles as the update step. It launches an
132
+ absolute `node` plus the installed package path, so an empty service PATH is
133
+ fine — but removing that Node version (`nvm uninstall`) stops the collector
134
+ until you re-run `usagefleet install` under the new one.
135
+
136
+ - **macOS** — a LaunchAgent (`~/Library/LaunchAgents`, RunAtLoad + KeepAlive),
137
+ booted immediately. Logs in `~/Library/Logs/usagefleet/`. Plist is mode `600`:
138
+ it holds your token.
139
+ - **Linux** — a `systemd --user` unit, plus `loginctl enable-linger $USER` so it
140
+ survives logout. If `systemctl` can't be driven it prints the manual steps.
141
+ - **Windows** — a Scheduled Task (`usagefleet`) starting at logon, restarting on
142
+ failure, running hidden through a `wscript` launcher in
143
+ `%LOCALAPPDATA%\usagefleet` (which carries the env values set at install
144
+ time). Log: `%LOCALAPPDATA%\usagefleet\usagefleet.log`. Inspect with
145
+ `schtasks /query /tn usagefleet /v /fo list`.
146
+
147
+ > **macOS limits under the service.** Usage collection works headless. Reading
148
+ > the login Keychain for the real limit % may be denied to a non-interactive
149
+ > agent — the collector logs a clear hint. Either approve `/usr/bin/security`
150
+ > access to the `Claude Code-credentials` item once, or set `ANTHROPIC_API_KEY`
151
+ > before `usagefleet install` so limits use the API key.
240
152
 
241
- > **Under a background service.** On macOS the LaunchAgent runs in your GUI
242
- > session, so notifications appear normally. On Linux a `systemd --user` service
243
- > needs access to your session bus (`DBUS_SESSION_BUS_ADDRESS`) for `notify-send`
244
- > to reach the notification daemon — typical for `--user` units in a graphical
245
- > login. `usagefleet install` bakes `USAGEFLEET_NOTIFY*` into the unit.
153
+ ## Updates
246
154
 
247
- ## Run as a background service
155
+ `watch` checks the npm registry at startup and every 6 hours; on a new version
156
+ it runs `npm install -g @usagefleet/cli@<version>` and re-runs `install` to
157
+ restart the service on it. `usagefleet update` does the same on demand. npm is
158
+ called through the absolute path next to the running `node`, because a
159
+ launchd/systemd service gets a minimal PATH.
160
+
161
+ Every failure is a no-op — registry unreachable, non-semver version, npm
162
+ missing or exiting non-zero, local `dev` builds — and the service is only
163
+ restarted after npm reports success.
164
+
165
+ ## From source
248
166
 
249
167
  ```bash
250
- usagefleet install # launchd (macOS) / systemd --user (Linux) / Task Scheduler (Windows)
251
- usagefleet uninstall
168
+ cd apps/cli
169
+ bun install
170
+ bun run src/index.ts status # or: npm run build && node dist/index.js status
252
171
  ```
253
-
254
- `install` is idempotent and reload-safe: re-running it rewrites the service
255
- definition and restarts it, so it doubles as the update step. The service is
256
- launched as an absolute `node` plus the installed package path, so an empty
257
- service PATH is fine — but removing that Node version (`nvm uninstall`) stops
258
- the collector until you re-run `usagefleet install` under the new one.
259
-
260
- - **macOS** — installs a LaunchAgent (`~/Library/LaunchAgents`, RunAtLoad +
261
- KeepAlive) and boots it (bootout → bootstrap → kickstart). Logs at
262
- `~/Library/Logs/usagefleet/usagefleet.*.log` (not `/tmp`, which is
263
- world-writable). The plist is written mode `600`: it holds your token.
264
- - **Linux** — writes a `--user` unit and runs `systemctl --user daemon-reload`,
265
- `enable --now`, `restart`, plus `loginctl enable-linger $USER` automatically so
266
- it survives logout. If `systemctl` can't be driven, it prints the manual steps.
267
- - **Windows** — registers a Scheduled Task (`usagefleet`) that starts at logon,
268
- restarts on failure, and runs **hidden** (no console window) through a
269
- generated `wscript` launcher in `%LOCALAPPDATA%\usagefleet`. Task XML has no
270
- env support, so the launcher carries the `USAGEFLEET_*` values that were set
271
- when you ran `install`, and redirects output to
272
- `%LOCALAPPDATA%\usagefleet\usagefleet.log` (truncated on each start).
273
- Inspect it with `schtasks /query /tn usagefleet /v /fo list`.
274
-
275
- The OS is reported automatically (`process.platform` → `mac`/`linux`/`windows`).
276
-
277
- > **macOS limits under the service.** Usage collection (reading JSONL files) works
278
- > headless. The **real limit %** feature reads the login Keychain, and a
279
- > non-interactive launchd agent may be denied that read — the collector logs a
280
- > clear hint when this happens. If it does, either approve `/usr/bin/security`
281
- > access to the `Claude Code-credentials` item once, or set `ANTHROPIC_API_KEY`
282
- > before `usagefleet install` (it's baked into the service) so limits use the
283
- > API key instead.
package/dist/collector.js CHANGED
@@ -102,7 +102,7 @@ export async function runOnce(cfg, log = () => {
102
102
  else if (outcome === 'plan') {
103
103
  // The device sits outside the account's device limit (402). Every other
104
104
  // file gets the same answer, so stop and say what unblocks it once.
105
- log('warn', `device outside your plan's device limit · free a slot or upgrade at ${cfg.endpoint}/devices · nothing is lost, uploads resume once it fits`);
105
+ log('warn', planWall(cfg.endpoint));
106
106
  result.failed = true;
107
107
  break;
108
108
  }
@@ -205,6 +205,11 @@ function pruneMissingFiles(state, scanned) {
205
205
  }
206
206
  return removed;
207
207
  }
208
+ /** The one thing that unblocks a device parked outside the account's device limit.
209
+ * Shared by both upload legs so the wording cannot drift between them. */
210
+ function planWall(endpoint) {
211
+ return `device outside your plan's device limit · free a slot or upgrade at ${endpoint}/devices · nothing is lost, uploads resume once it fits`;
212
+ }
208
213
  /**
209
214
  * Auto-detect the local Claude login, read the real 5h/weekly utilization from
210
215
  * Anthropic's rate-limit headers, and report it to the server. Best-effort —
@@ -235,9 +240,21 @@ export async function reportLimitsOnce(cfg, log = () => {
235
240
  log('warn', `limits fetch failed · ${error.message}`);
236
241
  return null;
237
242
  }
238
- const ok = await postLimits(report, cfg);
239
- if (!ok) {
240
- log('warn', 'limits upload failed');
243
+ // Same vocabulary as the usage leg: a rejection the operator can act on has to
244
+ // say which one it is, since this runs every cycle and an anonymous failure
245
+ // would repeat forever without ever naming the fix.
246
+ const outcome = await postLimits(report, cfg);
247
+ if (outcome === 'plan') {
248
+ log('warn', planWall(cfg.endpoint));
249
+ }
250
+ else if (outcome === 'auth') {
251
+ log('warn', 'limits rejected · device token invalid or revoked · re-run `usagefleet install --token <device-token>`');
252
+ }
253
+ else if (outcome === 'invalid') {
254
+ log('warn', 'limits rejected as malformed · this is a bug, please report it');
255
+ }
256
+ else if (outcome !== 'ok') {
257
+ log('warn', 'limits upload failed · retrying next cycle');
241
258
  }
242
259
  // Cache the reading so `status` can show current usage without spending
243
260
  // another billable API call.
package/dist/config.js CHANGED
@@ -17,7 +17,7 @@ export function loadConfig() {
17
17
  throw new Error('USAGEFLEET_TOKEN is not set');
18
18
  }
19
19
  if (!isSecureEndpoint(endpoint)) {
20
- throw new Error(`USAGEFLEET_ENDPOINT must be https (got ${endpoint}). It carries the device token on every request and self-update executes a binary fetched from it.`);
20
+ throw new Error(`endpoint must be https (got ${endpoint}). It carries the device token on every request. Set --endpoint or USAGEFLEET_ENDPOINT.`);
21
21
  }
22
22
  // Guard batch size: "0" (infinite loop), NaN (silent drop), fractional → 100.
23
23
  // Clamped to the server's own 1000-record cap, since a larger batch is
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { sendNotification } from './notify.js';
8
8
  import { detectOs } from './os.js';
9
9
  import { RELEASE_VERSION } from './release.js';
10
10
  import { serviceStatus } from './service.js';
11
- import { readStore, storePath, updateStore } from './store.js';
11
+ import { readStore, storePath } from './store.js';
12
12
  import { ago, bar, blue, dim, fail, green, header, hint, host, line, note, pct, row, state as stateLine, step, tilde, warn, yellow, } from './ui.js';
13
13
  import { checkForUpdate } from './update.js';
14
14
  function flag(name) {
@@ -194,23 +194,61 @@ function limitHealth(fiveHour, sevenDay) {
194
194
  * token without resetting tail offsets. Endpoint only matters when
195
195
  * self-hosting; unset keeps whatever is configured. */
196
196
  async function cmdInstall() {
197
+ // Apply the flags to the env loadConfig() reads rather than writing them to the
198
+ // store, so there is exactly one precedence chain and install() persists its
199
+ // single winner. Writing to the store first inverted the precedence: loadConfig
200
+ // prefers the env, so a stale USAGEFLEET_TOKEN in the install shell beat the
201
+ // flag and got written back over it, silently voiding token rotation. It also
202
+ // means a rejected value never reaches disk.
197
203
  const endpoint = flag('endpoint');
198
204
  const token = flag('token');
199
- if (endpoint || token) {
200
- updateStore(storePath(), store => {
201
- if (endpoint) {
202
- store.endpoint = endpoint;
203
- }
204
- if (token) {
205
- store.token = token;
206
- }
207
- });
205
+ if (endpoint) {
206
+ process.env.USAGEFLEET_ENDPOINT = endpoint;
207
+ }
208
+ if (token) {
209
+ process.env.USAGEFLEET_TOKEN = token;
208
210
  }
209
211
  const { install } = await import('./service.js');
210
212
  install();
211
213
  }
212
- /** Command list and env reference, in the same padded-column style as the
213
- * result lines: name in white, meaning in gray. */
214
+ /** Padded two-column list name in white, meaning in gray, like the result
215
+ * lines. Shared by `help` and `config`. */
216
+ function print(rows) {
217
+ const width = Math.max(...rows.map(([name]) => name.length));
218
+ for (const [name, meaning] of rows) {
219
+ console.log(` ${name.padEnd(width)} ${dim(meaning)}`);
220
+ }
221
+ }
222
+ /** Where settings live and every env var that overrides them. Reads nothing:
223
+ * it must work before a token exists, when the config is what you're fixing. */
224
+ function cmdConfig() {
225
+ const env = [
226
+ ['USAGEFLEET_ENDPOINT', 'server base URL (self-hosting only)'],
227
+ ['USAGEFLEET_TOKEN', 'device token from the Devices page'],
228
+ ['USAGEFLEET_PROJECTS', 'override ~/.claude/projects'],
229
+ ['USAGEFLEET_DESKTOP', 'override the Claude Desktop dir ("off" disables)'],
230
+ ['USAGEFLEET_PI', 'override pi session dirs, comma-separated'],
231
+ ['USAGEFLEET_INTERVAL', 'watch interval seconds (default 15)'],
232
+ ['USAGEFLEET_LIMITS_INTERVAL', 'seconds between limits pings (default 300)'],
233
+ ['USAGEFLEET_BATCH', 'records per upload (default 100, max 1000)'],
234
+ ['USAGEFLEET_NOTIFY', 'desktop notifications (0 disables)'],
235
+ ['USAGEFLEET_NOTIFY_THRESHOLDS', 'comma list of % alerts (default 80,95)'],
236
+ ['USAGEFLEET_HOOK', 'register the guard on install (0 skips)'],
237
+ ['USAGEFLEET_UPDATE', 'self-update while watching (0 disables)'],
238
+ ['USAGEFLEET_UPDATE_INTERVAL', 'seconds between update checks (default 21600)'],
239
+ ['USAGEFLEET_CONFIG', 'relocate the config file'],
240
+ ];
241
+ console.log(header());
242
+ console.log(hint('settings, tail offsets and notification marks'));
243
+ console.log('');
244
+ console.log(row('file', tilde(storePath())));
245
+ console.log('');
246
+ console.log(hint('env — overrides the file'));
247
+ print(env);
248
+ console.log('');
249
+ console.log(hint('`usagefleet status` shows the resolved values'));
250
+ }
251
+ /** Command list, in the same padded-column style as the result lines. */
214
252
  function help() {
215
253
  const commands = [
216
254
  ['run', 'scan once, upload usage + report limits'],
@@ -220,39 +258,16 @@ function help() {
220
258
  ['update', 'update to the latest release now'],
221
259
  ['notify-test', 'fire a test desktop notification'],
222
260
  ['status', 'service health, limits, resolved config'],
261
+ ['config', 'config file location and env overrides'],
223
262
  ['version', 'print the release version'],
224
263
  ['install --token <t>', 'configure + install the service and prompt guard'],
225
264
  ['uninstall', 'remove the service and the guard'],
226
265
  ];
227
- const env = [
228
- ['USAGEFLEET_ENDPOINT', 'server base URL (self-hosting only)'],
229
- ['USAGEFLEET_TOKEN', 'device token from the Devices page'],
230
- ['USAGEFLEET_PROJECTS', 'override ~/.claude/projects'],
231
- ['USAGEFLEET_DESKTOP', 'override the Claude Desktop dir ("off" disables)'],
232
- ['USAGEFLEET_PI', 'override pi session dirs, comma-separated'],
233
- ['USAGEFLEET_INTERVAL', 'watch interval seconds'],
234
- ['USAGEFLEET_NOTIFY', 'desktop notifications (0 disables)'],
235
- ['USAGEFLEET_HOOK', 'register the guard on install (0 skips)'],
236
- ['USAGEFLEET_UPDATE', 'self-update while watching (0 disables)'],
237
- ['USAGEFLEET_UPDATE_INTERVAL', 'seconds between update checks (default 21600)'],
238
- ['USAGEFLEET_NOTIFY_THRESHOLDS', 'comma list of % alerts (default 80,95)'],
239
- ['USAGEFLEET_CONFIG', 'relocate the config file'],
240
- ];
241
- const pad = (rows) => Math.max(...rows.map(([name]) => name.length));
242
- const print = (rows) => {
243
- const width = pad(rows);
244
- for (const [name, meaning] of rows) {
245
- console.log(` ${name.padEnd(width)} ${dim(meaning)}`);
246
- }
247
- };
248
266
  console.log(header());
249
- console.log(hint('claude usage collector'));
250
267
  console.log('');
251
268
  print(commands);
252
269
  console.log('');
253
- console.log(hint('env overrides ~/.config/usagefleet/config.json, which holds'));
254
- console.log(hint('settings, tail offsets and notification marks'));
255
- print(env);
270
+ console.log(hint('`usagefleet config` lists the config file and its env overrides'));
256
271
  }
257
272
  async function main() {
258
273
  // Log-and-continue for the long-running watch daemon: a stray rejection must
@@ -291,6 +306,9 @@ async function main() {
291
306
  case 'status': {
292
307
  return cmdStatus();
293
308
  }
309
+ case 'config': {
310
+ return cmdConfig();
311
+ }
294
312
  // Bare version, so the installer can compare builds without parsing help.
295
313
  case 'version':
296
314
  case '--version':
package/dist/release.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by .github/workflows/release.yml.
2
- export const RELEASE_VERSION = "1.2.69";
2
+ export const RELEASE_VERSION = "1.2.71";