@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/LICENSE +674 -0
- package/README.md +121 -233
- package/dist/collector.js +21 -4
- package/dist/config.js +1 -1
- package/dist/index.js +55 -37
- package/dist/release.js +1 -1
- package/dist/service.js +33 -16
- package/dist/uploader.js +10 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,199 +1,85 @@
|
|
|
1
1
|
# @usagefleet/cli
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
15
|
+
usagefleet install --token uf_xxx
|
|
19
16
|
```
|
|
20
17
|
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
|
138
|
-
usagefleet
|
|
139
|
-
usagefleet
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
73
|
+
`usagefleet notify-test` confirms it works. Tune with
|
|
74
|
+
`USAGEFLEET_NOTIFY_THRESHOLDS="50,80,95"`, disable with `USAGEFLEET_NOTIFY=0`.
|
|
168
75
|
|
|
169
|
-
|
|
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/
|
|
184
|
-
off by default
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
`
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
99
|
+
## Configuration
|
|
216
100
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
128
|
+
## Background service
|
|
235
129
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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',
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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(`
|
|
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
|
|
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
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
/**
|
|
213
|
-
*
|
|
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('
|
|
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.
|
|
2
|
+
export const RELEASE_VERSION = "1.2.71";
|