@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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @v1nvn/zai
2
2
 
3
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.ts` (`.ts`/`.tsx` the engine loads
4
+ the mod's hooks module at `hooks/register.tsx` (`.ts`/`.tsx` the engine loads
5
5
  directly, no build), engine tests at `tests/`. It reports GLM Coding Plan quota
6
6
  and usage for the current account — models, tools, limits — straight to the
7
7
  terminal; no browser, nothing written.
@@ -11,8 +11,8 @@ User-facing docs: [root README](../../README.md).
11
11
  ## Quickstart
12
12
 
13
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 shows its report as
15
- dim transcript rows (nothing execs until the command runs).
14
+ model never reads: `/zai-usage` execs the shipped CLI and opens its report as
15
+ a pane (nothing execs until the command runs).
16
16
 
17
17
  ```sh
18
18
  claude plugin marketplace add v1nvn/agentic
@@ -22,18 +22,18 @@ claude plugin install zai@agentic
22
22
  In a terminal, bare:
23
23
 
24
24
  ```sh
25
- npx -y @v1nvn/zai@0.36.0
25
+ npx -y @v1nvn/zai@0.36.1
26
26
  ```
27
27
 
28
28
  ## Usage
29
29
 
30
30
  | Invocation | Does |
31
31
  |---|---|
32
- | `/zai-usage` | the account's usage report, as dim transcript rows |
33
- | `npx -y @v1nvn/zai@0.36.0` | the same report, printed bare |
34
- | `npx -y @v1nvn/zai@0.36.0 --auth-token TOKEN` | same, key on the command line (visible in `ps`) |
35
- | `npx -y @v1nvn/zai@0.36.0 --auth-token=TOKEN` | `=` form — zsh quoting-safe |
36
- | `npx -y @v1nvn/zai@0.36.0 --base-url URL` | another GLM endpoint (default `api.z.ai`) |
32
+ | `/zai-usage` | the account's usage report, as a pane |
33
+ | `npx -y @v1nvn/zai@0.36.1` | the same report, printed bare |
34
+ | `npx -y @v1nvn/zai@0.36.1 --auth-token TOKEN` | same, key on the command line (visible in `ps`) |
35
+ | `npx -y @v1nvn/zai@0.36.1 --auth-token=TOKEN` | `=` form — zsh quoting-safe |
36
+ | `npx -y @v1nvn/zai@0.36.1 --base-url URL` | another GLM endpoint (default `api.z.ai`) |
37
37
 
38
38
  Each setting takes the first source that provides it:
39
39
 
@@ -73,17 +73,17 @@ and re-vendor `types/claude-code.d.ts` (repo root) from the engine-laid
73
73
  | File | Role |
74
74
  |---|---|
75
75
  | `src/index.ts` | bin entry (`zai-usage`) — dispatch, exit codes |
76
- | `src/usage.ts` | the GLM API call and the report it builds |
76
+ | `src/usage.ts` | the GLM API call and the report input it assembles |
77
77
  | `src/resolve.ts` | auth-token resolution chain |
78
- | `src/format.ts` | the printed table |
79
- | `hooks/register.ts` | the mod: registers `/zai-usage`, execs the CLI, shows its report |
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
80
  | `bin/usage.mjs` | the standalone build the mod execs — committed, synced by `yarn build` |
81
81
 
82
82
  ## Contracts
83
83
 
84
84
  - One job: print the report, exit 0. Nothing is written anywhere.
85
- - `/zai-usage` is one `node <plugin root>/bin/usage.mjs` exec through the
86
- session's Bash tool — the same query `npx zai-usage` runs, awaited as-is. The
87
- one allow rule it needs is `Bash(node <plugin root>/bin/usage.mjs)`; the
88
- first run asks for it once, then it is remembered. Its report is dim
89
- transcript rows the model never reads.
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
@@ -18,69 +18,6 @@ function printUsageAndExit(program) {
18
18
  }
19
19
  Number.MAX_SAFE_INTEGER;
20
20
  //#endregion
21
- //#region src/resolve.ts
22
- var DEFAULT_BASE_URL = "https://api.z.ai";
23
- var GLM_HOSTS = /* @__PURE__ */ new Set([
24
- "api.z.ai",
25
- "dev.bigmodel.cn",
26
- "open.bigmodel.cn"
27
- ]);
28
- function fromEnv(value) {
29
- return value ? value : void 0;
30
- }
31
- function originOf(value) {
32
- try {
33
- const url = new URL(value);
34
- return url.protocol === "https:" || url.protocol === "http:" ? url.origin : void 0;
35
- } catch {
36
- return;
37
- }
38
- }
39
- function isGlm(origin) {
40
- return GLM_HOSTS.has(new URL(origin).hostname);
41
- }
42
- function resolveBaseUrl(flag, env) {
43
- if (flag !== void 0) {
44
- const origin = originOf(flag);
45
- if (origin === void 0) throw new Error(`invalid base URL: ${flag}`);
46
- return {
47
- glm: isGlm(origin),
48
- origin
49
- };
50
- }
51
- const inherited = fromEnv(env.ANTHROPIC_BASE_URL);
52
- const origin = inherited === void 0 ? void 0 : originOf(inherited);
53
- if (origin !== void 0 && isGlm(origin)) return {
54
- glm: true,
55
- origin
56
- };
57
- return {
58
- glm: false,
59
- origin: DEFAULT_BASE_URL
60
- };
61
- }
62
- function buildProgram() {
63
- 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"));
64
- }
65
- function parseArgs(args) {
66
- const program = parseQuietly(buildProgram(), args);
67
- if (program === void 0) return;
68
- const { authToken, baseUrl } = program.opts();
69
- return {
70
- authToken: authToken || void 0,
71
- baseUrl: baseUrl || void 0
72
- };
73
- }
74
- function resolveConfig(env, parsed) {
75
- const base = resolveBaseUrl(parsed.baseUrl, env);
76
- const token = parsed.authToken ?? (base.glm ? fromEnv(env.ANTHROPIC_AUTH_TOKEN) : void 0);
77
- 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)");
78
- return {
79
- token,
80
- url: base.origin
81
- };
82
- }
83
- //#endregion
84
21
  //#region ../tokens/dist/text.js
85
22
  /**
86
23
  * Plain-text rendering primitives for the fixed-width reports. Output targets
@@ -158,13 +95,12 @@ function meter(pct, width) {
158
95
  //#endregion
159
96
  //#region src/format.ts
160
97
  /**
161
- * Plain-text usage report renderer.
162
- *
163
- * Rendered for a monospace terminal / hook-block `reason`, so it must NOT rely on
164
- * markdown (bold, tables, fenced code). Alignment comes from fixed-width columns
165
- * and unicode block glyphs. Input is the parsed `data` of the three ZAI/ZHIPU
166
- * monitor endpoints (model-usage, tool-usage, quota/limit — the last already
167
- * 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).
168
104
  */
169
105
  var VBLOCKS = "▁▂▃▄▅▆▇";
170
106
  function parseSlot(s) {
@@ -337,7 +273,22 @@ function hourlyVerticalChart({ x, tok, nh, maxTok, peakIdx, slotFn }) {
337
273
  if (mark.some((c) => c !== " ")) lines.push(mark.join("").trimEnd());
338
274
  return lines;
339
275
  }
340
- function render({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetMin, now = /* @__PURE__ */ new Date() }) {
276
+ function plain(text) {
277
+ return { text };
278
+ }
279
+ function dim(text) {
280
+ return {
281
+ text,
282
+ ink: "dim"
283
+ };
284
+ }
285
+ function bold(text) {
286
+ return {
287
+ text,
288
+ ink: "bold"
289
+ };
290
+ }
291
+ function reportLines({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetMin, now = /* @__PURE__ */ new Date() }) {
341
292
  const out = [];
342
293
  const toOffset = localOffsetMin != null ? localOffsetMin : -(/* @__PURE__ */ new Date()).getTimezoneOffset();
343
294
  function convertSlot(s) {
@@ -364,14 +315,16 @@ function render({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetM
364
315
  const firstSlot = convertSlot(x[0]);
365
316
  const lastSlot = convertSlot(x[nh - 1]);
366
317
  const win = firstSlot && lastSlot ? `${firstSlot.day} ${firstSlot.time} → ${lastSlot.day} ${lastSlot.time} · ${nh}h` : platform;
367
- out.push(rule());
368
- out.push(left + padL(win, 68 - left.length));
369
- out.push(rule());
370
- let lead = ` ${fmtTokens(total)} tokens across ${fmtNum(totalCalls)} model calls`;
371
- if (peakIdx >= 0 && peakSlot) lead += ` — ${Math.round(pctPeak)}% of it in a single hour (${peakSlot.day} ${peakSlot.time}, ${fmtTokens(peakTok)} tokens / ${fmtNum(peakCalls)} calls)`;
372
- lead += ".";
373
- out.push("");
374
- out.push(lead);
318
+ out.push([plain(rule())]);
319
+ out.push([bold(left), dim(padL(win, 68 - left.length))]);
320
+ out.push([plain(rule())]);
321
+ const head = plain(` ${fmtTokens(total)} tokens across ${fmtNum(totalCalls)} model calls`);
322
+ out.push([]);
323
+ if (peakIdx >= 0 && peakSlot) {
324
+ out.push([head, plain(" — ")]);
325
+ out.push([bold(`${Math.round(pctPeak)}%`), plain(" of it in a single hour")]);
326
+ out.push([plain(` (${peakSlot.day} ${peakSlot.time}, ${fmtTokens(peakTok)} tokens / ${fmtNum(peakCalls)} calls).`)]);
327
+ } else out.push([head, plain(".")]);
375
328
  const activeHours = tok.filter((t) => t > 0).length;
376
329
  const longest = idleRuns(tok).filter((r) => r.len >= 2).sort((a, b) => b.len - a.len).at(0);
377
330
  const tt = tool.totalUsage ?? {};
@@ -387,13 +340,17 @@ function render({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetM
387
340
  }
388
341
  const peakWinPct = total > 0 ? peakWinTokens / total * 100 : 0;
389
342
  const peakWin = localPeakWindow(apiOffsetMin, toOffset);
390
- out.push("");
391
- out.push(` Peak ${padR(peakSlot ? peakSlot.day + " " + peakSlot.time : "—", 15)}${padL(fmtTokens(peakTok), 7)} tokens · ${padL(fmtNum(peakCalls), 5)} calls`);
392
- out.push(` Active ${padR(`${activeHours} / ${nh} hours`, 15)}${longest ? idleLabel(longest, x, convertSlot) : "no idle gaps"}`);
393
- out.push(` Tools ${padR(`${toolTotal} calls`, 15)}${searchN} searches · ${readN} reads${zreadN ? ` · ${zreadN} zread` : ""}`);
394
- if (peakWin) out.push(` Peak hrs Mon–Fri ${peakWin.start}–${peakWin.end} · GLM-5.2 3× · ${peakWinActive}h active · ${fmtTokens(peakWinTokens)} (${Math.round(peakWinPct)}%)`);
395
- out.push("");
396
- out.push(" Hourly tokens · ↑ peak hour " + "─".repeat(Math.max(0, 39)));
343
+ out.push([]);
344
+ out.push([plain(` Peak ${padR(peakSlot ? peakSlot.day + " " + peakSlot.time : "—", 15)}${padL(fmtTokens(peakTok), 7)} tokens · ${padL(fmtNum(peakCalls), 5)} calls`)]);
345
+ out.push([plain(` Active ${padR(`${activeHours} / ${nh} hours`, 15)}${longest ? idleLabel(longest, x, convertSlot) : "no idle gaps"}`)]);
346
+ out.push([plain(` Tools ${padR(`${toolTotal} calls`, 15)}${searchN} searches · ${readN} reads${zreadN ? ` · ${zreadN} zread` : ""}`)]);
347
+ if (peakWin) out.push([
348
+ plain(` Peak hrs Mon–Fri ${peakWin.start}–${peakWin.end} · GLM-5.2 3× · ${peakWinActive}h active · ${fmtTokens(peakWinTokens)} (`),
349
+ bold(`${Math.round(peakWinPct)}%`),
350
+ plain(")")
351
+ ]);
352
+ out.push([]);
353
+ out.push([dim(" Hourly tokens · ↑ peak hour " + "─".repeat(Math.max(0, 39)))]);
397
354
  const chartLines = hourlyVerticalChart({
398
355
  x,
399
356
  tok,
@@ -403,48 +360,126 @@ function render({ platform, model, tool, quota, apiOffsetMin = 480, localOffsetM
403
360
  slotFn: convertSlot
404
361
  });
405
362
  if (chartLines) {
406
- out.push(...chartLines);
407
- if (peakIdx >= 0 && peakSlot) out.push(` ◂ peak ${peakSlot.day} ${peakSlot.time} ${fmtTokens(peakTok)} tokens · ${fmtNum(peakCalls)} calls`);
408
- } else out.push(" (not enough hourly data to chart)");
409
- out.push("");
410
- 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)))]);
411
368
  const mixSrc = model.modelSummaryList ?? model.modelDataList ?? [];
412
369
  const denom = mixSrc.reduce((a, m) => a + (m.totalTokens ?? 0), 0) || 1;
413
370
  const mixSorted = [...mixSrc].sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0));
414
371
  for (const m of mixSorted) {
415
372
  const pct = (m.totalTokens ?? 0) / denom * 100;
416
- out.push(` ${padR(m.modelName || "?", 11)}${padL(fmtTokens(m.totalTokens), 8)} ${padL(pct.toFixed(1) + "%", 6)} ${barField(pct, 100, 20)}`);
373
+ out.push([
374
+ plain(` ${padR(m.modelName || "?", 11)}${padL(fmtTokens(m.totalTokens), 8)} `),
375
+ bold(padL(pct.toFixed(1) + "%", 6)),
376
+ plain(` ${barField(pct, 100, 20)}`)
377
+ ]);
417
378
  }
418
- out.push("");
419
- out.push(" Limits " + "─".repeat(Math.max(0, 60)));
379
+ out.push([]);
380
+ out.push([dim(" Limits " + "─".repeat(Math.max(0, 60)))]);
420
381
  const bj = new Date(now.getTime() + apiOffsetMin * 6e4);
421
382
  const bjDay = bj.getUTCDay();
422
383
  const elapsedMin = bjDay >= 1 && bjDay <= 5 ? Math.max(0, Math.min(240, (bj.getUTCHours() - 14) * 60 + bj.getUTCMinutes())) : 0;
423
- if (peakWin) out.push(` ${padR("Peak", 16)}${padL(peakWin.start, 5)} ${meter(Math.round(elapsedMin / 240 * 100), 22)} ${peakWin.end}`);
384
+ if (peakWin) out.push([dim(` ${padR("Peak", 16)}`), plain(`${padL(peakWin.start, 5)} ${meter(Math.round(elapsedMin / 240 * 100), 22)} ${peakWin.end}`)]);
424
385
  const limits = quota.limits ?? [];
425
386
  const mcp = limits.find((l) => /mcp/i.test(l.type ?? ""));
426
387
  const tok5 = limits.find((l) => /token/i.test(l.type ?? ""));
427
388
  if (tok5) {
428
389
  const reset = new Date(tok5.nextResetTime ?? "").toLocaleTimeString("en-IN");
429
- out.push(` ${padR("Tokens · 5h", 16)}${padL(`${tok5.percentage || 0}%`, 5)} ${meter(tok5.percentage, 22)} ${reset}`);
390
+ out.push([
391
+ dim(` ${padR("Tokens · 5h", 16)}`),
392
+ bold(padL(`${tok5.percentage || 0}%`, 5)),
393
+ plain(` ${meter(tok5.percentage, 22)} ${reset}`)
394
+ ]);
430
395
  }
431
- if (tok5 && mcp) out.push("");
396
+ if (tok5 && mcp) out.push([]);
432
397
  if (mcp) {
433
398
  const reset = new Date(mcp.nextResetTime ?? "").toLocaleString("en-IN");
434
- out.push(` ${padR("MCP · this month", 16)}${padL(`${mcp.percentage || 0}%`, 5)} ${meter(mcp.percentage, 22)} ${reset}`);
399
+ out.push([
400
+ dim(` ${padR("MCP · this month", 16)}`),
401
+ bold(padL(`${mcp.percentage || 0}%`, 5)),
402
+ plain(` ${meter(mcp.percentage, 22)} ${reset}`)
403
+ ]);
435
404
  const parts = (mcp.usageDetails ?? []).map((d) => `${friendlyTool(d.modelCode)} ${fmtNum(d.usage)}`).join(" ");
436
- 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}`)]);
437
406
  }
438
- out.push("");
439
- out.push(rule());
440
- return out.join("\n");
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
+ };
444
+ }
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
+ };
441
477
  }
442
478
  //#endregion
443
479
  //#region src/usage.ts
444
480
  /**
445
- * Usage query. Fetches model usage, tool usage, and quota limits from the GLM
446
- * Coding Plan monitor API of the resolved base URL (paths are fixed) and
447
- * renders the plain-text report.
481
+ * Usage query: the three GLM monitor endpoints of the resolved base URL,
482
+ * assembled into the report input.
448
483
  */
449
484
  function queryParams(now = /* @__PURE__ */ new Date()) {
450
485
  const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, now.getHours(), 0, 0, 0);
@@ -500,12 +535,12 @@ async function fetchReport(config) {
500
535
  fetchJson(`${config.url}/api/monitor/usage/tool-usage`, "Tool usage", config.token, query),
501
536
  fetchJson(`${config.url}/api/monitor/usage/quota/limit`, "Quota limit", config.token)
502
537
  ]);
503
- return render({
538
+ return {
504
539
  platform,
505
540
  model,
506
541
  tool,
507
542
  quota: processQuotaLimit(quotaRaw)
508
- });
543
+ };
509
544
  } catch (e) {
510
545
  throw new Error(`Request failed: ${e.message}`, { cause: e });
511
546
  }
@@ -514,7 +549,8 @@ async function fetchReport(config) {
514
549
  //#region src/index.ts
515
550
  var parsed = parseArgs(process.argv.slice(2)) ?? printUsageAndExit(buildProgram());
516
551
  try {
517
- console.log(await fetchReport(resolveConfig(process.env, parsed)));
552
+ const input = await fetchReport(resolveConfig(process.env, parsed));
553
+ console.log(parsed.json ? JSON.stringify({ lines: reportLines(input) }) : render(input));
518
554
  } catch (e) {
519
555
  console.error(e.message);
520
556
  process.exit(1);