@usagefleet/cli 1.2.78 → 1.2.79
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 +17 -3
- package/dist/claude-limits.js +11 -9
- package/dist/guard.js +4 -1
- package/dist/hook.js +17 -4
- package/dist/index.js +6 -2
- package/dist/os.js +2 -1
- package/dist/parser.js +8 -5
- package/dist/release.js +1 -1
- package/dist/tailer.js +26 -1
- package/dist/update.js +23 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,6 +101,11 @@ on re-install, and skipped entirely with `USAGEFLEET_HOOK=0`):
|
|
|
101
101
|
}
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
+
If `USAGEFLEET_CONFIG` was set when you logged in, the installed command carries
|
|
105
|
+
it (`USAGEFLEET_CONFIG=<path> usagefleet guard`, or `set "..." && ...` on
|
|
106
|
+
Windows). That is what binds the hook to the right store, and so to the right
|
|
107
|
+
Anthropic account — see "Two Claude accounts on one machine" below.
|
|
108
|
+
|
|
104
109
|
`guard` **fails open** everywhere else — no config, server down, timeout (5s),
|
|
105
110
|
old server, 429 — because a tracker problem must never stop you working. Only
|
|
106
111
|
whole prompts are blocked, never tool calls mid-turn, so the current turn always
|
|
@@ -149,9 +154,18 @@ CLAUDE_CONFIG_DIR=~/.claude-work \
|
|
|
149
154
|
usagefleet login uf_...
|
|
150
155
|
```
|
|
151
156
|
|
|
152
|
-
Each reports its own account, and the dashboard keeps their limits apart.
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
Each reports its own account, and the dashboard keeps their limits apart. The
|
|
158
|
+
prompt guard follows: because `USAGEFLEET_CONFIG` was set for this `login`, the
|
|
159
|
+
hook written into `~/.claude-work/settings.json` carries it, so that account's
|
|
160
|
+
guard reads that account's token and blocks against the right subscription. With
|
|
161
|
+
a relocated config dir the macOS Keychain is skipped on purpose: that item
|
|
162
|
+
belongs to the default login.
|
|
163
|
+
|
|
164
|
+
**Only one of them can run as the background service.** The launchd label,
|
|
165
|
+
systemd unit and Scheduled Task name are fixed, so the second `login` rewrites
|
|
166
|
+
the first one's service definition rather than adding a second. Run the extra
|
|
167
|
+
collector yourself with the same env — `... usagefleet watch`, under your own
|
|
168
|
+
unit or terminal — and keep `login` for the account you want supervised.
|
|
155
169
|
|
|
156
170
|
## Background service
|
|
157
171
|
|
package/dist/claude-limits.js
CHANGED
|
@@ -3,13 +3,7 @@ export function parsePct(v) {
|
|
|
3
3
|
if (v == null || v === '') {
|
|
4
4
|
return null;
|
|
5
5
|
}
|
|
6
|
-
|
|
7
|
-
if (!Number.isFinite(n)) {
|
|
8
|
-
return null;
|
|
9
|
-
}
|
|
10
|
-
// Header is a percent (e.g. "37"); guard the 0–1 fraction form too.
|
|
11
|
-
const pct = n > 0 && n <= 1 ? n * 100 : n;
|
|
12
|
-
return Math.min(100, Math.max(0, Math.round(pct)));
|
|
6
|
+
return clampPct(Number(v));
|
|
13
7
|
}
|
|
14
8
|
export function parseReset(v) {
|
|
15
9
|
if (!v) {
|
|
@@ -76,9 +70,17 @@ async function fetchOauthModelLimits(token) {
|
|
|
76
70
|
}
|
|
77
71
|
return parseOauthUsage(body);
|
|
78
72
|
}
|
|
79
|
-
/**
|
|
80
|
-
*
|
|
73
|
+
/** Both sources report 0–100 percentages: the `-utilization` headers ("37") and
|
|
74
|
+
* oauth/usage's `utilization`/`percent` fields. Clamping is all they need — a
|
|
75
|
+
* 0–1 "fraction form" heuristic would read a real 1% as 100%, which reaches the
|
|
76
|
+
* headline number, the critical notification and guard's prompt block.
|
|
77
|
+
*
|
|
78
|
+
* Non-finite in, null out: `Infinity` (a JSON `1e999`, or a junk header) would
|
|
79
|
+
* otherwise clamp to a perfectly plausible 100 and block every prompt. */
|
|
81
80
|
function clampPct(n) {
|
|
81
|
+
if (!Number.isFinite(n)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
82
84
|
return Math.min(100, Math.max(0, Math.round(n)));
|
|
83
85
|
}
|
|
84
86
|
/** Normalize a scope's model name to a header-safe key ("Fable" → "fable"). */
|
package/dist/guard.js
CHANGED
|
@@ -50,7 +50,10 @@ export async function runGuard() {
|
|
|
50
50
|
catch {
|
|
51
51
|
return 0; // offline / timeout / bad JSON — fail open
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
// A 200 whose body is literally `null` has to fail open like every other
|
|
54
|
+
// unexpected shape. Reading `.blocked` off it here would throw past the
|
|
55
|
+
// try above and out of runGuard, which is the one thing this must never do.
|
|
56
|
+
const msg = blockMessage(view ?? {});
|
|
54
57
|
if (!msg) {
|
|
55
58
|
return 0;
|
|
56
59
|
}
|
package/dist/hook.js
CHANGED
|
@@ -9,9 +9,22 @@ const HOOK_TIMEOUT_S = 10;
|
|
|
9
9
|
/** Recognises a guard hook we installed (at any binary path, from any version)
|
|
10
10
|
* so install is idempotent and uninstall is precise. */
|
|
11
11
|
const GUARD_COMMAND = /usagefleet.*\bguard\b/;
|
|
12
|
-
/** `/path/to/usagefleet guard`, quoted for the shell Claude Code runs it in.
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/** `/path/to/usagefleet guard`, quoted for the shell Claude Code runs it in.
|
|
13
|
+
* Carries USAGEFLEET_CONFIG through when set: the documented two-subscription
|
|
14
|
+
* setup (apps/cli/README.md) gives each account its own store *and* its own
|
|
15
|
+
* Claude settings.json, so without it both hooks would read the default config
|
|
16
|
+
* and block against the wrong account. `platform` is a parameter so the two
|
|
17
|
+
* shells can be tested; cmd.exe has no inline `VAR=value cmd` form, and the
|
|
18
|
+
* POSIX prefix there would be read as a program name and never launch. */
|
|
19
|
+
export function guardCommand(program, configPath, platform = process.platform) {
|
|
20
|
+
const quote = (p) => (p.includes(' ') ? `"${p}"` : p);
|
|
21
|
+
const command = program.map(quote).join(' ');
|
|
22
|
+
if (!configPath) {
|
|
23
|
+
return command;
|
|
24
|
+
}
|
|
25
|
+
return platform === 'win32'
|
|
26
|
+
? `set "USAGEFLEET_CONFIG=${configPath}" && ${command}`
|
|
27
|
+
: `USAGEFLEET_CONFIG=${quote(configPath)} ${command}`;
|
|
15
28
|
}
|
|
16
29
|
/** Drop every guard hook we ever installed, leaving the rest of the file alone. */
|
|
17
30
|
export function withoutGuardHook(settings) {
|
|
@@ -96,7 +109,7 @@ export function installPromptHook(program) {
|
|
|
96
109
|
if (process.env.USAGEFLEET_HOOK === '0') {
|
|
97
110
|
return;
|
|
98
111
|
}
|
|
99
|
-
const command = guardCommand(program);
|
|
112
|
+
const command = guardCommand(program, process.env.USAGEFLEET_CONFIG);
|
|
100
113
|
editSettings(s => withGuardHook(s, command), path => console.log(step('hook', `prompt guard · ${tilde(path)}`)));
|
|
101
114
|
}
|
|
102
115
|
export function uninstallPromptHook() {
|
package/dist/index.js
CHANGED
|
@@ -289,11 +289,15 @@ async function main() {
|
|
|
289
289
|
// Log-and-continue for the long-running watch daemon: a stray rejection must
|
|
290
290
|
// not silently kill the background service. One-shot commands still set a
|
|
291
291
|
// non-zero exit via their own error paths.
|
|
292
|
+
//
|
|
293
|
+
// stderr, not `line()`: `usagefleet guard` runs as a UserPromptSubmit hook and
|
|
294
|
+
// Claude Code injects a hook's stdout into the model's context, so a crash
|
|
295
|
+
// notice on stdout would end up in the conversation.
|
|
292
296
|
process.on('unhandledRejection', reason => {
|
|
293
|
-
|
|
297
|
+
console.error(`${yellow('!')} unhandled rejection ${dim(String(reason))}`);
|
|
294
298
|
});
|
|
295
299
|
process.on('uncaughtException', err => {
|
|
296
|
-
|
|
300
|
+
console.error(`${yellow('!')} uncaught exception ${dim(err.message)}`);
|
|
297
301
|
});
|
|
298
302
|
const cmd = process.argv[2] ?? 'help';
|
|
299
303
|
switch (cmd) {
|
package/dist/os.js
CHANGED
package/dist/parser.js
CHANGED
|
@@ -23,8 +23,10 @@ function cacheCreation(u) {
|
|
|
23
23
|
* provider, model, responseId, usage:{input, output, cacheRead, cacheWrite}}}`.
|
|
24
24
|
* Only provider "anthropic" hits the user's Claude account — other providers
|
|
25
25
|
* (openai-codex, openrouter, …) are skipped. `output` already includes reasoning
|
|
26
|
-
* tokens (totalTokens = input + output + cacheRead + cacheWrite).
|
|
27
|
-
|
|
26
|
+
* tokens (totalTokens = input + output + cacheRead + cacheWrite).
|
|
27
|
+
* The line carries no working directory; `sessionCwd` comes from the file's
|
|
28
|
+
* session header (see piSessionCwd). */
|
|
29
|
+
function parsePiLine(o, sessionCwd) {
|
|
28
30
|
if (o.type !== 'message') {
|
|
29
31
|
return null;
|
|
30
32
|
}
|
|
@@ -47,7 +49,7 @@ function parsePiLine(o) {
|
|
|
47
49
|
return {
|
|
48
50
|
cacheCreationTokens: u.cacheWrite ?? 0,
|
|
49
51
|
cacheReadTokens: u.cacheRead ?? 0,
|
|
50
|
-
cwd:
|
|
52
|
+
cwd: sessionCwd,
|
|
51
53
|
gitBranch: null,
|
|
52
54
|
inputTokens: u.input ?? 0,
|
|
53
55
|
messageId: rid,
|
|
@@ -68,8 +70,9 @@ function parsePiLine(o) {
|
|
|
68
70
|
* `uuid` is the per-line idempotency key the server dedups on. `source` tags which
|
|
69
71
|
* app the file came from (the line itself carries no app identifier) and selects
|
|
70
72
|
* the format: `pi` files use pi's own schema, everything else Claude Code's.
|
|
73
|
+
* `sessionCwd` is the file-level working directory, used only by `pi`.
|
|
71
74
|
*/
|
|
72
|
-
export function parseLine(line, source = 'cli') {
|
|
75
|
+
export function parseLine(line, source = 'cli', sessionCwd = null) {
|
|
73
76
|
const trimmed = line.trim();
|
|
74
77
|
if (!trimmed) {
|
|
75
78
|
return null;
|
|
@@ -92,7 +95,7 @@ export function parseLine(line, source = 'cli') {
|
|
|
92
95
|
}
|
|
93
96
|
const o = parsed;
|
|
94
97
|
if (source === 'pi') {
|
|
95
|
-
return parsePiLine(o);
|
|
98
|
+
return parsePiLine(o, sessionCwd);
|
|
96
99
|
}
|
|
97
100
|
if (o.type !== 'assistant') {
|
|
98
101
|
return null;
|
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.79";
|
package/dist/tailer.js
CHANGED
|
@@ -3,6 +3,27 @@ import { parseLine } from './parser.js';
|
|
|
3
3
|
import { dim, line, tilde, yellow } from './ui.js';
|
|
4
4
|
/** Max bytes read from a single file per cycle (bounds memory on huge backlogs). */
|
|
5
5
|
const MAX_READ = 16 * 1024 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* The working directory of a pi session, read from the file's first line
|
|
8
|
+
* (`{"type":"session",…,"cwd":"/path"}`). pi's message lines carry no cwd, and
|
|
9
|
+
* tailing usually resumes past the header, so it is re-read per cycle rather
|
|
10
|
+
* than remembered. The session dir name encodes the same path but lossily
|
|
11
|
+
* (`/Developer/claude-track` and `/Developer/claude/track` collide), so the
|
|
12
|
+
* header is the only exact source.
|
|
13
|
+
*/
|
|
14
|
+
function piSessionCwd(fd) {
|
|
15
|
+
const head = Buffer.alloc(4096);
|
|
16
|
+
const read = readSync(fd, head, 0, head.length, 0);
|
|
17
|
+
const nl = head.indexOf(0x0a);
|
|
18
|
+
try {
|
|
19
|
+
const o = JSON.parse(head.subarray(0, nl === -1 ? read : nl).toString('utf-8'));
|
|
20
|
+
const cwd = o?.cwd;
|
|
21
|
+
return typeof cwd === 'string' && cwd.length > 0 ? cwd : null;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
6
27
|
/**
|
|
7
28
|
* Read new, newline-terminated lines from `filePath` starting at the previously
|
|
8
29
|
* stored offset. Handles rotation/truncation (inode change or size < offset →
|
|
@@ -27,8 +48,12 @@ export function tailFile(filePath, prev, source = 'cli') {
|
|
|
27
48
|
const length = Math.min(st.size - start, MAX_READ);
|
|
28
49
|
const buf = Buffer.alloc(length);
|
|
29
50
|
const fd = openSync(filePath, 'r');
|
|
51
|
+
let sessionCwd = null;
|
|
30
52
|
try {
|
|
31
53
|
readSync(fd, buf, 0, length, start);
|
|
54
|
+
if (source === 'pi') {
|
|
55
|
+
sessionCwd = piSessionCwd(fd);
|
|
56
|
+
}
|
|
32
57
|
}
|
|
33
58
|
finally {
|
|
34
59
|
closeSync(fd);
|
|
@@ -52,7 +77,7 @@ export function tailFile(filePath, prev, source = 'cli') {
|
|
|
52
77
|
const text = consumed.toString('utf-8');
|
|
53
78
|
const records = [];
|
|
54
79
|
for (const line of text.split('\n')) {
|
|
55
|
-
const rec = parseLine(line, source);
|
|
80
|
+
const rec = parseLine(line, source, sessionCwd);
|
|
56
81
|
if (rec) {
|
|
57
82
|
records.push(rec);
|
|
58
83
|
}
|
package/dist/update.js
CHANGED
|
@@ -19,14 +19,34 @@ function npmCommand() {
|
|
|
19
19
|
const sibling = join(dirname(process.execPath), process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
20
20
|
return existsSync(sibling) ? sibling : 'npm';
|
|
21
21
|
}
|
|
22
|
-
/**
|
|
22
|
+
/** An `npm install` that never returns would hang the watch loop forever, since
|
|
23
|
+
* the update check is awaited inline before the next tick is scheduled: the
|
|
24
|
+
* daemon would still be "running" while collecting nothing, with an empty log.
|
|
25
|
+
* Generous enough for a slow registry on a cold cache. */
|
|
26
|
+
const RUN_TIMEOUT_MS = 10 * 60_000;
|
|
27
|
+
/** Exit code of a finished child, or null when it could not be started, was
|
|
28
|
+
* killed for exceeding RUN_TIMEOUT_MS, or died on a signal. */
|
|
23
29
|
function run(cmd, args) {
|
|
24
30
|
return new Promise(resolve => {
|
|
25
31
|
// shell on Windows: node refuses to spawn a .cmd directly since the 2024
|
|
26
32
|
// argument-injection fix. Every argument here is a literal or VERSION-checked.
|
|
27
33
|
const child = spawn(cmd, args, { shell: process.platform === 'win32', stdio: 'ignore' });
|
|
28
|
-
|
|
29
|
-
|
|
34
|
+
const timer = setTimeout(() => child.kill(), RUN_TIMEOUT_MS);
|
|
35
|
+
// Node emits 'close' after 'error' for a failed spawn, so both handlers can
|
|
36
|
+
// run; `settled` makes the first one win. oxlint's promise rule sees the two
|
|
37
|
+
// registrations and can't see the guard between them.
|
|
38
|
+
let settled = false;
|
|
39
|
+
const finish = (code) => {
|
|
40
|
+
if (settled) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
settled = true;
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
// oxlint-disable-next-line promise/no-multiple-resolved
|
|
46
|
+
resolve(code);
|
|
47
|
+
};
|
|
48
|
+
child.on('error', () => finish(null));
|
|
49
|
+
child.on('close', code => finish(code));
|
|
30
50
|
});
|
|
31
51
|
}
|
|
32
52
|
/**
|