usagemax 0.3.8 → 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 +43 -14
- package/package.json +3 -2
- package/src/ccusage.js +21 -0
- package/src/cli.js +201 -26
- package/src/sources.js +50 -12
- package/src/transport.js +76 -0
- package/src/updates.js +71 -7
package/README.md
CHANGED
|
@@ -40,10 +40,14 @@ bunx usagemax sync --dry-run --explain
|
|
|
40
40
|
bunx usagemax sync
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The CLI checks npm's `latest` dist-tag at most twice per day
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
The CLI checks npm's `latest` dist-tag at most twice per day. Run
|
|
44
|
+
`usagemax update` to update a global installation through the package manager
|
|
45
|
+
that launched it (Bun or npm). Add `--check` for a read-only check, or
|
|
46
|
+
`--json` for automation. A command suffix such as `usagemax update sync` hands
|
|
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
|
|
50
|
+
`USAGEMAX_AUTO_UPDATE=1` for an explicit automatic handoff. Use `--no-update-check` or set
|
|
47
51
|
`USAGEMAX_DISABLE_UPDATE_CHECK=1` in offline environments. Interactive
|
|
48
52
|
terminals show a small stderr progress line; JSON,
|
|
49
53
|
quiet, CI, and scheduled runs remain machine-readable and quiet.
|
|
@@ -78,9 +82,13 @@ usagemax status Show local link state
|
|
|
78
82
|
usagemax doctor Check discovered sources
|
|
79
83
|
usagemax report [ccusage args] Run a local ccusage report
|
|
80
84
|
usagemax token status Diagnose a key piped on stdin
|
|
85
|
+
usagemax telemetry test Send one zero-token write-path smoke event
|
|
81
86
|
usagemax service install Opt into periodic OS checkpoints
|
|
82
87
|
usagemax service status|run|uninstall
|
|
83
88
|
usagemax unlink [--revoke] Remove local credentials
|
|
89
|
+
usagemax update Update the global CLI through Bun or npm
|
|
90
|
+
usagemax update --check Check without installing
|
|
91
|
+
usagemax update sync Run sync through the newest CLI release
|
|
84
92
|
```
|
|
85
93
|
|
|
86
94
|
Useful options:
|
|
@@ -90,21 +98,29 @@ bunx usagemax sync --full # all retained local history
|
|
|
90
98
|
bunx usagemax sync --archives # one-time compressed-history recovery
|
|
91
99
|
bunx usagemax sync --restart # restart an expired saved upload
|
|
92
100
|
bunx usagemax status --remote --json # remote check; secret is never printed
|
|
101
|
+
bunx usagemax telemetry test --json # one observability event; no accounting change
|
|
93
102
|
bunx usagemax doctor --deep --json # parse and audit retained history
|
|
94
103
|
bunx usagemax link UMX-… --no-sync # link without uploading yet
|
|
95
104
|
bunx usagemax --version --json # print CLI and ccusage versions
|
|
96
|
-
bunx usagemax update --json # machine-readable
|
|
105
|
+
bunx usagemax update --json # update and return machine-readable status
|
|
106
|
+
bunx usagemax update --check --json # check without installing
|
|
97
107
|
bunx usagemax unlink --json # safe automation result; no secret output
|
|
98
108
|
```
|
|
99
109
|
|
|
100
110
|
## Sources and coverage
|
|
101
111
|
|
|
102
|
-
UsageMax pins [ccusage v20.0.
|
|
112
|
+
UsageMax pins [ccusage v20.0.24](https://github.com/ccusage/ccusage/releases/tag/v20.0.24)
|
|
103
113
|
and supports its 16 adapters: Amp, Claude Code, Codebuff, Codex, GitHub Copilot
|
|
104
114
|
CLI, Factory Droid, Gemini CLI, Goose, Grok Build, Hermes, Kilo Code, Kimi CLI,
|
|
105
115
|
OpenClaw, OpenCode, Pi, and Qwen Code. Named Pi-format stores are discovered as
|
|
106
116
|
well.
|
|
107
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
|
+
|
|
108
124
|
The collector follows the supported provider environment overrides and bounded
|
|
109
125
|
home locations. It recognizes Claude Desktop sessions, `.cc-mirror`, renamed
|
|
110
126
|
Claude/Codex backup folders, and supported Windows homes from WSL. In WSL, use
|
|
@@ -129,14 +145,14 @@ stdin; never pass it as an argument or put it in a URL:
|
|
|
129
145
|
|
|
130
146
|
```bash
|
|
131
147
|
set +x
|
|
148
|
+
set -o pipefail
|
|
132
149
|
printf '%s' "$USAGEMAX_COLLECTOR_TOKEN" \
|
|
133
150
|
| bunx usagemax token status \
|
|
134
|
-
--device-id "$USAGEMAX_INSTALLATION_ID" \
|
|
135
151
|
--json
|
|
136
152
|
```
|
|
137
153
|
|
|
138
|
-
The `token status`
|
|
139
|
-
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
|
|
140
156
|
`node packages/cli/src/cli.js token status` from this repository until the new
|
|
141
157
|
package is published.
|
|
142
158
|
|
|
@@ -148,9 +164,10 @@ For a numeric-only result suitable for a smoke check:
|
|
|
148
164
|
|
|
149
165
|
```bash
|
|
150
166
|
set +x
|
|
167
|
+
set -o pipefail
|
|
151
168
|
printf '%s' "$USAGEMAX_COLLECTOR_TOKEN" \
|
|
152
169
|
| bunx usagemax token status \
|
|
153
|
-
--
|
|
170
|
+
--json \
|
|
154
171
|
| jq -r '[.httpStatus, (if .ingestAuthorized then 1 else 0 end)] | @tsv'
|
|
155
172
|
```
|
|
156
173
|
|
|
@@ -166,10 +183,22 @@ installation does not match. A recognized key missing `telemetry:write` has
|
|
|
166
183
|
`status: scope_missing`, `scopeStatus: missing_telemetry_write`, and
|
|
167
184
|
`ingestAuthorized: false`.
|
|
168
185
|
|
|
169
|
-
For a write-path smoke check,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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.
|
|
173
202
|
|
|
174
203
|
## Privacy and resource use
|
|
175
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, runLatest } 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,18 +146,59 @@ 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");
|
|
133
156
|
process.stdout.write(" usagemax doctor [--deep] [--json] [--quiet|--no-progress]\n");
|
|
134
157
|
process.stdout.write(" Check source coverage; --deep parses full history\n");
|
|
135
|
-
process.stdout.write(" usagemax update
|
|
136
|
-
process.stdout.write(" [--json]
|
|
158
|
+
process.stdout.write(" usagemax update Update the global CLI through Bun or npm\n");
|
|
159
|
+
process.stdout.write(" [--check] [--json] Check only or emit machine-readable status\n");
|
|
160
|
+
process.stdout.write(" usagemax update <command args> Run a command through the latest CLI\n");
|
|
137
161
|
process.stdout.write(" usagemax report [...args] Run a local ccusage report\n");
|
|
138
162
|
process.stdout.write(" usagemax unlink [--revoke] Remove locally; --revoke also disables uploads\n");
|
|
139
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.`);
|
|
140
202
|
}
|
|
141
203
|
|
|
142
204
|
function ccusageCliPath() {
|
|
@@ -144,12 +206,10 @@ function ccusageCliPath() {
|
|
|
144
206
|
}
|
|
145
207
|
|
|
146
208
|
async function ccusageJson(config, { full = false, env } = {}) {
|
|
147
|
-
const args = [ccusageCliPath(),
|
|
148
|
-
args.push("--sections", "daily,session");
|
|
209
|
+
const args = [ccusageCliPath(), ...ccusageDailyArgs(config, { full })];
|
|
149
210
|
// Reconcile yesterday once after the UTC date changes. All other incremental
|
|
150
211
|
// scans parse only today; a metadata fingerprint avoids invoking ccusage when
|
|
151
212
|
// no supported local source changed at all.
|
|
152
|
-
args.push(...reportDateArgs(config, { full }));
|
|
153
213
|
const { stdout } = await executeFile(process.execPath, args, {
|
|
154
214
|
encoding: "utf8",
|
|
155
215
|
maxBuffer: MAX_REPORT_BYTES,
|
|
@@ -431,6 +491,7 @@ function printRemoteStatus(view) {
|
|
|
431
491
|
if (view.credentialType) process.stdout.write(`Type: ${view.credentialType}${view.writeOnly ? "; write-only" : ""}\n`);
|
|
432
492
|
if (view.scopes?.length) process.stdout.write(`Scopes: ${view.scopes.join(", ")}\n`);
|
|
433
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`);
|
|
434
495
|
if (view.deviceBinding) process.stdout.write(`Device binding: ${view.deviceBinding}\n`);
|
|
435
496
|
if (view.profileHandle) process.stdout.write(`Profile: @${view.profileHandle}\n`);
|
|
436
497
|
if (view.deviceName) process.stdout.write(`Computer: ${view.deviceName}\n`);
|
|
@@ -449,9 +510,11 @@ async function status(args = []) {
|
|
|
449
510
|
if (!config) {
|
|
450
511
|
if (json) {
|
|
451
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;
|
|
452
514
|
return;
|
|
453
515
|
}
|
|
454
516
|
process.stdout.write("Not linked. Open https://usagemax.com/account to connect this computer.\n");
|
|
517
|
+
if (remote) process.exitCode = 1;
|
|
455
518
|
return;
|
|
456
519
|
}
|
|
457
520
|
const stableId = await stableInstallationId(configDirectory(), config.deviceId);
|
|
@@ -488,6 +551,7 @@ async function status(args = []) {
|
|
|
488
551
|
}
|
|
489
552
|
if (json) {
|
|
490
553
|
process.stdout.write(`${JSON.stringify({ ...local, remote: remoteView })}\n`);
|
|
554
|
+
if (!collectorCanWrite(remoteView)) process.exitCode = 1;
|
|
491
555
|
return;
|
|
492
556
|
}
|
|
493
557
|
}
|
|
@@ -504,6 +568,7 @@ async function status(args = []) {
|
|
|
504
568
|
if (config.pendingSync) process.stdout.write(`Pending sync: ${config.pendingSync.runId}; rerun sync to resume\n`);
|
|
505
569
|
process.stdout.write(`Profile: ${config.profileUrl || "https://usagemax.com/account"}\n`);
|
|
506
570
|
if (remote) printRemoteStatus(remoteView);
|
|
571
|
+
if (remote && !collectorCanWrite(remoteView)) process.exitCode = 1;
|
|
507
572
|
}
|
|
508
573
|
|
|
509
574
|
async function readTokenFromStdin() {
|
|
@@ -524,6 +589,8 @@ async function readTokenFromStdin() {
|
|
|
524
589
|
async function tokenStatus(args = []) {
|
|
525
590
|
const requestedDeviceId = option(args, "--device-id");
|
|
526
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;
|
|
527
594
|
const json = args.includes("--json");
|
|
528
595
|
const progress = createProgress({ json, quiet: args.includes("--quiet"), noProgress: args.includes("--no-progress") });
|
|
529
596
|
progress.start("Checking the collector credential…");
|
|
@@ -538,7 +605,7 @@ async function tokenStatus(args = []) {
|
|
|
538
605
|
const endpoint = validHttpsUrl(configuredEndpoint, { allowLocalhost: true });
|
|
539
606
|
try {
|
|
540
607
|
if (!endpoint) throw new Error("USAGEMAX_STATUS_ENDPOINT must use HTTPS, except for localhost development.");
|
|
541
|
-
const result = await requestCollectorStatus(endpoint, { token, deviceId
|
|
608
|
+
const result = await requestCollectorStatus(endpoint, { token, deviceId });
|
|
542
609
|
const view = collectorStatusView(result.httpStatus, result.body, token);
|
|
543
610
|
progress.succeed("Collector credential checked.");
|
|
544
611
|
if (json) process.stdout.write(`${JSON.stringify(view)}\n`);
|
|
@@ -546,6 +613,39 @@ async function tokenStatus(args = []) {
|
|
|
546
613
|
process.stdout.write("Credential format: valid (umx_ + 64 lowercase hexadecimal characters)\n");
|
|
547
614
|
printRemoteStatus(view);
|
|
548
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;
|
|
549
649
|
} catch (error) {
|
|
550
650
|
progress.stop();
|
|
551
651
|
throw error;
|
|
@@ -560,6 +660,7 @@ async function doctor(args = []) {
|
|
|
560
660
|
const config = await readConfig();
|
|
561
661
|
const env = await ccusageEnvironment();
|
|
562
662
|
const inventory = await sourceInventory({ env, home: ccusageHome(env) });
|
|
663
|
+
const windowsHomeDiscovery = await discoverWslWindowsHomeStatus();
|
|
563
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"}.`);
|
|
564
665
|
const archives = await discoverProviderArchives({ env, home: ccusageHome(env) });
|
|
565
666
|
const homes = String(env.USAGEMAX_DISCOVERED_HOMES || ccusageHome(env)).split(",").filter(Boolean);
|
|
@@ -569,6 +670,7 @@ async function doctor(args = []) {
|
|
|
569
670
|
configPath: configPath(),
|
|
570
671
|
linked: Boolean(config),
|
|
571
672
|
homes,
|
|
673
|
+
windowsHomeDiscovery,
|
|
572
674
|
detectedSources: inventory.sources,
|
|
573
675
|
files: inventory.files,
|
|
574
676
|
inventoryComplete: inventory.complete,
|
|
@@ -595,7 +697,16 @@ async function doctor(args = []) {
|
|
|
595
697
|
process.stdout.write(`Detected sources: ${inventory.sources.join(", ") || "none"} (${inventory.files}${inventory.truncated ? "+" : ""} data files)\n`);
|
|
596
698
|
process.stdout.write(`Supported sources: ${SUPPORTED_SOURCES.join(", ")} (+ named pi-format stores)\n`);
|
|
597
699
|
if (platform() === "linux" && process.env.WSL_DISTRO_NAME) {
|
|
598
|
-
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
|
+
}
|
|
599
710
|
}
|
|
600
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`);
|
|
601
712
|
if (!inventory.complete) process.stdout.write(`Inventory: incomplete (${inventory.errors} read error(s)${inventory.truncated ? ", file limit reached" : ""}); no-change shortcut disabled\n`);
|
|
@@ -621,27 +732,78 @@ async function report(args) {
|
|
|
621
732
|
}
|
|
622
733
|
|
|
623
734
|
async function update(args = []) {
|
|
624
|
-
const json = args
|
|
735
|
+
const { json, checkOnly, commandArgs, handoffArgs } = updateRequest(args);
|
|
625
736
|
const check = await checkForUpdate(configDirectory(), VERSION, { force: true });
|
|
737
|
+
const commandWillRun = updateCommandWillRun({ latest: check.latest, checkOnly, commandArgs });
|
|
626
738
|
if (!check.latest) {
|
|
627
|
-
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
|
+
};
|
|
628
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`);
|
|
629
748
|
return;
|
|
630
749
|
}
|
|
631
750
|
if (!check.newer) {
|
|
632
|
-
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
|
+
};
|
|
633
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`);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (handoffArgs.length) {
|
|
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
|
+
};
|
|
775
|
+
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI ${check.latest} is available (current ${VERSION}).\n`);
|
|
776
|
+
if (!json) process.stdout.write(`Launching UsageMax ${check.latest}…\n`);
|
|
777
|
+
return runLatest(handoffArgs);
|
|
778
|
+
}
|
|
779
|
+
const manager = packageManagerFor({ cliPath: process.argv[1], runtime: process.versions, env: process.env });
|
|
780
|
+
const installCommand = manager === "bun" ? "bun add --global usagemax@latest" : "npm install --global usagemax@latest";
|
|
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
|
+
};
|
|
790
|
+
if (checkOnly) {
|
|
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`);
|
|
634
793
|
return;
|
|
635
794
|
}
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
795
|
+
if (!json) process.stdout.write(`Updating UsageMax ${VERSION} → ${check.latest} via ${manager}…\n`);
|
|
796
|
+
try {
|
|
797
|
+
await updateGlobal({ latest: check.latest, manager, quiet: json });
|
|
798
|
+
} catch (error) {
|
|
799
|
+
const result = { ...available, status: "failed", error: error instanceof Error ? error.message : String(error) };
|
|
800
|
+
if (json) process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
801
|
+
else process.stderr.write(`UsageMax update failed: ${result.error}\n`);
|
|
802
|
+
process.exitCode = 1;
|
|
641
803
|
return;
|
|
642
804
|
}
|
|
643
|
-
|
|
644
|
-
|
|
805
|
+
const result = { ...available, status: "updated", installedVersion: check.latest };
|
|
806
|
+
process.stdout.write(json ? `${JSON.stringify(result)}\n` : `UsageMax CLI is now ${check.latest}.\n`);
|
|
645
807
|
}
|
|
646
808
|
|
|
647
809
|
async function maybeUpdate(command, args) {
|
|
@@ -694,8 +856,21 @@ async function removeLink(args = []) {
|
|
|
694
856
|
async function main() {
|
|
695
857
|
const args = process.argv.slice(2);
|
|
696
858
|
const command = args[0] || "sync";
|
|
697
|
-
if (["--help", "-h", "help"].includes(command)) return help();
|
|
859
|
+
if (["--help", "-h", "help"].includes(command)) return help(args[1]);
|
|
698
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));
|
|
699
874
|
if (command === "update") return update(args.slice(1));
|
|
700
875
|
if (await maybeUpdate(command, args)) return;
|
|
701
876
|
if (command === "service") {
|
|
@@ -718,8 +893,8 @@ async function main() {
|
|
|
718
893
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
719
894
|
return;
|
|
720
895
|
}
|
|
721
|
-
if (command === "report") return report(args.slice(1));
|
|
722
896
|
if (command === "token" && args[1] === "status") return tokenStatus(args.slice(2));
|
|
897
|
+
if (command === "telemetry" && args[1] === "test") return telemetryTest(args.slice(2));
|
|
723
898
|
if (["link", "sync", "status", "doctor", "unlink"].includes(command)) {
|
|
724
899
|
return withConfigLock(configDirectory(), async () => {
|
|
725
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,19 +94,65 @@ 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"] };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Pick the package manager that owns the current invocation. Bun's global and
|
|
106
|
+
* ephemeral package paths are explicit; npm remains the safe default for a
|
|
107
|
+
* normal Node installation. The override is useful for managed environments
|
|
108
|
+
* where the executable is wrapped by a fleet-specific package manager.
|
|
109
|
+
*/
|
|
110
|
+
export function packageManagerFor({ cliPath = process.argv[1], runtime = process.versions, env = process.env } = {}) {
|
|
111
|
+
const override = String(env.USAGEMAX_PACKAGE_MANAGER || "").trim().toLowerCase();
|
|
112
|
+
if (override === "bun" || override === "npm") return override;
|
|
113
|
+
const normalizedPath = String(cliPath || "").replaceAll("\\", "/");
|
|
114
|
+
if (normalizedPath.includes("/.bun/install/") || runtime?.bun) return "bun";
|
|
115
|
+
if (env.npm_execpath || String(env.npm_config_user_agent || "").startsWith("npm/")) return "npm";
|
|
116
|
+
return "npm";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Install the current public release into the user's global package scope. */
|
|
120
|
+
export async function updateGlobal({ latest = "latest", manager = packageManagerFor(), spawnImpl = spawn, env = process.env, quiet = false, platform = process.platform } = {}) {
|
|
121
|
+
if (manager !== "bun" && manager !== "npm") throw new Error(`Unsupported package manager: ${manager}`);
|
|
122
|
+
if (!/^\d+\.\d+\.\d+$/.test(String(latest)) && latest !== "latest") throw new Error("Invalid UsageMax release version.");
|
|
123
|
+
const command = manager === "bun" ? (platform === "win32" ? "bun.exe" : "bun") : (platform === "win32" ? "npm.cmd" : "npm");
|
|
124
|
+
const args = manager === "bun"
|
|
125
|
+
? ["add", "--global", `usagemax@${latest}`]
|
|
126
|
+
: ["install", "--global", `usagemax@${latest}`];
|
|
127
|
+
const child = spawnImpl(command, args, {
|
|
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",
|
|
131
|
+
env: { ...env, NPM_CONFIG_UPDATE_NOTIFIER: "false" },
|
|
132
|
+
});
|
|
133
|
+
return new Promise((resolve, reject) => {
|
|
134
|
+
child.once("error", reject);
|
|
135
|
+
child.once("exit", (status) => {
|
|
136
|
+
const code = status ?? 1;
|
|
137
|
+
if (code === 0) resolve({ manager, command, args });
|
|
138
|
+
else reject(Object.assign(new Error(`${command} ${args.join(" ")} exited with code ${code}.`), { code: "USAGEMAX_UPDATE_FAILED", exitCode: code }));
|
|
139
|
+
});
|
|
140
|
+
});
|
|
84
141
|
}
|
|
85
142
|
|
|
86
143
|
/** Re-run a command through the current npm dist-tag without requiring @latest. */
|
|
87
|
-
export async function runLatest(args, {
|
|
88
|
-
|
|
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 });
|
|
89
153
|
const child = spawnImpl(selected.command, [...selected.prefix, "usagemax@latest", "--", ...args], {
|
|
90
154
|
stdio: "inherit",
|
|
91
|
-
env: { ...
|
|
155
|
+
env: { ...env, USAGEMAX_UPDATE_HANDOFF: "1" },
|
|
92
156
|
});
|
|
93
157
|
const code = await new Promise((resolve, reject) => {
|
|
94
158
|
child.once("error", reject);
|