@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 +35 -18
- package/dist/index.js +149 -163
- package/dist/index.js.map +1 -1
- package/dist/usage.mjs +3521 -0
- package/dist/usage.mjs.map +1 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# @v1nvn/zai
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
the
|
|
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 —
|
|
11
|
-
|
|
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.
|
|
25
|
+
npx -y @v1nvn/zai@0.36.1
|
|
22
26
|
```
|
|
23
27
|
|
|
24
28
|
## Usage
|
|
25
29
|
|
|
26
30
|
| Invocation | Does |
|
|
27
31
|
|---|---|
|
|
28
|
-
|
|
|
29
|
-
| `npx -y @v1nvn/zai@0.
|
|
30
|
-
| `npx -y @v1nvn/zai@0.
|
|
31
|
-
| `npx -y @v1nvn/zai@0.
|
|
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
|
|
46
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
72
|
-
|
|
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
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
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
|
|
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
|
|
424
|
-
out.push(rule());
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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(
|
|
450
|
-
|
|
451
|
-
|
|
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(
|
|
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)}
|
|
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(
|
|
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(
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
|
501
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
|