usagemax 0.3.7 → 0.3.8

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
@@ -53,7 +53,7 @@ logs. This example only inspects local source coverage:
53
53
 
54
54
  ```bash
55
55
  set +x
56
- bunx usagemax doctor --deep --json | jq '{complete, sources: [.sources[] | {name, status}]}'
56
+ bunx usagemax doctor --deep --json | jq '{complete: .inventoryComplete, sources: .detectedSources}'
57
57
  ```
58
58
 
59
59
  The JSON shape is intended for local automation; unsupported or unavailable
@@ -92,11 +92,14 @@ bunx usagemax sync --restart # restart an expired saved upload
92
92
  bunx usagemax status --remote --json # remote check; secret is never printed
93
93
  bunx usagemax doctor --deep --json # parse and audit retained history
94
94
  bunx usagemax link UMX-… --no-sync # link without uploading yet
95
+ bunx usagemax --version --json # print CLI and ccusage versions
96
+ bunx usagemax update --json # machine-readable release check
97
+ bunx usagemax unlink --json # safe automation result; no secret output
95
98
  ```
96
99
 
97
100
  ## Sources and coverage
98
101
 
99
- UsageMax pins [ccusage v20.0.20](https://github.com/ccusage/ccusage/releases/tag/v20.0.20)
102
+ UsageMax pins [ccusage v20.0.23](https://github.com/ccusage/ccusage/releases/tag/v20.0.23)
100
103
  and supports its 16 adapters: Amp, Claude Code, Codebuff, Codex, GitHub Copilot
101
104
  CLI, Factory Droid, Gemini CLI, Goose, Grok Build, Hermes, Kilo Code, Kimi CLI,
102
105
  OpenClaw, OpenCode, Pi, and Qwen Code. Named Pi-format stores are discovered as
@@ -132,7 +135,7 @@ printf '%s' "$USAGEMAX_COLLECTOR_TOKEN" \
132
135
  --json
133
136
  ```
134
137
 
135
- The `token status` command is included in CLI `0.3.7`. If a fresh environment
138
+ The `token status` command is included in CLI `0.3.8`. If a fresh environment
136
139
  still has an older npm tag, run `usagemax update token status` or
137
140
  `node packages/cli/src/cli.js token status` from this repository until the new
138
141
  package is published.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "usagemax",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Link local coding-agent usage to your UsageMax profile",
5
5
  "keywords": [
6
6
  "usagemax",
@@ -57,6 +57,6 @@
57
57
  "prepublishOnly": "npm test"
58
58
  },
59
59
  "dependencies": {
60
- "ccusage": "20.0.20"
60
+ "ccusage": "20.0.23"
61
61
  }
62
62
  }
package/src/cli.js CHANGED
@@ -26,7 +26,7 @@ process.title = "UsageMax";
26
26
 
27
27
  const require = createRequire(import.meta.url);
28
28
  const executeFile = promisify(execFile);
29
- const VERSION = "0.3.7";
29
+ const VERSION = "0.3.8";
30
30
  const PUBLIC_API_ORIGIN = "https://usagemax.com/api";
31
31
  const DEFAULT_LINK_ENDPOINT = `${PUBLIC_API_ORIGIN}/v1/devices/link`;
32
32
  const DEFAULT_STATUS_ENDPOINT = `${PUBLIC_API_ORIGIN}/v1/devices/status`;
@@ -121,6 +121,7 @@ function help() {
121
121
  process.stdout.write(" usagemax sync [--full] [--archives] [--restart] [--dry-run] [--explain] [--json]\n");
122
122
  process.stdout.write(" Reconcile once; --archives performs one-time recovery\n");
123
123
  process.stdout.write(" [--quiet|--no-progress] [--check-updates|--no-update-check] Control progress and the cached release check\n");
124
+ process.stdout.write(" [--json] Emit one JSON object per link/sync phase\n");
124
125
  process.stdout.write(" usagemax status Show local link status\n");
125
126
  process.stdout.write(" --remote [--json] Verify the stored collector credential without printing it\n");
126
127
  process.stdout.write(" [--quiet|--no-progress] Disable interactive progress output\n");
@@ -132,8 +133,10 @@ function help() {
132
133
  process.stdout.write(" usagemax doctor [--deep] [--json] [--quiet|--no-progress]\n");
133
134
  process.stdout.write(" Check source coverage; --deep parses full history\n");
134
135
  process.stdout.write(" usagemax update [command args] Check npm and optionally run the latest CLI\n");
136
+ process.stdout.write(" [--json] Emit machine-readable update status\n");
135
137
  process.stdout.write(" usagemax report [...args] Run a local ccusage report\n");
136
138
  process.stdout.write(" usagemax unlink [--revoke] Remove locally; --revoke also disables uploads\n");
139
+ process.stdout.write(" [--json] Emit machine-readable unlink status\n");
137
140
  }
138
141
 
139
142
  function ccusageCliPath() {
@@ -182,6 +185,7 @@ function warnVersion(body) {
182
185
  async function link(args) {
183
186
  const code = normalizeLinkCode(args[0]);
184
187
  if (!code) throw new Error("Paste the one-use UMX link code shown at usagemax.com/account.");
188
+ const json = args.includes("--json");
185
189
  const configuredEndpoint = process.env.USAGEMAX_LINK_ENDPOINT || DEFAULT_LINK_ENDPOINT;
186
190
  const endpoint = validHttpsUrl(configuredEndpoint, { allowLocalhost: true });
187
191
  if (!endpoint) throw new Error("USAGEMAX_LINK_ENDPOINT must use HTTPS, except for localhost development.");
@@ -223,13 +227,17 @@ async function link(args) {
223
227
  };
224
228
  await writeConfig(config);
225
229
  warnVersion(body);
226
- process.stdout.write(`Linked ${savedName} to ${config.profileHandle ? `@${config.profileHandle}` : "UsageMax"}.\n`);
230
+ if (json) {
231
+ process.stdout.write(`${JSON.stringify({ linked: true, version: VERSION, deviceName: savedName, profileHandle: config.profileHandle || null, profileUrl: config.profileUrl, deviceIdConfigured: true, sync: args.includes("--no-sync") ? "skipped" : "started" })}\n`);
232
+ } else {
233
+ process.stdout.write(`Linked ${savedName} to ${config.profileHandle ? `@${config.profileHandle}` : "UsageMax"}.\n`);
234
+ }
227
235
  if (args.includes("--no-sync")) {
228
- process.stdout.write("No usage was uploaded. Run `bunx usagemax sync --full` when you are ready.\n");
236
+ if (!json) process.stdout.write("No usage was uploaded. Run `bunx usagemax sync --full` when you are ready.\n");
229
237
  return;
230
238
  }
231
- process.stdout.write("Running the first one-shot sync…\n");
232
- await sync(["--full"], config);
239
+ if (!json) process.stdout.write("Running the first one-shot sync…\n");
240
+ await sync(["--full", ...(json ? ["--json"] : [])], config);
233
241
  }
234
242
 
235
243
  async function sync(args, suppliedConfig) {
@@ -254,6 +262,7 @@ async function sync(args, suppliedConfig) {
254
262
  }
255
263
 
256
264
  async function syncPrepared(args, suppliedConfig, recovery, progress = createProgress({ noProgress: true })) {
265
+ const startedAt = Date.now();
257
266
  const config = suppliedConfig || await readConfig();
258
267
  if (!config) throw new Error("This computer is not linked. Open https://usagemax.com/account and create a link code.");
259
268
  config.deviceId = await stableInstallationId(configDirectory(), config.deviceId);
@@ -269,7 +278,7 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
269
278
  if (config.pendingSync) {
270
279
  progress.update("Resuming the saved upload checkpoint…");
271
280
  if (dryRun) {
272
- const result = { ...config.pendingSync.result, dryRun: true, pendingRunId: config.pendingSync.runId };
281
+ const result = { ...config.pendingSync.result, dryRun: true, pendingRunId: config.pendingSync.runId, cliVersion: VERSION, ccusageVersion: CCUSAGE_VERSION, durationMs: Date.now() - startedAt };
273
282
  process.stdout.write(json ? `${JSON.stringify(result)}\n` : `Dry run: saved run ${config.pendingSync.runId} awaits resume; no upload.\n`);
274
283
  progress.succeed("Dry run complete; saved upload remains untouched.");
275
284
  return result;
@@ -280,9 +289,10 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
280
289
  warn: warnVersion,
281
290
  onProgress: ({ index, total, operation, acknowledged }) => progress.update(`${acknowledged ? "Uploaded" : "Uploading"} ${index}/${total} · ${operation}`),
282
291
  });
292
+ const summary = { ...result, cliVersion: VERSION, ccusageVersion: CCUSAGE_VERSION, durationMs: Date.now() - startedAt };
283
293
  progress.succeed("Resumed and completed the saved sync.");
284
- process.stdout.write(json ? `${JSON.stringify(result)}\n` : "Resumed and completed the saved sync. Run sync again to scan newer local changes.\n");
285
- return result;
294
+ process.stdout.write(json ? `${JSON.stringify(summary)}\n` : "Resumed and completed the saved sync. Run sync again to scan newer local changes.\n");
295
+ return summary;
286
296
  }
287
297
  const inventory = await sourceInventory({ env: recovery.env, home: ccusageHome(recovery.env) });
288
298
  progress.update(`Found ${inventory.sources.length} source${inventory.sources.length === 1 ? "" : "s"} and ${inventory.files}${inventory.truncated ? "+" : ""} local data file${inventory.files === 1 ? "" : "s"}.`);
@@ -292,9 +302,12 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
292
302
  today, now: Date.now(), inventoryVersion: SOURCE_INVENTORY_VERSION, requestedFull, requestedArchives,
293
303
  });
294
304
  if (skip) {
295
- const result = { accepted: 0, changedRows: 0, sessions: 0, sources: inventory.sources, corrections: 0, scanned: false, full: false, coverage: config.lastCoverage || "partial" };
305
+ const result = { accepted: 0, changedRows: 0, sessions: 0, sources: inventory.sources, corrections: 0, scanned: false, full: false, coverage: config.lastCoverage || "partial", skipReason: "inventory_unchanged", cliVersion: VERSION, ccusageVersion: CCUSAGE_VERSION, durationMs: Date.now() - startedAt };
296
306
  if (json) process.stdout.write(`${JSON.stringify(result)}\n`);
297
- else process.stdout.write("Already up to date. Local usage files have not changed; no logs were parsed or uploaded.\n");
307
+ else {
308
+ process.stdout.write("Already up to date. Local usage files have not changed; no logs were parsed or uploaded.\n");
309
+ if (explain) process.stdout.write(`Skipped because the source inventory fingerprint is unchanged; coverage remains ${result.coverage}.\n`);
310
+ }
298
311
  progress.succeed("No local changes; upload skipped.");
299
312
  return;
300
313
  }
@@ -334,6 +347,8 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
334
347
  coverage: authoritative ? "complete" : "partial",
335
348
  coverageReason: "Parser does not certify complete source/day coverage; decreases and deletions are protected.",
336
349
  range: { from: days[0], to: days.at(-1) },
350
+ cliVersion: VERSION,
351
+ ccusageVersion: CCUSAGE_VERSION,
337
352
  };
338
353
  progress.update(`Prepared ${partitions.length} usage chunk${partitions.length === 1 ? "" : "s"} and ${sessions.length} session identifier${sessions.length === 1 ? "" : "s"}.`);
339
354
  if (requestedArchives) {
@@ -341,6 +356,7 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
341
356
  result.unsupportedArchives = recovery.unsupported;
342
357
  }
343
358
  if (dryRun) {
359
+ result.durationMs = Date.now() - startedAt;
344
360
  if (json) process.stdout.write(`${JSON.stringify({ ...result, dryRun: true })}\n`);
345
361
  else {
346
362
  process.stdout.write(`Dry run: ${partitions.length} partition(s), ${result.changedRows} changed row(s), ${sessions.length} private session identifiers, no upload.\n`);
@@ -389,6 +405,7 @@ async function syncPrepared(args, suppliedConfig, recovery, progress = createPro
389
405
  warn: warnVersion,
390
406
  onProgress: ({ index, total, operation, acknowledged }) => progress.update(`${acknowledged ? "Uploaded" : "Uploading"} ${index}/${total} · ${operation}`),
391
407
  });
408
+ result.durationMs = Date.now() - startedAt;
392
409
  progress.succeed(`Sync complete · ${partitions.length} chunk${partitions.length === 1 ? "" : "s"}, ${sessions.length} session identifier${sessions.length === 1 ? "" : "s"}.`);
393
410
  if (json) process.stdout.write(`${JSON.stringify(result)}\n`);
394
411
  else {
@@ -431,7 +448,7 @@ async function status(args = []) {
431
448
  const progress = createProgress({ json, quiet: args.includes("--quiet"), noProgress: args.includes("--no-progress") });
432
449
  if (!config) {
433
450
  if (json) {
434
- process.stdout.write(`${JSON.stringify({ linked: false, remote: remote ? { status: "not_linked" } : undefined })}\n`);
451
+ process.stdout.write(`${JSON.stringify({ linked: false, version: VERSION, ccusageVersion: CCUSAGE_VERSION, configPath: configPath(), remote: remote ? { status: "not_linked" } : undefined })}\n`);
435
452
  return;
436
453
  }
437
454
  process.stdout.write("Not linked. Open https://usagemax.com/account to connect this computer.\n");
@@ -444,12 +461,18 @@ async function status(args = []) {
444
461
  }
445
462
  const local = {
446
463
  linked: true,
464
+ version: VERSION,
465
+ ccusageVersion: CCUSAGE_VERSION,
466
+ configPath: configPath(),
447
467
  deviceName: config.deviceName || deviceLabel(),
448
468
  profileHandle: config.profileHandle || null,
449
469
  deviceIdConfigured: Boolean(config.deviceId),
450
470
  lastSyncAt: config.lastSyncAt || null,
451
471
  lastFullSyncAt: config.lastFullSyncAt || null,
452
472
  pendingSync: config.pendingSync?.runId || null,
473
+ coverage: config.lastCoverage || null,
474
+ knownSources: Array.isArray(config.knownSources) ? config.knownSources : [],
475
+ snapshotRows: Object.keys(config.snapshots || {}).length,
453
476
  profileUrl: config.profileUrl || "https://usagemax.com/account",
454
477
  };
455
478
  let remoteView;
@@ -473,8 +496,11 @@ async function status(args = []) {
473
496
  return;
474
497
  }
475
498
  process.stdout.write(`Linked: ${config.deviceName || deviceLabel()}${config.profileHandle ? ` → @${config.profileHandle}` : ""}\n`);
499
+ process.stdout.write(`CLI: ${VERSION} · ccusage ${CCUSAGE_VERSION}\n`);
476
500
  process.stdout.write(`Last sync: ${config.lastSyncAt || "never"}\n`);
477
501
  process.stdout.write(`Last full reconciliation: ${config.lastFullSyncAt || "never"}\n`);
502
+ process.stdout.write(`Coverage: ${config.lastCoverage || "unknown"} · ${Object.keys(config.snapshots || {}).length} checkpoint rows\n`);
503
+ if (Array.isArray(config.knownSources) && config.knownSources.length) process.stdout.write(`Known sources: ${config.knownSources.join(", ")}\n`);
478
504
  if (config.pendingSync) process.stdout.write(`Pending sync: ${config.pendingSync.runId}; rerun sync to resume\n`);
479
505
  process.stdout.write(`Profile: ${config.profileUrl || "https://usagemax.com/account"}\n`);
480
506
  if (remote) printRemoteStatus(remoteView);
@@ -538,6 +564,9 @@ async function doctor(args = []) {
538
564
  const archives = await discoverProviderArchives({ env, home: ccusageHome(env) });
539
565
  const homes = String(env.USAGEMAX_DISCOVERED_HOMES || ccusageHome(env)).split(",").filter(Boolean);
540
566
  const result = {
567
+ cliVersion: VERSION,
568
+ ccusageVersion: CCUSAGE_VERSION,
569
+ configPath: configPath(),
541
570
  linked: Boolean(config),
542
571
  homes,
543
572
  detectedSources: inventory.sources,
@@ -592,22 +621,26 @@ async function report(args) {
592
621
  }
593
622
 
594
623
  async function update(args = []) {
624
+ const json = args.includes("--json");
595
625
  const check = await checkForUpdate(configDirectory(), VERSION, { force: true });
596
626
  if (!check.latest) {
597
- process.stdout.write(`UsageMax CLI ${VERSION} · update check unavailable.\n`);
627
+ const result = { version: VERSION, latest: null, newer: false, status: "unavailable" };
628
+ process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI ${VERSION} · update check unavailable.\n`);
598
629
  return;
599
630
  }
600
631
  if (!check.newer) {
601
- process.stdout.write(`UsageMax CLI ${VERSION} is current.\n`);
632
+ const result = { version: VERSION, latest: check.latest, newer: false, status: "current" };
633
+ process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI ${VERSION} is current.\n`);
602
634
  return;
603
635
  }
604
- process.stdout.write(`UsageMax CLI ${check.latest} is available (current ${VERSION}).\n`);
636
+ const result = { version: VERSION, latest: check.latest, newer: true, status: "available", command: "usagemax update sync" };
637
+ process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI ${check.latest} is available (current ${VERSION}).\n`);
605
638
  const commandArgs = args.filter((arg) => arg !== "--check");
606
639
  if (!commandArgs.length || args.includes("--check")) {
607
- process.stdout.write("Run `usagemax update sync` to hand off the next command to the latest npm release.\n");
640
+ if (!json) process.stdout.write("Run `usagemax update sync` to hand off the next command to the latest npm release.\n");
608
641
  return;
609
642
  }
610
- process.stdout.write(`Launching UsageMax ${check.latest}…\n`);
643
+ if (!json) process.stdout.write(`Launching UsageMax ${check.latest}…\n`);
611
644
  return runLatest(commandArgs);
612
645
  }
613
646
 
@@ -629,11 +662,13 @@ async function maybeUpdate(command, args) {
629
662
  async function removeLink(args = []) {
630
663
  const path = configPath();
631
664
  const config = await readConfig();
665
+ const json = args.includes("--json");
632
666
  if (!config) {
633
- process.stdout.write("This computer is not linked.\n");
667
+ process.stdout.write(json ? `${JSON.stringify({ unlinked: false, reason: "not_linked" })}\n` : "This computer is not linked.\n");
634
668
  return;
635
669
  }
636
- if (args.includes("--revoke")) {
670
+ const revoked = args.includes("--revoke");
671
+ if (revoked) {
637
672
  const endpoint = validHttpsUrl(config.revokeUrl, { allowLocalhost: true });
638
673
  if (!endpoint) throw new Error("This collector does not have a valid revoke endpoint. Revoke it at https://usagemax.com/account.");
639
674
  const response = await fetch(endpoint, {
@@ -647,16 +682,20 @@ async function removeLink(args = []) {
647
682
  if (!response.ok) throw new Error("UsageMax could not revoke this collector. It remains linked locally.");
648
683
  }
649
684
  await unlink(path);
650
- process.stdout.write(args.includes("--revoke")
651
- ? "Revoked this collector and removed its local key. Existing usage totals were retained.\n"
652
- : "Removed the local UsageMax collector key. This computer's private installation identity was retained so relinking cannot duplicate its usage. Revoke the collector in your account if this computer is no longer trusted.\n");
685
+ if (json) {
686
+ process.stdout.write(`${JSON.stringify({ unlinked: true, revoked, retainedInstallationIdentity: true })}\n`);
687
+ } else {
688
+ process.stdout.write(revoked
689
+ ? "Revoked this collector and removed its local key. Existing usage totals were retained.\n"
690
+ : "Removed the local UsageMax collector key. This computer's private installation identity was retained so relinking cannot duplicate its usage. Revoke the collector in your account if this computer is no longer trusted.\n");
691
+ }
653
692
  }
654
693
 
655
694
  async function main() {
656
695
  const args = process.argv.slice(2);
657
696
  const command = args[0] || "sync";
658
697
  if (["--help", "-h", "help"].includes(command)) return help();
659
- if (["--version", "-v"].includes(command)) return process.stdout.write(`${VERSION}\n`);
698
+ if (["--version", "-v"].includes(command)) return process.stdout.write(args.includes("--json") ? `${JSON.stringify({ version: VERSION, ccusageVersion: CCUSAGE_VERSION })}\n` : `${VERSION}\n`);
660
699
  if (command === "update") return update(args.slice(1));
661
700
  if (await maybeUpdate(command, args)) return;
662
701
  if (command === "service") {
package/src/sources.js CHANGED
@@ -3,7 +3,7 @@ import { readFile, readdir, stat } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
 
6
- export const CCUSAGE_VERSION = "20.0.20";
6
+ export const CCUSAGE_VERSION = "20.0.23";
7
7
  export const SOURCE_INVENTORY_VERSION = 3;
8
8
  export const SUPPORTED_SOURCES = [
9
9
  "amp",