@usagefleet/cli 1.2.183 → 1.2.185

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
@@ -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. `usagefleet login` registers a Claude Code `UserPromptSubmit` hook in
131
- `~/.claude/settings.json` (removed by `uninstall`, refreshed rather than stacked
132
- on re-install, and skipped entirely with `USAGEFLEET_HOOK=0`):
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: refreshed on re-install,
147
- removed by `uninstall`, skipped with `USAGEFLEET_HOOK=0`.
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
 
@@ -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. So are `install`/`init`, the former
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 (hook, update, notifications): an env value wins when
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
- * Register `usagefleet guard` as a Claude Code UserPromptSubmit hook, so a
108
- * group with blocking enabled actually refuses prompts. Called by
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` (which self-update re-runs, refreshing the
12
- * baked binary path), removed by `uninstall`, skipped by USAGEFLEET_HOOK=0 or
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
- // USAGEFLEET_HOOK=0 at login keeps it out. Refuses pi prompts while this
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 (flagOff(process.env.USAGEFLEET_HOOK, readStore().hook) || !existsSync(agentDir)) {
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.183";
2
+ export const RELEASE_VERSION = "1.2.185";
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
- // Same binary, different entry point: the service watches, the hooks enforce.
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
@@ -51,7 +51,6 @@ function normalize(raw) {
51
51
  agentDir: raw.agentDir,
52
52
  batch: raw.batch,
53
53
  desktopDir: raw.desktopDir,
54
- hook: raw.hook,
55
54
  interval: raw.interval,
56
55
  jcodeDir: raw.jcodeDir,
57
56
  limits: raw.limits,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usagefleet/cli",
3
- "version": "1.2.183",
3
+ "version": "1.2.185",
4
4
  "description": "Tails Claude Code, Claude Desktop, and pi agent JSONL logs and reports token usage to a UsageFleet server.",
5
5
  "keywords": [
6
6
  "claude",