@usagefleet/cli 1.2.59 → 1.2.70
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/LICENSE +674 -0
- package/README.md +6 -6
- package/dist/claude-creds.js +2 -2
- package/dist/collector.js +48 -18
- package/dist/config.js +5 -5
- package/dist/hook.js +4 -3
- package/dist/index.js +123 -85
- package/dist/notifier.js +3 -3
- package/dist/release.js +2 -8
- package/dist/service.js +65 -33
- package/dist/store.js +1 -1
- package/dist/tailer.js +2 -1
- package/dist/ui.js +65 -8
- package/dist/update.js +12 -9
- package/dist/uploader.js +14 -5
- package/package.json +7 -2
package/dist/release.js
CHANGED
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// must never be replaced by a published one behind your back.
|
|
4
|
-
// The annotation is load-bearing: without it the literal type would be 'dev'
|
|
5
|
-
// here and '1.2.3' in CI, so every `=== '1.2.59'` check compiles locally and
|
|
6
|
-
// fails the release build as a comparison with no overlap.
|
|
7
|
-
// oxlint-disable-next-line typescript/no-inferrable-types -- see above
|
|
8
|
-
export const RELEASE_VERSION = '1.2.59';
|
|
1
|
+
// Generated by .github/workflows/release.yml.
|
|
2
|
+
export const RELEASE_VERSION = "1.2.70";
|
package/dist/service.js
CHANGED
|
@@ -2,9 +2,10 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir, tmpdir } from 'node:os';
|
|
4
4
|
import { delimiter, join } from 'node:path';
|
|
5
|
+
import { DEFAULT_ENDPOINT, loadConfig } from './config.js';
|
|
5
6
|
import { installPromptHook, uninstallPromptHook } from './hook.js';
|
|
6
|
-
import { readStore } from './store.js';
|
|
7
|
-
import { row, step } from './ui.js';
|
|
7
|
+
import { readStore, storePath, updateStore } from './store.js';
|
|
8
|
+
import { fail, header, hint, host, row, step, tilde, warn } from './ui.js';
|
|
8
9
|
const LABEL = 'dev.usagefleet.collector';
|
|
9
10
|
/** Scheduled Task name on Windows (mirrors the launchd label / systemd unit). */
|
|
10
11
|
const TASK = 'usagefleet';
|
|
@@ -215,18 +216,40 @@ function xml(s) {
|
|
|
215
216
|
.replaceAll("'", ''');
|
|
216
217
|
}
|
|
217
218
|
export function install() {
|
|
218
|
-
// Pre-flight: refuse to install a service
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
219
|
+
// Pre-flight: refuse to install a service whose baked `watch` would throw on
|
|
220
|
+
// every launch, because the service manager crash-loops it invisibly (only the
|
|
221
|
+
// log file shows it). Resolving through loadConfig() is what makes this a real
|
|
222
|
+
// pre-flight rather than a lookalike: it is the same call `watch` makes, so a
|
|
223
|
+
// missing token or a non-https endpoint fails here or not at all.
|
|
224
|
+
let cfg;
|
|
225
|
+
try {
|
|
226
|
+
cfg = loadConfig();
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
console.error(fail('config', error.message));
|
|
230
|
+
console.error(hint(' usagefleet install --endpoint <url> --token <device-token>'));
|
|
231
|
+
return process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
// Config that only ever lived in this shell's env is lost to every later
|
|
234
|
+
// invocation: `usagefleet guard` runs from Claude Code's environment, which
|
|
235
|
+
// carries no USAGEFLEET_* vars (hook.ts bakes the command, not the env). The
|
|
236
|
+
// endpoint would fall back to the hosted default and send this device's token
|
|
237
|
+
// there; the token would be missing outright and the guard would fail open. So
|
|
238
|
+
// pin both to disk. The default endpoint is stored as absent rather than
|
|
239
|
+
// written out, so it can still move under an existing install.
|
|
240
|
+
const desiredEndpoint = cfg.endpoint === DEFAULT_ENDPOINT ? undefined : cfg.endpoint;
|
|
241
|
+
const stored = readStore();
|
|
242
|
+
if (stored.token !== cfg.token || stored.endpoint !== desiredEndpoint) {
|
|
243
|
+
// Only on a real change: `update` re-runs install every six hours, and this
|
|
244
|
+
// file is shared with the running collector's offset writes.
|
|
245
|
+
updateStore(storePath(), store => {
|
|
246
|
+
store.token = cfg.token;
|
|
247
|
+
store.endpoint = desiredEndpoint;
|
|
248
|
+
});
|
|
229
249
|
}
|
|
250
|
+
console.log(header());
|
|
251
|
+
console.log('');
|
|
252
|
+
console.log(step('configured', host(cfg.endpoint)));
|
|
230
253
|
// Windows: stop a running task first, or `schtasks /run` below is ignored (the
|
|
231
254
|
// task is IgnoreNew) — leaving the OLD version resident after an "update".
|
|
232
255
|
if (process.platform === 'win32') {
|
|
@@ -238,8 +261,7 @@ export function install() {
|
|
|
238
261
|
const prog = programArgs();
|
|
239
262
|
const shadow = shadowingBinary(process.env.PATH, process.argv[1] ?? process.execPath);
|
|
240
263
|
if (shadow) {
|
|
241
|
-
console.warn(`
|
|
242
|
-
`but your shell keeps running that one — delete it: rm ${shadow}`);
|
|
264
|
+
console.log(warn('path', `another usagefleet runs first · rm ${tilde(shadow)}`));
|
|
243
265
|
}
|
|
244
266
|
const env = presentEnv();
|
|
245
267
|
// Same binary, different entry point: the service watches, the hook enforces.
|
|
@@ -276,7 +298,7 @@ ${envXml}
|
|
|
276
298
|
mkdirSync(join(homedir(), 'Library', 'LaunchAgents'), { recursive: true });
|
|
277
299
|
mkdirSync(macLogDir(), { recursive: true });
|
|
278
300
|
// 0600: this file carries USAGEFLEET_TOKEN and ANTHROPIC_API_KEY, the same
|
|
279
|
-
// secrets
|
|
301
|
+
// secrets the config file deliberately holds at 0600.
|
|
280
302
|
writeFileSync(path, plist, { encoding: 'utf-8', mode: 0o600 });
|
|
281
303
|
chmodSync(path, 0o600); // writeFileSync's mode does not apply to an existing file
|
|
282
304
|
const domain = `gui/${process.getuid?.()}`;
|
|
@@ -313,7 +335,8 @@ ${envXml}
|
|
|
313
335
|
/* best-effort */
|
|
314
336
|
}
|
|
315
337
|
console.log(step('service', 'launchd · starts at login'));
|
|
316
|
-
|
|
338
|
+
console.log(row('logs', tilde(macLogDir())));
|
|
339
|
+
return collectingNow();
|
|
317
340
|
}
|
|
318
341
|
if (process.platform === 'linux') {
|
|
319
342
|
// systemd: quote values, escape backslash/quote, reject newlines.
|
|
@@ -378,13 +401,12 @@ WantedBy=default.target
|
|
|
378
401
|
}
|
|
379
402
|
}
|
|
380
403
|
console.log(step('service', 'systemd · starts at login'));
|
|
404
|
+
return collectingNow();
|
|
381
405
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
console.log(' loginctl enable-linger $USER # keep running after logout');
|
|
387
|
-
}
|
|
406
|
+
console.log(warn('service', 'systemctl not driveable · enable it manually'));
|
|
407
|
+
console.log(hint(' systemctl --user daemon-reload'));
|
|
408
|
+
console.log(hint(' systemctl --user enable --now usagefleet'));
|
|
409
|
+
console.log(hint(' loginctl enable-linger $USER keep running after logout'));
|
|
388
410
|
return;
|
|
389
411
|
}
|
|
390
412
|
if (process.platform === 'win32') {
|
|
@@ -405,18 +427,26 @@ WantedBy=default.target
|
|
|
405
427
|
schtasks('/create', '/tn', TASK, '/sc', 'onlogon', '/f', '/tr', `wscript.exe //B //Nologo "${vbsPath}"`);
|
|
406
428
|
rmSync(xmlPath, { force: true });
|
|
407
429
|
if (!created) {
|
|
408
|
-
console.error('
|
|
409
|
-
|
|
430
|
+
console.error(fail('service', 'scheduled task rejected · register it manually'));
|
|
431
|
+
console.error(hint(` schtasks /create /tn ${TASK} /sc onlogon /tr "wscript.exe //B //Nologo \\"${vbsPath}\\""`));
|
|
410
432
|
process.exit(1);
|
|
411
433
|
}
|
|
412
434
|
// Start now so install/update takes effect immediately, not at next logon.
|
|
413
435
|
schtasks('/run', '/tn', TASK);
|
|
414
436
|
console.log(step('service', 'scheduled task · starts at logon'));
|
|
415
|
-
console.log(row('logs', windowsLogPath()));
|
|
416
|
-
return;
|
|
437
|
+
console.log(row('logs', tilde(windowsLogPath())));
|
|
438
|
+
return collectingNow();
|
|
417
439
|
}
|
|
418
|
-
console.log(`
|
|
419
|
-
console.log(`
|
|
440
|
+
console.log(warn('service', `no autostart on ${process.platform} · run it yourself`));
|
|
441
|
+
console.log(hint(` ${prog.join(' ')}`));
|
|
442
|
+
}
|
|
443
|
+
/** Closing lines of a successful install: what is happening, and the two
|
|
444
|
+
* commands worth knowing next. */
|
|
445
|
+
function collectingNow() {
|
|
446
|
+
console.log('');
|
|
447
|
+
console.log(hint('collecting now.'));
|
|
448
|
+
console.log(hint(' usagefleet status current state'));
|
|
449
|
+
console.log(hint(' usagefleet watch foreground, live log'));
|
|
420
450
|
}
|
|
421
451
|
/** Is the background service actually up? This is the one question `status`
|
|
422
452
|
* has to answer, so every probe is best-effort: an unreadable or unparseable
|
|
@@ -491,7 +521,8 @@ export function uninstall() {
|
|
|
491
521
|
}
|
|
492
522
|
}
|
|
493
523
|
removeStableBin();
|
|
494
|
-
console.log(
|
|
524
|
+
console.log(step('removed', 'launchd agent'));
|
|
525
|
+
console.log(row('leftover', `${tilde(path)} · delete to fully clean up`));
|
|
495
526
|
return;
|
|
496
527
|
}
|
|
497
528
|
if (process.platform === 'linux') {
|
|
@@ -504,7 +535,8 @@ export function uninstall() {
|
|
|
504
535
|
/* ignore */
|
|
505
536
|
}
|
|
506
537
|
removeStableBin();
|
|
507
|
-
console.log(
|
|
538
|
+
console.log(step('removed', 'systemd unit'));
|
|
539
|
+
console.log(row('leftover', `${tilde(systemdUnitPath())} · delete to fully clean up`));
|
|
508
540
|
return;
|
|
509
541
|
}
|
|
510
542
|
if (process.platform === 'win32') {
|
|
@@ -517,8 +549,8 @@ export function uninstall() {
|
|
|
517
549
|
/* ignore */
|
|
518
550
|
}
|
|
519
551
|
removeStableBin();
|
|
520
|
-
console.log(deleted ? `
|
|
552
|
+
console.log(deleted ? step('removed', `scheduled task ${TASK}`) : row('service', `no task ${TASK} found`));
|
|
521
553
|
return;
|
|
522
554
|
}
|
|
523
|
-
console.log(`
|
|
555
|
+
console.log(row('service', `nothing to uninstall on ${process.platform}`));
|
|
524
556
|
}
|
package/dist/store.js
CHANGED
|
@@ -86,7 +86,7 @@ export function readStore(path = storePath()) {
|
|
|
86
86
|
}
|
|
87
87
|
/**
|
|
88
88
|
* Read-modify-write the store atomically. Re-reading inside the call is what
|
|
89
|
-
* lets `usagefleet
|
|
89
|
+
* lets `usagefleet install` change the token while the service is mid-cycle: the
|
|
90
90
|
* service's next save picks up the new token instead of overwriting it with the
|
|
91
91
|
* copy it loaded minutes ago.
|
|
92
92
|
*
|
package/dist/tailer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { closeSync, openSync, readSync, statSync } from 'node:fs';
|
|
2
2
|
import { parseLine } from './parser.js';
|
|
3
|
+
import { dim, line, tilde, yellow } from './ui.js';
|
|
3
4
|
/** Max bytes read from a single file per cycle (bounds memory on huge backlogs). */
|
|
4
5
|
const MAX_READ = 16 * 1024 * 1024;
|
|
5
6
|
/**
|
|
@@ -38,7 +39,7 @@ export function tailFile(filePath, prev, source = 'cli') {
|
|
|
38
39
|
// No newline in a full MAX_READ window = one pathologically long line.
|
|
39
40
|
// Skip past it so the file can't stall forever.
|
|
40
41
|
if (length >= MAX_READ) {
|
|
41
|
-
|
|
42
|
+
line(yellow('!'), `skipped a line > ${MAX_READ} bytes ${dim(`· ${tilde(filePath)} at ${start}`)}`);
|
|
42
43
|
return {
|
|
43
44
|
consumedBytes: length,
|
|
44
45
|
nextState: { ...base, offset: start + length },
|
package/dist/ui.js
CHANGED
|
@@ -1,35 +1,92 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* a padded label column so lines align.
|
|
2
|
+
* The CLI's only output surface, "quiet" style: lowercase labels, one accent
|
|
3
|
+
* colour, detail in gray, a padded label column so lines align. Every command
|
|
4
|
+
* — install, status, watch, the collector's own stream — prints through these
|
|
5
|
+
* helpers, so a TTY and a service log read the same.
|
|
4
6
|
*
|
|
5
7
|
* Colour is dropped when stdout is not a TTY (service logs, CI, pipes) or when
|
|
6
|
-
* NO_COLOR is set.
|
|
8
|
+
* NO_COLOR is set. Glyphs and bars are plain characters, so a log file still
|
|
9
|
+
* shows them.
|
|
7
10
|
*/
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { detectOs } from './os.js';
|
|
13
|
+
import { RELEASE_VERSION } from './release.js';
|
|
8
14
|
const useColor = process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
9
15
|
function paint(code) {
|
|
10
16
|
return s => (useColor ? `\u001B[${code}m${s}\u001B[0m` : s);
|
|
11
17
|
}
|
|
12
18
|
export const dim = paint('90');
|
|
19
|
+
/** One step below `dim`: timestamps, which are structure rather than content. */
|
|
20
|
+
export const dimmer = paint('2;90');
|
|
13
21
|
export const green = paint('32');
|
|
14
22
|
export const yellow = paint('33');
|
|
15
23
|
export const red = paint('31');
|
|
16
24
|
export const blue = paint('34');
|
|
17
25
|
export const bold = paint('1');
|
|
18
|
-
/** Width of the label column shared by
|
|
19
|
-
const LABEL =
|
|
20
|
-
/** "
|
|
26
|
+
/** Width of the label column shared by every labelled line. */
|
|
27
|
+
const LABEL = 12;
|
|
28
|
+
/** "usagefleet 1.2.55 mac-arm64" — the banner an interactive command opens
|
|
29
|
+
* with. `detail` replaces the platform when a command has something better to
|
|
30
|
+
* say about itself (watch states its interval). */
|
|
31
|
+
export function header(detail = `${detectOs()}-${process.arch}`) {
|
|
32
|
+
const build = RELEASE_VERSION === 'dev' ? ' · local build, self-update off' : '';
|
|
33
|
+
return `${blue('usagefleet')} ${dim(`${RELEASE_VERSION}${build} ${detail}`)}`;
|
|
34
|
+
}
|
|
35
|
+
/** Server without its scheme: the host is the part worth reading. */
|
|
36
|
+
export function host(endpoint) {
|
|
37
|
+
return endpoint.replace(/^https?:\/\//, '').replace(/\/$/, '');
|
|
38
|
+
}
|
|
39
|
+
/** "✓ installed ~/.local/bin/usagefleet" — a completed step. */
|
|
21
40
|
export function step(label, detail = '') {
|
|
22
41
|
return `${green('✓')} ${label.padEnd(LABEL)} ${dim(detail)}`;
|
|
23
42
|
}
|
|
24
|
-
/** "
|
|
43
|
+
/** "✗ verify failed expected 4f8c…" — a step that did not happen. */
|
|
44
|
+
export function fail(label, detail = '') {
|
|
45
|
+
return `${red('✗')} ${label.padEnd(LABEL)} ${dim(detail)}`;
|
|
46
|
+
}
|
|
47
|
+
/** "! service systemctl unavailable" — worked, but not fully. */
|
|
48
|
+
export function warn(label, detail = '') {
|
|
49
|
+
return `${yellow('!')} ${label.padEnd(LABEL)} ${dim(detail)}`;
|
|
50
|
+
}
|
|
51
|
+
/** "● service running" — state with a health-coloured dot. */
|
|
25
52
|
export function state(health, label, detail) {
|
|
26
53
|
const dot = health === 'ok' ? green('●') : health === 'warn' ? yellow('●') : red('●');
|
|
27
54
|
return `${dot} ${label.padEnd(LABEL)} ${detail}`;
|
|
28
55
|
}
|
|
29
|
-
/** " config
|
|
56
|
+
/** " config ~/.config/usagefleet/config.json" — a plain detail line. */
|
|
30
57
|
export function row(label, detail) {
|
|
31
58
|
return ` ${label.padEnd(LABEL)} ${dim(detail)}`;
|
|
32
59
|
}
|
|
60
|
+
/** A closing suggestion, or any line that is context rather than result. */
|
|
61
|
+
export function hint(text) {
|
|
62
|
+
return dim(text);
|
|
63
|
+
}
|
|
64
|
+
/** Home-relative path, because `~/.claude/projects` reads and wraps better than
|
|
65
|
+
* the absolute one — and hides the user's account name in a pasted terminal. */
|
|
66
|
+
export function tilde(path) {
|
|
67
|
+
const home = homedir();
|
|
68
|
+
return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
|
|
69
|
+
}
|
|
70
|
+
/** Day of the last printed stream line. The date is stated on rollover only:
|
|
71
|
+
* HH:MM:SS alone is unreadable in a service log that spans a week, while a
|
|
72
|
+
* one-shot command is already dated by the shell that ran it. */
|
|
73
|
+
let lastDay = '';
|
|
74
|
+
/**
|
|
75
|
+
* One line of the live stream: "09:14:02 ↑ 12 sent · 12 accepted".
|
|
76
|
+
* Used by `watch`, `run` and every message the collector emits, so the service
|
|
77
|
+
* log is the same stream a foreground run shows.
|
|
78
|
+
*/
|
|
79
|
+
export function line(glyph, text) {
|
|
80
|
+
const now = new Date();
|
|
81
|
+
const day = now.toLocaleDateString('en-CA');
|
|
82
|
+
if (lastDay && day !== lastDay) {
|
|
83
|
+
console.log(dimmer(`── ${day}`));
|
|
84
|
+
}
|
|
85
|
+
lastDay = day;
|
|
86
|
+
console.log(`${dimmer(now.toTimeString().slice(0, 8))} ${glyph} ${text}`);
|
|
87
|
+
}
|
|
88
|
+
/** Neutral stream glyph, for messages that are neither good nor bad news. */
|
|
89
|
+
export const note = dim('·');
|
|
33
90
|
/** Percentage as a fixed-width string, so successive log lines line up. */
|
|
34
91
|
export function pct(value) {
|
|
35
92
|
return `${value ?? '?'}%`.padStart(4);
|
package/dist/update.js
CHANGED
|
@@ -37,11 +37,14 @@ function run(cmd, args) {
|
|
|
37
37
|
* install is worse than one that skips a release. `force` is the manual
|
|
38
38
|
* `usagefleet update`, which ignores USAGEFLEET_UPDATE=0 but still refuses to
|
|
39
39
|
* touch a dev build.
|
|
40
|
+
*
|
|
41
|
+
* `log` carries the level so the caller can pick the right glyph: the CLI
|
|
42
|
+
* renders progress as a step and every dead end as a warning.
|
|
40
43
|
*/
|
|
41
44
|
export async function checkForUpdate(log, force = false) {
|
|
42
45
|
if (RELEASE_VERSION === 'dev') {
|
|
43
46
|
if (force) {
|
|
44
|
-
log('
|
|
47
|
+
log('warn', 'dev build · install the published package first');
|
|
45
48
|
}
|
|
46
49
|
return null;
|
|
47
50
|
}
|
|
@@ -62,7 +65,7 @@ export async function checkForUpdate(log, force = false) {
|
|
|
62
65
|
const res = await fetch(`${REGISTRY}/${PACKAGE}/latest`, { signal: AbortSignal.timeout(15_000) });
|
|
63
66
|
if (!res.ok) {
|
|
64
67
|
if (force) {
|
|
65
|
-
log(`
|
|
68
|
+
log('warn', `registry has no release info (${res.status})`);
|
|
66
69
|
}
|
|
67
70
|
return null;
|
|
68
71
|
}
|
|
@@ -70,7 +73,7 @@ export async function checkForUpdate(log, force = false) {
|
|
|
70
73
|
}
|
|
71
74
|
catch (error) {
|
|
72
75
|
if (force) {
|
|
73
|
-
log(`
|
|
76
|
+
log('warn', `npm registry unreachable · ${error.message}`);
|
|
74
77
|
}
|
|
75
78
|
return null;
|
|
76
79
|
}
|
|
@@ -79,22 +82,22 @@ export async function checkForUpdate(log, force = false) {
|
|
|
79
82
|
}
|
|
80
83
|
if (latest === RELEASE_VERSION) {
|
|
81
84
|
if (force) {
|
|
82
|
-
log(`
|
|
85
|
+
log('ok', `already current · ${RELEASE_VERSION}`);
|
|
83
86
|
}
|
|
84
87
|
return null;
|
|
85
88
|
}
|
|
86
|
-
log(
|
|
89
|
+
log('ok', `${RELEASE_VERSION} → ${latest} · installing ${PACKAGE}…`);
|
|
87
90
|
const code = await run(npmCommand(), ['install', '--global', `${PACKAGE}@${latest}`]);
|
|
88
91
|
if (code !== 0) {
|
|
89
|
-
log(code === null
|
|
90
|
-
? '
|
|
91
|
-
: `
|
|
92
|
+
log('warn', code === null
|
|
93
|
+
? 'npm not available · reinstall with `npm i -g @usagefleet/cli`'
|
|
94
|
+
: `npm install failed (exit ${code}) · if the global prefix needs root, run it yourself`);
|
|
92
95
|
return null;
|
|
93
96
|
}
|
|
94
97
|
// Detached: `install` rewrites the service definition and restarts it, which
|
|
95
98
|
// kills this process tree. npm replaced the file behind `self`, so this is
|
|
96
99
|
// already the new version.
|
|
97
100
|
spawn(process.execPath, [self, 'install'], { detached: true, stdio: 'ignore' }).unref();
|
|
98
|
-
log(`
|
|
101
|
+
log('ok', `installed ${latest} · restarting service`);
|
|
99
102
|
return latest;
|
|
100
103
|
}
|
package/dist/uploader.js
CHANGED
|
@@ -44,7 +44,9 @@ export async function uploadBatch(payload, cfg) {
|
|
|
44
44
|
}
|
|
45
45
|
return { fatal: 'transient', ok: false };
|
|
46
46
|
}
|
|
47
|
-
/** Map a
|
|
47
|
+
/** Map a non-OK status to a failure kind. uploadBatch only ever passes a 4xx it
|
|
48
|
+
* has already decided not to retry; postLimits passes anything, and the 5xx/429
|
|
49
|
+
* fall-through to 'transient' is the answer it wants. */
|
|
48
50
|
function classifyClientError(status) {
|
|
49
51
|
if (status === 401 || status === 403) {
|
|
50
52
|
return 'auth';
|
|
@@ -52,7 +54,10 @@ function classifyClientError(status) {
|
|
|
52
54
|
if (status === 400 || status === 422) {
|
|
53
55
|
return 'invalid';
|
|
54
56
|
}
|
|
55
|
-
|
|
57
|
+
if (status === 402) {
|
|
58
|
+
return 'plan';
|
|
59
|
+
}
|
|
60
|
+
return 'transient'; // 404, 408, 413, … — the data is fine
|
|
56
61
|
}
|
|
57
62
|
/** Parse a Retry-After header (delta-seconds OR HTTP-date), clamped to [0, 60s]. */
|
|
58
63
|
function retryAfterMs(header, fallback) {
|
|
@@ -73,7 +78,11 @@ function retryAfterMs(header, fallback) {
|
|
|
73
78
|
}
|
|
74
79
|
return Math.min(Math.max(wait, 0), 60_000) + Math.floor(Math.random() * 500);
|
|
75
80
|
}
|
|
76
|
-
/** Report the account's real limit utilization to the server.
|
|
81
|
+
/** Report the account's real limit utilization to the server. Shares uploadBatch's
|
|
82
|
+
* failure vocabulary so a plan wall reads as one on this leg too: it runs every
|
|
83
|
+
* cycle even when no usage records moved, so collapsing 402 to a bare failure
|
|
84
|
+
* would log an unactionable warning forever. Single-shot by design — a stale
|
|
85
|
+
* reading is worth less than the next cycle's fresh one. */
|
|
77
86
|
export async function postLimits(report, cfg) {
|
|
78
87
|
try {
|
|
79
88
|
const res = await fetch(`${cfg.endpoint}/api/v1/limits`, {
|
|
@@ -85,9 +94,9 @@ export async function postLimits(report, cfg) {
|
|
|
85
94
|
method: 'POST',
|
|
86
95
|
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
87
96
|
});
|
|
88
|
-
return res.ok;
|
|
97
|
+
return res.ok ? 'ok' : classifyClientError(res.status);
|
|
89
98
|
}
|
|
90
99
|
catch {
|
|
91
|
-
return
|
|
100
|
+
return 'transient';
|
|
92
101
|
}
|
|
93
102
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usagefleet/cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.70",
|
|
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",
|
|
@@ -10,7 +10,12 @@
|
|
|
10
10
|
"usage"
|
|
11
11
|
],
|
|
12
12
|
"homepage": "https://usagefleet.com",
|
|
13
|
-
"license": "
|
|
13
|
+
"license": "GPL-3.0-or-later",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/rokartur/usagefleet.git",
|
|
17
|
+
"directory": "apps/cli"
|
|
18
|
+
},
|
|
14
19
|
"bin": {
|
|
15
20
|
"usagefleet": "dist/index.js"
|
|
16
21
|
},
|