@usagefleet/cli 1.2.94 → 1.2.97

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
@@ -104,6 +104,11 @@ on re-install, and skipped entirely with `USAGEFLEET_HOOK=0`):
104
104
  }
105
105
  ```
106
106
 
107
+ If the pi coding agent is on the machine (`~/.pi/agent` exists), `login` also
108
+ writes a pi extension — `~/.pi/agent/extensions/usagefleet-guard.ts` — that runs
109
+ the same guard before every pi prompt. Same lifecycle: refreshed on re-install,
110
+ removed by `uninstall`, skipped with `USAGEFLEET_HOOK=0`.
111
+
107
112
  If `USAGEFLEET_CONFIG` was set when you logged in, the installed command carries
108
113
  it (`USAGEFLEET_CONFIG=<path> usagefleet guard`, or `set "..." && ...` on
109
114
  Windows). That is what binds the hook to the right store, and so to the right
@@ -137,7 +142,7 @@ Env vars override the file:
137
142
  | `USAGEFLEET_CONFIG` | override the config file path |
138
143
  | `USAGEFLEET_UPDATE` | `0` turns the self-update check off |
139
144
  | `USAGEFLEET_UPDATE_INTERVAL` | seconds between update checks (default `21600` = 6h) |
140
- | `USAGEFLEET_HOOK` | `0` keeps the prompt-blocking hook out of `~/.claude/settings.json` |
145
+ | `USAGEFLEET_HOOK` | `0` keeps the prompt-blocking guard out of Claude Code and pi |
141
146
  | `CLAUDE_CONFIG_DIR` | Claude Code's own knob: which login to watch (default `~/.claude`) |
142
147
 
143
148
  When run as a service, `login` bakes every `USAGEFLEET_*` value currently set
package/dist/paths.js CHANGED
@@ -44,6 +44,13 @@ export function defaultPiSessionsDirs() {
44
44
  }
45
45
  return [...new Set(dirs)];
46
46
  }
47
+ /** pi's agent dir, the root for its sessions and extensions.
48
+ * PI_CODING_AGENT_DIR is pi's own relocation knob; unlike the service-side
49
+ * scan above, the guard installer runs from the user's shell where that env
50
+ * is actually present, so honouring just the primary root is enough. */
51
+ export function piAgentDir() {
52
+ return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), '.pi', 'agent');
53
+ }
47
54
  /** Claude Code's config dir. CLAUDE_CONFIG_DIR is Claude Code's own relocation
48
55
  * knob, and a relocated dir is a second, independent login: its own settings,
49
56
  * its own credentials, its own Anthropic account. */
@@ -0,0 +1,99 @@
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { writeFileAtomic } from './atomic-write.js';
4
+ import { piAgentDir } from './paths.js';
5
+ import { step, tilde } from './ui.js';
6
+ /** pi has no settings-file hooks; its extension point is a TypeScript file
7
+ * auto-discovered from `<agent-dir>/extensions/*.ts`. So the pi guard is a
8
+ * generated file rather than a settings entry, with the same lifecycle as the
9
+ * Claude hook: written by `login` (which self-update re-runs, refreshing the
10
+ * baked binary path), removed by `uninstall`, skipped by USAGEFLEET_HOOK=0. */
11
+ const FILE = 'usagefleet-guard.ts';
12
+ export function piGuardPath(agentDir = piAgentDir()) {
13
+ return join(agentDir, 'extensions', FILE);
14
+ }
15
+ /** The generated extension is self-contained on purpose — it shells out to the
16
+ * baked `usagefleet guard` command instead of importing this package, so a CLI
17
+ * update can never strand it against moved internals. Program and config are
18
+ * baked exactly like guardCommand does for the Claude hook. */
19
+ export function piGuardExtension(program, configPath) {
20
+ const env = configPath ? `{ ...process.env, USAGEFLEET_CONFIG: ${JSON.stringify(configPath)} }` : 'process.env';
21
+ return `// Generated by \`usagefleet login\` — do not edit; \`usagefleet uninstall\` removes it,
22
+ // USAGEFLEET_HOOK=0 at login keeps it out. Refuses pi prompts while this
23
+ // device's group has blocking enabled and is over its budget, mirroring the
24
+ // Claude Code UserPromptSubmit hook.
25
+ import { execFile } from 'node:child_process'
26
+
27
+ const GUARD = ${JSON.stringify(program)}
28
+ const ENV = ${env}
29
+
30
+ /** Exit 2 is guard's one "refuse this prompt" signal, stderr the reason meant
31
+ * for the user. Anything else — exit 0, a missing binary, a wedged process
32
+ * killed by the timeout — fails open, the same guarantee \`usagefleet guard\`
33
+ * itself makes. The 7s outer bound only covers a wedged process; guard's own
34
+ * fetch gives up at 5s. */
35
+ function runGuard(): Promise<string | null> {
36
+ return new Promise(resolve => {
37
+ execFile(GUARD[0], GUARD.slice(1), { env: ENV, timeout: 7000 }, (error, _out, stderr) => {
38
+ const code = error === null ? 0 : error.code
39
+ resolve(code === 2 ? stderr.trim() || 'usagefleet: prompts are blocked for this group.' : null)
40
+ })
41
+ })
42
+ }
43
+
44
+ type InputEvent = { source?: string }
45
+ type InputContext = { hasUI: boolean; ui: { notify(message: string, level: 'error'): void } }
46
+ type InputHandler = (event: InputEvent, ctx: InputContext) => Promise<{ action: 'handled' } | undefined>
47
+
48
+ export default function (pi: { on(event: 'input', handler: InputHandler): void }) {
49
+ pi.on('input', async (event, ctx) => {
50
+ // Extension-injected messages pass unguarded: silently swallowing one
51
+ // wedges the extension that sent it, and the human this block is aimed
52
+ // at is the one typing.
53
+ if (event.source === 'extension') {
54
+ return undefined
55
+ }
56
+ const blocked = await runGuard()
57
+ if (!blocked) {
58
+ return undefined
59
+ }
60
+ if (ctx.hasUI) {
61
+ ctx.ui.notify(blocked, 'error')
62
+ } else {
63
+ console.error(blocked)
64
+ }
65
+ return { action: 'handled' }
66
+ })
67
+ }
68
+ `;
69
+ }
70
+ /** Skips machines without pi (agent dir absent) rather than conjuring a `.pi`
71
+ * tree that pi itself never made. */
72
+ export function installPiGuard(program, agentDir = piAgentDir()) {
73
+ if (process.env.USAGEFLEET_HOOK === '0' || !existsSync(agentDir)) {
74
+ return;
75
+ }
76
+ const path = piGuardPath(agentDir);
77
+ const next = piGuardExtension(program, process.env.USAGEFLEET_CONFIG);
78
+ let current = '';
79
+ try {
80
+ current = readFileSync(path, 'utf-8');
81
+ }
82
+ catch {
83
+ /* not installed yet */
84
+ }
85
+ if (current === next) {
86
+ return;
87
+ }
88
+ mkdirSync(dirname(path), { recursive: true });
89
+ writeFileAtomic(path, next);
90
+ console.log(step('hook', `pi prompt guard · ${tilde(path)}`));
91
+ }
92
+ export function uninstallPiGuard(agentDir = piAgentDir()) {
93
+ const path = piGuardPath(agentDir);
94
+ if (!existsSync(path)) {
95
+ return;
96
+ }
97
+ rmSync(path, { force: true });
98
+ console.log(step('removed', `pi prompt guard · ${tilde(path)}`));
99
+ }
package/dist/release.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // Generated by .github/workflows/release.yml.
2
- export const RELEASE_VERSION = "1.2.94";
2
+ export const RELEASE_VERSION = "1.2.97";
package/dist/service.js CHANGED
@@ -4,6 +4,7 @@ import { homedir, tmpdir } from 'node:os';
4
4
  import { delimiter, join } from 'node:path';
5
5
  import { ENDPOINT, loadConfig } from './config.js';
6
6
  import { installPromptHook, uninstallPromptHook } from './hook.js';
7
+ import { installPiGuard, uninstallPiGuard } from './pi-hook.js';
7
8
  import { readStore, storePath, updateStore } from './store.js';
8
9
  import { fail, header, hint, host, row, step, tilde, warn } from './ui.js';
9
10
  const LABEL = 'dev.usagefleet.collector';
@@ -261,9 +262,11 @@ export function install() {
261
262
  console.log(warn('path', `another usagefleet runs first · rm ${tilde(shadow)}`));
262
263
  }
263
264
  const env = presentEnv();
264
- // Same binary, different entry point: the service watches, the hook enforces.
265
+ // Same binary, different entry point: the service watches, the hooks enforce.
265
266
  // `prog` ends in "watch"; everything before it is how to launch this build.
266
- installPromptHook([...prog.slice(0, -1), 'guard']);
267
+ const guardProg = [...prog.slice(0, -1), 'guard'];
268
+ installPromptHook(guardProg);
269
+ installPiGuard(guardProg);
267
270
  if (process.platform === 'darwin') {
268
271
  const envXml = env.map(([k, v]) => ` <key>${xml(k)}</key><string>${xml(v)}</string>`).join('\n');
269
272
  const progXml = prog.map(p => ` <string>${xml(p)}</string>`).join('\n');
@@ -511,6 +514,7 @@ function removeStableBin() {
511
514
  }
512
515
  export function uninstall() {
513
516
  uninstallPromptHook();
517
+ uninstallPiGuard();
514
518
  if (process.platform === 'darwin') {
515
519
  const path = macPlistPath();
516
520
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usagefleet/cli",
3
- "version": "1.2.94",
3
+ "version": "1.2.97",
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",