usagemax 0.3.9 → 0.3.10
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 +33 -10
- package/package.json +3 -2
- package/src/ccusage.js +21 -0
- package/src/cli.js +177 -22
- package/src/sources.js +50 -12
- package/src/transport.js +76 -0
- package/src/updates.js +35 -8
package/README.md
CHANGED
|
@@ -44,7 +44,9 @@ The CLI checks npm's `latest` dist-tag at most twice per day. Run
|
|
|
44
44
|
`usagemax update` to update a global installation through the package manager
|
|
45
45
|
that launched it (Bun or npm). Add `--check` for a read-only check, or
|
|
46
46
|
`--json` for automation. A command suffix such as `usagemax update sync` hands
|
|
47
|
-
that command to the newest release without typing `@latest`.
|
|
47
|
+
that command to the newest release without typing `@latest`. When `--check` or
|
|
48
|
+
`--no-install` is present, the suffix is reported in JSON and never handed off;
|
|
49
|
+
in a terminal, install the update and then run the printed command. Set
|
|
48
50
|
`USAGEMAX_AUTO_UPDATE=1` for an explicit automatic handoff. Use `--no-update-check` or set
|
|
49
51
|
`USAGEMAX_DISABLE_UPDATE_CHECK=1` in offline environments. Interactive
|
|
50
52
|
terminals show a small stderr progress line; JSON,
|
|
@@ -80,6 +82,7 @@ usagemax status Show local link state
|
|
|
80
82
|
usagemax doctor Check discovered sources
|
|
81
83
|
usagemax report [ccusage args] Run a local ccusage report
|
|
82
84
|
usagemax token status Diagnose a key piped on stdin
|
|
85
|
+
usagemax telemetry test Send one zero-token write-path smoke event
|
|
83
86
|
usagemax service install Opt into periodic OS checkpoints
|
|
84
87
|
usagemax service status|run|uninstall
|
|
85
88
|
usagemax unlink [--revoke] Remove local credentials
|
|
@@ -95,6 +98,7 @@ bunx usagemax sync --full # all retained local history
|
|
|
95
98
|
bunx usagemax sync --archives # one-time compressed-history recovery
|
|
96
99
|
bunx usagemax sync --restart # restart an expired saved upload
|
|
97
100
|
bunx usagemax status --remote --json # remote check; secret is never printed
|
|
101
|
+
bunx usagemax telemetry test --json # one observability event; no accounting change
|
|
98
102
|
bunx usagemax doctor --deep --json # parse and audit retained history
|
|
99
103
|
bunx usagemax link UMX-… --no-sync # link without uploading yet
|
|
100
104
|
bunx usagemax --version --json # print CLI and ccusage versions
|
|
@@ -105,12 +109,18 @@ bunx usagemax unlink --json # safe automation result; no secret o
|
|
|
105
109
|
|
|
106
110
|
## Sources and coverage
|
|
107
111
|
|
|
108
|
-
UsageMax pins [ccusage v20.0.
|
|
112
|
+
UsageMax pins [ccusage v20.0.24](https://github.com/ccusage/ccusage/releases/tag/v20.0.24)
|
|
109
113
|
and supports its 16 adapters: Amp, Claude Code, Codebuff, Codex, GitHub Copilot
|
|
110
114
|
CLI, Factory Droid, Gemini CLI, Goose, Grok Build, Hermes, Kilo Code, Kimi CLI,
|
|
111
115
|
OpenClaw, OpenCode, Pi, and Qwen Code. Named Pi-format stores are discovered as
|
|
112
116
|
well.
|
|
113
117
|
|
|
118
|
+
The collector explicitly requests ccusage's per-model breakdown so models used
|
|
119
|
+
on the same day remain individually attributed instead of collapsing into a
|
|
120
|
+
generic `mixed` row. This collector update triggers one full reconciliation of
|
|
121
|
+
retained history from 2024-01-01 on the next sync; subsequent runs return to the
|
|
122
|
+
normal incremental schedule.
|
|
123
|
+
|
|
114
124
|
The collector follows the supported provider environment overrides and bounded
|
|
115
125
|
home locations. It recognizes Claude Desktop sessions, `.cc-mirror`, renamed
|
|
116
126
|
Claude/Codex backup folders, and supported Windows homes from WSL. In WSL, use
|
|
@@ -135,14 +145,14 @@ stdin; never pass it as an argument or put it in a URL:
|
|
|
135
145
|
|
|
136
146
|
```bash
|
|
137
147
|
set +x
|
|
148
|
+
set -o pipefail
|
|
138
149
|
printf '%s' "$USAGEMAX_COLLECTOR_TOKEN" \
|
|
139
150
|
| bunx usagemax token status \
|
|
140
|
-
--device-id "$USAGEMAX_INSTALLATION_ID" \
|
|
141
151
|
--json
|
|
142
152
|
```
|
|
143
153
|
|
|
144
|
-
The `token status`
|
|
145
|
-
still has an older npm tag, run `usagemax update token status` or
|
|
154
|
+
The `token status` and `telemetry test` commands are included in CLI `0.3.10`.
|
|
155
|
+
If a fresh environment still has an older npm tag, run `usagemax update token status` or
|
|
146
156
|
`node packages/cli/src/cli.js token status` from this repository until the new
|
|
147
157
|
package is published.
|
|
148
158
|
|
|
@@ -154,9 +164,10 @@ For a numeric-only result suitable for a smoke check:
|
|
|
154
164
|
|
|
155
165
|
```bash
|
|
156
166
|
set +x
|
|
167
|
+
set -o pipefail
|
|
157
168
|
printf '%s' "$USAGEMAX_COLLECTOR_TOKEN" \
|
|
158
169
|
| bunx usagemax token status \
|
|
159
|
-
--
|
|
170
|
+
--json \
|
|
160
171
|
| jq -r '[.httpStatus, (if .ingestAuthorized then 1 else 0 end)] | @tsv'
|
|
161
172
|
```
|
|
162
173
|
|
|
@@ -172,10 +183,22 @@ installation does not match. A recognized key missing `telemetry:write` has
|
|
|
172
183
|
`status: scope_missing`, `scopeStatus: missing_telemetry_write`, and
|
|
173
184
|
`ingestAuthorized: false`.
|
|
174
185
|
|
|
175
|
-
For a write-path smoke check,
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
186
|
+
For a write-path smoke check, run `usagemax telemetry test`. A linked install
|
|
187
|
+
uses its stored collector key. For an advanced key, pipe it through stdin:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
set +x
|
|
191
|
+
printf '%s' "$USAGEMAX_COLLECTOR_TOKEN" \
|
|
192
|
+
| bunx usagemax telemetry test \
|
|
193
|
+
--token-stdin \
|
|
194
|
+
--json
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The command sends one `agent_state` event with all token counters and
|
|
198
|
+
`costMicros` set to zero. It never sends prompts or raw logs. The event is
|
|
199
|
+
observability-only and does not update accounting; the first accepted write
|
|
200
|
+
binds an otherwise unbound advanced key to the CLI installation. Rejected
|
|
201
|
+
credentials return a non-zero process exit code for reliable scripts.
|
|
179
202
|
|
|
180
203
|
## Privacy and resource use
|
|
181
204
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "usagemax",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Link local coding-agent usage to your UsageMax profile",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"usagemax",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"files": [
|
|
35
35
|
"src/archives.js",
|
|
36
|
+
"src/ccusage.js",
|
|
36
37
|
"src/cli.js",
|
|
37
38
|
"src/core.js",
|
|
38
39
|
"src/installation.js",
|
|
@@ -57,6 +58,6 @@
|
|
|
57
58
|
"prepublishOnly": "npm test"
|
|
58
59
|
},
|
|
59
60
|
"dependencies": {
|
|
60
|
-
"ccusage": "20.0.
|
|
61
|
+
"ccusage": "20.0.24"
|
|
61
62
|
}
|
|
62
63
|
}
|
package/src/ccusage.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { reportDateArgs } from "./core.js";
|
|
2
|
+
|
|
3
|
+
/** Build the stable, model-complete ccusage command used by sync and doctor. */
|
|
4
|
+
export function ccusageDailyArgs(config, options = {}) {
|
|
5
|
+
return [
|
|
6
|
+
"daily",
|
|
7
|
+
"--json",
|
|
8
|
+
"--offline",
|
|
9
|
+
"--mode",
|
|
10
|
+
"calculate",
|
|
11
|
+
"--timezone",
|
|
12
|
+
"UTC",
|
|
13
|
+
"--by-agent",
|
|
14
|
+
"--breakdown",
|
|
15
|
+
"--order",
|
|
16
|
+
"asc",
|
|
17
|
+
"--sections",
|
|
18
|
+
"daily,session",
|
|
19
|
+
...reportDateArgs(config, options),
|
|
20
|
+
];
|
|
21
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -11,14 +11,15 @@ import { promisify } from "node:util";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
|
|
13
13
|
import { prepareArchiveRecovery } from "./archives.js";
|
|
14
|
-
import { buildSessionPlan, buildSnapshotPlan, normalizeLinkCode,
|
|
14
|
+
import { buildSessionPlan, buildSnapshotPlan, normalizeLinkCode, scanPolicy, sourceSummary, validHttpsUrl } from "./core.js";
|
|
15
|
+
import { ccusageDailyArgs } from "./ccusage.js";
|
|
15
16
|
import { stableInstallationId } from "./installation.js";
|
|
16
17
|
import { createProgress } from "./progress.js";
|
|
17
18
|
import { intervalMinutes, manageService, runScheduledSync } from "./service.js";
|
|
18
|
-
import { collectorStatusView, requestCollectorStatus, requestSnapshot } from "./transport.js";
|
|
19
|
+
import { collectorCanWrite, collectorStatusView, requestCollectorStatus, requestSnapshot, requestTelemetrySmokeTest } from "./transport.js";
|
|
19
20
|
import { resumeUpload, restartExpiredUpload, withConfigLock } from "./resume.js";
|
|
20
|
-
import { CCUSAGE_VERSION, ccusageEnvironment, ccusageHome, discoverProviderArchives, SOURCE_INVENTORY_VERSION, sourceInventory, SUPPORTED_SOURCES } from "./sources.js";
|
|
21
|
-
import { checkForUpdate, packageManagerFor, runLatest, updateGlobal } from "./updates.js";
|
|
21
|
+
import { CCUSAGE_VERSION, ccusageEnvironment, ccusageHome, discoverProviderArchives, discoverWslWindowsHomeStatus, SOURCE_INVENTORY_VERSION, sourceInventory, SUPPORTED_SOURCES } from "./sources.js";
|
|
22
|
+
import { checkForUpdate, packageManagerFor, runLatest, updateCommandWillRun, updateGlobal, updateRequest } from "./updates.js";
|
|
22
23
|
|
|
23
24
|
// Make the short-lived collector recognizable in Activity Monitor and `ps`.
|
|
24
25
|
// Windows may still display the underlying node.exe image name in Task Manager.
|
|
@@ -26,7 +27,7 @@ process.title = "UsageMax";
|
|
|
26
27
|
|
|
27
28
|
const require = createRequire(import.meta.url);
|
|
28
29
|
const executeFile = promisify(execFile);
|
|
29
|
-
const VERSION = "0.3.
|
|
30
|
+
const VERSION = "0.3.10";
|
|
30
31
|
const PUBLIC_API_ORIGIN = "https://usagemax.com/api";
|
|
31
32
|
const DEFAULT_LINK_ENDPOINT = `${PUBLIC_API_ORIGIN}/v1/devices/link`;
|
|
32
33
|
const DEFAULT_STATUS_ENDPOINT = `${PUBLIC_API_ORIGIN}/v1/devices/status`;
|
|
@@ -111,7 +112,27 @@ function deviceLabel() {
|
|
|
111
112
|
return "Computer";
|
|
112
113
|
}
|
|
113
114
|
|
|
114
|
-
function help() {
|
|
115
|
+
function help(command) {
|
|
116
|
+
if (command) {
|
|
117
|
+
const usage = {
|
|
118
|
+
link: "Usage: usagemax link <one-use-code> [--no-sync] [--name <name>] [--json]",
|
|
119
|
+
sync: "Usage: usagemax sync [--full] [--archives] [--restart] [--dry-run] [--explain] [--json] [--quiet|--no-progress]",
|
|
120
|
+
status: "Usage: usagemax status [--remote] [--json] [--quiet|--no-progress]",
|
|
121
|
+
doctor: "Usage: usagemax doctor [--deep] [--json] [--quiet|--no-progress]",
|
|
122
|
+
"token status": "Usage: printf '%s' \"$USAGEMAX_COLLECTOR_TOKEN\" | usagemax token status [--device-id <uuid>] [--json] [--quiet|--no-progress]\nRead-only credential check; the token is read from stdin and never printed.",
|
|
123
|
+
"telemetry test": "Usage: usagemax telemetry test [--token-stdin] [--device-id <uuid>] [--json] [--quiet|--no-progress]\nSend one content-free zero-token observability event to verify the write path.",
|
|
124
|
+
service: "Usage: usagemax service install [--every <minutes>] | status | run | uninstall",
|
|
125
|
+
"service install": "Usage: usagemax service install [--every <minutes>]",
|
|
126
|
+
"service status": "Usage: usagemax service status",
|
|
127
|
+
"service run": "Usage: usagemax service run",
|
|
128
|
+
"service uninstall": "Usage: usagemax service uninstall",
|
|
129
|
+
update: "Usage: usagemax update [--check|--no-install] [--json]\n usagemax update <command args> [--json]",
|
|
130
|
+
report: "Usage: usagemax report [...ccusage args]",
|
|
131
|
+
unlink: "Usage: usagemax unlink [--revoke] [--json]",
|
|
132
|
+
}[command];
|
|
133
|
+
process.stdout.write(`${usage || `No help is available for '${command}'. Run usagemax --help.`}\n`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
115
136
|
process.stdout.write(`UsageMax ${VERSION}\n\n`);
|
|
116
137
|
process.stdout.write("Link aggregate coding-agent usage to your UsageMax profile.\n\n");
|
|
117
138
|
process.stdout.write("Commands:\n");
|
|
@@ -125,8 +146,10 @@ function help() {
|
|
|
125
146
|
process.stdout.write(" usagemax status Show local link status\n");
|
|
126
147
|
process.stdout.write(" --remote [--json] Verify the stored collector credential without printing it\n");
|
|
127
148
|
process.stdout.write(" [--quiet|--no-progress] Disable interactive progress output\n");
|
|
128
|
-
process.stdout.write(" usagemax token status [--device-id <uuid>] [--json]\n");
|
|
149
|
+
process.stdout.write(" usagemax token status [--device-id <uuid>] [--json] [--quiet|--no-progress]\n");
|
|
129
150
|
process.stdout.write(" Diagnose a key piped on stdin; never pass it as an argument\n");
|
|
151
|
+
process.stdout.write(" usagemax telemetry test [--token-stdin] [--device-id <uuid>] [--json] [--quiet|--no-progress]\n");
|
|
152
|
+
process.stdout.write(" Send one zero-token observability smoke event\n");
|
|
130
153
|
process.stdout.write(" usagemax service install [--every 15]\n");
|
|
131
154
|
process.stdout.write(" Opt into lightweight OS-scheduled sync\n");
|
|
132
155
|
process.stdout.write(" usagemax service status|run|uninstall\n");
|
|
@@ -138,6 +161,44 @@ function help() {
|
|
|
138
161
|
process.stdout.write(" usagemax report [...args] Run a local ccusage report\n");
|
|
139
162
|
process.stdout.write(" usagemax unlink [--revoke] Remove locally; --revoke also disables uploads\n");
|
|
140
163
|
process.stdout.write(" [--json] Emit machine-readable unlink status\n");
|
|
164
|
+
process.stdout.write("\nAdd --help after a command for its usage.\n");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function validateCommandArgs(command, args) {
|
|
168
|
+
const specs = {
|
|
169
|
+
link: { flags: ["--no-sync", "--json", "--check-updates", "--no-update-check"], values: ["--name"], positional: 1 },
|
|
170
|
+
sync: { flags: ["--full", "--archives", "--restart", "--dry-run", "--explain", "--json", "--quiet", "--no-progress", "--check-updates", "--no-update-check"], values: [], positional: 0 },
|
|
171
|
+
status: { flags: ["--remote", "--json", "--quiet", "--no-progress"], values: [], positional: 0 },
|
|
172
|
+
doctor: { flags: ["--deep", "--json", "--quiet", "--no-progress", "--check-updates", "--no-update-check"], values: [], positional: 0 },
|
|
173
|
+
unlink: { flags: ["--revoke", "--json"], values: [], positional: 0 },
|
|
174
|
+
"token status": { flags: ["--json", "--quiet", "--no-progress"], values: ["--device-id"], positional: 0 },
|
|
175
|
+
"telemetry test": { flags: ["--token-stdin", "--json", "--quiet", "--no-progress"], values: ["--device-id"], positional: 0 },
|
|
176
|
+
};
|
|
177
|
+
if (command === "service") {
|
|
178
|
+
const action = args[0] || "status";
|
|
179
|
+
if (!["install", "status", "run", "uninstall"].includes(action)) throw new Error(`Unknown service action: ${action}. Run usagemax service --help.`);
|
|
180
|
+
command = `service ${action}`;
|
|
181
|
+
args = args.slice(1);
|
|
182
|
+
specs[command] = { flags: [], values: ["--config-dir", ...(action === "install" ? ["--every"] : [])], positional: 0 };
|
|
183
|
+
}
|
|
184
|
+
const spec = specs[command];
|
|
185
|
+
if (!spec) return;
|
|
186
|
+
let positional = 0;
|
|
187
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
188
|
+
const arg = args[index];
|
|
189
|
+
if (!arg.startsWith("-")) {
|
|
190
|
+
positional += 1;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (spec.flags.includes(arg)) continue;
|
|
194
|
+
if (spec.values.includes(arg)) {
|
|
195
|
+
if (index + 1 >= args.length || args[index + 1].startsWith("-")) throw new Error(`${arg} requires a value.`);
|
|
196
|
+
index += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
throw new Error(`Unknown option for ${command}: ${arg}. Run usagemax ${command} --help.`);
|
|
200
|
+
}
|
|
201
|
+
if (positional > spec.positional) throw new Error(`Unexpected argument for ${command}. Run usagemax ${command} --help.`);
|
|
141
202
|
}
|
|
142
203
|
|
|
143
204
|
function ccusageCliPath() {
|
|
@@ -145,12 +206,10 @@ function ccusageCliPath() {
|
|
|
145
206
|
}
|
|
146
207
|
|
|
147
208
|
async function ccusageJson(config, { full = false, env } = {}) {
|
|
148
|
-
const args = [ccusageCliPath(),
|
|
149
|
-
args.push("--sections", "daily,session");
|
|
209
|
+
const args = [ccusageCliPath(), ...ccusageDailyArgs(config, { full })];
|
|
150
210
|
// Reconcile yesterday once after the UTC date changes. All other incremental
|
|
151
211
|
// scans parse only today; a metadata fingerprint avoids invoking ccusage when
|
|
152
212
|
// no supported local source changed at all.
|
|
153
|
-
args.push(...reportDateArgs(config, { full }));
|
|
154
213
|
const { stdout } = await executeFile(process.execPath, args, {
|
|
155
214
|
encoding: "utf8",
|
|
156
215
|
maxBuffer: MAX_REPORT_BYTES,
|
|
@@ -432,6 +491,7 @@ function printRemoteStatus(view) {
|
|
|
432
491
|
if (view.credentialType) process.stdout.write(`Type: ${view.credentialType}${view.writeOnly ? "; write-only" : ""}\n`);
|
|
433
492
|
if (view.scopes?.length) process.stdout.write(`Scopes: ${view.scopes.join(", ")}\n`);
|
|
434
493
|
if (view.scopeStatus) process.stdout.write(`Ingest scope: ${view.scopeStatus === "valid" ? "authorized" : "missing telemetry:write"}\n`);
|
|
494
|
+
if (typeof view.ingestAuthorized === "boolean") process.stdout.write(`Ingestion: ${view.ingestAuthorized ? "authorized" : "blocked"}\n`);
|
|
435
495
|
if (view.deviceBinding) process.stdout.write(`Device binding: ${view.deviceBinding}\n`);
|
|
436
496
|
if (view.profileHandle) process.stdout.write(`Profile: @${view.profileHandle}\n`);
|
|
437
497
|
if (view.deviceName) process.stdout.write(`Computer: ${view.deviceName}\n`);
|
|
@@ -450,9 +510,11 @@ async function status(args = []) {
|
|
|
450
510
|
if (!config) {
|
|
451
511
|
if (json) {
|
|
452
512
|
process.stdout.write(`${JSON.stringify({ linked: false, version: VERSION, ccusageVersion: CCUSAGE_VERSION, configPath: configPath(), remote: remote ? { status: "not_linked" } : undefined })}\n`);
|
|
513
|
+
if (remote) process.exitCode = 1;
|
|
453
514
|
return;
|
|
454
515
|
}
|
|
455
516
|
process.stdout.write("Not linked. Open https://usagemax.com/account to connect this computer.\n");
|
|
517
|
+
if (remote) process.exitCode = 1;
|
|
456
518
|
return;
|
|
457
519
|
}
|
|
458
520
|
const stableId = await stableInstallationId(configDirectory(), config.deviceId);
|
|
@@ -489,6 +551,7 @@ async function status(args = []) {
|
|
|
489
551
|
}
|
|
490
552
|
if (json) {
|
|
491
553
|
process.stdout.write(`${JSON.stringify({ ...local, remote: remoteView })}\n`);
|
|
554
|
+
if (!collectorCanWrite(remoteView)) process.exitCode = 1;
|
|
492
555
|
return;
|
|
493
556
|
}
|
|
494
557
|
}
|
|
@@ -505,6 +568,7 @@ async function status(args = []) {
|
|
|
505
568
|
if (config.pendingSync) process.stdout.write(`Pending sync: ${config.pendingSync.runId}; rerun sync to resume\n`);
|
|
506
569
|
process.stdout.write(`Profile: ${config.profileUrl || "https://usagemax.com/account"}\n`);
|
|
507
570
|
if (remote) printRemoteStatus(remoteView);
|
|
571
|
+
if (remote && !collectorCanWrite(remoteView)) process.exitCode = 1;
|
|
508
572
|
}
|
|
509
573
|
|
|
510
574
|
async function readTokenFromStdin() {
|
|
@@ -525,6 +589,8 @@ async function readTokenFromStdin() {
|
|
|
525
589
|
async function tokenStatus(args = []) {
|
|
526
590
|
const requestedDeviceId = option(args, "--device-id");
|
|
527
591
|
if (requestedDeviceId && !DEVICE_PATTERN.test(requestedDeviceId)) throw new Error("--device-id must be a UUID.");
|
|
592
|
+
const localConfig = await readConfig();
|
|
593
|
+
const deviceId = requestedDeviceId || localConfig?.deviceId;
|
|
528
594
|
const json = args.includes("--json");
|
|
529
595
|
const progress = createProgress({ json, quiet: args.includes("--quiet"), noProgress: args.includes("--no-progress") });
|
|
530
596
|
progress.start("Checking the collector credential…");
|
|
@@ -539,7 +605,7 @@ async function tokenStatus(args = []) {
|
|
|
539
605
|
const endpoint = validHttpsUrl(configuredEndpoint, { allowLocalhost: true });
|
|
540
606
|
try {
|
|
541
607
|
if (!endpoint) throw new Error("USAGEMAX_STATUS_ENDPOINT must use HTTPS, except for localhost development.");
|
|
542
|
-
const result = await requestCollectorStatus(endpoint, { token, deviceId
|
|
608
|
+
const result = await requestCollectorStatus(endpoint, { token, deviceId });
|
|
543
609
|
const view = collectorStatusView(result.httpStatus, result.body, token);
|
|
544
610
|
progress.succeed("Collector credential checked.");
|
|
545
611
|
if (json) process.stdout.write(`${JSON.stringify(view)}\n`);
|
|
@@ -547,6 +613,39 @@ async function tokenStatus(args = []) {
|
|
|
547
613
|
process.stdout.write("Credential format: valid (umx_ + 64 lowercase hexadecimal characters)\n");
|
|
548
614
|
printRemoteStatus(view);
|
|
549
615
|
}
|
|
616
|
+
if (!collectorCanWrite(view)) process.exitCode = 1;
|
|
617
|
+
} catch (error) {
|
|
618
|
+
progress.stop();
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async function telemetryTest(args = []) {
|
|
624
|
+
const requestedDeviceId = option(args, "--device-id");
|
|
625
|
+
if (requestedDeviceId && !DEVICE_PATTERN.test(requestedDeviceId)) throw new Error("--device-id must be a UUID.");
|
|
626
|
+
const json = args.includes("--json");
|
|
627
|
+
const progress = createProgress({ json, quiet: args.includes("--quiet"), noProgress: args.includes("--no-progress") });
|
|
628
|
+
const localConfig = await readConfig();
|
|
629
|
+
let token;
|
|
630
|
+
try {
|
|
631
|
+
if (!localConfig || args.includes("--token-stdin")) {
|
|
632
|
+
token = await readTokenFromStdin();
|
|
633
|
+
} else {
|
|
634
|
+
token = localConfig.token;
|
|
635
|
+
}
|
|
636
|
+
const deviceId = requestedDeviceId || await stableInstallationId(configDirectory(), localConfig?.deviceId);
|
|
637
|
+
const configuredEndpoint = process.env.USAGEMAX_TELEMETRY_ENDPOINT || localConfig?.ingestUrl || `${PUBLIC_API_ORIGIN}/v1/telemetry/llm`;
|
|
638
|
+
const endpoint = validHttpsUrl(configuredEndpoint, { allowLocalhost: true });
|
|
639
|
+
if (!endpoint) throw new Error("USAGEMAX_TELEMETRY_ENDPOINT must use HTTPS, except for localhost development.");
|
|
640
|
+
progress.start("Sending a zero-token telemetry smoke event…");
|
|
641
|
+
const result = await requestTelemetrySmokeTest(endpoint, { token, deviceId });
|
|
642
|
+
if (result.ok) progress.succeed("Telemetry smoke event accepted.");
|
|
643
|
+
else progress.fail("Telemetry smoke event was rejected.");
|
|
644
|
+
const summary = { ...result, deviceIdConfigured: true };
|
|
645
|
+
if (json) process.stdout.write(`${JSON.stringify(summary)}\n`);
|
|
646
|
+
else if (result.ok) process.stdout.write(`UsageMax accepted the smoke event (HTTP ${result.httpStatus}). No token or cost totals were recorded.\n`);
|
|
647
|
+
else process.stdout.write(`Telemetry check failed (${result.status}${result.httpStatus ? `, HTTP ${result.httpStatus}` : ""}). Run usagemax token status --device-id ${deviceId} to diagnose the credential.\n`);
|
|
648
|
+
if (!result.ok) process.exitCode = 1;
|
|
550
649
|
} catch (error) {
|
|
551
650
|
progress.stop();
|
|
552
651
|
throw error;
|
|
@@ -561,6 +660,7 @@ async function doctor(args = []) {
|
|
|
561
660
|
const config = await readConfig();
|
|
562
661
|
const env = await ccusageEnvironment();
|
|
563
662
|
const inventory = await sourceInventory({ env, home: ccusageHome(env) });
|
|
663
|
+
const windowsHomeDiscovery = await discoverWslWindowsHomeStatus();
|
|
564
664
|
progress.update(`Found ${inventory.sources.length} source${inventory.sources.length === 1 ? "" : "s"} and ${inventory.files}${inventory.truncated ? "+" : ""} local data file${inventory.files === 1 ? "" : "s"}.`);
|
|
565
665
|
const archives = await discoverProviderArchives({ env, home: ccusageHome(env) });
|
|
566
666
|
const homes = String(env.USAGEMAX_DISCOVERED_HOMES || ccusageHome(env)).split(",").filter(Boolean);
|
|
@@ -570,6 +670,7 @@ async function doctor(args = []) {
|
|
|
570
670
|
configPath: configPath(),
|
|
571
671
|
linked: Boolean(config),
|
|
572
672
|
homes,
|
|
673
|
+
windowsHomeDiscovery,
|
|
573
674
|
detectedSources: inventory.sources,
|
|
574
675
|
files: inventory.files,
|
|
575
676
|
inventoryComplete: inventory.complete,
|
|
@@ -596,7 +697,16 @@ async function doctor(args = []) {
|
|
|
596
697
|
process.stdout.write(`Detected sources: ${inventory.sources.join(", ") || "none"} (${inventory.files}${inventory.truncated ? "+" : ""} data files)\n`);
|
|
597
698
|
process.stdout.write(`Supported sources: ${SUPPORTED_SOURCES.join(", ")} (+ named pi-format stores)\n`);
|
|
598
699
|
if (platform() === "linux" && process.env.WSL_DISTRO_NAME) {
|
|
599
|
-
process.stdout.write(`
|
|
700
|
+
process.stdout.write(`Windows home discovery: ${windowsHomeDiscovery.status}`);
|
|
701
|
+
if (windowsHomeDiscovery.selected.length) {
|
|
702
|
+
process.stdout.write(` (${windowsHomeDiscovery.selected.join(", ")})`);
|
|
703
|
+
}
|
|
704
|
+
process.stdout.write("\n");
|
|
705
|
+
if (windowsHomeDiscovery.status === "ambiguous") {
|
|
706
|
+
process.stdout.write("No Windows profile was selected to avoid mixing users. Set USAGEMAX_ADDITIONAL_HOME to the exact profile path you own, for example /mnt/c/Users/<your-profile>.\n");
|
|
707
|
+
} else if (windowsHomeDiscovery.status === "unavailable") {
|
|
708
|
+
process.stdout.write("No supported provider directory was found in readable Windows profiles. If your data is elsewhere, set USAGEMAX_ADDITIONAL_HOME to its exact profile path.\n");
|
|
709
|
+
}
|
|
600
710
|
}
|
|
601
711
|
if (archives.length) process.stdout.write(`Recovery: ${archives.length} compressed provider archive(s) detected; run \`bunx usagemax sync --archives\` once to reconcile them\n`);
|
|
602
712
|
if (!inventory.complete) process.stdout.write(`Inventory: incomplete (${inventory.errors} read error(s)${inventory.truncated ? ", file limit reached" : ""}); no-change shortcut disabled\n`);
|
|
@@ -622,32 +732,64 @@ async function report(args) {
|
|
|
622
732
|
}
|
|
623
733
|
|
|
624
734
|
async function update(args = []) {
|
|
625
|
-
const json = args
|
|
626
|
-
const checkOnly = args.includes("--check") || args.includes("--no-install");
|
|
627
|
-
const commandArgs = args.filter((arg) => !["--check", "--no-install", "--json"].includes(arg));
|
|
628
|
-
const handoffArgs = commandArgs.length ? [...commandArgs, ...(json ? ["--json"] : [])] : [];
|
|
735
|
+
const { json, checkOnly, commandArgs, handoffArgs } = updateRequest(args);
|
|
629
736
|
const check = await checkForUpdate(configDirectory(), VERSION, { force: true });
|
|
737
|
+
const commandWillRun = updateCommandWillRun({ latest: check.latest, checkOnly, commandArgs });
|
|
630
738
|
if (!check.latest) {
|
|
631
|
-
const result = {
|
|
739
|
+
const result = {
|
|
740
|
+
version: VERSION,
|
|
741
|
+
latest: null,
|
|
742
|
+
newer: false,
|
|
743
|
+
status: "unavailable",
|
|
744
|
+
...(commandArgs.length ? { command: `usagemax ${commandArgs.join(" ")}`, commandWillRun: false } : {}),
|
|
745
|
+
};
|
|
632
746
|
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI ${VERSION} · update check unavailable.\n`);
|
|
747
|
+
if (!json && commandArgs.length) process.stdout.write(`The requested command was not run because no release could be verified: \`usagemax ${commandArgs.join(" ")}\`.\n`);
|
|
633
748
|
return;
|
|
634
749
|
}
|
|
635
750
|
if (!check.newer) {
|
|
636
|
-
const result = {
|
|
751
|
+
const result = {
|
|
752
|
+
version: VERSION,
|
|
753
|
+
latest: check.latest,
|
|
754
|
+
newer: false,
|
|
755
|
+
status: "current",
|
|
756
|
+
...(commandArgs.length ? { command: `usagemax ${commandArgs.join(" ")}`, commandWillRun } : {}),
|
|
757
|
+
};
|
|
637
758
|
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI ${VERSION} is current.\n`);
|
|
759
|
+
if (commandWillRun) {
|
|
760
|
+
if (!json) process.stdout.write(`Launching UsageMax ${check.latest}…\n`);
|
|
761
|
+
return runLatest(handoffArgs);
|
|
762
|
+
}
|
|
763
|
+
if (!json && commandArgs.length) process.stdout.write(`Check-only: the requested command was not run: \`usagemax ${commandArgs.join(" ")}\`.\n`);
|
|
638
764
|
return;
|
|
639
765
|
}
|
|
640
766
|
if (handoffArgs.length) {
|
|
641
|
-
const result = {
|
|
767
|
+
const result = {
|
|
768
|
+
version: VERSION,
|
|
769
|
+
latest: check.latest,
|
|
770
|
+
newer: true,
|
|
771
|
+
status: "available",
|
|
772
|
+
command: `usagemax ${commandArgs.join(" ")}`,
|
|
773
|
+
commandWillRun: true,
|
|
774
|
+
};
|
|
642
775
|
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI ${check.latest} is available (current ${VERSION}).\n`);
|
|
643
776
|
if (!json) process.stdout.write(`Launching UsageMax ${check.latest}…\n`);
|
|
644
777
|
return runLatest(handoffArgs);
|
|
645
778
|
}
|
|
646
779
|
const manager = packageManagerFor({ cliPath: process.argv[1], runtime: process.versions, env: process.env });
|
|
647
780
|
const installCommand = manager === "bun" ? "bun add --global usagemax@latest" : "npm install --global usagemax@latest";
|
|
648
|
-
const available = {
|
|
781
|
+
const available = {
|
|
782
|
+
version: VERSION,
|
|
783
|
+
latest: check.latest,
|
|
784
|
+
newer: true,
|
|
785
|
+
status: "available",
|
|
786
|
+
manager,
|
|
787
|
+
installCommand,
|
|
788
|
+
...(commandArgs.length ? { command: `usagemax ${commandArgs.join(" ")}`, commandWillRun: false } : {}),
|
|
789
|
+
};
|
|
649
790
|
if (checkOnly) {
|
|
650
791
|
process.stdout.write(json ? `${JSON.stringify(available)}\n` : `UsageMax CLI ${check.latest} is available (current ${VERSION}). Run \`${installCommand}\`.\n`);
|
|
792
|
+
if (!json && commandArgs.length) process.stdout.write(`Check-only: the requested command was not run. After installing, run \`usagemax ${commandArgs.join(" ")}\`.\n`);
|
|
651
793
|
return;
|
|
652
794
|
}
|
|
653
795
|
if (!json) process.stdout.write(`Updating UsageMax ${VERSION} → ${check.latest} via ${manager}…\n`);
|
|
@@ -714,8 +856,21 @@ async function removeLink(args = []) {
|
|
|
714
856
|
async function main() {
|
|
715
857
|
const args = process.argv.slice(2);
|
|
716
858
|
const command = args[0] || "sync";
|
|
717
|
-
if (["--help", "-h", "help"].includes(command)) return help();
|
|
859
|
+
if (["--help", "-h", "help"].includes(command)) return help(args[1]);
|
|
718
860
|
if (["--version", "-v"].includes(command)) return process.stdout.write(args.includes("--json") ? `${JSON.stringify({ version: VERSION, ccusageVersion: CCUSAGE_VERSION })}\n` : `${VERSION}\n`);
|
|
861
|
+
if (command === "report") return report(args.slice(1));
|
|
862
|
+
const invocationHelp = command === "update"
|
|
863
|
+
? ["--help", "-h"].includes(args[1])
|
|
864
|
+
: args.slice(1).some((arg) => arg === "--help" || arg === "-h");
|
|
865
|
+
if (invocationHelp) {
|
|
866
|
+
const helpCommand = command === "token" && args[1] === "status" ? "token status"
|
|
867
|
+
: command === "telemetry" ? "telemetry test"
|
|
868
|
+
: command === "service" ? `service ${args[1] || "status"}` : command;
|
|
869
|
+
return help(helpCommand);
|
|
870
|
+
}
|
|
871
|
+
if (command === "token" && args[1] !== "status") throw new Error("Use `usagemax token status`; run usagemax --help for all commands.");
|
|
872
|
+
if (command === "telemetry" && args[1] !== "test") throw new Error("Use `usagemax telemetry test`; run usagemax --help for all commands.");
|
|
873
|
+
validateCommandArgs(command === "token" ? "token status" : command === "telemetry" ? "telemetry test" : command, command === "service" ? args.slice(1) : command === "token" || command === "telemetry" ? args.slice(2) : args.slice(1));
|
|
719
874
|
if (command === "update") return update(args.slice(1));
|
|
720
875
|
if (await maybeUpdate(command, args)) return;
|
|
721
876
|
if (command === "service") {
|
|
@@ -738,8 +893,8 @@ async function main() {
|
|
|
738
893
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
739
894
|
return;
|
|
740
895
|
}
|
|
741
|
-
if (command === "report") return report(args.slice(1));
|
|
742
896
|
if (command === "token" && args[1] === "status") return tokenStatus(args.slice(2));
|
|
897
|
+
if (command === "telemetry" && args[1] === "test") return telemetryTest(args.slice(2));
|
|
743
898
|
if (["link", "sync", "status", "doctor", "unlink"].includes(command)) {
|
|
744
899
|
return withConfigLock(configDirectory(), async () => {
|
|
745
900
|
if (command === "link") return link(args.slice(1));
|
package/src/sources.js
CHANGED
|
@@ -3,8 +3,10 @@ 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.
|
|
7
|
-
|
|
6
|
+
export const CCUSAGE_VERSION = "20.0.24";
|
|
7
|
+
// Reconcile retained history when the collector output contract changes, not
|
|
8
|
+
// just when a source path is added. v4 enables explicit per-model breakdowns.
|
|
9
|
+
export const SOURCE_INVENTORY_VERSION = 4;
|
|
8
10
|
export const SUPPORTED_SOURCES = [
|
|
9
11
|
"amp",
|
|
10
12
|
"claude",
|
|
@@ -69,13 +71,24 @@ async function entries(value) {
|
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
async function providerBearingHome(home, pathApi) {
|
|
72
|
-
|
|
74
|
+
// Only inspect known provider roots. Avoid treating generic profile folders
|
|
75
|
+
// (for example .local/share itself) as evidence that a Windows profile is
|
|
76
|
+
// relevant; that would make multi-user WSL selection noisy and unsafe.
|
|
77
|
+
const markers = [
|
|
78
|
+
".claude", ".config/claude", ".codex", ".factory", ".gemini", ".openclaw", ".clawdbot", ".moltbot", ".moldbot", ".hermes", ".grok",
|
|
79
|
+
".kimi", ".kimi-code", ".qwen", ".copilot", ".pi",
|
|
80
|
+
".local/share/amp", ".local/share/goose", ".local/share/Block/goose", ".local/share/kilo", ".local/share/opencode",
|
|
81
|
+
"AppData/Roaming/Block/goose",
|
|
82
|
+
".config/manicode", ".config/manicode-dev", ".config/manicode-staging",
|
|
83
|
+
];
|
|
73
84
|
const checks = await Promise.all(markers.map((marker) => existsDirectory(pathApi.join(home, marker))));
|
|
74
85
|
return checks.some(Boolean);
|
|
75
86
|
}
|
|
76
87
|
|
|
77
|
-
async function
|
|
78
|
-
if (platform !== "linux" || !String(env.WSL_DISTRO_NAME ?? "").trim())
|
|
88
|
+
export async function discoverWslWindowsHomeStatus({ env = process.env, platform = process.platform, pathApi = path } = {}) {
|
|
89
|
+
if (platform !== "linux" || !String(env.WSL_DISTRO_NAME ?? "").trim()) {
|
|
90
|
+
return { status: "not-applicable", candidates: [], selected: [] };
|
|
91
|
+
}
|
|
79
92
|
const usersRoot = String(env.USAGEMAX_WSL_USERS_DIR ?? "/mnt/c/Users").trim();
|
|
80
93
|
const candidates = [];
|
|
81
94
|
for (const entry of await entries(usersRoot)) {
|
|
@@ -86,7 +99,8 @@ async function discoverWslWindowsHomes(env, platform, pathApi) {
|
|
|
86
99
|
// A WSL distro can see every Windows profile. Auto-select only when there is
|
|
87
100
|
// one unambiguous provider-bearing profile; multi-user systems opt in with
|
|
88
101
|
// USAGEMAX_ADDITIONAL_HOME so one employee never absorbs another's usage.
|
|
89
|
-
|
|
102
|
+
const status = candidates.length === 1 ? "discovered" : candidates.length > 1 ? "ambiguous" : "unavailable";
|
|
103
|
+
return { status, candidates, selected: status === "discovered" ? candidates : [] };
|
|
90
104
|
}
|
|
91
105
|
|
|
92
106
|
async function discoverBackupRoots(home, pathApi) {
|
|
@@ -229,15 +243,15 @@ export function sourceDefinitions({ env = process.env, home = ccusageHome(env),
|
|
|
229
243
|
definitions.push(tree("pi", root, "jsonl"));
|
|
230
244
|
}
|
|
231
245
|
|
|
232
|
-
const
|
|
233
|
-
const gooseDatabases =
|
|
234
|
-
?
|
|
246
|
+
const gooseRoots = envList(env, "GOOSE_PATH_ROOT", []);
|
|
247
|
+
const gooseDatabases = gooseRoots.length
|
|
248
|
+
? gooseRoots.map((root) => join(root, "data", "sessions", "sessions.db"))
|
|
235
249
|
: [
|
|
236
250
|
join(home, ".local", "share", "goose", "sessions", "sessions.db"),
|
|
237
251
|
join(home, "Library", "Application Support", "goose", "sessions", "sessions.db"),
|
|
238
252
|
join(home, ".local", "share", "Block", "goose", "sessions", "sessions.db"),
|
|
239
253
|
];
|
|
240
|
-
if (!
|
|
254
|
+
if (!gooseRoots.length && String(env.APPDATA ?? "").trim()) {
|
|
241
255
|
gooseDatabases.push(join(String(env.APPDATA).trim(), "Block", "goose", "data", "sessions", "sessions.db"));
|
|
242
256
|
}
|
|
243
257
|
for (const database of gooseDatabases) definitions.push(...db("goose", database));
|
|
@@ -260,7 +274,9 @@ export function sourceDefinitions({ env = process.env, home = ccusageHome(env),
|
|
|
260
274
|
definitions.push(tree("qwen", join(root, "projects"), "jsonl"));
|
|
261
275
|
}
|
|
262
276
|
|
|
263
|
-
|
|
277
|
+
for (const root of envList(env, "COPILOT_OTEL_DIR", [join(home, ".copilot", "otel")])) {
|
|
278
|
+
definitions.push(tree("copilot", root, "jsonl"));
|
|
279
|
+
}
|
|
264
280
|
if (String(env.COPILOT_OTEL_FILE_EXPORTER_PATH ?? "").trim()) {
|
|
265
281
|
definitions.push(file("copilot", String(env.COPILOT_OTEL_FILE_EXPORTER_PATH).trim()));
|
|
266
282
|
}
|
|
@@ -283,8 +299,29 @@ export async function ccusageEnvironment({ env = process.env, platform = process
|
|
|
283
299
|
const effective = { ...env };
|
|
284
300
|
const home = ccusageHome(effective);
|
|
285
301
|
const configuredHomes = commaList(effective.USAGEMAX_ADDITIONAL_HOME).map((item) => expandTilde(item, home, pathApi));
|
|
286
|
-
const
|
|
302
|
+
const wslDiscovery = await discoverWslWindowsHomeStatus({ env: effective, platform, pathApi });
|
|
303
|
+
const wslHomes = wslDiscovery.selected;
|
|
287
304
|
const additionalHomes = uniquePaths([...configuredHomes, ...wslHomes], pathApi).filter((item) => pathApi.normalize(item) !== pathApi.normalize(home));
|
|
305
|
+
const discoveredWindowsGooseRoots = [];
|
|
306
|
+
if (platform === "linux" && String(effective.WSL_DISTRO_NAME ?? "").trim()) {
|
|
307
|
+
for (const candidate of additionalHomes) {
|
|
308
|
+
const root = pathApi.join(candidate, "AppData", "Roaming", "Block", "goose");
|
|
309
|
+
if (await existsDirectory(root)) discoveredWindowsGooseRoots.push(root);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (discoveredWindowsGooseRoots.length) {
|
|
313
|
+
const explicitRoots = commaList(effective.GOOSE_PATH_ROOT);
|
|
314
|
+
const defaultRoots = [
|
|
315
|
+
pathApi.join(home, ".local", "share", "goose"),
|
|
316
|
+
pathApi.join(home, "Library", "Application Support", "goose"),
|
|
317
|
+
pathApi.join(home, ".local", "share", "Block", "goose"),
|
|
318
|
+
...(String(effective.APPDATA ?? "").trim() ? [pathApi.join(String(effective.APPDATA).trim(), "Block", "goose")] : []),
|
|
319
|
+
];
|
|
320
|
+
effective.GOOSE_PATH_ROOT = uniquePaths([
|
|
321
|
+
...(explicitRoots.length ? explicitRoots : defaultRoots),
|
|
322
|
+
...discoveredWindowsGooseRoots,
|
|
323
|
+
], pathApi).join(",");
|
|
324
|
+
}
|
|
288
325
|
const homes = [home, ...additionalHomes];
|
|
289
326
|
const backups = await Promise.all(homes.map((candidate) => discoverBackupRoots(candidate, pathApi)));
|
|
290
327
|
const nestedClaude = (await Promise.all(homes.map((candidate) => discoverNestedClaudeRoots(candidate, pathApi)))).flat();
|
|
@@ -318,6 +355,7 @@ export async function ccusageEnvironment({ env = process.env, platform = process
|
|
|
318
355
|
setDiscoveredList(effective, "KIMI_DATA_DIR", [pathApi.join(home, ".kimi"), pathApi.join(home, ".kimi-code")], additionalHomes.flatMap((candidate) => [pathApi.join(candidate, ".kimi"), pathApi.join(candidate, ".kimi-code")]), pathApi);
|
|
319
356
|
setDiscoveredList(effective, "QWEN_DATA_DIR", [pathApi.join(home, ".qwen")], additions([".qwen"]), pathApi);
|
|
320
357
|
setDiscoveredList(effective, "GEMINI_DATA_DIR", [pathApi.join(home, ".gemini", "tmp")], additions([".gemini", "tmp"]), pathApi);
|
|
358
|
+
setDiscoveredList(effective, "COPILOT_OTEL_DIR", [pathApi.join(home, ".copilot", "otel")], additions([".copilot", "otel"]), pathApi);
|
|
321
359
|
|
|
322
360
|
effective.USAGEMAX_DISCOVERED_HOMES = homes.join(",");
|
|
323
361
|
|
package/src/transport.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { setTimeout as delay } from "node:timers/promises";
|
|
2
3
|
|
|
3
4
|
export function retryAfterMs(value, now = Date.now()) {
|
|
@@ -15,6 +16,15 @@ const collectorStates = new Set(["active", "revoked", "workspace_disabled", "mem
|
|
|
15
16
|
const deviceBindings = new Set(["unbound", "bound", "matched", "mismatch"]);
|
|
16
17
|
const collectorTokenPattern = /umx_[a-f0-9]{64}/gi;
|
|
17
18
|
|
|
19
|
+
export function collectorCanWrite(view) {
|
|
20
|
+
return view?.httpStatus === 200
|
|
21
|
+
&& view?.status === "active"
|
|
22
|
+
&& view?.credentialType === "collector"
|
|
23
|
+
&& view?.scopeStatus === "valid"
|
|
24
|
+
&& view?.ingestAuthorized === true
|
|
25
|
+
&& view?.deviceBinding !== "mismatch";
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
function safeText(value, secret) {
|
|
19
29
|
let text = value;
|
|
20
30
|
if (secret) text = text.split(secret).join("[redacted]");
|
|
@@ -83,6 +93,72 @@ export async function requestCollectorStatus(endpoint, config, {
|
|
|
83
93
|
return { httpStatus: response.status, body };
|
|
84
94
|
}
|
|
85
95
|
|
|
96
|
+
/** Send one zero-token observability event to prove the authenticated write path. */
|
|
97
|
+
export async function requestTelemetrySmokeTest(endpoint, config, {
|
|
98
|
+
timeout = 15_000,
|
|
99
|
+
fetchImpl = fetch,
|
|
100
|
+
now = Date.now,
|
|
101
|
+
eventId = randomUUID(),
|
|
102
|
+
} = {}) {
|
|
103
|
+
const eventKey = `usagemax-cli-test:${eventId}`;
|
|
104
|
+
const event = {
|
|
105
|
+
eventKey,
|
|
106
|
+
eventType: "agent_state",
|
|
107
|
+
provider: "usagemax-cli",
|
|
108
|
+
model: "usagemax-connectivity-check",
|
|
109
|
+
source: "usagemax-cli",
|
|
110
|
+
agentName: "UsageMax CLI",
|
|
111
|
+
status: "ok",
|
|
112
|
+
state: "connected",
|
|
113
|
+
occurredAt: now(),
|
|
114
|
+
inputTokens: 0,
|
|
115
|
+
outputTokens: 0,
|
|
116
|
+
cacheReadTokens: 0,
|
|
117
|
+
cacheWriteTokens: 0,
|
|
118
|
+
reasoningTokens: 0,
|
|
119
|
+
totalTokens: 0,
|
|
120
|
+
costMicros: 0,
|
|
121
|
+
costBasis: "unknown",
|
|
122
|
+
completeness: "unknown",
|
|
123
|
+
accountingMode: "observability",
|
|
124
|
+
schemaVersion: 1,
|
|
125
|
+
};
|
|
126
|
+
const body = JSON.stringify({ events: [event] });
|
|
127
|
+
let response;
|
|
128
|
+
try {
|
|
129
|
+
response = await fetchImpl(endpoint, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
authorization: `Bearer ${config.token}`,
|
|
133
|
+
"content-type": "application/json",
|
|
134
|
+
"x-usagemax-device-id": config.deviceId,
|
|
135
|
+
"idempotency-key": `usagemax-cli:${config.deviceId}:${eventId}`,
|
|
136
|
+
},
|
|
137
|
+
body,
|
|
138
|
+
cache: "no-store",
|
|
139
|
+
signal: AbortSignal.timeout(timeout),
|
|
140
|
+
});
|
|
141
|
+
} catch {
|
|
142
|
+
return { ok: false, status: "unavailable", httpStatus: null };
|
|
143
|
+
}
|
|
144
|
+
const result = record(await response.json().catch(() => null));
|
|
145
|
+
const ok = response.ok && result?.ok === true;
|
|
146
|
+
return {
|
|
147
|
+
ok,
|
|
148
|
+
status: ok ? "accepted" : response.status === 401 ? "unauthorized"
|
|
149
|
+
: response.status === 403 ? "forbidden"
|
|
150
|
+
: response.status === 409 ? "device_or_idempotency_conflict"
|
|
151
|
+
: response.status === 429 ? "rate_limited"
|
|
152
|
+
: response.status >= 500 ? "unavailable" : "rejected",
|
|
153
|
+
httpStatus: response.status,
|
|
154
|
+
...(Number.isSafeInteger(result?.accepted) ? { accepted: result.accepted } : {}),
|
|
155
|
+
...(Number.isSafeInteger(result?.duplicates) ? { duplicates: result.duplicates } : {}),
|
|
156
|
+
...(typeof result?.replay === "boolean" ? { replay: result.replay } : {}),
|
|
157
|
+
observabilityOnly: true,
|
|
158
|
+
accountingUpdated: false,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
86
162
|
// Only snapshot operations have server receipts. Never automatically replay a
|
|
87
163
|
// one-use link request or apply this policy to arbitrary POST operations.
|
|
88
164
|
export async function requestSnapshot(endpoint, config, operation, payload, {
|
package/src/updates.js
CHANGED
|
@@ -20,6 +20,24 @@ export function compareVersions(left, right) {
|
|
|
20
20
|
return 0;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Parse the read-only and command-handoff options shared by `usagemax update`. */
|
|
24
|
+
export function updateRequest(args = []) {
|
|
25
|
+
const json = args.includes("--json");
|
|
26
|
+
const checkOnly = args.includes("--check") || args.includes("--no-install");
|
|
27
|
+
const commandArgs = args.filter((arg) => !["--check", "--no-install", "--json"].includes(arg));
|
|
28
|
+
return {
|
|
29
|
+
json,
|
|
30
|
+
checkOnly,
|
|
31
|
+
commandArgs,
|
|
32
|
+
handoffArgs: commandArgs.length && !checkOnly ? [...commandArgs, ...(json ? ["--json"] : [])] : [],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A command suffix runs only after a usable npm release was confirmed. */
|
|
37
|
+
export function updateCommandWillRun({ latest, checkOnly, commandArgs = [] }) {
|
|
38
|
+
return Boolean(latest && !checkOnly && commandArgs.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
function cachePath(directory) {
|
|
24
42
|
return join(directory, "update-check.json");
|
|
25
43
|
}
|
|
@@ -76,11 +94,11 @@ export async function checkForUpdate(directory, currentVersion, { force = false,
|
|
|
76
94
|
return { ...result, newer: compareVersions(result.latest, currentVersion) > 0 };
|
|
77
95
|
}
|
|
78
96
|
|
|
79
|
-
function runner() {
|
|
80
|
-
const override =
|
|
97
|
+
function runner({ manager, env = process.env, platform = process.platform }) {
|
|
98
|
+
const override = env.USAGEMAX_PACKAGE_RUNNER?.trim();
|
|
81
99
|
if (override) return { command: override, prefix: [] };
|
|
82
|
-
if (
|
|
83
|
-
return { command: "
|
|
100
|
+
if (manager === "bun") return { command: platform === "win32" ? "bunx.exe" : "bunx", prefix: ["--bun"] };
|
|
101
|
+
return { command: platform === "win32" ? "npm.cmd" : "npm", prefix: ["exec", "--yes"] };
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
/**
|
|
@@ -107,7 +125,9 @@ export async function updateGlobal({ latest = "latest", manager = packageManager
|
|
|
107
125
|
? ["add", "--global", `usagemax@${latest}`]
|
|
108
126
|
: ["install", "--global", `usagemax@${latest}`];
|
|
109
127
|
const child = spawnImpl(command, args, {
|
|
110
|
-
|
|
128
|
+
// Do not leave child output pipes undrained: a JSON-mode package update
|
|
129
|
+
// can otherwise block once Bun/npm fills the operating-system pipe buffer.
|
|
130
|
+
stdio: quiet ? "ignore" : "inherit",
|
|
111
131
|
env: { ...env, NPM_CONFIG_UPDATE_NOTIFIER: "false" },
|
|
112
132
|
});
|
|
113
133
|
return new Promise((resolve, reject) => {
|
|
@@ -121,11 +141,18 @@ export async function updateGlobal({ latest = "latest", manager = packageManager
|
|
|
121
141
|
}
|
|
122
142
|
|
|
123
143
|
/** Re-run a command through the current npm dist-tag without requiring @latest. */
|
|
124
|
-
export async function runLatest(args, {
|
|
125
|
-
|
|
144
|
+
export async function runLatest(args, {
|
|
145
|
+
spawnImpl = spawn,
|
|
146
|
+
cliPath = process.argv[1],
|
|
147
|
+
runtime = process.versions,
|
|
148
|
+
env = process.env,
|
|
149
|
+
platform = process.platform,
|
|
150
|
+
} = {}) {
|
|
151
|
+
const manager = packageManagerFor({ cliPath, runtime, env });
|
|
152
|
+
const selected = runner({ manager, env, platform });
|
|
126
153
|
const child = spawnImpl(selected.command, [...selected.prefix, "usagemax@latest", "--", ...args], {
|
|
127
154
|
stdio: "inherit",
|
|
128
|
-
env: { ...
|
|
155
|
+
env: { ...env, USAGEMAX_UPDATE_HANDOFF: "1" },
|
|
129
156
|
});
|
|
130
157
|
const code = await new Promise((resolve, reject) => {
|
|
131
158
|
child.once("error", reject);
|