@v1nvn/zai 0.35.1 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,18 @@
1
1
  # @v1nvn/zai
2
2
 
3
- The CLI behind the zai plugin: it reports GLM Coding Plan quota and usage for
4
- the current account, straight to the terminal — no model tokens, no browser.
3
+ The home of the zai plugin — and its plugin root: the `zai-usage` CLI in `src/`,
4
+ the mod's hooks module at `hooks/register.tsx` (`.ts`/`.tsx` the engine loads
5
+ directly, no build), engine tests at `tests/`. It reports GLM Coding Plan quota
6
+ and usage for the current account — models, tools, limits — straight to the
7
+ terminal; no browser, nothing written.
5
8
 
6
9
  User-facing docs: [root README](../../README.md).
7
10
 
8
11
  ## Quickstart
9
12
 
10
- In Claude Code, the plugin is the way in — `/zai:usage` runs this CLI through
11
- a `UserPromptExpansion` hook with zero model tokens:
13
+ In Claude Code, the plugin is the way in — a mod serves the one surface the
14
+ model never reads: `/zai-usage` execs the shipped CLI and opens its report as
15
+ a pane (nothing execs until the command runs).
12
16
 
13
17
  ```sh
14
18
  claude plugin marketplace add v1nvn/agentic
@@ -18,17 +22,18 @@ claude plugin install zai@agentic
18
22
  In a terminal, bare:
19
23
 
20
24
  ```sh
21
- npx -y @v1nvn/zai@0.35.1
25
+ npx -y @v1nvn/zai@0.36.1
22
26
  ```
23
27
 
24
28
  ## Usage
25
29
 
26
30
  | Invocation | Does |
27
31
  |---|---|
28
- | `npx -y @v1nvn/zai@0.35.1` | usage report — models, quota window, remaining balance |
29
- | `npx -y @v1nvn/zai@0.35.1 --auth-token TOKEN` | same, key on the command line (visible in `ps`) |
30
- | `npx -y @v1nvn/zai@0.35.1 --auth-token=TOKEN` | `=` form — zsh quoting-safe |
31
- | `npx -y @v1nvn/zai@0.35.1 --base-url URL` | another GLM endpoint (default `api.z.ai`) |
32
+ | `/zai-usage` | the account's usage report, as a pane |
33
+ | `npx -y @v1nvn/zai@0.36.1` | the same report, printed bare |
34
+ | `npx -y @v1nvn/zai@0.36.1 --auth-token TOKEN` | same, key on the command line (visible in `ps`) |
35
+ | `npx -y @v1nvn/zai@0.36.1 --auth-token=TOKEN` | `=` form — zsh quoting-safe |
36
+ | `npx -y @v1nvn/zai@0.36.1 --base-url URL` | another GLM endpoint (default `api.z.ai`) |
32
37
 
33
38
  Each setting takes the first source that provides it:
34
39
 
@@ -42,8 +47,8 @@ Each setting takes the first source that provides it:
42
47
  to a GLM Coding Plan. Claude Code routed elsewhere (plain Anthropic, another
43
48
  proxy) is not a zai configuration; set `ZAI_AUTH_TOKEN`. Bigmodel accounts
44
49
  point the base URL at their host; the monitor paths are identical
45
- (`ZAI_BASE_URL=https://open.bigmodel.cn`). The hook reads the same env from
46
- the Claude Code process; flags are a CLI affordance — `hooks.json` is static.
50
+ (`ZAI_BASE_URL=https://open.bigmodel.cn`). The mod's exec runs inside the
51
+ session's shell, so it inherits the same environment `npx zai-usage` reads.
47
52
 
48
53
  The API labels every bucket in Beijing time (UTC+8); the report shifts each
49
54
  timestamp to your local zone for display.
@@ -51,22 +56,34 @@ timestamp to your local zone for display.
51
56
  ## Develop
52
57
 
53
58
  ```sh
54
- yarn workspace @v1nvn/zai build
55
- yarn workspace @v1nvn/zai test
56
- yarn lint && yarn typecheck # from the repo root
59
+ yarn workspace @v1nvn/zai test # the CLI side (vitest)
60
+ claude plugin validate packages/zai # the mod, as the engine reads it
61
+ claude plugin test packages/zai # the mod, through the engine
62
+ yarn lint && yarn typecheck # from the repo root (typecheck
63
+ # covers tsconfig.mods.json too)
57
64
  ```
58
65
 
66
+ The mod API is early access and moves between Claude Code releases — a build
67
+ that refuses the module loads nothing, so validate after every engine update
68
+ and re-vendor `types/claude-code.d.ts` (repo root) from the engine-laid
69
+ `.claude-plugin/types/` when it changes. Read `references/mods.md` first.
70
+
59
71
  ## Modules
60
72
 
61
73
  | File | Role |
62
74
  |---|---|
63
75
  | `src/index.ts` | bin entry (`zai-usage`) — dispatch, exit codes |
64
- | `src/usage.ts` | the GLM API call and the report it builds |
76
+ | `src/usage.ts` | the GLM API call and the report input it assembles |
65
77
  | `src/resolve.ts` | auth-token resolution chain |
66
- | `src/format.ts` | the printed table |
78
+ | `src/format.ts` | the report's lines — printed by the CLI, drawn by the pane |
79
+ | `hooks/register.tsx` | the mod: registers `/zai-usage`, execs the CLI, opens its report as a pane |
80
+ | `bin/usage.mjs` | the standalone build the mod execs — committed, synced by `yarn build` |
67
81
 
68
82
  ## Contracts
69
83
 
70
84
  - One job: print the report, exit 0. Nothing is written anywhere.
71
- - The hook path and the terminal path run the same bin — the hook only
72
- feeds it the session's transcript context.
85
+ - `/zai-usage` is one `node <plugin root>/bin/usage.mjs --json` exec through
86
+ the session's Bash tool — the same query `npx zai-usage` runs, awaited
87
+ as-is. The one allow rule it needs is
88
+ `Bash(node <plugin root>/bin/usage.mjs --json)`; the first run asks for it
89
+ once, then it is remembered. Its report is a pane the model never reads.
package/dist/index.js CHANGED
@@ -1,42 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command, Option } from "commander";
3
3
  //#region ../core/dist/index.js
4
- function readAll(stream) {
5
- return new Promise((resolve, reject) => {
6
- let data = "";
7
- stream.setEncoding("utf8");
8
- stream.on("data", (chunk) => {
9
- data += chunk;
10
- });
11
- stream.on("end", () => {
12
- resolve(data);
13
- });
14
- stream.on("error", reject);
15
- });
16
- }
17
- /**
18
- * Read the Claude Code hook event JSON from stdin; empty or unparseable input
19
- * yields an empty event, never a thrown error — a hook must always answer.
20
- */
21
- function readHookEvent(stream = process.stdin) {
22
- return readAll(stream).then((data) => {
23
- try {
24
- return JSON.parse(data);
25
- } catch {
26
- return {};
27
- }
28
- }, () => ({}));
29
- }
30
- /**
31
- * Print the UserPromptExpansion decision for a zero-token command: `block`
32
- * keeps the command from reaching the model, with the output as the `reason`.
33
- */
34
- function emitHookBlock(reason) {
35
- process.stdout.write(`${JSON.stringify({
36
- decision: "block",
37
- reason
38
- })}\n`);
39
- }
40
4
  function parseQuietly(program, args, recover) {
41
5
  try {
42
6
  program.allowExcessArguments(false).exitOverride().configureOutput({
@@ -52,90 +16,8 @@ function printUsageAndExit(program) {
52
16
  console.error(program.helpInformation());
53
17
  process.exit(1);
54
18
  }
55
- async function hookOrPrint(hook, failure, run, stdin = process.stdin) {
56
- if (hook) {
57
- let reason;
58
- try {
59
- reason = await run(await readHookEvent(stdin));
60
- } catch (e) {
61
- reason = `${failure}: ${e.message}`;
62
- }
63
- emitHookBlock(reason);
64
- return;
65
- }
66
- try {
67
- console.log(await run(void 0));
68
- } catch (e) {
69
- console.error(e.message);
70
- process.exit(1);
71
- }
72
- }
73
19
  Number.MAX_SAFE_INTEGER;
74
20
  //#endregion
75
- //#region src/resolve.ts
76
- var DEFAULT_BASE_URL = "https://api.z.ai";
77
- var GLM_HOSTS = /* @__PURE__ */ new Set([
78
- "api.z.ai",
79
- "dev.bigmodel.cn",
80
- "open.bigmodel.cn"
81
- ]);
82
- function fromEnv(value) {
83
- return value ? value : void 0;
84
- }
85
- function originOf(value) {
86
- try {
87
- const url = new URL(value);
88
- return url.protocol === "https:" || url.protocol === "http:" ? url.origin : void 0;
89
- } catch {
90
- return;
91
- }
92
- }
93
- function isGlm(origin) {
94
- return GLM_HOSTS.has(new URL(origin).hostname);
95
- }
96
- function resolveBaseUrl(flag, env) {
97
- if (flag !== void 0) {
98
- const origin = originOf(flag);
99
- if (origin === void 0) throw new Error(`invalid base URL: ${flag}`);
100
- return {
101
- glm: isGlm(origin),
102
- origin
103
- };
104
- }
105
- const inherited = fromEnv(env.ANTHROPIC_BASE_URL);
106
- const origin = inherited === void 0 ? void 0 : originOf(inherited);
107
- if (origin !== void 0 && isGlm(origin)) return {
108
- glm: true,
109
- origin
110
- };
111
- return {
112
- glm: false,
113
- origin: DEFAULT_BASE_URL
114
- };
115
- }
116
- function buildProgram() {
117
- return new Command().name("zai-usage").addOption(new Option("--auth-token <token>", "API key").env("ZAI_AUTH_TOKEN")).addOption(new Option("--base-url <url>", "base URL").env("ZAI_BASE_URL")).option("--hook", "emit a UserPromptExpansion block instead of printing");
118
- }
119
- function parseArgs(args) {
120
- const program = parseQuietly(buildProgram(), args);
121
- if (program === void 0) return;
122
- const { authToken, baseUrl, hook } = program.opts();
123
- return {
124
- authToken: authToken || void 0,
125
- baseUrl: baseUrl || void 0,
126
- hook: hook ?? false
127
- };
128
- }
129
- function resolveConfig(env, parsed) {
130
- const base = resolveBaseUrl(parsed.baseUrl, env);
131
- const token = parsed.authToken ?? (base.glm ? fromEnv(env.ANTHROPIC_AUTH_TOKEN) : void 0);
132
- if (token === void 0) throw new Error(env.ANTHROPIC_AUTH_TOKEN ? "ANTHROPIC_AUTH_TOKEN is set, but the resolved base URL does not name a GLM host, so it is not a GLM Coding Plan token — set ZAI_AUTH_TOKEN (or --auth-token)" : "no API key: set ZAI_AUTH_TOKEN (or --auth-token)");
133
- return {
134
- token,
135
- url: base.origin
136
- };
137
- }
138
- //#endregion
139
21
  //#region ../tokens/dist/text.js
140
22
  /**
141
23
  * Plain-text rendering primitives for the fixed-width reports. Output targets
@@ -213,13 +95,12 @@ function meter(pct, width) {
213
95
  //#endregion
214
96
  //#region src/format.ts
215
97
  /**
216
- * Plain-text usage report renderer.
217
- *
218
- * Rendered for a monospace terminal / hook-block `reason`, so it must NOT rely on
219
- * markdown (bold, tables, fenced code). Alignment comes from fixed-width columns
220
- * and unicode block glyphs. Input is the parsed `data` of the three ZAI/ZHIPU
221
- * monitor endpoints (model-usage, tool-usage, quota/limit — the last already
222
- * passed through processQuotaLimit).
98
+ * Usage-report line model: `reportLines` lays the report out once as ink-tagged
99
+ * segments (the pane draws those), and `render` joins them into the CLI's
100
+ * monospace terminal / hook-block `reason` bytes — no markdown; alignment comes
101
+ * from fixed-width columns and unicode block glyphs. Input is the parsed `data`
102
+ * of the three ZAI/ZHIPU monitor endpoints (model-usage, tool-usage,
103
+ * quota/limit — the last already passed through processQuotaLimit).
223
104
  */
224
105
  var VBLOCKS = "▁▂▃▄▅▆▇";
225
106
  function parseSlot(s) {
@@ -392,7 +273,22 @@ function hourlyVerticalChart({ x, tok, nh, maxTok, peakIdx, slotFn }) {
392
273
  if (mark.some((c) => c !== " ")) lines.push(mark.join("").trimEnd());
393
274
  return lines;
394
275
  }
395
- function render({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetMin, now = /* @__PURE__ */ new Date() }) {
276
+ function plain(text) {
277
+ return { text };
278
+ }
279
+ function dim(text) {
280
+ return {
281
+ text,
282
+ ink: "dim"
283
+ };
284
+ }
285
+ function bold(text) {
286
+ return {
287
+ text,
288
+ ink: "bold"
289
+ };
290
+ }
291
+ function reportLines({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetMin, now = /* @__PURE__ */ new Date() }) {
396
292
  const out = [];
397
293
  const toOffset = localOffsetMin != null ? localOffsetMin : -(/* @__PURE__ */ new Date()).getTimezoneOffset();
398
294
  function convertSlot(s) {
@@ -419,14 +315,16 @@ function render({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetM
419
315
  const firstSlot = convertSlot(x[0]);
420
316
  const lastSlot = convertSlot(x[nh - 1]);
421
317
  const win = firstSlot && lastSlot ? `${firstSlot.day} ${firstSlot.time} → ${lastSlot.day} ${lastSlot.time} · ${nh}h` : platform;
422
- out.push(rule());
423
- out.push(left + padL(win, 68 - left.length));
424
- out.push(rule());
425
- let lead = ` ${fmtTokens(total)} tokens across ${fmtNum(totalCalls)} model calls`;
426
- if (peakIdx >= 0 && peakSlot) lead += ` — ${Math.round(pctPeak)}% of it in a single hour (${peakSlot.day} ${peakSlot.time}, ${fmtTokens(peakTok)} tokens / ${fmtNum(peakCalls)} calls)`;
427
- lead += ".";
428
- out.push("");
429
- out.push(lead);
318
+ out.push([plain(rule())]);
319
+ out.push([bold(left), dim(padL(win, 68 - left.length))]);
320
+ out.push([plain(rule())]);
321
+ const head = plain(` ${fmtTokens(total)} tokens across ${fmtNum(totalCalls)} model calls`);
322
+ out.push([]);
323
+ if (peakIdx >= 0 && peakSlot) {
324
+ out.push([head, plain(" — ")]);
325
+ out.push([bold(`${Math.round(pctPeak)}%`), plain(" of it in a single hour")]);
326
+ out.push([plain(` (${peakSlot.day} ${peakSlot.time}, ${fmtTokens(peakTok)} tokens / ${fmtNum(peakCalls)} calls).`)]);
327
+ } else out.push([head, plain(".")]);
430
328
  const activeHours = tok.filter((t) => t > 0).length;
431
329
  const longest = idleRuns(tok).filter((r) => r.len >= 2).sort((a, b) => b.len - a.len).at(0);
432
330
  const tt = tool.totalUsage ?? {};
@@ -442,13 +340,17 @@ function render({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetM
442
340
  }
443
341
  const peakWinPct = total > 0 ? peakWinTokens / total * 100 : 0;
444
342
  const peakWin = localPeakWindow(apiOffsetMin, toOffset);
445
- out.push("");
446
- out.push(` Peak ${padR(peakSlot ? peakSlot.day + " " + peakSlot.time : "—", 15)}${padL(fmtTokens(peakTok), 7)} tokens · ${padL(fmtNum(peakCalls), 5)} calls`);
447
- out.push(` Active ${padR(`${activeHours} / ${nh} hours`, 15)}${longest ? idleLabel(longest, x, convertSlot) : "no idle gaps"}`);
448
- out.push(` Tools ${padR(`${toolTotal} calls`, 15)}${searchN} searches · ${readN} reads${zreadN ? ` · ${zreadN} zread` : ""}`);
449
- if (peakWin) out.push(` Peak hrs Mon–Fri ${peakWin.start}–${peakWin.end} · GLM-5.2 3× · ${peakWinActive}h active · ${fmtTokens(peakWinTokens)} (${Math.round(peakWinPct)}%)`);
450
- out.push("");
451
- out.push(" Hourly tokens · ↑ peak hour " + "─".repeat(Math.max(0, 39)));
343
+ out.push([]);
344
+ out.push([plain(` Peak ${padR(peakSlot ? peakSlot.day + " " + peakSlot.time : "—", 15)}${padL(fmtTokens(peakTok), 7)} tokens · ${padL(fmtNum(peakCalls), 5)} calls`)]);
345
+ out.push([plain(` Active ${padR(`${activeHours} / ${nh} hours`, 15)}${longest ? idleLabel(longest, x, convertSlot) : "no idle gaps"}`)]);
346
+ out.push([plain(` Tools ${padR(`${toolTotal} calls`, 15)}${searchN} searches · ${readN} reads${zreadN ? ` · ${zreadN} zread` : ""}`)]);
347
+ if (peakWin) out.push([
348
+ plain(` Peak hrs Mon–Fri ${peakWin.start}–${peakWin.end} · GLM-5.2 3× · ${peakWinActive}h active · ${fmtTokens(peakWinTokens)} (`),
349
+ bold(`${Math.round(peakWinPct)}%`),
350
+ plain(")")
351
+ ]);
352
+ out.push([]);
353
+ out.push([dim(" Hourly tokens · ↑ peak hour " + "─".repeat(Math.max(0, 39)))]);
452
354
  const chartLines = hourlyVerticalChart({
453
355
  x,
454
356
  tok,
@@ -458,48 +360,126 @@ function render({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetM
458
360
  slotFn: convertSlot
459
361
  });
460
362
  if (chartLines) {
461
- out.push(...chartLines);
462
- if (peakIdx >= 0 && peakSlot) out.push(` ◂ peak ${peakSlot.day} ${peakSlot.time} ${fmtTokens(peakTok)} tokens · ${fmtNum(peakCalls)} calls`);
463
- } else out.push(" (not enough hourly data to chart)");
464
- out.push("");
465
- out.push(" Model mix " + "─".repeat(Math.max(0, 57)));
363
+ out.push(...chartLines.map((l) => [plain(l)]));
364
+ if (peakIdx >= 0 && peakSlot) out.push([plain(` ◂ peak ${peakSlot.day} ${peakSlot.time} ${fmtTokens(peakTok)} tokens · ${fmtNum(peakCalls)} calls`)]);
365
+ } else out.push([plain(" (not enough hourly data to chart)")]);
366
+ out.push([]);
367
+ out.push([dim(" Model mix " + "─".repeat(Math.max(0, 57)))]);
466
368
  const mixSrc = model.modelSummaryList ?? model.modelDataList ?? [];
467
369
  const denom = mixSrc.reduce((a, m) => a + (m.totalTokens ?? 0), 0) || 1;
468
370
  const mixSorted = [...mixSrc].sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
469
371
  for (const m of mixSorted) {
470
372
  const pct = (m.totalTokens ?? 0) / denom * 100;
471
- out.push(` ${padR(m.modelName || "?", 11)}${padL(fmtTokens(m.totalTokens), 8)} ${padL(pct.toFixed(1) + "%", 6)} ${barField(pct, 100, 20)}`);
373
+ out.push([
374
+ plain(` ${padR(m.modelName || "?", 11)}${padL(fmtTokens(m.totalTokens), 8)} `),
375
+ bold(padL(pct.toFixed(1) + "%", 6)),
376
+ plain(` ${barField(pct, 100, 20)}`)
377
+ ]);
472
378
  }
473
- out.push("");
474
- out.push(" Limits " + "─".repeat(Math.max(0, 60)));
379
+ out.push([]);
380
+ out.push([dim(" Limits " + "─".repeat(Math.max(0, 60)))]);
475
381
  const bj = new Date(now.getTime() + apiOffsetMin * 6e4);
476
382
  const bjDay = bj.getUTCDay();
477
383
  const elapsedMin = bjDay >= 1 && bjDay <= 5 ? Math.max(0, Math.min(240, (bj.getUTCHours() - 14) * 60 + bj.getUTCMinutes())) : 0;
478
- if (peakWin) out.push(` ${padR("Peak", 16)}${padL(peakWin.start, 5)} ${meter(Math.round(elapsedMin / 240 * 100), 22)} ${peakWin.end}`);
384
+ if (peakWin) out.push([dim(` ${padR("Peak", 16)}`), plain(`${padL(peakWin.start, 5)} ${meter(Math.round(elapsedMin / 240 * 100), 22)} ${peakWin.end}`)]);
479
385
  const limits = quota.limits ?? [];
480
386
  const mcp = limits.find((l) => /mcp/i.test(l.type ?? ""));
481
387
  const tok5 = limits.find((l) => /token/i.test(l.type ?? ""));
482
388
  if (tok5) {
483
389
  const reset = new Date(tok5.nextResetTime ?? "").toLocaleTimeString("en-IN");
484
- out.push(` ${padR("Tokens · 5h", 16)}${padL(`${tok5.percentage || 0}%`, 5)} ${meter(tok5.percentage, 22)} ${reset}`);
390
+ out.push([
391
+ dim(` ${padR("Tokens · 5h", 16)}`),
392
+ bold(padL(`${tok5.percentage || 0}%`, 5)),
393
+ plain(` ${meter(tok5.percentage, 22)} ${reset}`)
394
+ ]);
485
395
  }
486
- if (tok5 && mcp) out.push("");
396
+ if (tok5 && mcp) out.push([]);
487
397
  if (mcp) {
488
398
  const reset = new Date(mcp.nextResetTime ?? "").toLocaleString("en-IN");
489
- out.push(` ${padR("MCP · this month", 16)}${padL(`${mcp.percentage || 0}%`, 5)} ${meter(mcp.percentage, 22)} ${reset}`);
399
+ out.push([
400
+ dim(` ${padR("MCP · this month", 16)}`),
401
+ bold(padL(`${mcp.percentage || 0}%`, 5)),
402
+ plain(` ${meter(mcp.percentage, 22)} ${reset}`)
403
+ ]);
490
404
  const parts = (mcp.usageDetails ?? []).map((d) => `${friendlyTool(d.modelCode)} ${fmtNum(d.usage)}`).join(" ");
491
- if (parts) out.push(` ${fmtNum(mcp.currentUsage)}M / ${fmtNum(mcp.totol)}M · ${parts}`);
405
+ if (parts) out.push([plain(` ${fmtNum(mcp.currentUsage)}M / ${fmtNum(mcp.totol)}M · ${parts}`)]);
406
+ }
407
+ out.push([]);
408
+ out.push([plain(rule())]);
409
+ return out;
410
+ }
411
+ function render(input) {
412
+ return reportLines(input).map((line) => line.map((seg) => seg.text).join("")).join("\n");
413
+ }
414
+ //#endregion
415
+ //#region src/resolve.ts
416
+ var DEFAULT_BASE_URL = "https://api.z.ai";
417
+ var GLM_HOSTS = /* @__PURE__ */ new Set([
418
+ "api.z.ai",
419
+ "dev.bigmodel.cn",
420
+ "open.bigmodel.cn"
421
+ ]);
422
+ function fromEnv(value) {
423
+ return value ? value : void 0;
424
+ }
425
+ function originOf(value) {
426
+ try {
427
+ const url = new URL(value);
428
+ return url.protocol === "https:" || url.protocol === "http:" ? url.origin : void 0;
429
+ } catch {
430
+ return;
431
+ }
432
+ }
433
+ function isGlm(origin) {
434
+ return GLM_HOSTS.has(new URL(origin).hostname);
435
+ }
436
+ function resolveBaseUrl(flag, env) {
437
+ if (flag !== void 0) {
438
+ const origin = originOf(flag);
439
+ if (origin === void 0) throw new Error(`invalid base URL: ${flag}`);
440
+ return {
441
+ glm: isGlm(origin),
442
+ origin
443
+ };
492
444
  }
493
- out.push("");
494
- out.push(rule());
495
- return out.join("\n");
445
+ const inherited = fromEnv(env.ANTHROPIC_BASE_URL);
446
+ const origin = inherited === void 0 ? void 0 : originOf(inherited);
447
+ if (origin !== void 0 && isGlm(origin)) return {
448
+ glm: true,
449
+ origin
450
+ };
451
+ return {
452
+ glm: false,
453
+ origin: DEFAULT_BASE_URL
454
+ };
455
+ }
456
+ function buildProgram() {
457
+ return new Command().name("zai-usage").addOption(new Option("--auth-token <token>", "API key").env("ZAI_AUTH_TOKEN")).addOption(new Option("--base-url <url>", "base URL").env("ZAI_BASE_URL")).addOption(new Option("--json", "print the report lines as JSON for the zai mod"));
458
+ }
459
+ function parseArgs(args) {
460
+ const program = parseQuietly(buildProgram(), args);
461
+ if (program === void 0) return;
462
+ const { authToken, baseUrl, json } = program.opts();
463
+ return {
464
+ authToken: authToken || void 0,
465
+ baseUrl: baseUrl || void 0,
466
+ json: json === true
467
+ };
468
+ }
469
+ function resolveConfig(env, parsed) {
470
+ const base = resolveBaseUrl(parsed.baseUrl, env);
471
+ const token = parsed.authToken ?? (base.glm ? fromEnv(env.ANTHROPIC_AUTH_TOKEN) : void 0);
472
+ if (token === void 0) throw new Error(env.ANTHROPIC_AUTH_TOKEN ? "ANTHROPIC_AUTH_TOKEN is set, but the resolved base URL does not name a GLM host, so it is not a GLM Coding Plan token — set ZAI_AUTH_TOKEN (or --auth-token)" : "no API key: set ZAI_AUTH_TOKEN (or --auth-token)");
473
+ return {
474
+ token,
475
+ url: base.origin
476
+ };
496
477
  }
497
478
  //#endregion
498
479
  //#region src/usage.ts
499
480
  /**
500
- * Usage query. Fetches model usage, tool usage, and quota limits from the GLM
501
- * Coding Plan monitor API of the resolved base URL (paths are fixed) and
502
- * renders the plain-text report.
481
+ * Usage query: the three GLM monitor endpoints of the resolved base URL,
482
+ * assembled into the report input.
503
483
  */
504
484
  function queryParams(now = /* @__PURE__ */ new Date()) {
505
485
  const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, now.getHours(), 0, 0, 0);
@@ -555,12 +535,12 @@ async function fetchReport(config) {
555
535
  fetchJson(`${config.url}/api/monitor/usage/tool-usage`, "Tool usage", config.token, query),
556
536
  fetchJson(`${config.url}/api/monitor/usage/quota/limit`, "Quota limit", config.token)
557
537
  ]);
558
- return render({
538
+ return {
559
539
  platform,
560
540
  model,
561
541
  tool,
562
542
  quota: processQuotaLimit(quotaRaw)
563
- });
543
+ };
564
544
  } catch (e) {
565
545
  throw new Error(`Request failed: ${e.message}`, { cause: e });
566
546
  }
@@ -568,7 +548,13 @@ async function fetchReport(config) {
568
548
  //#endregion
569
549
  //#region src/index.ts
570
550
  var parsed = parseArgs(process.argv.slice(2)) ?? printUsageAndExit(buildProgram());
571
- await hookOrPrint(parsed.hook, "query failed", () => fetchReport(resolveConfig(process.env, parsed)));
551
+ try {
552
+ const input = await fetchReport(resolveConfig(process.env, parsed));
553
+ console.log(parsed.json ? JSON.stringify({ lines: reportLines(input) }) : render(input));
554
+ } catch (e) {
555
+ console.error(e.message);
556
+ process.exit(1);
557
+ }
572
558
  //#endregion
573
559
  export {};
574
560