@v1nvn/zai 0.36.0 → 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/dist/usage.mjs CHANGED
@@ -23,6 +23,400 @@ function printUsageAndExit(program) {
23
23
  }
24
24
  Number.MAX_SAFE_INTEGER;
25
25
  //#endregion
26
+ //#region ../tokens/dist/text.js
27
+ /**
28
+ * Plain-text rendering primitives for the fixed-width reports. Output targets
29
+ * a monospace terminal — the pane — so everything here is fixed-width:
30
+ * padding, block-glyph bars, and compact number formatting.
31
+ */
32
+ var MONTHS = [
33
+ "Jan",
34
+ "Feb",
35
+ "Mar",
36
+ "Apr",
37
+ "May",
38
+ "Jun",
39
+ "Jul",
40
+ "Aug",
41
+ "Sep",
42
+ "Oct",
43
+ "Nov",
44
+ "Dec"
45
+ ];
46
+ var EIGHTHS = [
47
+ "",
48
+ "▏",
49
+ "▎",
50
+ "▍",
51
+ "▌",
52
+ "▋",
53
+ "▊",
54
+ "▉"
55
+ ];
56
+ function rule() {
57
+ return "─".repeat(68);
58
+ }
59
+ function pad2(n) {
60
+ return String(n).padStart(2, "0");
61
+ }
62
+ function fmtTokens(n) {
63
+ if (n == null || Number.isNaN(n)) return "—";
64
+ if (n >= 1e9) return (n / 1e9).toFixed(1) + "B";
65
+ if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
66
+ if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
67
+ return String(n);
68
+ }
69
+ function fmtNum(n) {
70
+ return (n || 0).toLocaleString("en-US");
71
+ }
72
+ function padR(s, n) {
73
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
74
+ }
75
+ function padL(s, n) {
76
+ return s.length >= n ? s : " ".repeat(n - s.length) + s;
77
+ }
78
+ /** Fixed-width bar field (width cols): █ blocks + an eighth-fraction + trailing spaces. */
79
+ function barField(v, max, width) {
80
+ if (!v || v <= 0 || max <= 0) return " ".repeat(width);
81
+ const scaled = v / max * width;
82
+ let full = Math.floor(scaled);
83
+ let fi = Math.round((scaled - full) * 8);
84
+ if (fi === 8) {
85
+ full += 1;
86
+ fi = 0;
87
+ }
88
+ if (full === 0 && fi === 0) fi = 1;
89
+ let s = "█".repeat(Math.min(full, width));
90
+ if (full < width && fi > 0) s += EIGHTHS[fi] ?? "";
91
+ if (s.length < width) s += " ".repeat(width - s.length);
92
+ return s.slice(0, width);
93
+ }
94
+ /** Filled/empty meter: █ for used, ░ for remaining. */
95
+ function meter(pct, width) {
96
+ let filled = Math.round((pct || 0) / 100 * width);
97
+ filled = Math.max(0, Math.min(width, filled));
98
+ return "█".repeat(filled) + "░".repeat(width - filled);
99
+ }
100
+ //#endregion
101
+ //#region src/format.ts
102
+ /**
103
+ * Usage-report line model: `reportLines` lays the report out once as ink-tagged
104
+ * segments (the pane draws those), and `render` joins them into the CLI's
105
+ * monospace terminal / hook-block `reason` bytes — no markdown; alignment comes
106
+ * from fixed-width columns and unicode block glyphs. Input is the parsed `data`
107
+ * of the three ZAI/ZHIPU monitor endpoints (model-usage, tool-usage,
108
+ * quota/limit — the last already passed through processQuotaLimit).
109
+ */
110
+ var VBLOCKS = "▁▂▃▄▅▆▇";
111
+ function parseSlot(s) {
112
+ const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/.exec(s ?? "");
113
+ if (!m) return null;
114
+ return {
115
+ h: +m[4],
116
+ day: `${MONTHS[+m[2] - 1]} ${m[3]}`,
117
+ time: `${m[4]}:${m[5]}`
118
+ };
119
+ }
120
+ /**
121
+ * Re-express a naive "YYYY-MM-DD HH:MM" string from one UTC offset to another.
122
+ * Offsets are minutes east of UTC (Beijing = 480, IST = 330). Pure — does not
123
+ * depend on the runtime timezone, so it is deterministic under test.
124
+ */
125
+ function shiftSlot(s, fromOffsetMin, toOffsetMin) {
126
+ const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})/.exec(s);
127
+ if (!m) return null;
128
+ const utcMs = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5]) - fromOffsetMin * 6e4;
129
+ const dt = new Date(utcMs + toOffsetMin * 6e4);
130
+ return {
131
+ h: dt.getUTCHours(),
132
+ day: `${MONTHS[dt.getUTCMonth()]} ${pad2(dt.getUTCDate())}`,
133
+ time: `${pad2(dt.getUTCHours())}:${pad2(dt.getUTCMinutes())}`
134
+ };
135
+ }
136
+ function friendlyTool(code) {
137
+ switch ((code ?? "").toLowerCase()) {
138
+ case "search-prime": return "web search";
139
+ case "web-reader": return "web read";
140
+ default: return code || "other";
141
+ }
142
+ }
143
+ function idleRuns(tok) {
144
+ const runs = [];
145
+ let i = 0;
146
+ while (i < tok.length) if ((tok[i] || 0) === 0) {
147
+ let j = i;
148
+ while (j < tok.length && (tok[j] || 0) === 0) j++;
149
+ runs.push({
150
+ i,
151
+ j: j - 1,
152
+ len: j - i
153
+ });
154
+ i = j;
155
+ } else i++;
156
+ return runs;
157
+ }
158
+ function idleLabel(run, x, slotFn) {
159
+ const a = slotFn(x[run.i]);
160
+ const b = slotFn(x[run.j]);
161
+ if (!a || !b) return `idle (${run.len}h)`;
162
+ return `idle ${`${a.day} ${a.time}`} → ${a.day === b.day ? b.time : `${b.day} ${b.time}`} (${run.len}h)`;
163
+ }
164
+ function isPeakBucket(s) {
165
+ const m = /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):/.exec(s ?? "");
166
+ if (!m) return false;
167
+ const wd = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])).getUTCDay();
168
+ return wd >= 1 && wd <= 5 && +m[4] >= 14 && +m[4] <= 17;
169
+ }
170
+ /** Local-time start/end of the 14:00–18:00 Beijing peak window (e.g. 11:30 / 15:30). */
171
+ function localPeakWindow(apiOffsetMin, toOffsetMin) {
172
+ const a = shiftSlot("2000-01-03 14:00", apiOffsetMin, toOffsetMin);
173
+ const b = shiftSlot("2000-01-03 18:00", apiOffsetMin, toOffsetMin);
174
+ if (!a || !b) return null;
175
+ return {
176
+ start: a.time,
177
+ end: b.time
178
+ };
179
+ }
180
+ /**
181
+ * Vertical bar chart of hourly tokens. Returns an array of plain-text lines
182
+ * (without the section header), or null if there are fewer than 2 buckets.
183
+ *
184
+ * Bars are scaled to `maxTok` across `ROWS` rows; the topmost filled cell of a
185
+ * bar uses a fractional lower-block glyph (▁▂▃▄▅▆▇) for sub-row height. The
186
+ * y-axis labels only the peak (top) and 0 (baseline); the header window and the
187
+ * caller's peak annotation carry the exact values.
188
+ */
189
+ function hourlyVerticalChart({ x, tok, nh, maxTok, peakIdx, slotFn }) {
190
+ const ROWS = 6;
191
+ if (nh < 2 || maxTok <= 0) return null;
192
+ const lines = [];
193
+ const maxLab = fmtTokens(maxTok);
194
+ const L = maxLab.length;
195
+ const AXIS = 1 + L + 1;
196
+ const P0 = AXIS + 2;
197
+ const PW = 68 - P0;
198
+ const slot = Math.max(1, Math.floor(PW / nh));
199
+ const barW = slot >= 2 ? slot - 1 : 1;
200
+ function barLeft(i) {
201
+ return P0 + i * slot;
202
+ }
203
+ function barCenter(i) {
204
+ return barLeft(i) + Math.floor(barW / 2);
205
+ }
206
+ const usedCols = nh * slot;
207
+ function blank() {
208
+ return Array(68).fill(" ");
209
+ }
210
+ function glyph(i, r) {
211
+ const v = tok[i] || 0;
212
+ if (v <= 0) return " ";
213
+ const fill = v / maxTok * ROWS - r;
214
+ if (fill >= 1) return "█";
215
+ if (fill > 0) {
216
+ let e = Math.round(fill * 8);
217
+ if (e < 1) e = 1;
218
+ if (e > 7) e = 7;
219
+ return VBLOCKS[e - 1];
220
+ }
221
+ return " ";
222
+ }
223
+ for (let r = 5; r >= 0; r--) {
224
+ const line = blank();
225
+ line[AXIS] = r === 5 ? "┤" : "│";
226
+ if (r === 5) for (let k = 0; k < L; k++) line[AXIS - 1 - L + k] = maxLab[k];
227
+ for (let i = 0; i < nh; i++) {
228
+ const g = glyph(i, r);
229
+ if (g !== " ") for (let c = 0; c < barW; c++) line[barLeft(i) + c] = g;
230
+ }
231
+ lines.push(line.join("").trimEnd());
232
+ }
233
+ function slotOf(i) {
234
+ return slotFn(x[i]);
235
+ }
236
+ function isTick(i) {
237
+ if (i === 0 || i === nh - 1) return true;
238
+ const s = slotOf(i);
239
+ return !!s && s.h % 6 === 0;
240
+ }
241
+ const base = blank();
242
+ base[AXIS - 2] = "0";
243
+ base[AXIS] = "└";
244
+ for (let c = AXIS + 1; c < P0 + usedCols; c++) base[c] = "─";
245
+ for (let i = 0; i < nh; i++) if (isTick(i)) base[barCenter(i)] = "┬";
246
+ lines.push(base.join("").trimEnd());
247
+ const hr = blank();
248
+ let last = -10;
249
+ for (let i = 0; i < nh; i++) {
250
+ if (!isTick(i)) continue;
251
+ const col = barCenter(i);
252
+ if (col - last < 3) continue;
253
+ const s = slotOf(i);
254
+ const txt = s ? pad2(s.h) : " ";
255
+ hr[col] = txt[0];
256
+ hr[col + 1] = txt[1];
257
+ last = col;
258
+ }
259
+ lines.push(hr.join("").trimEnd());
260
+ const day = blank();
261
+ let lastDay = null;
262
+ last = -10;
263
+ for (let i = 0; i < nh; i++) {
264
+ if (!isTick(i)) continue;
265
+ const s = slotOf(i);
266
+ if (!s || s.day === lastDay) continue;
267
+ const col = barCenter(i);
268
+ if (col - last >= s.day.length && col + s.day.length <= 68) {
269
+ for (let k = 0; k < s.day.length; k++) day[col + k] = s.day[k];
270
+ lastDay = s.day;
271
+ last = col + s.day.length;
272
+ }
273
+ }
274
+ if (day.some((c) => c !== " ")) lines.push(day.join("").trimEnd());
275
+ const mark = blank();
276
+ for (let i = 0; i < nh; i++) if (isPeakBucket(x[i])) mark[barCenter(i)] = "↑";
277
+ if (peakIdx >= 0) mark[barCenter(peakIdx)] = "◂";
278
+ if (mark.some((c) => c !== " ")) lines.push(mark.join("").trimEnd());
279
+ return lines;
280
+ }
281
+ function plain(text) {
282
+ return { text };
283
+ }
284
+ function dim(text) {
285
+ return {
286
+ text,
287
+ ink: "dim"
288
+ };
289
+ }
290
+ function bold(text) {
291
+ return {
292
+ text,
293
+ ink: "bold"
294
+ };
295
+ }
296
+ function reportLines({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetMin, now = /* @__PURE__ */ new Date() }) {
297
+ const out = [];
298
+ const toOffset = localOffsetMin != null ? localOffsetMin : -(/* @__PURE__ */ new Date()).getTimezoneOffset();
299
+ function convertSlot(s) {
300
+ return shiftSlot(s, apiOffsetMin, toOffset) ?? parseSlot(s);
301
+ }
302
+ const x = model.x_time ?? [];
303
+ const tok = model.tokensUsage ?? [];
304
+ const calls = model.modelCallCount ?? [];
305
+ const nh = x.length;
306
+ const tu = model.totalUsage ?? {};
307
+ const total = tu.totalTokensUsage != null ? tu.totalTokensUsage : tok.reduce((a, b) => a + (b || 0), 0);
308
+ const totalCalls = tu.totalModelCallCount != null ? tu.totalModelCallCount : calls.reduce((a, b) => a + (b || 0), 0);
309
+ let peakIdx = -1;
310
+ let peakTok = 0;
311
+ for (let i = 0; i < tok.length; i++) if (tok[i] > peakTok) {
312
+ peakTok = tok[i];
313
+ peakIdx = i;
314
+ }
315
+ const peakCalls = peakIdx >= 0 ? calls[peakIdx] || 0 : 0;
316
+ const pctPeak = total > 0 ? peakTok / total * 100 : 0;
317
+ const peakSlot = peakIdx >= 0 ? convertSlot(x[peakIdx]) : null;
318
+ const level = quota.level ? quota.level.charAt(0).toUpperCase() + quota.level.slice(1) : "";
319
+ const left = ` GLM Coding Plan${level ? " · " + level : ""}`;
320
+ const firstSlot = convertSlot(x[0]);
321
+ const lastSlot = convertSlot(x[nh - 1]);
322
+ const win = firstSlot && lastSlot ? `${firstSlot.day} ${firstSlot.time} → ${lastSlot.day} ${lastSlot.time} · ${nh}h` : platform;
323
+ out.push([plain(rule())]);
324
+ out.push([bold(left), dim(padL(win, 68 - left.length))]);
325
+ out.push([plain(rule())]);
326
+ const head = plain(` ${fmtTokens(total)} tokens across ${fmtNum(totalCalls)} model calls`);
327
+ out.push([]);
328
+ if (peakIdx >= 0 && peakSlot) {
329
+ out.push([head, plain(" — ")]);
330
+ out.push([bold(`${Math.round(pctPeak)}%`), plain(" of it in a single hour")]);
331
+ out.push([plain(` (${peakSlot.day} ${peakSlot.time}, ${fmtTokens(peakTok)} tokens / ${fmtNum(peakCalls)} calls).`)]);
332
+ } else out.push([head, plain(".")]);
333
+ const activeHours = tok.filter((t) => t > 0).length;
334
+ const longest = idleRuns(tok).filter((r) => r.len >= 2).sort((a, b) => b.len - a.len).at(0);
335
+ const tt = tool.totalUsage ?? {};
336
+ const searchN = tt.totalNetworkSearchCount || 0;
337
+ const readN = tt.totalWebReadMcpCount || 0;
338
+ const zreadN = tt.totalZreadMcpCount || 0;
339
+ const toolTotal = tt.totalSearchMcpCount != null ? tt.totalSearchMcpCount : searchN + readN + zreadN;
340
+ let peakWinActive = 0;
341
+ let peakWinTokens = 0;
342
+ for (let k = 0; k < nh; k++) if (isPeakBucket(x[k]) && tok[k] > 0) {
343
+ peakWinActive++;
344
+ peakWinTokens += tok[k];
345
+ }
346
+ const peakWinPct = total > 0 ? peakWinTokens / total * 100 : 0;
347
+ const peakWin = localPeakWindow(apiOffsetMin, toOffset);
348
+ out.push([]);
349
+ out.push([plain(` Peak ${padR(peakSlot ? peakSlot.day + " " + peakSlot.time : "—", 15)}${padL(fmtTokens(peakTok), 7)} tokens · ${padL(fmtNum(peakCalls), 5)} calls`)]);
350
+ out.push([plain(` Active ${padR(`${activeHours} / ${nh} hours`, 15)}${longest ? idleLabel(longest, x, convertSlot) : "no idle gaps"}`)]);
351
+ out.push([plain(` Tools ${padR(`${toolTotal} calls`, 15)}${searchN} searches · ${readN} reads${zreadN ? ` · ${zreadN} zread` : ""}`)]);
352
+ if (peakWin) out.push([
353
+ plain(` Peak hrs Mon–Fri ${peakWin.start}–${peakWin.end} · GLM-5.2 3× · ${peakWinActive}h active · ${fmtTokens(peakWinTokens)} (`),
354
+ bold(`${Math.round(peakWinPct)}%`),
355
+ plain(")")
356
+ ]);
357
+ out.push([]);
358
+ out.push([dim(" Hourly tokens · ↑ peak hour " + "─".repeat(Math.max(0, 39)))]);
359
+ const chartLines = hourlyVerticalChart({
360
+ x,
361
+ tok,
362
+ nh,
363
+ maxTok: peakTok || 1,
364
+ peakIdx,
365
+ slotFn: convertSlot
366
+ });
367
+ if (chartLines) {
368
+ out.push(...chartLines.map((l) => [plain(l)]));
369
+ if (peakIdx >= 0 && peakSlot) out.push([plain(` ◂ peak ${peakSlot.day} ${peakSlot.time} ${fmtTokens(peakTok)} tokens · ${fmtNum(peakCalls)} calls`)]);
370
+ } else out.push([plain(" (not enough hourly data to chart)")]);
371
+ out.push([]);
372
+ out.push([dim(" Model mix " + "─".repeat(Math.max(0, 57)))]);
373
+ const mixSrc = model.modelSummaryList ?? model.modelDataList ?? [];
374
+ const denom = mixSrc.reduce((a, m) => a + (m.totalTokens ?? 0), 0) || 1;
375
+ const mixSorted = [...mixSrc].sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
376
+ for (const m of mixSorted) {
377
+ const pct = (m.totalTokens ?? 0) / denom * 100;
378
+ out.push([
379
+ plain(` ${padR(m.modelName || "?", 11)}${padL(fmtTokens(m.totalTokens), 8)} `),
380
+ bold(padL(pct.toFixed(1) + "%", 6)),
381
+ plain(` ${barField(pct, 100, 20)}`)
382
+ ]);
383
+ }
384
+ out.push([]);
385
+ out.push([dim(" Limits " + "─".repeat(Math.max(0, 60)))]);
386
+ const bj = new Date(now.getTime() + apiOffsetMin * 6e4);
387
+ const bjDay = bj.getUTCDay();
388
+ const elapsedMin = bjDay >= 1 && bjDay <= 5 ? Math.max(0, Math.min(240, (bj.getUTCHours() - 14) * 60 + bj.getUTCMinutes())) : 0;
389
+ if (peakWin) out.push([dim(` ${padR("Peak", 16)}`), plain(`${padL(peakWin.start, 5)} ${meter(Math.round(elapsedMin / 240 * 100), 22)} ${peakWin.end}`)]);
390
+ const limits = quota.limits ?? [];
391
+ const mcp = limits.find((l) => /mcp/i.test(l.type ?? ""));
392
+ const tok5 = limits.find((l) => /token/i.test(l.type ?? ""));
393
+ if (tok5) {
394
+ const reset = new Date(tok5.nextResetTime ?? "").toLocaleTimeString("en-IN");
395
+ out.push([
396
+ dim(` ${padR("Tokens · 5h", 16)}`),
397
+ bold(padL(`${tok5.percentage || 0}%`, 5)),
398
+ plain(` ${meter(tok5.percentage, 22)} ${reset}`)
399
+ ]);
400
+ }
401
+ if (tok5 && mcp) out.push([]);
402
+ if (mcp) {
403
+ const reset = new Date(mcp.nextResetTime ?? "").toLocaleString("en-IN");
404
+ out.push([
405
+ dim(` ${padR("MCP · this month", 16)}`),
406
+ bold(padL(`${mcp.percentage || 0}%`, 5)),
407
+ plain(` ${meter(mcp.percentage, 22)} ${reset}`)
408
+ ]);
409
+ const parts = (mcp.usageDetails ?? []).map((d) => `${friendlyTool(d.modelCode)} ${fmtNum(d.usage)}`).join(" ");
410
+ if (parts) out.push([plain(` ${fmtNum(mcp.currentUsage)}M / ${fmtNum(mcp.totol)}M · ${parts}`)]);
411
+ }
412
+ out.push([]);
413
+ out.push([plain(rule())]);
414
+ return out;
415
+ }
416
+ function render(input) {
417
+ return reportLines(input).map((line) => line.map((seg) => seg.text).join("")).join("\n");
418
+ }
419
+ //#endregion
26
420
  //#region ../../node_modules/commander/lib/error.js
27
421
  /**
28
422
  * CommanderError class
@@ -2980,431 +3374,72 @@ new Command();
2980
3374
  //#endregion
2981
3375
  //#region src/resolve.ts
2982
3376
  var DEFAULT_BASE_URL = "https://api.z.ai";
2983
- var GLM_HOSTS = /* @__PURE__ */ new Set([
2984
- "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
- };
3377
+ var GLM_HOSTS = /* @__PURE__ */ new Set([
3378
+ "api.z.ai",
3379
+ "dev.bigmodel.cn",
3380
+ "open.bigmodel.cn"
3381
+ ]);
3382
+ function fromEnv(value) {
3383
+ return value ? value : void 0;
3154
3384
  }
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";
3385
+ function originOf(value) {
3386
+ try {
3387
+ const url = new URL(value);
3388
+ return url.protocol === "https:" || url.protocol === "http:" ? url.origin : void 0;
3389
+ } catch {
3390
+ return;
3160
3391
  }
3161
3392
  }
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;
3393
+ function isGlm(origin) {
3394
+ return GLM_HOSTS.has(new URL(origin).hostname);
3176
3395
  }
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)`;
3396
+ function resolveBaseUrl(flag, env) {
3397
+ if (flag !== void 0) {
3398
+ const origin = originOf(flag);
3399
+ if (origin === void 0) throw new Error(`invalid base URL: ${flag}`);
3400
+ return {
3401
+ glm: isGlm(origin),
3402
+ origin
3403
+ };
3404
+ }
3405
+ const inherited = fromEnv(env.ANTHROPIC_BASE_URL);
3406
+ const origin = inherited === void 0 ? void 0 : originOf(inherited);
3407
+ if (origin !== void 0 && isGlm(origin)) return {
3408
+ glm: true,
3409
+ origin
3410
+ };
3411
+ return {
3412
+ glm: false,
3413
+ origin: DEFAULT_BASE_URL
3414
+ };
3182
3415
  }
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;
3416
+ function buildProgram() {
3417
+ 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
3418
  }
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;
3419
+ function parseArgs(args) {
3420
+ const program = parseQuietly(buildProgram(), args);
3421
+ if (program === void 0) return;
3422
+ const { authToken, baseUrl, json } = program.opts();
3194
3423
  return {
3195
- start: a.time,
3196
- end: b.time
3424
+ authToken: authToken || void 0,
3425
+ baseUrl: baseUrl || void 0,
3426
+ json: json === true
3197
3427
  };
3198
3428
  }
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");
3429
+ function resolveConfig(env, parsed) {
3430
+ const base = resolveBaseUrl(parsed.baseUrl, env);
3431
+ const token = parsed.authToken ?? (base.glm ? fromEnv(env.ANTHROPIC_AUTH_TOKEN) : void 0);
3432
+ 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)");
3433
+ return {
3434
+ token,
3435
+ url: base.origin
3436
+ };
3401
3437
  }
3402
3438
  //#endregion
3403
3439
  //#region src/usage.ts
3404
3440
  /**
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.
3441
+ * Usage query: the three GLM monitor endpoints of the resolved base URL,
3442
+ * assembled into the report input.
3408
3443
  */
3409
3444
  function queryParams(now = /* @__PURE__ */ new Date()) {
3410
3445
  const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, now.getHours(), 0, 0, 0);
@@ -3460,12 +3495,12 @@ async function fetchReport(config) {
3460
3495
  fetchJson(`${config.url}/api/monitor/usage/tool-usage`, "Tool usage", config.token, query),
3461
3496
  fetchJson(`${config.url}/api/monitor/usage/quota/limit`, "Quota limit", config.token)
3462
3497
  ]);
3463
- return render({
3498
+ return {
3464
3499
  platform,
3465
3500
  model,
3466
3501
  tool,
3467
3502
  quota: processQuotaLimit(quotaRaw)
3468
- });
3503
+ };
3469
3504
  } catch (e) {
3470
3505
  throw new Error(`Request failed: ${e.message}`, { cause: e });
3471
3506
  }
@@ -3474,7 +3509,8 @@ async function fetchReport(config) {
3474
3509
  //#region src/index.ts
3475
3510
  var parsed = parseArgs(process.argv.slice(2)) ?? printUsageAndExit(buildProgram());
3476
3511
  try {
3477
- console.log(await fetchReport(resolveConfig(process.env, parsed)));
3512
+ const input = await fetchReport(resolveConfig(process.env, parsed));
3513
+ console.log(parsed.json ? JSON.stringify({ lines: reportLines(input) }) : render(input));
3478
3514
  } catch (e) {
3479
3515
  console.error(e.message);
3480
3516
  process.exit(1);