@usagefleet/cli 1.2.182 → 1.2.184
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 +7 -7
- package/dist/claude-limits.js +23 -1
- package/dist/completion.js +3 -3
- package/dist/config.js +1 -1
- package/dist/hook.js +6 -13
- package/dist/index.js +2 -2
- package/dist/pi-hook.js +4 -7
- package/dist/release.js +1 -1
- package/dist/service.js +10 -5
- package/dist/store.js +0 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,7 +56,6 @@ usagefleet status # service health, last limits reading, resolved config
|
|
|
56
56
|
usagefleet run # one scan: upload usage + report limits
|
|
57
57
|
usagefleet watch # poll continuously (what the service runs)
|
|
58
58
|
usagefleet limits # report ONLY your real 5h/weekly limit usage
|
|
59
|
-
usagefleet guard # exit 2 if this device's group is over a blocking limit
|
|
60
59
|
usagefleet notify-test # fire a sample desktop notification
|
|
61
60
|
usagefleet update # upgrade now (it also self-updates every 6h)
|
|
62
61
|
usagefleet config # config file location + every env override
|
|
@@ -127,9 +126,11 @@ A group can be set to **refuse new prompts** once it has burned its budget slice
|
|
|
127
126
|
page, both off by default. Per-model limits (e.g. a model's weekly cap) follow
|
|
128
127
|
the switch of their window. Each device also has its own blocking toggle on the
|
|
129
128
|
Fleet page: switched off, that machine is never refused, whatever its group
|
|
130
|
-
says.
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
says. Blocking is switched only on the dashboard: the device has no setting for
|
|
130
|
+
it. `usagefleet login` registers a Claude Code `UserPromptSubmit` hook in
|
|
131
|
+
`~/.claude/settings.json`, and the running collector restores it on every cycle
|
|
132
|
+
if it goes missing (it also drops a `disableAllHooks` there). Only `uninstall`
|
|
133
|
+
removes it:
|
|
133
134
|
|
|
134
135
|
```json
|
|
135
136
|
{
|
|
@@ -143,8 +144,8 @@ on re-install, and skipped entirely with `USAGEFLEET_HOOK=0`):
|
|
|
143
144
|
|
|
144
145
|
If the pi coding agent is on the machine (`~/.pi/agent` exists), `login` also
|
|
145
146
|
writes a pi extension — `~/.pi/agent/extensions/usagefleet-guard.ts` — that runs
|
|
146
|
-
the same guard before every pi prompt. Same lifecycle:
|
|
147
|
-
removed by `uninstall
|
|
147
|
+
the same guard before every pi prompt. Same lifecycle: restored by the running
|
|
148
|
+
collector, removed by `uninstall`.
|
|
148
149
|
|
|
149
150
|
If `USAGEFLEET_CONFIG` was set when you logged in, the installed command carries
|
|
150
151
|
it (`USAGEFLEET_CONFIG=<path> usagefleet guard`, or `set "..." && ...` on
|
|
@@ -183,7 +184,6 @@ it (`usagefleet config` prints this same table):
|
|
|
183
184
|
| `batch` | `USAGEFLEET_BATCH` | records per upload (default 100, server caps at 1000) |
|
|
184
185
|
| `update` | `USAGEFLEET_UPDATE` | `false`/`0` turns the self-update check off |
|
|
185
186
|
| `updateInterval` | `USAGEFLEET_UPDATE_INTERVAL` | seconds between update checks (default `21600` = 6h) |
|
|
186
|
-
| `hook` | `USAGEFLEET_HOOK` | `false`/`0` keeps the prompt-blocking guard out of Claude Code and pi |
|
|
187
187
|
| — | `USAGEFLEET_CONFIG` | override the config file path (env only — it locates the file) |
|
|
188
188
|
| — | `CLAUDE_CONFIG_DIR` | Claude Code's own knob: which login to watch (default `~/.claude`) |
|
|
189
189
|
|
package/dist/claude-limits.js
CHANGED
|
@@ -198,9 +198,31 @@ export function oauthLimitsReport(body) {
|
|
|
198
198
|
if (account.fiveHourPct == null && account.sevenDayPct == null) {
|
|
199
199
|
return null;
|
|
200
200
|
}
|
|
201
|
-
const
|
|
201
|
+
const surfaces = parseSurfaces(body);
|
|
202
|
+
const report = {
|
|
203
|
+
...account,
|
|
204
|
+
modelLimits: parseOauthUsage(body),
|
|
205
|
+
source: 'sub',
|
|
206
|
+
...(surfaces.length > 0 && { surfaces }),
|
|
207
|
+
};
|
|
202
208
|
return account.fiveHourPct == null && listsNoSessionLimit(body) ? { ...report, noFiveHourWindow: true } : report;
|
|
203
209
|
}
|
|
210
|
+
export function parseSurfaces(body) {
|
|
211
|
+
const rows = body?.seven_day_breakdown?.rows;
|
|
212
|
+
if (!Array.isArray(rows)) {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
const out = [];
|
|
216
|
+
for (const row of rows) {
|
|
217
|
+
const pct = typeof row?.percent === 'number' ? clampPct(row.percent) : null;
|
|
218
|
+
if (typeof row?.key === 'string' && row.key !== '' && pct !== null) {
|
|
219
|
+
const label = typeof row.display_name === 'string' && row.display_name !== '' ? row.display_name : row.key;
|
|
220
|
+
// Cut to the server's ceilings: an over-long row would get the whole limits post rejected.
|
|
221
|
+
out.push({ key: row.key.slice(0, 64), label: label.slice(0, 64), pct });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return out.slice(0, 16);
|
|
225
|
+
}
|
|
204
226
|
// Ultra plans have no 5h window. Max lists a `kind: "session"` entry in limits[];
|
|
205
227
|
// a payload without limits[] says nothing either way.
|
|
206
228
|
function listsNoSessionLimit(body) {
|
package/dist/completion.js
CHANGED
|
@@ -8,12 +8,12 @@ import { writeFileAtomic } from './atomic-write.js';
|
|
|
8
8
|
*
|
|
9
9
|
* `watch` is deliberately absent: it is the entrypoint the installed service
|
|
10
10
|
* runs, not something to type. It still dispatches — every plist and unit on
|
|
11
|
-
* disk names it — it just isn't advertised.
|
|
11
|
+
* disk names it — it just isn't advertised. Nor is `guard`, the entrypoint the
|
|
12
|
+
* prompt hooks run; blocking is switched on the dashboard. So are `install`/`init`, the former
|
|
12
13
|
* names of `login`, kept dispatching for commands already pasted into scripts. */
|
|
13
14
|
export const commands = [
|
|
14
15
|
{ name: 'run', meaning: 'scan once, upload usage + report limits' },
|
|
15
16
|
{ name: 'limits', meaning: 'report only your real 5h/weekly usage' },
|
|
16
|
-
{ name: 'guard', meaning: 'exit 2 when the group is over a blocking limit' },
|
|
17
17
|
{ name: 'update', meaning: 'update to the latest release now' },
|
|
18
18
|
{ name: 'notify-test', meaning: 'fire a test desktop notification' },
|
|
19
19
|
{ name: 'status', meaning: 'service health, limits, resolved config' },
|
|
@@ -26,7 +26,7 @@ export const shells = ['zsh', 'fish'];
|
|
|
26
26
|
/** Everything `main` dispatches, advertised or not — the pool a mistyped command
|
|
27
27
|
* is matched against. Kept next to `commands` so a new case in the switch has
|
|
28
28
|
* one obvious place to be listed. */
|
|
29
|
-
const dispatchable = [...commands.map(c => c.name), 'watch', 'install', 'init', 'help'];
|
|
29
|
+
const dispatchable = [...commands.map(c => c.name), 'watch', 'guard', 'install', 'init', 'help'];
|
|
30
30
|
/** The command a typo probably meant, or undefined when nothing is close enough
|
|
31
31
|
* to guess: `statu` → `status`, `frobnicate` → nothing. */
|
|
32
32
|
export function suggest(input) {
|
package/dist/config.js
CHANGED
|
@@ -53,7 +53,7 @@ export function positiveNumber(env, fromFile) {
|
|
|
53
53
|
}
|
|
54
54
|
return null;
|
|
55
55
|
}
|
|
56
|
-
/** The off-switches (
|
|
56
|
+
/** The off-switches (update, notifications): an env value wins when
|
|
57
57
|
* present — 0/false/off/no disable, anything else enables — otherwise `false`
|
|
58
58
|
* under the config-file key disables. */
|
|
59
59
|
export function flagOff(env, fromFile) {
|
package/dist/hook.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { writeFileAtomic } from './atomic-write.js';
|
|
4
|
-
import { flagOff } from './config.js';
|
|
5
4
|
import { claudeSettingsPath } from './paths.js';
|
|
6
|
-
import { readStore } from './store.js';
|
|
7
5
|
import { step, tilde, warn } from './ui.js';
|
|
8
6
|
/** Outer bound on the hook, in seconds. runGuard's own fetch gives up after 5s
|
|
9
7
|
* and fails open; this only matters if the process itself wedges. */
|
|
@@ -55,9 +53,10 @@ export function withoutGuardHook(settings) {
|
|
|
55
53
|
};
|
|
56
54
|
}
|
|
57
55
|
/** Strip-then-append, so re-running install refreshes a stale binary path
|
|
58
|
-
* instead of stacking a second hook.
|
|
56
|
+
* instead of stacking a second hook. Drops `disableAllHooks`, which would
|
|
57
|
+
* switch the guard off without touching the hook itself. */
|
|
59
58
|
export function withGuardHook(settings, command) {
|
|
60
|
-
const base = withoutGuardHook(settings);
|
|
59
|
+
const { disableAllHooks: _, ...base } = withoutGuardHook(settings);
|
|
61
60
|
const groups = base.hooks?.UserPromptSubmit ?? [];
|
|
62
61
|
return {
|
|
63
62
|
...base,
|
|
@@ -103,16 +102,10 @@ function editSettings(transform, onWrite) {
|
|
|
103
102
|
writeFileAtomic(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
104
103
|
onWrite(path);
|
|
105
104
|
}
|
|
106
|
-
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* `usagefleet login`; USAGEFLEET_HOOK=0 or `"hook": false` in the config file
|
|
110
|
-
* keeps settings.json untouched.
|
|
111
|
-
*/
|
|
105
|
+
/** Register `usagefleet guard` as a Claude Code UserPromptSubmit hook, so a
|
|
106
|
+
* group with blocking enabled actually refuses prompts. No off-switch: whether
|
|
107
|
+
* a device blocks is decided on the dashboard, not on the device. */
|
|
112
108
|
export function installPromptHook(program) {
|
|
113
|
-
if (flagOff(process.env.USAGEFLEET_HOOK, readStore().hook)) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
109
|
const command = guardCommand(program, process.env.USAGEFLEET_CONFIG);
|
|
117
110
|
editSettings(s => withGuardHook(s, command), path => console.log(step('hook', `prompt guard · ${tilde(path)}`)));
|
|
118
111
|
}
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { loadNotifyConfig } from './notifier.js';
|
|
|
9
9
|
import { sendNotification } from './notify.js';
|
|
10
10
|
import { detectOs } from './os.js';
|
|
11
11
|
import { RELEASE_VERSION } from './release.js';
|
|
12
|
-
import { isServiceProcess, serviceStatus } from './service.js';
|
|
12
|
+
import { installGuards, isServiceProcess, serviceStatus } from './service.js';
|
|
13
13
|
import { readStore, storePath, updateStore } from './store.js';
|
|
14
14
|
import { ago, bar, blue, dim, fail, green, header, hint, host, line, note, pct, row, state as stateLine, step, tilde, warn, yellow, } from './ui.js';
|
|
15
15
|
import { checkForUpdate } from './update.js';
|
|
@@ -106,6 +106,7 @@ async function cmdWatch() {
|
|
|
106
106
|
}
|
|
107
107
|
running = true;
|
|
108
108
|
try {
|
|
109
|
+
installGuards();
|
|
109
110
|
const r = await runOnce(cfg, stream, dirCache, tokenOwners);
|
|
110
111
|
// Dropped records are real data loss, so they must show up even in a
|
|
111
112
|
// cycle that uploaded nothing.
|
|
@@ -298,7 +299,6 @@ function cmdConfig() {
|
|
|
298
299
|
['notifications', 'USAGEFLEET_NOTIFY', 'desktop notifications (false/0 disables)'],
|
|
299
300
|
['notifyMode', 'USAGEFLEET_NOTIFY_MODE', 'alert preset: off | quiet | standard | steady | critical'],
|
|
300
301
|
['notifyThresholds', 'USAGEFLEET_NOTIFY_THRESHOLDS', '% alerts · array; env comma list (replaces the preset)'],
|
|
301
|
-
['hook', 'USAGEFLEET_HOOK', 'register the prompt guard on install (false/0 skips)'],
|
|
302
302
|
['update', 'USAGEFLEET_UPDATE', 'self-update while watching (false/0 disables)'],
|
|
303
303
|
['updateInterval', 'USAGEFLEET_UPDATE_INTERVAL', 'seconds between update checks (default 21600)'],
|
|
304
304
|
['—', 'USAGEFLEET_CONFIG', 'relocate the config file (env only — it locates the file)'],
|
package/dist/pi-hook.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { writeFileAtomic } from './atomic-write.js';
|
|
4
|
-
import { flagOff } from './config.js';
|
|
5
4
|
import { piAgentDir } from './paths.js';
|
|
6
|
-
import { readStore } from './store.js';
|
|
7
5
|
import { step, tilde } from './ui.js';
|
|
8
6
|
/** pi has no settings-file hooks; its extension point is a TypeScript file
|
|
9
7
|
* auto-discovered from `<agent-dir>/extensions/*.ts`. So the pi guard is a
|
|
10
8
|
* generated file rather than a settings entry, with the same lifecycle as the
|
|
11
|
-
* Claude hook: written by `login
|
|
12
|
-
*
|
|
13
|
-
* `"hook": false` in the config file. */
|
|
9
|
+
* Claude hook: written by `login`, restored by every `watch` tick, removed by
|
|
10
|
+
* `uninstall`. */
|
|
14
11
|
const FILE = 'usagefleet-guard.ts';
|
|
15
12
|
export function piGuardPath(agentDir = piAgentDir()) {
|
|
16
13
|
return join(agentDir, 'extensions', FILE);
|
|
@@ -22,7 +19,7 @@ export function piGuardPath(agentDir = piAgentDir()) {
|
|
|
22
19
|
export function piGuardExtension(program, configPath) {
|
|
23
20
|
const env = configPath ? `{ ...process.env, USAGEFLEET_CONFIG: ${JSON.stringify(configPath)} }` : 'process.env';
|
|
24
21
|
return `// Generated by \`usagefleet login\` — do not edit; \`usagefleet uninstall\` removes it,
|
|
25
|
-
//
|
|
22
|
+
// the collector restores it while it runs. Refuses pi prompts while this
|
|
26
23
|
// device's group has blocking enabled and is over its budget, mirroring the
|
|
27
24
|
// Claude Code UserPromptSubmit hook.
|
|
28
25
|
import { execFile } from 'node:child_process'
|
|
@@ -73,7 +70,7 @@ export default function (pi: { on(event: 'input', handler: InputHandler): void }
|
|
|
73
70
|
/** Skips machines without pi (agent dir absent) rather than conjuring a `.pi`
|
|
74
71
|
* tree that pi itself never made. */
|
|
75
72
|
export function installPiGuard(program, agentDir = piAgentDir()) {
|
|
76
|
-
if (
|
|
73
|
+
if (!existsSync(agentDir)) {
|
|
77
74
|
return;
|
|
78
75
|
}
|
|
79
76
|
const path = piGuardPath(agentDir);
|
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.184";
|
package/dist/service.js
CHANGED
|
@@ -123,6 +123,15 @@ function programArgs() {
|
|
|
123
123
|
// hold it at ~34 MB for the same CPU. Only works as a launch flag, and bun ignores it.
|
|
124
124
|
return [stableNodePath(process.execPath), '--max-semi-space-size=1', script, 'watch'];
|
|
125
125
|
}
|
|
126
|
+
/** Write the Claude Code hook and the pi extension, or restore whichever went
|
|
127
|
+
* missing: `login` calls it once and every `watch` tick again, so a removed
|
|
128
|
+
* guard is back within one interval. Same binary, different entry point: the
|
|
129
|
+
* service watches, the hooks run the unadvertised `guard` command. */
|
|
130
|
+
export function installGuards() {
|
|
131
|
+
const guardProg = [...programArgs().slice(0, -1), 'guard'];
|
|
132
|
+
installPromptHook(guardProg);
|
|
133
|
+
installPiGuard(guardProg);
|
|
134
|
+
}
|
|
126
135
|
function macPlistPath() {
|
|
127
136
|
return join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
128
137
|
}
|
|
@@ -292,11 +301,7 @@ export function install() {
|
|
|
292
301
|
console.log(warn('path', `another usagefleet runs first · rm ${tilde(shadow)}`));
|
|
293
302
|
}
|
|
294
303
|
const env = presentEnv();
|
|
295
|
-
|
|
296
|
-
// `prog` ends in "watch"; everything before it is how to launch this build.
|
|
297
|
-
const guardProg = [...prog.slice(0, -1), 'guard'];
|
|
298
|
-
installPromptHook(guardProg);
|
|
299
|
-
installPiGuard(guardProg);
|
|
304
|
+
installGuards();
|
|
300
305
|
if (process.platform === 'darwin') {
|
|
301
306
|
const envXml = env.map(([k, v]) => ` <key>${xml(k)}</key><string>${xml(v)}</string>`).join('\n');
|
|
302
307
|
const progXml = prog.map(p => ` <string>${xml(p)}</string>`).join('\n');
|
package/dist/store.js
CHANGED