@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 +6 -1
- package/dist/paths.js +7 -0
- package/dist/pi-hook.js +99 -0
- package/dist/release.js +1 -1
- package/dist/service.js +6 -2
- package/package.json +1 -1
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
|
|
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. */
|
package/dist/pi-hook.js
ADDED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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 {
|