@v1nvn/zai 0.36.0 → 0.37.0

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/dist/usage.mjs CHANGED
@@ -21,8 +21,408 @@ function printUsageAndExit(program) {
21
21
  console.error(program.helpInformation());
22
22
  process.exit(1);
23
23
  }
24
+ async function runMain(main) {
25
+ try {
26
+ await main();
27
+ } catch (e) {
28
+ console.error(e.message);
29
+ process.exit(1);
30
+ }
31
+ }
24
32
  Number.MAX_SAFE_INTEGER;
25
33
  //#endregion
34
+ //#region ../tokens/dist/assets/text-D_R3UWAk.js
35
+ function plain(text) {
36
+ return { text };
37
+ }
38
+ function dim(text) {
39
+ return {
40
+ text,
41
+ ink: "dim"
42
+ };
43
+ }
44
+ function bold(text) {
45
+ return {
46
+ text,
47
+ ink: "bold"
48
+ };
49
+ }
50
+ var MONTHS = [
51
+ "Jan",
52
+ "Feb",
53
+ "Mar",
54
+ "Apr",
55
+ "May",
56
+ "Jun",
57
+ "Jul",
58
+ "Aug",
59
+ "Sep",
60
+ "Oct",
61
+ "Nov",
62
+ "Dec"
63
+ ];
64
+ var EIGHTHS = [
65
+ "",
66
+ "▏",
67
+ "▎",
68
+ "▍",
69
+ "▌",
70
+ "▋",
71
+ "▊",
72
+ "▉"
73
+ ];
74
+ function rule() {
75
+ return "─".repeat(68);
76
+ }
77
+ function pad2(n) {
78
+ return String(n).padStart(2, "0");
79
+ }
80
+ function fmtTokens(n) {
81
+ if (n == null || Number.isNaN(n)) return "—";
82
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
83
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
84
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
85
+ return String(n);
86
+ }
87
+ function fmtNum(n) {
88
+ return (n || 0).toLocaleString("en-US");
89
+ }
90
+ function padR(s, n) {
91
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
92
+ }
93
+ function padL(s, n) {
94
+ return s.length >= n ? s : " ".repeat(n - s.length) + s;
95
+ }
96
+ /** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */
97
+ function barField(v, max, width) {
98
+ if (!v || v <= 0 || max <= 0) return " ".repeat(width);
99
+ const scaled = v / max * width;
100
+ let full = Math.floor(scaled);
101
+ let fi = Math.round((scaled - full) * 8);
102
+ if (fi === 8) {
103
+ full += 1;
104
+ fi = 0;
105
+ }
106
+ if (full === 0 && fi === 0) fi = 1;
107
+ let s = "█".repeat(Math.min(full, width));
108
+ if (full < width && fi > 0) s += EIGHTHS[fi] ?? "";
109
+ if (s.length < width) s += " ".repeat(width - s.length);
110
+ return s.slice(0, width);
111
+ }
112
+ /** Filled/empty meter: █ for used, ░ for remaining. */
113
+ function meter(pct, width) {
114
+ let filled = Math.round((pct || 0) / 100 * width);
115
+ filled = Math.max(0, Math.min(width, filled));
116
+ return "█".repeat(filled) + "░".repeat(width - filled);
117
+ }
118
+ function renderLines(lines) {
119
+ return lines.map((line) => line.map((seg) => seg.text).join("")).join("\n");
120
+ }
121
+ //#endregion
122
+ //#region src/format.ts
123
+ /**
124
+ * Usage-report line model: `reportLines` lays the report out once as ink-tagged
125
+ * segments (the pane draws those), and `render` joins them into the CLI's
126
+ * monospace terminal / hook-block `reason` bytes — no markdown; alignment comes
127
+ * from fixed-width columns and unicode block glyphs. Input is the parsed `data`
128
+ * of the three ZAI/ZHIPU monitor endpoints (model-usage, tool-usage,
129
+ * quota/limit — the last already passed through processQuotaLimit).
130
+ */
131
+ var VBLOCKS = "▁▂▃▄▅▆▇";
132
+ function parseSlot(s) {
133
+ const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/.exec(s ?? "");
134
+ if (!m) return null;
135
+ return {
136
+ h: +m[4],
137
+ day: `${MONTHS[+m[2] - 1]} ${m[3]}`,
138
+ time: `${m[4]}:${m[5]}`
139
+ };
140
+ }
141
+ /**
142
+ * Re-express a naive "YYYY-MM-DD HH:MM" string from one UTC offset to another.
143
+ * Offsets are minutes east of UTC (Beijing = 480, IST = 330). Pure — does not
144
+ * depend on the runtime timezone, so it is deterministic under test.
145
+ */
146
+ function shiftSlot(s, fromOffsetMin, toOffsetMin) {
147
+ const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/.exec(s);
148
+ if (!m) return null;
149
+ const utcMs = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]) - fromOffsetMin * 6e4;
150
+ const dt = new Date(utcMs + toOffsetMin * 6e4);
151
+ return {
152
+ h: dt.getUTCHours(),
153
+ day: `${MONTHS[dt.getUTCMonth()]} ${pad2(dt.getUTCDate())}`,
154
+ time: `${pad2(dt.getUTCHours())}:${pad2(dt.getUTCMinutes())}`
155
+ };
156
+ }
157
+ function friendlyTool(code) {
158
+ switch ((code ?? "").toLowerCase()) {
159
+ case "search-prime": return "web search";
160
+ case "web-reader": return "web read";
161
+ default: return code || "other";
162
+ }
163
+ }
164
+ function idleRuns(tok) {
165
+ const runs = [];
166
+ let i = 0;
167
+ while (i < tok.length) if ((tok[i] || 0) === 0) {
168
+ let j = i;
169
+ while (j < tok.length && (tok[j] || 0) === 0) j++;
170
+ runs.push({
171
+ i,
172
+ j: j - 1,
173
+ len: j - i
174
+ });
175
+ i = j;
176
+ } else i++;
177
+ return runs;
178
+ }
179
+ function idleLabel(run, x, slotFn) {
180
+ const a = slotFn(x[run.i]);
181
+ const b = slotFn(x[run.j]);
182
+ if (!a || !b) return `idle (${run.len}h)`;
183
+ return `idle ${`${a.day} ${a.time}`} → ${a.day === b.day ? b.time : `${b.day} ${b.time}`} (${run.len}h)`;
184
+ }
185
+ function isPeakBucket(s) {
186
+ const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):/.exec(s ?? "");
187
+ if (!m) return false;
188
+ const wd = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])).getUTCDay();
189
+ return wd >= 1 && wd <= 5 && +m[4] >= 14 && +m[4] <= 17;
190
+ }
191
+ /** Local-time start/end of the 14:00–18:00 Beijing peak window (e.g. 11:30 / 15:30). */
192
+ function localPeakWindow(apiOffsetMin, toOffsetMin) {
193
+ const a = shiftSlot("2000-01-03 14:00", apiOffsetMin, toOffsetMin);
194
+ const b = shiftSlot("2000-01-03 18:00", apiOffsetMin, toOffsetMin);
195
+ if (!a || !b) return null;
196
+ return {
197
+ start: a.time,
198
+ end: b.time
199
+ };
200
+ }
201
+ /**
202
+ * Vertical bar chart of hourly tokens. Returns an array of plain-text lines
203
+ * (without the section header), or null if there are fewer than 2 buckets.
204
+ *
205
+ * Bars are scaled to `maxTok` across `ROWS` rows; the topmost filled cell of a
206
+ * bar uses a fractional lower-block glyph (▁▂▃▄▅▆▇) for sub-row height. The
207
+ * y-axis labels only the peak (top) and 0 (baseline); the header window and the
208
+ * caller's peak annotation carry the exact values.
209
+ */
210
+ function hourlyVerticalChart({ x, tok, nh, maxTok, peakIdx, slotFn }) {
211
+ const ROWS = 6;
212
+ if (nh < 2 || maxTok <= 0) return null;
213
+ const lines = [];
214
+ const maxLab = fmtTokens(maxTok);
215
+ const L = maxLab.length;
216
+ const AXIS = 1 + L + 1;
217
+ const P0 = AXIS + 2;
218
+ const PW = 68 - P0;
219
+ const slot = Math.max(1, Math.floor(PW / nh));
220
+ const barW = slot >= 2 ? slot - 1 : 1;
221
+ function barLeft(i) {
222
+ return P0 + i * slot;
223
+ }
224
+ function barCenter(i) {
225
+ return barLeft(i) + Math.floor(barW / 2);
226
+ }
227
+ const usedCols = nh * slot;
228
+ function blank() {
229
+ return Array(68).fill(" ");
230
+ }
231
+ function glyph(i, r) {
232
+ const v = tok[i] || 0;
233
+ if (v <= 0) return " ";
234
+ const fill = v / maxTok * ROWS - r;
235
+ if (fill >= 1) return "█";
236
+ if (fill > 0) {
237
+ let e = Math.round(fill * 8);
238
+ if (e < 1) e = 1;
239
+ if (e > 7) e = 7;
240
+ return VBLOCKS[e - 1];
241
+ }
242
+ return " ";
243
+ }
244
+ for (let r = 5; r >= 0; r--) {
245
+ const line = blank();
246
+ line[AXIS] = r === 5 ? "┤" : "│";
247
+ if (r === 5) for (let k = 0; k < L; k++) line[AXIS - 1 - L + k] = maxLab[k];
248
+ for (let i = 0; i < nh; i++) {
249
+ const g = glyph(i, r);
250
+ if (g !== " ") for (let c = 0; c < barW; c++) line[barLeft(i) + c] = g;
251
+ }
252
+ lines.push(line.join("").trimEnd());
253
+ }
254
+ function slotOf(i) {
255
+ return slotFn(x[i]);
256
+ }
257
+ function isTick(i) {
258
+ if (i === 0 || i === nh - 1) return true;
259
+ const s = slotOf(i);
260
+ return !!s && s.h % 6 === 0;
261
+ }
262
+ const base = blank();
263
+ base[AXIS - 2] = "0";
264
+ base[AXIS] = "└";
265
+ for (let c = AXIS + 1; c < P0 + usedCols; c++) base[c] = "─";
266
+ for (let i = 0; i < nh; i++) if (isTick(i)) base[barCenter(i)] = "┬";
267
+ lines.push(base.join("").trimEnd());
268
+ const hr = blank();
269
+ let last = -10;
270
+ for (let i = 0; i < nh; i++) {
271
+ if (!isTick(i)) continue;
272
+ const col = barCenter(i);
273
+ if (col - last < 3) continue;
274
+ const s = slotOf(i);
275
+ const txt = s ? pad2(s.h) : " ";
276
+ hr[col] = txt[0];
277
+ hr[col + 1] = txt[1];
278
+ last = col;
279
+ }
280
+ lines.push(hr.join("").trimEnd());
281
+ const day = blank();
282
+ let lastDay = null;
283
+ last = -10;
284
+ for (let i = 0; i < nh; i++) {
285
+ if (!isTick(i)) continue;
286
+ const s = slotOf(i);
287
+ if (!s || s.day === lastDay) continue;
288
+ const col = barCenter(i);
289
+ if (col - last >= s.day.length && col + s.day.length <= 68) {
290
+ for (let k = 0; k < s.day.length; k++) day[col + k] = s.day[k];
291
+ lastDay = s.day;
292
+ last = col + s.day.length;
293
+ }
294
+ }
295
+ if (day.some((c) => c !== " ")) lines.push(day.join("").trimEnd());
296
+ const mark = blank();
297
+ for (let i = 0; i < nh; i++) if (isPeakBucket(x[i])) mark[barCenter(i)] = "↑";
298
+ if (peakIdx >= 0) mark[barCenter(peakIdx)] = "◂";
299
+ if (mark.some((c) => c !== " ")) lines.push(mark.join("").trimEnd());
300
+ return lines;
301
+ }
302
+ function reportLines({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetMin, now = /* @__PURE__ */ new Date() }) {
303
+ const out = [];
304
+ const toOffset = localOffsetMin != null ? localOffsetMin : -(/* @__PURE__ */ new Date()).getTimezoneOffset();
305
+ function convertSlot(s) {
306
+ return shiftSlot(s, apiOffsetMin, toOffset) ?? parseSlot(s);
307
+ }
308
+ const x = model.x_time ?? [];
309
+ const tok = model.tokensUsage ?? [];
310
+ const calls = model.modelCallCount ?? [];
311
+ const nh = x.length;
312
+ const tu = model.totalUsage ?? {};
313
+ const total = tu.totalTokensUsage != null ? tu.totalTokensUsage : tok.reduce((a, b) => a + (b || 0), 0);
314
+ const totalCalls = tu.totalModelCallCount != null ? tu.totalModelCallCount : calls.reduce((a, b) => a + (b || 0), 0);
315
+ let peakIdx = -1;
316
+ let peakTok = 0;
317
+ for (let i = 0; i < tok.length; i++) if (tok[i] > peakTok) {
318
+ peakTok = tok[i];
319
+ peakIdx = i;
320
+ }
321
+ const peakCalls = peakIdx >= 0 ? calls[peakIdx] || 0 : 0;
322
+ const pctPeak = total > 0 ? peakTok / total * 100 : 0;
323
+ const peakSlot = peakIdx >= 0 ? convertSlot(x[peakIdx]) : null;
324
+ const level = quota.level ? quota.level.charAt(0).toUpperCase() + quota.level.slice(1) : "";
325
+ const left = ` GLM Coding Plan${level ? " · " + level : ""}`;
326
+ const firstSlot = convertSlot(x[0]);
327
+ const lastSlot = convertSlot(x[nh - 1]);
328
+ const win = firstSlot && lastSlot ? `${firstSlot.day} ${firstSlot.time} → ${lastSlot.day} ${lastSlot.time} · ${nh}h` : platform;
329
+ out.push([plain(rule())]);
330
+ out.push([bold(left), dim(padL(win, 68 - left.length))]);
331
+ out.push([plain(rule())]);
332
+ const head = plain(` ${fmtTokens(total)} tokens across ${fmtNum(totalCalls)} model calls`);
333
+ out.push([]);
334
+ if (peakIdx >= 0 && peakSlot) {
335
+ out.push([head, plain(" — ")]);
336
+ out.push([bold(`${Math.round(pctPeak)}%`), plain(" of it in a single hour")]);
337
+ out.push([plain(` (${peakSlot.day} ${peakSlot.time}, ${fmtTokens(peakTok)} tokens / ${fmtNum(peakCalls)} calls).`)]);
338
+ } else out.push([head, plain(".")]);
339
+ const activeHours = tok.filter((t) => t > 0).length;
340
+ const longest = idleRuns(tok).filter((r) => r.len >= 2).sort((a, b) => b.len - a.len).at(0);
341
+ const tt = tool.totalUsage ?? {};
342
+ const searchN = tt.totalNetworkSearchCount || 0;
343
+ const readN = tt.totalWebReadMcpCount || 0;
344
+ const zreadN = tt.totalZreadMcpCount || 0;
345
+ const toolTotal = tt.totalSearchMcpCount != null ? tt.totalSearchMcpCount : searchN + readN + zreadN;
346
+ let peakWinActive = 0;
347
+ let peakWinTokens = 0;
348
+ for (let k = 0; k < nh; k++) if (isPeakBucket(x[k]) && tok[k] > 0) {
349
+ peakWinActive++;
350
+ peakWinTokens += tok[k];
351
+ }
352
+ const peakWinPct = total > 0 ? peakWinTokens / total * 100 : 0;
353
+ const peakWin = localPeakWindow(apiOffsetMin, toOffset);
354
+ out.push([]);
355
+ out.push([plain(` Peak ${padR(peakSlot ? peakSlot.day + " " + peakSlot.time : "—", 15)}${padL(fmtTokens(peakTok), 7)} tokens · ${padL(fmtNum(peakCalls), 5)} calls`)]);
356
+ out.push([plain(` Active ${padR(`${activeHours} / ${nh} hours`, 15)}${longest ? idleLabel(longest, x, convertSlot) : "no idle gaps"}`)]);
357
+ out.push([plain(` Tools ${padR(`${toolTotal} calls`, 15)}${searchN} searches · ${readN} reads${zreadN ? ` · ${zreadN} zread` : ""}`)]);
358
+ if (peakWin) out.push([
359
+ plain(` Peak hrs Mon–Fri ${peakWin.start}–${peakWin.end} · GLM-5.2 3× · ${peakWinActive}h active · ${fmtTokens(peakWinTokens)} (`),
360
+ bold(`${Math.round(peakWinPct)}%`),
361
+ plain(")")
362
+ ]);
363
+ out.push([]);
364
+ out.push([dim(" Hourly tokens · ↑ peak hour " + "─".repeat(Math.max(0, 39)))]);
365
+ const chartLines = hourlyVerticalChart({
366
+ x,
367
+ tok,
368
+ nh,
369
+ maxTok: peakTok || 1,
370
+ peakIdx,
371
+ slotFn: convertSlot
372
+ });
373
+ if (chartLines) {
374
+ out.push(...chartLines.map((l) => [plain(l)]));
375
+ if (peakIdx >= 0 && peakSlot) out.push([plain(` ◂ peak ${peakSlot.day} ${peakSlot.time} ${fmtTokens(peakTok)} tokens · ${fmtNum(peakCalls)} calls`)]);
376
+ } else out.push([plain(" (not enough hourly data to chart)")]);
377
+ out.push([]);
378
+ out.push([dim(" Model mix " + "─".repeat(Math.max(0, 57)))]);
379
+ const mixSrc = model.modelSummaryList ?? model.modelDataList ?? [];
380
+ const denom = mixSrc.reduce((a, m) => a + (m.totalTokens ?? 0), 0) || 1;
381
+ const mixSorted = [...mixSrc].sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
382
+ for (const m of mixSorted) {
383
+ const pct = (m.totalTokens ?? 0) / denom * 100;
384
+ out.push([
385
+ plain(` ${padR(m.modelName || "?", 11)}${padL(fmtTokens(m.totalTokens), 8)} `),
386
+ bold(padL(pct.toFixed(1) + "%", 6)),
387
+ plain(` ${barField(pct, 100, 20)}`)
388
+ ]);
389
+ }
390
+ out.push([]);
391
+ out.push([dim(" Limits " + "─".repeat(Math.max(0, 60)))]);
392
+ const bj = new Date(now.getTime() + apiOffsetMin * 6e4);
393
+ const bjDay = bj.getUTCDay();
394
+ const elapsedMin = bjDay >= 1 && bjDay <= 5 ? Math.max(0, Math.min(240, (bj.getUTCHours() - 14) * 60 + bj.getUTCMinutes())) : 0;
395
+ if (peakWin) out.push([dim(` ${padR("Peak", 16)}`), plain(`${padL(peakWin.start, 5)} ${meter(Math.round(elapsedMin / 240 * 100), 22)} ${peakWin.end}`)]);
396
+ const limits = quota.limits ?? [];
397
+ const mcp = limits.find((l) => /mcp/i.test(l.type ?? ""));
398
+ const tok5 = limits.find((l) => /token/i.test(l.type ?? ""));
399
+ if (tok5) {
400
+ const reset = new Date(tok5.nextResetTime ?? "").toLocaleTimeString("en-IN");
401
+ out.push([
402
+ dim(` ${padR("Tokens · 5h", 16)}`),
403
+ bold(padL(`${tok5.percentage || 0}%`, 5)),
404
+ plain(` ${meter(tok5.percentage, 22)} ${reset}`)
405
+ ]);
406
+ }
407
+ if (tok5 && mcp) out.push([]);
408
+ if (mcp) {
409
+ const reset = new Date(mcp.nextResetTime ?? "").toLocaleString("en-IN");
410
+ out.push([
411
+ dim(` ${padR("MCP · this month", 16)}`),
412
+ bold(padL(`${mcp.percentage || 0}%`, 5)),
413
+ plain(` ${meter(mcp.percentage, 22)} ${reset}`)
414
+ ]);
415
+ const parts = (mcp.usageDetails ?? []).map((d) => `${friendlyTool(d.modelCode)} ${fmtNum(d.usage)}`).join(" ");
416
+ if (parts) out.push([plain(` ${fmtNum(mcp.currentUsage)}M / ${fmtNum(mcp.totol)}M · ${parts}`)]);
417
+ }
418
+ out.push([]);
419
+ out.push([plain(rule())]);
420
+ return out;
421
+ }
422
+ function render(input) {
423
+ return renderLines(reportLines(input));
424
+ }
425
+ //#endregion
26
426
  //#region ../../node_modules/commander/lib/error.js
27
427
  /**
28
428
  * CommanderError class
@@ -2982,429 +3382,70 @@ new Command();
2982
3382
  var DEFAULT_BASE_URL = "https://api.z.ai";
2983
3383
  var GLM_HOSTS = /* @__PURE__ */ new Set([
2984
3384
  "api.z.ai",
2985
- "dev.bigmodel.cn",
2986
- "open.bigmodel.cn"
2987
- ]);
2988
- function fromEnv(value) {
2989
- return value ? value : void 0;
2990
- }
2991
- function originOf(value) {
2992
- try {
2993
- const url = new URL(value);
2994
- return url.protocol === "https:" || url.protocol === "http:" ? url.origin : void 0;
2995
- } catch {
2996
- return;
2997
- }
2998
- }
2999
- function isGlm(origin) {
3000
- return GLM_HOSTS.has(new URL(origin).hostname);
3001
- }
3002
- function resolveBaseUrl(flag, env) {
3003
- if (flag !== void 0) {
3004
- const origin = originOf(flag);
3005
- if (origin === void 0) throw new Error(`invalid base URL: ${flag}`);
3006
- return {
3007
- glm: isGlm(origin),
3008
- origin
3009
- };
3010
- }
3011
- const inherited = fromEnv(env.ANTHROPIC_BASE_URL);
3012
- const origin = inherited === void 0 ? void 0 : originOf(inherited);
3013
- if (origin !== void 0 && isGlm(origin)) return {
3014
- glm: true,
3015
- origin
3016
- };
3017
- return {
3018
- glm: false,
3019
- origin: DEFAULT_BASE_URL
3020
- };
3021
- }
3022
- function buildProgram() {
3023
- 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"));
3024
- }
3025
- function parseArgs(args) {
3026
- const program = parseQuietly(buildProgram(), args);
3027
- if (program === void 0) return;
3028
- const { authToken, baseUrl } = program.opts();
3029
- return {
3030
- authToken: authToken || void 0,
3031
- baseUrl: baseUrl || void 0
3032
- };
3033
- }
3034
- function resolveConfig(env, parsed) {
3035
- const base = resolveBaseUrl(parsed.baseUrl, env);
3036
- const token = parsed.authToken ?? (base.glm ? fromEnv(env.ANTHROPIC_AUTH_TOKEN) : void 0);
3037
- 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)");
3038
- return {
3039
- token,
3040
- url: base.origin
3041
- };
3042
- }
3043
- //#endregion
3044
- //#region ../tokens/dist/text.js
3045
- /**
3046
- * Plain-text rendering primitives for the fixed-width reports. Output targets
3047
- * a monospace terminal — the pane — so everything here is fixed-width:
3048
- * padding, block-glyph bars, and compact number formatting.
3049
- */
3050
- var MONTHS = [
3051
- "Jan",
3052
- "Feb",
3053
- "Mar",
3054
- "Apr",
3055
- "May",
3056
- "Jun",
3057
- "Jul",
3058
- "Aug",
3059
- "Sep",
3060
- "Oct",
3061
- "Nov",
3062
- "Dec"
3063
- ];
3064
- var EIGHTHS = [
3065
- "",
3066
- "▏",
3067
- "▎",
3068
- "▍",
3069
- "▌",
3070
- "▋",
3071
- "▊",
3072
- "▉"
3073
- ];
3074
- function rule() {
3075
- return "─".repeat(68);
3076
- }
3077
- function pad2(n) {
3078
- return String(n).padStart(2, "0");
3079
- }
3080
- function fmtTokens(n) {
3081
- if (n == null || Number.isNaN(n)) return "—";
3082
- if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
3083
- if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
3084
- if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
3085
- return String(n);
3086
- }
3087
- function fmtNum(n) {
3088
- return (n || 0).toLocaleString("en-US");
3089
- }
3090
- function padR(s, n) {
3091
- return s.length >= n ? s : s + " ".repeat(n - s.length);
3092
- }
3093
- function padL(s, n) {
3094
- return s.length >= n ? s : " ".repeat(n - s.length) + s;
3095
- }
3096
- /** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */
3097
- function barField(v, max, width) {
3098
- if (!v || v <= 0 || max <= 0) return " ".repeat(width);
3099
- const scaled = v / max * width;
3100
- let full = Math.floor(scaled);
3101
- let fi = Math.round((scaled - full) * 8);
3102
- if (fi === 8) {
3103
- full += 1;
3104
- fi = 0;
3105
- }
3106
- if (full === 0 && fi === 0) fi = 1;
3107
- let s = "█".repeat(Math.min(full, width));
3108
- if (full < width && fi > 0) s += EIGHTHS[fi] ?? "";
3109
- if (s.length < width) s += " ".repeat(width - s.length);
3110
- return s.slice(0, width);
3111
- }
3112
- /** Filled/empty meter: █ for used, ░ for remaining. */
3113
- function meter(pct, width) {
3114
- let filled = Math.round((pct || 0) / 100 * width);
3115
- filled = Math.max(0, Math.min(width, filled));
3116
- return "█".repeat(filled) + "░".repeat(width - filled);
3117
- }
3118
- //#endregion
3119
- //#region src/format.ts
3120
- /**
3121
- * Plain-text usage report renderer.
3122
- *
3123
- * Rendered for a monospace terminal / hook-block `reason`, so it must NOT rely on
3124
- * markdown (bold, tables, fenced code). Alignment comes from fixed-width columns
3125
- * and unicode block glyphs. Input is the parsed `data` of the three ZAI/ZHIPU
3126
- * monitor endpoints (model-usage, tool-usage, quota/limit — the last already
3127
- * passed through processQuotaLimit).
3128
- */
3129
- var VBLOCKS = "▁▂▃▄▅▆▇";
3130
- function parseSlot(s) {
3131
- const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/.exec(s ?? "");
3132
- if (!m) return null;
3133
- return {
3134
- h: +m[4],
3135
- day: `${MONTHS[+m[2] - 1]} ${m[3]}`,
3136
- time: `${m[4]}:${m[5]}`
3137
- };
3138
- }
3139
- /**
3140
- * Re-express a naive "YYYY-MM-DD HH:MM" string from one UTC offset to another.
3141
- * Offsets are minutes east of UTC (Beijing = 480, IST = 330). Pure — does not
3142
- * depend on the runtime timezone, so it is deterministic under test.
3143
- */
3144
- function shiftSlot(s, fromOffsetMin, toOffsetMin) {
3145
- const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/.exec(s);
3146
- if (!m) return null;
3147
- const utcMs = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]) - fromOffsetMin * 6e4;
3148
- const dt = new Date(utcMs + toOffsetMin * 6e4);
3149
- return {
3150
- h: dt.getUTCHours(),
3151
- day: `${MONTHS[dt.getUTCMonth()]} ${pad2(dt.getUTCDate())}`,
3152
- time: `${pad2(dt.getUTCHours())}:${pad2(dt.getUTCMinutes())}`
3153
- };
3385
+ "dev.bigmodel.cn",
3386
+ "open.bigmodel.cn"
3387
+ ]);
3388
+ function fromEnv(value) {
3389
+ return value ? value : void 0;
3154
3390
  }
3155
- function friendlyTool(code) {
3156
- switch ((code ?? "").toLowerCase()) {
3157
- case "search-prime": return "web search";
3158
- case "web-reader": return "web read";
3159
- default: return code || "other";
3391
+ function originOf(value) {
3392
+ try {
3393
+ const url = new URL(value);
3394
+ return url.protocol === "https:" || url.protocol === "http:" ? url.origin : void 0;
3395
+ } catch {
3396
+ return;
3160
3397
  }
3161
3398
  }
3162
- function idleRuns(tok) {
3163
- const runs = [];
3164
- let i = 0;
3165
- while (i < tok.length) if ((tok[i] || 0) === 0) {
3166
- let j = i;
3167
- while (j < tok.length && (tok[j] || 0) === 0) j++;
3168
- runs.push({
3169
- i,
3170
- j: j - 1,
3171
- len: j - i
3172
- });
3173
- i = j;
3174
- } else i++;
3175
- return runs;
3399
+ function isGlm(origin) {
3400
+ return GLM_HOSTS.has(new URL(origin).hostname);
3176
3401
  }
3177
- function idleLabel(run, x, slotFn) {
3178
- const a = slotFn(x[run.i]);
3179
- const b = slotFn(x[run.j]);
3180
- if (!a || !b) return `idle (${run.len}h)`;
3181
- return `idle ${`${a.day} ${a.time}`} → ${a.day === b.day ? b.time : `${b.day} ${b.time}`} (${run.len}h)`;
3402
+ function resolveBaseUrl(flag, env) {
3403
+ if (flag !== void 0) {
3404
+ const origin = originOf(flag);
3405
+ if (origin === void 0) throw new Error(`invalid base URL: ${flag}`);
3406
+ return {
3407
+ glm: isGlm(origin),
3408
+ origin
3409
+ };
3410
+ }
3411
+ const inherited = fromEnv(env.ANTHROPIC_BASE_URL);
3412
+ const origin = inherited === void 0 ? void 0 : originOf(inherited);
3413
+ if (origin !== void 0 && isGlm(origin)) return {
3414
+ glm: true,
3415
+ origin
3416
+ };
3417
+ return {
3418
+ glm: false,
3419
+ origin: DEFAULT_BASE_URL
3420
+ };
3182
3421
  }
3183
- function isPeakBucket(s) {
3184
- const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):/.exec(s ?? "");
3185
- if (!m) return false;
3186
- const wd = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])).getUTCDay();
3187
- return wd >= 1 && wd <= 5 && +m[4] >= 14 && +m[4] <= 17;
3422
+ function buildProgram() {
3423
+ 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"));
3188
3424
  }
3189
- /** Local-time start/end of the 14:00–18:00 Beijing peak window (e.g. 11:30 / 15:30). */
3190
- function localPeakWindow(apiOffsetMin, toOffsetMin) {
3191
- const a = shiftSlot("2000-01-03 14:00", apiOffsetMin, toOffsetMin);
3192
- const b = shiftSlot("2000-01-03 18:00", apiOffsetMin, toOffsetMin);
3193
- if (!a || !b) return null;
3425
+ function parseArgs(args) {
3426
+ const program = parseQuietly(buildProgram(), args);
3427
+ if (program === void 0) return;
3428
+ const { authToken, baseUrl, json } = program.opts();
3194
3429
  return {
3195
- start: a.time,
3196
- end: b.time
3430
+ authToken: authToken || void 0,
3431
+ baseUrl: baseUrl || void 0,
3432
+ json: json === true
3197
3433
  };
3198
3434
  }
3199
- /**
3200
- * Vertical bar chart of hourly tokens. Returns an array of plain-text lines
3201
- * (without the section header), or null if there are fewer than 2 buckets.
3202
- *
3203
- * Bars are scaled to `maxTok` across `ROWS` rows; the topmost filled cell of a
3204
- * bar uses a fractional lower-block glyph (▁▂▃▄▅▆▇) for sub-row height. The
3205
- * y-axis labels only the peak (top) and 0 (baseline); the header window and the
3206
- * caller's peak annotation carry the exact values.
3207
- */
3208
- function hourlyVerticalChart({ x, tok, nh, maxTok, peakIdx, slotFn }) {
3209
- const ROWS = 6;
3210
- if (nh < 2 || maxTok <= 0) return null;
3211
- const lines = [];
3212
- const maxLab = fmtTokens(maxTok);
3213
- const L = maxLab.length;
3214
- const AXIS = 1 + L + 1;
3215
- const P0 = AXIS + 2;
3216
- const PW = 68 - P0;
3217
- const slot = Math.max(1, Math.floor(PW / nh));
3218
- const barW = slot >= 2 ? slot - 1 : 1;
3219
- function barLeft(i) {
3220
- return P0 + i * slot;
3221
- }
3222
- function barCenter(i) {
3223
- return barLeft(i) + Math.floor(barW / 2);
3224
- }
3225
- const usedCols = nh * slot;
3226
- function blank() {
3227
- return Array(68).fill(" ");
3228
- }
3229
- function glyph(i, r) {
3230
- const v = tok[i] || 0;
3231
- if (v <= 0) return " ";
3232
- const fill = v / maxTok * ROWS - r;
3233
- if (fill >= 1) return "█";
3234
- if (fill > 0) {
3235
- let e = Math.round(fill * 8);
3236
- if (e < 1) e = 1;
3237
- if (e > 7) e = 7;
3238
- return VBLOCKS[e - 1];
3239
- }
3240
- return " ";
3241
- }
3242
- for (let r = 5; r >= 0; r--) {
3243
- const line = blank();
3244
- line[AXIS] = r === 5 ? "┤" : "│";
3245
- if (r === 5) for (let k = 0; k < L; k++) line[AXIS - 1 - L + k] = maxLab[k];
3246
- for (let i = 0; i < nh; i++) {
3247
- const g = glyph(i, r);
3248
- if (g !== " ") for (let c = 0; c < barW; c++) line[barLeft(i) + c] = g;
3249
- }
3250
- lines.push(line.join("").trimEnd());
3251
- }
3252
- function slotOf(i) {
3253
- return slotFn(x[i]);
3254
- }
3255
- function isTick(i) {
3256
- if (i === 0 || i === nh - 1) return true;
3257
- const s = slotOf(i);
3258
- return !!s && s.h % 6 === 0;
3259
- }
3260
- const base = blank();
3261
- base[AXIS - 2] = "0";
3262
- base[AXIS] = "└";
3263
- for (let c = AXIS + 1; c < P0 + usedCols; c++) base[c] = "─";
3264
- for (let i = 0; i < nh; i++) if (isTick(i)) base[barCenter(i)] = "┬";
3265
- lines.push(base.join("").trimEnd());
3266
- const hr = blank();
3267
- let last = -10;
3268
- for (let i = 0; i < nh; i++) {
3269
- if (!isTick(i)) continue;
3270
- const col = barCenter(i);
3271
- if (col - last < 3) continue;
3272
- const s = slotOf(i);
3273
- const txt = s ? pad2(s.h) : " ";
3274
- hr[col] = txt[0];
3275
- hr[col + 1] = txt[1];
3276
- last = col;
3277
- }
3278
- lines.push(hr.join("").trimEnd());
3279
- const day = blank();
3280
- let lastDay = null;
3281
- last = -10;
3282
- for (let i = 0; i < nh; i++) {
3283
- if (!isTick(i)) continue;
3284
- const s = slotOf(i);
3285
- if (!s || s.day === lastDay) continue;
3286
- const col = barCenter(i);
3287
- if (col - last >= s.day.length && col + s.day.length <= 68) {
3288
- for (let k = 0; k < s.day.length; k++) day[col + k] = s.day[k];
3289
- lastDay = s.day;
3290
- last = col + s.day.length;
3291
- }
3292
- }
3293
- if (day.some((c) => c !== " ")) lines.push(day.join("").trimEnd());
3294
- const mark = blank();
3295
- for (let i = 0; i < nh; i++) if (isPeakBucket(x[i])) mark[barCenter(i)] = "↑";
3296
- if (peakIdx >= 0) mark[barCenter(peakIdx)] = "◂";
3297
- if (mark.some((c) => c !== " ")) lines.push(mark.join("").trimEnd());
3298
- return lines;
3299
- }
3300
- function render({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetMin, now = /* @__PURE__ */ new Date() }) {
3301
- const out = [];
3302
- const toOffset = localOffsetMin != null ? localOffsetMin : -(/* @__PURE__ */ new Date()).getTimezoneOffset();
3303
- function convertSlot(s) {
3304
- return shiftSlot(s, apiOffsetMin, toOffset) ?? parseSlot(s);
3305
- }
3306
- const x = model.x_time ?? [];
3307
- const tok = model.tokensUsage ?? [];
3308
- const calls = model.modelCallCount ?? [];
3309
- const nh = x.length;
3310
- const tu = model.totalUsage ?? {};
3311
- const total = tu.totalTokensUsage != null ? tu.totalTokensUsage : tok.reduce((a, b) => a + (b || 0), 0);
3312
- const totalCalls = tu.totalModelCallCount != null ? tu.totalModelCallCount : calls.reduce((a, b) => a + (b || 0), 0);
3313
- let peakIdx = -1;
3314
- let peakTok = 0;
3315
- for (let i = 0; i < tok.length; i++) if (tok[i] > peakTok) {
3316
- peakTok = tok[i];
3317
- peakIdx = i;
3318
- }
3319
- const peakCalls = peakIdx >= 0 ? calls[peakIdx] || 0 : 0;
3320
- const pctPeak = total > 0 ? peakTok / total * 100 : 0;
3321
- const peakSlot = peakIdx >= 0 ? convertSlot(x[peakIdx]) : null;
3322
- const level = quota.level ? quota.level.charAt(0).toUpperCase() + quota.level.slice(1) : "";
3323
- const left = ` GLM Coding Plan${level ? " · " + level : ""}`;
3324
- const firstSlot = convertSlot(x[0]);
3325
- const lastSlot = convertSlot(x[nh - 1]);
3326
- const win = firstSlot && lastSlot ? `${firstSlot.day} ${firstSlot.time} → ${lastSlot.day} ${lastSlot.time} · ${nh}h` : platform;
3327
- out.push(rule());
3328
- out.push(left + padL(win, 68 - left.length));
3329
- out.push(rule());
3330
- let lead = ` ${fmtTokens(total)} tokens across ${fmtNum(totalCalls)} model calls`;
3331
- if (peakIdx >= 0 && peakSlot) lead += ` — ${Math.round(pctPeak)}% of it in a single hour (${peakSlot.day} ${peakSlot.time}, ${fmtTokens(peakTok)} tokens / ${fmtNum(peakCalls)} calls)`;
3332
- lead += ".";
3333
- out.push("");
3334
- out.push(lead);
3335
- const activeHours = tok.filter((t) => t > 0).length;
3336
- const longest = idleRuns(tok).filter((r) => r.len >= 2).sort((a, b) => b.len - a.len).at(0);
3337
- const tt = tool.totalUsage ?? {};
3338
- const searchN = tt.totalNetworkSearchCount || 0;
3339
- const readN = tt.totalWebReadMcpCount || 0;
3340
- const zreadN = tt.totalZreadMcpCount || 0;
3341
- const toolTotal = tt.totalSearchMcpCount != null ? tt.totalSearchMcpCount : searchN + readN + zreadN;
3342
- let peakWinActive = 0;
3343
- let peakWinTokens = 0;
3344
- for (let k = 0; k < nh; k++) if (isPeakBucket(x[k]) && tok[k] > 0) {
3345
- peakWinActive++;
3346
- peakWinTokens += tok[k];
3347
- }
3348
- const peakWinPct = total > 0 ? peakWinTokens / total * 100 : 0;
3349
- const peakWin = localPeakWindow(apiOffsetMin, toOffset);
3350
- out.push("");
3351
- out.push(` Peak ${padR(peakSlot ? peakSlot.day + " " + peakSlot.time : "—", 15)}${padL(fmtTokens(peakTok), 7)} tokens · ${padL(fmtNum(peakCalls), 5)} calls`);
3352
- out.push(` Active ${padR(`${activeHours} / ${nh} hours`, 15)}${longest ? idleLabel(longest, x, convertSlot) : "no idle gaps"}`);
3353
- out.push(` Tools ${padR(`${toolTotal} calls`, 15)}${searchN} searches · ${readN} reads${zreadN ? ` · ${zreadN} zread` : ""}`);
3354
- if (peakWin) out.push(` Peak hrs Mon–Fri ${peakWin.start}–${peakWin.end} · GLM-5.2 3× · ${peakWinActive}h active · ${fmtTokens(peakWinTokens)} (${Math.round(peakWinPct)}%)`);
3355
- out.push("");
3356
- out.push(" Hourly tokens · ↑ peak hour " + "─".repeat(Math.max(0, 39)));
3357
- const chartLines = hourlyVerticalChart({
3358
- x,
3359
- tok,
3360
- nh,
3361
- maxTok: peakTok || 1,
3362
- peakIdx,
3363
- slotFn: convertSlot
3364
- });
3365
- if (chartLines) {
3366
- out.push(...chartLines);
3367
- if (peakIdx >= 0 && peakSlot) out.push(` ◂ peak ${peakSlot.day} ${peakSlot.time} ${fmtTokens(peakTok)} tokens · ${fmtNum(peakCalls)} calls`);
3368
- } else out.push(" (not enough hourly data to chart)");
3369
- out.push("");
3370
- out.push(" Model mix " + "─".repeat(Math.max(0, 57)));
3371
- const mixSrc = model.modelSummaryList ?? model.modelDataList ?? [];
3372
- const denom = mixSrc.reduce((a, m) => a + (m.totalTokens ?? 0), 0) || 1;
3373
- const mixSorted = [...mixSrc].sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
3374
- for (const m of mixSorted) {
3375
- const pct = (m.totalTokens ?? 0) / denom * 100;
3376
- out.push(` ${padR(m.modelName || "?", 11)}${padL(fmtTokens(m.totalTokens), 8)} ${padL(pct.toFixed(1) + "%", 6)} ${barField(pct, 100, 20)}`);
3377
- }
3378
- out.push("");
3379
- out.push(" Limits " + "─".repeat(Math.max(0, 60)));
3380
- const bj = new Date(now.getTime() + apiOffsetMin * 6e4);
3381
- const bjDay = bj.getUTCDay();
3382
- const elapsedMin = bjDay >= 1 && bjDay <= 5 ? Math.max(0, Math.min(240, (bj.getUTCHours() - 14) * 60 + bj.getUTCMinutes())) : 0;
3383
- if (peakWin) out.push(` ${padR("Peak", 16)}${padL(peakWin.start, 5)} ${meter(Math.round(elapsedMin / 240 * 100), 22)} ${peakWin.end}`);
3384
- const limits = quota.limits ?? [];
3385
- const mcp = limits.find((l) => /mcp/i.test(l.type ?? ""));
3386
- const tok5 = limits.find((l) => /token/i.test(l.type ?? ""));
3387
- if (tok5) {
3388
- const reset = new Date(tok5.nextResetTime ?? "").toLocaleTimeString("en-IN");
3389
- out.push(` ${padR("Tokens · 5h", 16)}${padL(`${tok5.percentage || 0}%`, 5)} ${meter(tok5.percentage, 22)} ${reset}`);
3390
- }
3391
- if (tok5 && mcp) out.push("");
3392
- if (mcp) {
3393
- const reset = new Date(mcp.nextResetTime ?? "").toLocaleString("en-IN");
3394
- out.push(` ${padR("MCP · this month", 16)}${padL(`${mcp.percentage || 0}%`, 5)} ${meter(mcp.percentage, 22)} ${reset}`);
3395
- const parts = (mcp.usageDetails ?? []).map((d) => `${friendlyTool(d.modelCode)} ${fmtNum(d.usage)}`).join(" ");
3396
- if (parts) out.push(` ${fmtNum(mcp.currentUsage)}M / ${fmtNum(mcp.totol)}M · ${parts}`);
3397
- }
3398
- out.push("");
3399
- out.push(rule());
3400
- return out.join("\n");
3435
+ function resolveConfig(env, parsed) {
3436
+ const base = resolveBaseUrl(parsed.baseUrl, env);
3437
+ const token = parsed.authToken ?? (base.glm ? fromEnv(env.ANTHROPIC_AUTH_TOKEN) : void 0);
3438
+ 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)");
3439
+ return {
3440
+ token,
3441
+ url: base.origin
3442
+ };
3401
3443
  }
3402
3444
  //#endregion
3403
3445
  //#region src/usage.ts
3404
3446
  /**
3405
- * Usage query. Fetches model usage, tool usage, and quota limits from the GLM
3406
- * Coding Plan monitor API of the resolved base URL (paths are fixed) and
3407
- * renders the plain-text report.
3447
+ * Usage query: the three GLM monitor endpoints of the resolved base URL,
3448
+ * assembled into the report input.
3408
3449
  */
3409
3450
  function queryParams(now = /* @__PURE__ */ new Date()) {
3410
3451
  const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, now.getHours(), 0, 0, 0);
@@ -3460,12 +3501,12 @@ async function fetchReport(config) {
3460
3501
  fetchJson(`${config.url}/api/monitor/usage/tool-usage`, "Tool usage", config.token, query),
3461
3502
  fetchJson(`${config.url}/api/monitor/usage/quota/limit`, "Quota limit", config.token)
3462
3503
  ]);
3463
- return render({
3504
+ return {
3464
3505
  platform,
3465
3506
  model,
3466
3507
  tool,
3467
3508
  quota: processQuotaLimit(quotaRaw)
3468
- });
3509
+ };
3469
3510
  } catch (e) {
3470
3511
  throw new Error(`Request failed: ${e.message}`, { cause: e });
3471
3512
  }
@@ -3473,12 +3514,10 @@ async function fetchReport(config) {
3473
3514
  //#endregion
3474
3515
  //#region src/index.ts
3475
3516
  var parsed = parseArgs(process.argv.slice(2)) ?? printUsageAndExit(buildProgram());
3476
- try {
3477
- console.log(await fetchReport(resolveConfig(process.env, parsed)));
3478
- } catch (e) {
3479
- console.error(e.message);
3480
- process.exit(1);
3481
- }
3517
+ await runMain(async () => {
3518
+ const input = await fetchReport(resolveConfig(process.env, parsed));
3519
+ console.log(parsed.json ? JSON.stringify({ lines: reportLines(input) }) : render(input));
3520
+ });
3482
3521
  //#endregion
3483
3522
  export {};
3484
3523