zcode-supervisor 0.0.1__py3-none-any.whl

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.
@@ -0,0 +1,2097 @@
1
+ #!/usr/bin/env node
2
+ // Minimal Codex-side controller for ZCode.
3
+ // Uses stable surfaces first: app metadata, cua-driver launch, and Electron CDP.
4
+
5
+ import { execFile } from "node:child_process";
6
+ import { randomUUID } from "node:crypto";
7
+ import { access, chmod, mkdir, open, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
8
+ import http from "node:http";
9
+ import { tmpdir } from "node:os";
10
+ import { dirname, extname, join, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { promisify } from "node:util";
13
+ import { openUsageExpression, summaryExpression, usageSnapshotExpression } from "./browser_scripts.mjs";
14
+ import {
15
+ DEFAULT_PROVIDER_MAX_ATTEMPTS,
16
+ DEFAULT_PROVIDER_RETRY_DELAY_MS,
17
+ classifyProviderError,
18
+ classifyProviderRunState,
19
+ usageAvailableFromStdout,
20
+ } from "./provider_errors.mjs";
21
+
22
+ const execFileAsync = promisify(execFile);
23
+ const DEFAULT_PORT = 9223;
24
+ const DEFAULT_BUNDLE_ID = "dev.zcode.app";
25
+ const DEFAULT_ZCODE_CLI = "/Applications/ZCode.app/Contents/Resources/glm/zcode.cjs";
26
+ const PROMPT_TIMEOUT_MS = 30 * 60 * 1000;
27
+ const TOOL_DIR = dirname(fileURLToPath(import.meta.url));
28
+ const SUPERVISOR_SCRIPT = resolve(TOOL_DIR, "..", "zcode_supervisor", "zcode_supervisor.py");
29
+ const DEFAULT_USAGE_PROVIDER = "zai";
30
+ const DEFAULT_USAGE_SNAPSHOT_TIMEOUT_MS = 20_000;
31
+ const DEFAULT_ZAI_QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
32
+ const DEFAULT_VISION_SERVICE = "zai-mcp-server";
33
+ const IMAGE_EXTENSIONS = new Set([".bmp", ".gif", ".jpeg", ".jpg", ".png", ".webp"]);
34
+ const SECRET_PATH_NEEDLES = [".env", "id_rsa", "id_ed25519", ".ssh", "credential", "credentials"];
35
+
36
+ function usage() {
37
+ console.log(`zcodectl
38
+
39
+ Usage:
40
+ node tools/zcode_control/zcodectl.mjs doctor
41
+ node tools/zcode_control/zcodectl.mjs cli-path
42
+ node tools/zcode_control/zcodectl.mjs cli-doctor
43
+ node tools/zcode_control/zcodectl.mjs cli-preflight
44
+ node tools/zcode_control/zcodectl.mjs cli-version
45
+ node tools/zcode_control/zcodectl.mjs bootstrap-cli-config [--provider zai|bigmodel] [--model glm-5.2] [--source-config <json>] [--cli-config <json>] [--out <json>]
46
+ node tools/zcode_control/zcodectl.mjs vision-preflight [--workspace <path>] [--vision-service zai-mcp-server] [--cli-config <json>] [--out <json>]
47
+ node tools/zcode_control/zcodectl.mjs cli-prompt (--text <prompt> | --text-file <path>) [--workspace <path>] [--mode plan|edit|build|yolo] [--json] [--out <json>]
48
+ node tools/zcode_control/zcodectl.mjs run-packet --packet <json> [--mode plan|edit|build|yolo] [--max-attempts 2] [--retry-delay-ms 60000] [--validation-timeout 60] [--usage-snapshot-source auto|zai-api|codexbar|none] [--usage-provider zai] [--vision-preflight auto|required|off] [--json] [--out <json>]
49
+ node tools/zcode_control/zcodectl.mjs launch [--port 9223] [--new-instance]
50
+ node tools/zcode_control/zcodectl.mjs targets [--port 9223]
51
+ node tools/zcode_control/zcodectl.mjs text [--port 9223] [--max 4000]
52
+ node tools/zcode_control/zcodectl.mjs eval --expr <js> [--port 9223]
53
+ node tools/zcode_control/zcodectl.mjs textboxes [--port 9223]
54
+ node tools/zcode_control/zcodectl.mjs buttons [--port 9223]
55
+ node tools/zcode_control/zcodectl.mjs summary [--port 9223]
56
+ node tools/zcode_control/zcodectl.mjs open-usage [--port 9223]
57
+ node tools/zcode_control/zcodectl.mjs usage [--out <json>] [--port 9223]
58
+ node tools/zcode_control/zcodectl.mjs new-task --workspace <name> [--port 9223]
59
+ node tools/zcode_control/zcodectl.mjs set-mode --mode <mode> [--port 9223]
60
+ node tools/zcode_control/zcodectl.mjs set-composer (--text <text> | --text-file <path>) [--port 9223]
61
+ node tools/zcode_control/zcodectl.mjs submit-task (--text <prompt> | --text-file <path>) [--port 9223]
62
+ node tools/zcode_control/zcodectl.mjs goal (--text <prompt> | --text-file <path>) [--port 9223]
63
+ node tools/zcode_control/zcodectl.mjs wait-idle [--timeout-ms 300000] [--interval-ms 2000] [--port 9223]
64
+ node tools/zcode_control/zcodectl.mjs click --text <label> [--port 9223]
65
+ node tools/zcode_control/zcodectl.mjs click-contains --text <needle> [--port 9223]
66
+ node tools/zcode_control/zcodectl.mjs screenshot --out <png> [--port 9223]
67
+
68
+ Notes:
69
+ - launch uses cua-driver with Electron remote debugging enabled.
70
+ - cli-* and run-packet use the bundled ZCode headless CLI when available.
71
+ - vision-preflight checks for the ZCode/Z.AI image MCP service without printing secrets.
72
+ - run-packet captures before/after quota snapshots via the Z.AI API or CodexBar when available.
73
+ - eval runs JavaScript inside the ZCode renderer. Do not use it for secrets.
74
+ `);
75
+ }
76
+
77
+ function parseArgs(argv) {
78
+ const [command, ...rest] = argv;
79
+ const args = { command, port: DEFAULT_PORT };
80
+ for (let index = 0; index < rest.length; index += 1) {
81
+ const arg = rest[index];
82
+ if (arg === "--port") args.port = Number(rest[++index]);
83
+ else if (arg === "--max") args.max = Number(rest[++index]);
84
+ else if (arg === "--expr") args.expr = rest[++index];
85
+ else if (arg === "--out") args.out = rest[++index];
86
+ else if (arg === "--text") args.text = rest[++index];
87
+ else if (arg === "--text-file") args.textFile = rest[++index];
88
+ else if (arg === "--packet") args.packet = rest[++index];
89
+ else if (arg === "--workspace") args.workspace = rest[++index];
90
+ else if (arg === "--mode") args.mode = rest[++index];
91
+ else if (arg === "--provider") args.provider = rest[++index];
92
+ else if (arg === "--model") args.model = rest[++index];
93
+ else if (arg === "--lite-model") args.liteModel = rest[++index];
94
+ else if (arg === "--source-config") args.sourceConfig = rest[++index];
95
+ else if (arg === "--cli-config") args.cliConfig = rest[++index];
96
+ else if (arg === "--json") args.json = true;
97
+ else if (arg === "--dry-run") args.dryRun = true;
98
+ else if (arg === "--no-bootstrap") args.noBootstrap = true;
99
+ else if (arg === "--attach") {
100
+ args.attach ??= [];
101
+ args.attach.push(rest[++index]);
102
+ }
103
+ else if (arg === "--resume") args.resume = rest[++index];
104
+ else if (arg === "--continue") args.continue = true;
105
+ else if (arg === "--target") args.target = rest[++index];
106
+ else if (arg === "--target-replace") args.targetReplace = true;
107
+ else if (arg === "--timeout-ms") args.timeoutMs = Number(rest[++index]);
108
+ else if (arg === "--interval-ms") args.intervalMs = Number(rest[++index]);
109
+ else if (arg === "--max-attempts") args.maxAttempts = Number(rest[++index]);
110
+ else if (arg === "--retry-delay-ms") args.retryDelayMs = Number(rest[++index]);
111
+ else if (arg === "--validation-timeout") args.validationTimeout = Number(rest[++index]);
112
+ else if (arg === "--usage-snapshot-source") args.usageSnapshotSource = rest[++index];
113
+ else if (arg === "--usage-provider") args.usageProvider = rest[++index];
114
+ else if (arg === "--usage-snapshot-timeout-ms") args.usageSnapshotTimeoutMs = Number(rest[++index]);
115
+ else if (arg === "--codexbar-path") args.codexbarPath = rest[++index];
116
+ else if (arg === "--zai-quota-url") args.zaiQuotaUrl = rest[++index];
117
+ else if (arg === "--vision-preflight") args.visionPreflight = rest[++index];
118
+ else if (arg === "--vision-service") args.visionService = rest[++index];
119
+ else if (arg === "--new-instance") args.newInstance = true;
120
+ else throw new Error(`Unknown argument: ${arg}`);
121
+ }
122
+ return args;
123
+ }
124
+
125
+ async function readPrompt(args) {
126
+ if (args.text && args.textFile) {
127
+ throw new Error("Use either --text or --text-file, not both");
128
+ }
129
+ if (args.textFile) {
130
+ return readFile(resolve(args.textFile), "utf8");
131
+ }
132
+ if (args.text) return args.text;
133
+ throw new Error("--text or --text-file is required");
134
+ }
135
+
136
+ async function runJson(cmd, args) {
137
+ const { stdout } = await execFileAsync(cmd, args, {
138
+ maxBuffer: 10 * 1024 * 1024,
139
+ });
140
+ return JSON.parse(stdout);
141
+ }
142
+
143
+ async function pathExists(path) {
144
+ try {
145
+ await access(path);
146
+ return true;
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+
152
+ function homeFile(...parts) {
153
+ const home = process.env.HOME;
154
+ if (!home) throw new Error("HOME is not set");
155
+ return join(home, ...parts);
156
+ }
157
+
158
+ function defaultCliConfigPath() {
159
+ return homeFile(".zcode", "cli", "config.json");
160
+ }
161
+
162
+ function defaultGuiConfigPath() {
163
+ return homeFile(".zcode", "v2", "config.json");
164
+ }
165
+
166
+ function isRecord(value) {
167
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
168
+ }
169
+
170
+ async function readJsonFile(path) {
171
+ return JSON.parse(await readFile(path, "utf8"));
172
+ }
173
+
174
+ async function readJsonFileOrEmpty(path) {
175
+ if (!(await pathExists(path))) return {};
176
+ const value = await readJsonFile(path);
177
+ if (!isRecord(value)) throw new Error(`Config file must be a JSON object: ${path}`);
178
+ return value;
179
+ }
180
+
181
+ async function writeJsonFileAtomic(path, value) {
182
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
183
+ const tempPath = join(
184
+ dirname(path),
185
+ `.config.json.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`,
186
+ );
187
+ try {
188
+ await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, {
189
+ mode: 0o600,
190
+ });
191
+ await rename(tempPath, path);
192
+ await chmod(path, 0o600).catch(() => {});
193
+ } catch (error) {
194
+ await unlink(tempPath).catch(() => {});
195
+ throw error;
196
+ }
197
+ }
198
+
199
+ async function resolveZcodeCliPath() {
200
+ const candidates = [process.env.ZCODE_CLI_PATH, DEFAULT_ZCODE_CLI].filter(Boolean);
201
+ for (const candidate of candidates) {
202
+ if (await pathExists(candidate)) return candidate;
203
+ }
204
+ throw new Error(`ZCode CLI not found. Set ZCODE_CLI_PATH or install ZCode.app.`);
205
+ }
206
+
207
+ async function runZcodeCli(cliArgs, options = {}) {
208
+ const cliPath = await resolveZcodeCliPath();
209
+ const timeout = options.timeoutMs ?? 0;
210
+ try {
211
+ const { stdout, stderr } = await execFileAsync("node", [cliPath, ...cliArgs], {
212
+ cwd: options.cwd ?? process.cwd(),
213
+ env: {
214
+ ...process.env,
215
+ ...(options.env ?? {}),
216
+ },
217
+ maxBuffer: 50 * 1024 * 1024,
218
+ timeout: timeout > 0 ? timeout : undefined,
219
+ });
220
+ return enrichCliResult({ ok: true, cli_path: cliPath, stdout, stderr, exit_code: 0 });
221
+ } catch (error) {
222
+ return enrichCliResult({
223
+ ok: false,
224
+ cli_path: cliPath,
225
+ stdout: error.stdout ?? "",
226
+ stderr: error.stderr ?? error.message,
227
+ exit_code: typeof error.code === "number" ? error.code : 1,
228
+ });
229
+ }
230
+ }
231
+
232
+ function enrichCliResult(result) {
233
+ const provider = classifyProviderError({
234
+ stdout: result.stdout,
235
+ stderr: result.stderr,
236
+ exitCode: result.exit_code,
237
+ });
238
+ return {
239
+ ...result,
240
+ cli_ok: result.ok,
241
+ usage_available: usageAvailableFromStdout(result.stdout),
242
+ ...provider,
243
+ };
244
+ }
245
+
246
+ async function printCliResult(result, out) {
247
+ if (out) {
248
+ const outputPath = resolve(out);
249
+ await mkdir(dirname(outputPath), { recursive: true });
250
+ await writeFile(outputPath, `${JSON.stringify(result, null, 2)}\n`);
251
+ }
252
+ if (result.stdout) process.stdout.write(result.stdout);
253
+ if (result.stderr) process.stderr.write(result.stderr);
254
+ if (!result.ok) process.exitCode = result.exit_code || 1;
255
+ }
256
+
257
+ async function cliPath() {
258
+ console.log(await resolveZcodeCliPath());
259
+ }
260
+
261
+ async function cliDoctor(out) {
262
+ const result = await runZcodeCli(["doctor", "--json"]);
263
+ await printCliResult(result, out);
264
+ }
265
+
266
+ async function cliVersion(out) {
267
+ const result = await runZcodeCli(["version", "--json"]);
268
+ await printCliResult(result, out);
269
+ }
270
+
271
+ function redactCliConfig(config) {
272
+ const modelConfig = isRecord(config.model) ? config.model : {};
273
+ const providerConfig = isRecord(config.provider) ? config.provider : {};
274
+ const mcpServers = summarizeMcpServers(config);
275
+ const visionService = detectVisionService(config, DEFAULT_VISION_SERVICE);
276
+ const availableModels = Array.isArray(modelConfig.available) ? modelConfig.available : null;
277
+ const availableShapeOk = availableModels === null || availableModels.every(
278
+ (model) => isRecord(model) && typeof model.provider === "string" && typeof model.model === "string",
279
+ );
280
+ const providers = Object.entries(providerConfig).map(([id, provider]) => {
281
+ const providerRecord = isRecord(provider) ? provider : {};
282
+ const options = isRecord(providerRecord.options) ? providerRecord.options : {};
283
+ return {
284
+ id,
285
+ kind: typeof providerRecord.kind === "string" ? providerRecord.kind : null,
286
+ name: typeof providerRecord.name === "string" ? providerRecord.name : null,
287
+ base_url: typeof options.baseURL === "string" ? options.baseURL : null,
288
+ api_key_required: Boolean(options.apiKeyRequired),
289
+ has_api_key: typeof options.apiKey === "string" && options.apiKey.trim().length > 0,
290
+ models: Object.keys(isRecord(providerRecord.models) ? providerRecord.models : {}).sort(),
291
+ };
292
+ });
293
+ const mainModel = typeof config.model === "string"
294
+ ? config.model
295
+ : typeof modelConfig.main === "string"
296
+ ? modelConfig.main
297
+ : null;
298
+ const liteModel = typeof modelConfig.lite === "string" ? modelConfig.lite : null;
299
+ return {
300
+ has_model: Boolean(mainModel),
301
+ main_model: mainModel,
302
+ lite_model: liteModel,
303
+ provider_ids: providers.map((provider) => provider.id),
304
+ providers,
305
+ mcp_servers: mcpServers,
306
+ vision_service: {
307
+ configured: Boolean(visionService),
308
+ service: DEFAULT_VISION_SERVICE,
309
+ server: visionService?.name ?? null,
310
+ },
311
+ has_coding_plan_api_key: providers.some(
312
+ (provider) => ["zai", "bigmodel"].includes(provider.id) && provider.has_api_key,
313
+ ),
314
+ config_shape_ok: availableShapeOk,
315
+ diagnostics: availableShapeOk
316
+ ? []
317
+ : ["model.available must not be an array of strings in ZCode CLI 0.14.5 config"],
318
+ };
319
+ }
320
+
321
+ function mcpServerEntries(config) {
322
+ const mcpConfig = isRecord(config.mcp) ? config.mcp : {};
323
+ if (isRecord(mcpConfig.servers)) return Object.entries(mcpConfig.servers);
324
+ if (isRecord(config.mcpServers)) return Object.entries(config.mcpServers);
325
+ return [];
326
+ }
327
+
328
+ function summarizeMcpServers(config) {
329
+ return mcpServerEntries(config).map(([name, server]) => {
330
+ const record = isRecord(server) ? server : {};
331
+ return {
332
+ name,
333
+ enabled: record.enable !== false,
334
+ type: typeof record.type === "string" ? record.type : "stdio",
335
+ command: typeof record.command === "string" ? record.command : null,
336
+ args_count: Array.isArray(record.args) ? record.args.length : 0,
337
+ has_env: isRecord(record.env) && Object.keys(record.env).length > 0,
338
+ };
339
+ });
340
+ }
341
+
342
+ function detectVisionService(config, serviceName) {
343
+ const normalizedService = normalizeVisionServiceName(serviceName);
344
+ const compactService = compactVisionServiceName(serviceName);
345
+ const allowZaiAliases = compactService === compactVisionServiceName(DEFAULT_VISION_SERVICE);
346
+ for (const [name, server] of mcpServerEntries(config)) {
347
+ const record = isRecord(server) ? server : {};
348
+ if (record.enable === false) continue;
349
+ const haystack = [
350
+ name,
351
+ record.command,
352
+ ...(Array.isArray(record.args) ? record.args : []),
353
+ ]
354
+ .filter((item) => typeof item === "string")
355
+ .join(" ")
356
+ .toLowerCase();
357
+ const normalizedHaystack = normalizeVisionServiceName(haystack);
358
+ const compactHaystack = compactVisionServiceName(haystack);
359
+ if (
360
+ normalizedHaystack.includes(normalizedService)
361
+ || compactHaystack.includes(compactService)
362
+ || (
363
+ allowZaiAliases
364
+ && (
365
+ compactHaystack.includes("zaimcpserver")
366
+ || compactHaystack.includes("zaimcp")
367
+ || normalizedHaystack.includes("zai-mcp")
368
+ )
369
+ )
370
+ ) {
371
+ return {
372
+ name,
373
+ command: typeof record.command === "string" ? record.command : null,
374
+ };
375
+ }
376
+ }
377
+ return null;
378
+ }
379
+
380
+ function normalizeVisionServiceName(value) {
381
+ return String(value ?? "").toLowerCase().replaceAll("_", "-");
382
+ }
383
+
384
+ function compactVisionServiceName(value) {
385
+ return String(value ?? "").toLowerCase().replace(/[^a-z0-9]/g, "");
386
+ }
387
+
388
+ async function inspectMcpConfig(path, source, serviceName) {
389
+ const exists = await pathExists(path);
390
+ if (!exists) {
391
+ return {
392
+ source,
393
+ path,
394
+ exists: false,
395
+ servers: [],
396
+ vision_service: null,
397
+ };
398
+ }
399
+ const config = await readJsonFileOrEmpty(path);
400
+ return {
401
+ source,
402
+ path,
403
+ exists: true,
404
+ note: "secret values are redacted",
405
+ servers: summarizeMcpServers(config),
406
+ vision_service: detectVisionService(config, serviceName),
407
+ };
408
+ }
409
+
410
+ async function inspectVisionServices(args, workspace, serviceName) {
411
+ const configs = [];
412
+ configs.push(await inspectMcpConfig(argsPathOrDefault(args.cliConfig, defaultCliConfigPath()), "user-cli", serviceName));
413
+ configs.push(await inspectMcpConfig(join(workspace, ".zcode", "config.json"), "workspace-zcode", serviceName));
414
+ configs.push(await inspectMcpConfig(join(workspace, ".agents", "mcp.json"), "workspace-agents", serviceName));
415
+ const detected = configs.find((config) => Boolean(config.vision_service));
416
+ return {
417
+ ok: Boolean(detected),
418
+ service: serviceName,
419
+ workspace,
420
+ detected_source: detected?.source ?? null,
421
+ detected_server: detected?.vision_service?.name ?? null,
422
+ configs,
423
+ next_action: detected
424
+ ? null
425
+ : `Configure and enable ${serviceName} in ZCode MCP settings before running required image-understanding tasks.`,
426
+ };
427
+ }
428
+
429
+ async function inspectCliConfig(path = defaultCliConfigPath()) {
430
+ const exists = await pathExists(path);
431
+ if (!exists) {
432
+ return {
433
+ path,
434
+ exists: false,
435
+ note: "CLI config does not exist",
436
+ };
437
+ }
438
+ const config = await readJsonFileOrEmpty(path);
439
+ return {
440
+ path,
441
+ exists: true,
442
+ note: "secret values are redacted",
443
+ ...redactCliConfig(config),
444
+ };
445
+ }
446
+
447
+ async function cliPreflight(args) {
448
+ const cliPathValue = await resolveZcodeCliPath();
449
+ const configPath = argsPathOrDefault(args.cliConfig, defaultCliConfigPath());
450
+ const version = await runZcodeCli(["--version"]);
451
+ const doctorResult = await runZcodeCli(["doctor", "--json"]);
452
+ const config = await inspectCliConfig(configPath);
453
+ const promptReady = Boolean(
454
+ config.exists && config.has_model && config.has_coding_plan_api_key && config.config_shape_ok,
455
+ );
456
+ const payload = {
457
+ ok: version.ok && doctorResult.ok && promptReady,
458
+ cli_path: cliPathValue,
459
+ cli_version: version.stdout.trim() || null,
460
+ config,
461
+ doctor: parseJsonOrText(doctorResult.stdout),
462
+ prompt_ready: promptReady,
463
+ };
464
+ if (!payload.prompt_ready) {
465
+ payload.next_action = "Run zcodectl bootstrap-cli-config, or run ZCode CLI login/configuration before headless prompts.";
466
+ }
467
+ const textPayload = JSON.stringify(payload, null, 2);
468
+ if (args.out) {
469
+ const outputPath = resolve(args.out);
470
+ await mkdir(dirname(outputPath), { recursive: true });
471
+ await writeFile(outputPath, `${textPayload}\n`);
472
+ }
473
+ console.log(textPayload);
474
+ if (!payload.ok) process.exitCode = 1;
475
+ }
476
+
477
+ function argsPathOrDefault(path, fallback) {
478
+ return path ? resolve(path) : fallback;
479
+ }
480
+
481
+ function normalizeModelId(model) {
482
+ const value = String(model ?? "").trim();
483
+ if (!value) throw new Error("model must not be empty");
484
+ return value.toLowerCase();
485
+ }
486
+
487
+ function sourceProviderCandidates(providerId) {
488
+ if (providerId === "zai") {
489
+ return ["builtin:zai-coding-plan", "builtin:zai", "builtin:zai-start-plan"];
490
+ }
491
+ if (providerId === "bigmodel") {
492
+ return ["builtin:bigmodel-coding-plan", "builtin:bigmodel", "builtin:bigmodel-start-plan"];
493
+ }
494
+ throw new Error("--provider must be zai or bigmodel");
495
+ }
496
+
497
+ function displayProviderName(providerId) {
498
+ return providerId === "bigmodel" ? "Bigmodel Coding Plan" : "Z.AI Coding Plan";
499
+ }
500
+
501
+ function fallbackProviderBaseUrl(providerId) {
502
+ return providerId === "bigmodel"
503
+ ? "https://open.bigmodel.cn/api/anthropic"
504
+ : "https://api.z.ai/api/anthropic";
505
+ }
506
+
507
+ function pickSourceProvider(guiConfig, providerId) {
508
+ const providers = isRecord(guiConfig.provider) ? guiConfig.provider : {};
509
+ for (const id of sourceProviderCandidates(providerId)) {
510
+ const provider = providers[id];
511
+ if (!isRecord(provider)) continue;
512
+ const options = isRecord(provider.options) ? provider.options : {};
513
+ const apiKey = typeof options.apiKey === "string" ? options.apiKey.trim() : "";
514
+ if (!apiKey) continue;
515
+ return { id, provider, options, apiKey };
516
+ }
517
+ throw new Error(`No ${providerId} API key found in GUI config`);
518
+ }
519
+
520
+ function normalizedSourceModelIds(sourceProvider, preferredModels) {
521
+ const sourceModels = isRecord(sourceProvider.models) ? sourceProvider.models : {};
522
+ const ids = new Set(preferredModels.map(normalizeModelId));
523
+ for (const model of Object.keys(sourceModels)) ids.add(normalizeModelId(model));
524
+ return [...ids];
525
+ }
526
+
527
+ function modelDisplayName(modelId) {
528
+ return modelId
529
+ .split("-")
530
+ .map((part) => (part === "glm" ? "GLM" : part.toUpperCase()))
531
+ .join("-");
532
+ }
533
+
534
+ async function bootstrapCliConfig(args) {
535
+ const providerId = args.provider ?? "zai";
536
+ const mainModelId = normalizeModelId(args.model ?? "glm-5.2");
537
+ const sourceConfigPath = argsPathOrDefault(args.sourceConfig, defaultGuiConfigPath());
538
+ const cliConfigPath = argsPathOrDefault(args.cliConfig, defaultCliConfigPath());
539
+ const existedBefore = await pathExists(cliConfigPath);
540
+ const guiConfig = await readJsonFile(sourceConfigPath);
541
+ if (!isRecord(guiConfig)) throw new Error(`GUI config must be a JSON object: ${sourceConfigPath}`);
542
+ const source = pickSourceProvider(guiConfig, providerId);
543
+ const existing = await readJsonFileOrEmpty(cliConfigPath);
544
+ const providerConfig = isRecord(existing.provider) ? existing.provider : {};
545
+ const existingProvider = isRecord(providerConfig[providerId]) ? providerConfig[providerId] : {};
546
+ const existingOptions = isRecord(existingProvider.options) ? existingProvider.options : {};
547
+ const existingModels = isRecord(existingProvider.models) ? existingProvider.models : {};
548
+ const sourceBaseUrl = typeof source.options.baseURL === "string" && source.options.baseURL.trim()
549
+ ? source.options.baseURL.trim()
550
+ : fallbackProviderBaseUrl(providerId);
551
+ const sourceModelIds = normalizedSourceModelIds(source.provider, [mainModelId]);
552
+ const liteModelId = args.liteModel
553
+ ? normalizeModelId(args.liteModel)
554
+ : sourceModelIds.includes("glm-5-turbo")
555
+ ? "glm-5-turbo"
556
+ : null;
557
+ const preferredModelIds = liteModelId ? [mainModelId, liteModelId, ...sourceModelIds] : [mainModelId, ...sourceModelIds];
558
+ const modelIds = [...new Set(preferredModelIds)];
559
+ const models = { ...existingModels };
560
+ for (const modelId of modelIds) {
561
+ models[modelId] = {
562
+ ...(isRecord(models[modelId]) ? models[modelId] : {}),
563
+ name: isRecord(models[modelId]) && typeof models[modelId].name === "string"
564
+ ? models[modelId].name
565
+ : modelDisplayName(modelId),
566
+ };
567
+ }
568
+ const modelConfig = isRecord(existing.model) ? { ...existing.model } : {};
569
+ modelConfig.main = `${providerId}/${mainModelId}`;
570
+ if (liteModelId) modelConfig.lite = `${providerId}/${liteModelId}`;
571
+ delete modelConfig.available;
572
+ const nextConfig = {
573
+ ...existing,
574
+ provider: {
575
+ ...providerConfig,
576
+ [providerId]: {
577
+ ...existingProvider,
578
+ kind: typeof source.provider.kind === "string" ? source.provider.kind : "anthropic",
579
+ name: typeof source.provider.name === "string" ? source.provider.name : displayProviderName(providerId),
580
+ options: {
581
+ ...existingOptions,
582
+ apiKeyRequired: true,
583
+ baseURL: sourceBaseUrl,
584
+ apiKey: source.apiKey,
585
+ },
586
+ models,
587
+ },
588
+ },
589
+ model: modelConfig,
590
+ };
591
+ if (!args.dryRun) await writeJsonFileAtomic(cliConfigPath, nextConfig);
592
+ const payload = {
593
+ ok: true,
594
+ dry_run: Boolean(args.dryRun),
595
+ cli_config: {
596
+ path: cliConfigPath,
597
+ existed_before: existedBefore,
598
+ wrote_file: !args.dryRun,
599
+ permissions: args.dryRun ? null : "0600",
600
+ },
601
+ source: {
602
+ path: sourceConfigPath,
603
+ provider_id: source.id,
604
+ base_url: sourceBaseUrl,
605
+ has_api_key: true,
606
+ },
607
+ target: {
608
+ provider_id: providerId,
609
+ main_model: modelConfig.main,
610
+ lite_model: modelConfig.lite ?? null,
611
+ configured_models: modelIds.map((modelId) => `${providerId}/${modelId}`),
612
+ },
613
+ secret_handling: "API key copied locally from GUI config to CLI config; secret values were not printed.",
614
+ preflight: redactCliConfig(nextConfig),
615
+ };
616
+ const textPayload = JSON.stringify(payload, null, 2);
617
+ if (!args.quiet && args.out) {
618
+ const outputPath = resolve(args.out);
619
+ await mkdir(dirname(outputPath), { recursive: true });
620
+ await writeFile(outputPath, `${textPayload}\n`);
621
+ }
622
+ if (!args.quiet) console.log(textPayload);
623
+ return payload;
624
+ }
625
+
626
+ async function ensureCliPromptReady(args) {
627
+ if (args.noBootstrap) return;
628
+ const config = await inspectCliConfig(argsPathOrDefault(args.cliConfig, defaultCliConfigPath()));
629
+ if (config.exists && config.has_model && config.has_coding_plan_api_key && config.config_shape_ok) return;
630
+ await bootstrapCliConfig({
631
+ provider: args.provider,
632
+ model: args.model,
633
+ liteModel: args.liteModel,
634
+ sourceConfig: args.sourceConfig,
635
+ cliConfig: args.cliConfig,
636
+ dryRun: false,
637
+ quiet: true,
638
+ });
639
+ }
640
+
641
+ function parseJsonOrText(raw) {
642
+ try {
643
+ return JSON.parse(raw);
644
+ } catch {
645
+ return raw;
646
+ }
647
+ }
648
+
649
+ function parseJsonObject(raw) {
650
+ const parsed = parseJsonOrText(raw);
651
+ return isRecord(parsed) ? parsed : null;
652
+ }
653
+
654
+ function finiteNumber(value) {
655
+ if (value === null || value === undefined || typeof value === "boolean") return null;
656
+ if (typeof value === "string" && value.trim() === "") return null;
657
+ const number = Number(value);
658
+ return Number.isFinite(number) ? number : null;
659
+ }
660
+
661
+ function firstFiniteNumber(...values) {
662
+ for (const value of values) {
663
+ const number = finiteNumber(value);
664
+ if (number !== null) return number;
665
+ }
666
+ return null;
667
+ }
668
+
669
+ function tailText(value, max = 2000) {
670
+ return String(value ?? "").slice(-max);
671
+ }
672
+
673
+ function normalizedZcodeUsageFromStdout(stdout) {
674
+ const payload = parseJsonObject(stdout);
675
+ const usage = isRecord(payload?.usage) ? payload.usage : null;
676
+ if (!usage) {
677
+ return {
678
+ payload,
679
+ usage: null,
680
+ normalized: null,
681
+ projection: isRecord(payload?.projection) ? payload.projection : null,
682
+ response: payload?.response ?? null,
683
+ };
684
+ }
685
+ const normalized = {
686
+ total_tokens: firstFiniteNumber(usage.totalTokens, usage.total_tokens, usage.tokens_total, usage.tokensUsed),
687
+ input_tokens: firstFiniteNumber(usage.inputTokens, usage.input_tokens),
688
+ output_tokens: firstFiniteNumber(usage.outputTokens, usage.output_tokens),
689
+ cache_read_tokens: firstFiniteNumber(usage.cacheReadTokens, usage.cache_read_tokens),
690
+ cache_write_tokens: firstFiniteNumber(usage.cacheWriteTokens, usage.cache_write_tokens),
691
+ };
692
+ return {
693
+ payload,
694
+ usage,
695
+ normalized,
696
+ projection: isRecord(payload?.projection) ? payload.projection : null,
697
+ response: payload?.response ?? null,
698
+ };
699
+ }
700
+
701
+ function usageSnapshotMode(args) {
702
+ const mode = String(
703
+ args.usageSnapshotSource ?? process.env.ZCODE_USAGE_SNAPSHOT_SOURCE ?? "auto",
704
+ ).trim().toLowerCase();
705
+ if (["off", "false", "0"].includes(mode)) return "none";
706
+ if (["auto", "zai-api", "codexbar", "none"].includes(mode)) return mode;
707
+ throw new Error("--usage-snapshot-source must be auto, zai-api, codexbar, or none");
708
+ }
709
+
710
+ function usageProvider(args) {
711
+ return args.usageProvider ?? process.env.ZCODE_USAGE_PROVIDER ?? DEFAULT_USAGE_PROVIDER;
712
+ }
713
+
714
+ function codexBarPath(args) {
715
+ return args.codexbarPath ?? process.env.CODEXBAR_PATH ?? "codexbar";
716
+ }
717
+
718
+ function zaiQuotaUrl(args) {
719
+ return args.zaiQuotaUrl ?? process.env.ZCODE_ZAI_QUOTA_URL ?? DEFAULT_ZAI_QUOTA_URL;
720
+ }
721
+
722
+ function isoFromEpochMillis(value) {
723
+ const number = finiteNumber(value);
724
+ if (number === null || number <= 0) return null;
725
+ const millis = number > 10_000_000_000 ? number : number * 1000;
726
+ const date = new Date(millis);
727
+ return Number.isNaN(date.getTime()) ? null : date.toISOString();
728
+ }
729
+
730
+ function providerApiKeyFromConfig(config, providerId) {
731
+ const providers = isRecord(config.provider) ? config.provider : {};
732
+ const provider = isRecord(providers[providerId]) ? providers[providerId] : {};
733
+ const options = isRecord(provider.options) ? provider.options : {};
734
+ const apiKey = typeof options.apiKey === "string" ? options.apiKey.trim() : "";
735
+ return apiKey || null;
736
+ }
737
+
738
+ async function launchctlGetenv(name) {
739
+ try {
740
+ const { stdout } = await execFileAsync("launchctl", ["getenv", name], {
741
+ maxBuffer: 1024 * 1024,
742
+ timeout: 5000,
743
+ });
744
+ return stdout.trim() || null;
745
+ } catch {
746
+ return null;
747
+ }
748
+ }
749
+
750
+ async function resolveZaiQuotaApiKey(args) {
751
+ const envKey = typeof process.env.ZAI_API_KEY === "string" ? process.env.ZAI_API_KEY.trim() : "";
752
+ if (envKey) return { ok: true, api_key: envKey, source: "env:ZAI_API_KEY" };
753
+ const launchctlKey = await launchctlGetenv("ZAI_API_KEY");
754
+ if (launchctlKey) return { ok: true, api_key: launchctlKey, source: "launchctl:ZAI_API_KEY" };
755
+ const cliConfigPath = argsPathOrDefault(args.cliConfig, defaultCliConfigPath());
756
+ try {
757
+ const cliConfig = await readJsonFileOrEmpty(cliConfigPath);
758
+ const apiKey = providerApiKeyFromConfig(cliConfig, "zai");
759
+ if (apiKey) return { ok: true, api_key: apiKey, source: "zcode-cli-config" };
760
+ } catch (error) {
761
+ return { ok: false, source: "zcode-cli-config", error: error.message };
762
+ }
763
+ return { ok: false, source: "zcode-cli-config", error: "ZAI_API_KEY not found" };
764
+ }
765
+
766
+ function normalizeCodexBarWindow(name, value) {
767
+ if (!isRecord(value)) return null;
768
+ return {
769
+ name,
770
+ used_percent: finiteNumber(value.usedPercent ?? value.used_percent),
771
+ reset_description: typeof value.resetDescription === "string" ? value.resetDescription : null,
772
+ resets_at: typeof value.resetsAt === "string" ? value.resetsAt : null,
773
+ window_minutes: firstFiniteNumber(value.windowMinutes, value.window_minutes),
774
+ };
775
+ }
776
+
777
+ function normalizeCodexBarPayload(payload, provider) {
778
+ const rows = Array.isArray(payload) ? payload : isRecord(payload) ? [payload] : [];
779
+ const row = rows.find((item) => item?.provider === provider) ?? rows[0] ?? null;
780
+ const usage = isRecord(row?.usage) ? row.usage : {};
781
+ const windows = {};
782
+ for (const name of ["primary", "secondary", "tertiary"]) {
783
+ const normalized = normalizeCodexBarWindow(name, usage[name]);
784
+ if (normalized) windows[name] = normalized;
785
+ }
786
+ const quotaCandidates = Object.values(windows)
787
+ .filter((window) => window.used_percent !== null)
788
+ .map((window) => ({
789
+ name: window.name,
790
+ value: window.used_percent,
791
+ line: `${window.name}.usedPercent (${window.reset_description ?? "quota window"})`,
792
+ }));
793
+ const best = quotaCandidates[0] ?? null;
794
+ return {
795
+ identity: isRecord(usage.identity) ? usage.identity : {},
796
+ codexbar_source: typeof row?.source === "string" ? row.source : null,
797
+ updated_at: typeof usage.updatedAt === "string" ? usage.updatedAt : null,
798
+ windows,
799
+ best: {
800
+ tokens_total: null,
801
+ tokens_line: null,
802
+ quota_percent: best?.value ?? null,
803
+ quota_percent_line: best?.line ?? null,
804
+ },
805
+ token_candidates: [],
806
+ quota_percent_candidates: quotaCandidates,
807
+ };
808
+ }
809
+
810
+ function normalizeZaiLimit(name, limit) {
811
+ if (!isRecord(limit)) return null;
812
+ const rawUsedPercent = finiteNumber(limit.percentage ?? limit.usedPercent ?? limit.used_percent);
813
+ const usage = finiteNumber(limit.usage);
814
+ const remaining = finiteNumber(limit.remaining);
815
+ const tokenCountsAvailable = usage !== null && remaining !== null;
816
+ const authoritative = rawUsedPercent !== null;
817
+ const unavailableReason = rawUsedPercent === null ? "zai_limit_missing_percentage" : null;
818
+ return {
819
+ name,
820
+ type: typeof limit.type === "string" ? limit.type : null,
821
+ used_percent: rawUsedPercent,
822
+ raw_used_percent: rawUsedPercent,
823
+ non_authoritative_used_percent: null,
824
+ authoritative,
825
+ token_counts_available: tokenCountsAvailable,
826
+ quota_percent_unavailable_reason: unavailableReason,
827
+ reset_description: name === "primary" ? "Tokens limit" : "Time limit",
828
+ resets_at: isoFromEpochMillis(limit.nextResetTime ?? limit.resetsAt ?? limit.resets_at),
829
+ usage,
830
+ remaining,
831
+ };
832
+ }
833
+
834
+ function normalizeZaiApiPayload(payload, provider) {
835
+ const data = isRecord(payload?.data) ? payload.data : {};
836
+ const limits = Array.isArray(data.limits) ? data.limits : [];
837
+ const byType = new Map(limits.filter(isRecord).map((limit) => [limit.type, limit]));
838
+ const candidates = [
839
+ ["primary", byType.get("TOKENS_LIMIT")],
840
+ ["secondary", byType.get("TIME_LIMIT")],
841
+ ];
842
+ const windows = {};
843
+ for (const [name, limit] of candidates) {
844
+ const normalized = normalizeZaiLimit(name, limit);
845
+ if (normalized) windows[name] = normalized;
846
+ }
847
+ const primaryWindow = windows.primary ?? null;
848
+ const quotaCandidates = primaryWindow && primaryWindow.used_percent !== null
849
+ ? [
850
+ {
851
+ name: primaryWindow.name,
852
+ value: primaryWindow.used_percent,
853
+ line: `${primaryWindow.type ?? primaryWindow.name}.percentage (${primaryWindow.reset_description})`,
854
+ },
855
+ ]
856
+ : [];
857
+ const rawQuotaCandidates = Object.values(windows)
858
+ .filter((window) => window.raw_used_percent !== null)
859
+ .map((window) => ({
860
+ name: window.name,
861
+ value: window.raw_used_percent,
862
+ line: `${window.type ?? window.name}.percentage (${window.reset_description})`,
863
+ authoritative: window.authoritative !== false,
864
+ unavailable_reason: window.quota_percent_unavailable_reason ?? null,
865
+ }));
866
+ const best = quotaCandidates[0] ?? null;
867
+ const rawBest = rawQuotaCandidates[0] ?? null;
868
+ return {
869
+ identity: { providerID: provider },
870
+ plan: typeof data.level === "string" ? data.level : null,
871
+ windows,
872
+ best: {
873
+ tokens_total: null,
874
+ tokens_line: null,
875
+ quota_percent: best?.value ?? null,
876
+ quota_percent_line: best?.line ?? null,
877
+ raw_quota_percent: rawBest?.value ?? null,
878
+ quota_percent_authoritative: Boolean(best),
879
+ quota_percent_unavailable_reason: best ? null : primaryWindow?.quota_percent_unavailable_reason ?? null,
880
+ },
881
+ token_candidates: [],
882
+ quota_percent_candidates: quotaCandidates,
883
+ quota_percent_raw_candidates: rawQuotaCandidates,
884
+ };
885
+ }
886
+
887
+ async function captureCodexBarUsageSnapshot(args, phase) {
888
+ const mode = usageSnapshotMode(args);
889
+ const provider = usageProvider(args);
890
+ if (mode === "none") {
891
+ return { ok: false, phase, source: "none", provider, reason: "disabled" };
892
+ }
893
+ const startedAt = new Date().toISOString();
894
+ const command = codexBarPath(args);
895
+ const timeoutMs = positiveIntOrDefault(args.usageSnapshotTimeoutMs, DEFAULT_USAGE_SNAPSHOT_TIMEOUT_MS);
896
+ try {
897
+ const { stdout, stderr } = await execFileAsync(
898
+ command,
899
+ ["usage", "--provider", provider, "--format", "json"],
900
+ {
901
+ maxBuffer: 5 * 1024 * 1024,
902
+ timeout: timeoutMs,
903
+ },
904
+ );
905
+ const payload = JSON.parse(stdout);
906
+ return {
907
+ ok: true,
908
+ phase,
909
+ source: "codexbar",
910
+ provider,
911
+ captured_at: startedAt,
912
+ command,
913
+ stderr_tail: tailText(stderr, 1000),
914
+ raw: payload,
915
+ ...normalizeCodexBarPayload(payload, provider),
916
+ };
917
+ } catch (error) {
918
+ const isMissing = error.code === "ENOENT";
919
+ return {
920
+ ok: false,
921
+ phase,
922
+ source: "codexbar",
923
+ provider,
924
+ captured_at: startedAt,
925
+ command,
926
+ error_type: isMissing ? "not_found" : "command_failed",
927
+ exit_code: typeof error.code === "number" ? error.code : null,
928
+ message: error.message,
929
+ stdout_tail: tailText(error.stdout, 1000),
930
+ stderr_tail: tailText(error.stderr, 1000),
931
+ auto_mode: mode === "auto",
932
+ };
933
+ }
934
+ }
935
+
936
+ function snapshotFailure({ phase, source, provider, startedAt, errorType, message, extra = {} }) {
937
+ return {
938
+ ok: false,
939
+ phase,
940
+ source,
941
+ provider,
942
+ captured_at: startedAt,
943
+ error_type: errorType,
944
+ message,
945
+ ...extra,
946
+ };
947
+ }
948
+
949
+ async function captureZaiApiUsageSnapshot(args, phase) {
950
+ const provider = usageProvider(args);
951
+ const startedAt = new Date().toISOString();
952
+ if (provider !== "zai") {
953
+ return snapshotFailure({
954
+ phase,
955
+ source: "zai-api",
956
+ provider,
957
+ startedAt,
958
+ errorType: "unsupported_provider",
959
+ message: "Z.AI quota API snapshots require --usage-provider zai",
960
+ });
961
+ }
962
+ const credential = await resolveZaiQuotaApiKey(args);
963
+ if (!credential.ok) {
964
+ return snapshotFailure({
965
+ phase,
966
+ source: "zai-api",
967
+ provider,
968
+ startedAt,
969
+ errorType: "missing_api_key",
970
+ message: credential.error,
971
+ extra: { credential_source: credential.source },
972
+ });
973
+ }
974
+ return fetchZaiApiUsageSnapshot(args, phase, provider, startedAt, credential);
975
+ }
976
+
977
+ async function fetchZaiApiUsageSnapshot(args, phase, provider, startedAt, credential) {
978
+ const timeoutMs = positiveIntOrDefault(args.usageSnapshotTimeoutMs, DEFAULT_USAGE_SNAPSHOT_TIMEOUT_MS);
979
+ const url = zaiQuotaUrl(args);
980
+ const controller = new AbortController();
981
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
982
+ try {
983
+ const response = await fetch(url, {
984
+ headers: {
985
+ Authorization: `Bearer ${credential.api_key}`,
986
+ "Content-Type": "application/json",
987
+ },
988
+ signal: controller.signal,
989
+ });
990
+ const text = await response.text();
991
+ const payload = parseJsonOrText(text);
992
+ if (!response.ok) {
993
+ return snapshotFailure({
994
+ phase,
995
+ source: "zai-api",
996
+ provider,
997
+ startedAt,
998
+ errorType: "http_error",
999
+ message: `HTTP ${response.status}`,
1000
+ extra: { status_code: response.status, body_tail: tailText(text, 1000) },
1001
+ });
1002
+ }
1003
+ if (!isRecord(payload)) {
1004
+ return snapshotFailure({
1005
+ phase,
1006
+ source: "zai-api",
1007
+ provider,
1008
+ startedAt,
1009
+ errorType: "invalid_json",
1010
+ message: "Z.AI quota API response was not a JSON object",
1011
+ });
1012
+ }
1013
+ if (payload.code !== undefined && payload.code !== 200) {
1014
+ return snapshotFailure({
1015
+ phase,
1016
+ source: "zai-api",
1017
+ provider,
1018
+ startedAt,
1019
+ errorType: "api_error",
1020
+ message: `Z.AI quota API returned code ${payload.code}`,
1021
+ extra: { body_tail: tailText(text, 1000) },
1022
+ });
1023
+ }
1024
+ return {
1025
+ ok: true,
1026
+ phase,
1027
+ source: "zai-api",
1028
+ provider,
1029
+ captured_at: startedAt,
1030
+ credential_source: credential.source,
1031
+ raw: payload,
1032
+ ...normalizeZaiApiPayload(payload, provider),
1033
+ };
1034
+ } catch (error) {
1035
+ return snapshotFailure({
1036
+ phase,
1037
+ source: "zai-api",
1038
+ provider,
1039
+ startedAt,
1040
+ errorType: error.name === "AbortError" ? "timeout" : "request_failed",
1041
+ message: error.message,
1042
+ });
1043
+ } finally {
1044
+ clearTimeout(timer);
1045
+ }
1046
+ }
1047
+
1048
+ async function captureUsageSnapshot(args, phase) {
1049
+ const mode = usageSnapshotMode(args);
1050
+ const provider = usageProvider(args);
1051
+ if (mode === "none") return { ok: false, phase, source: "none", provider, reason: "disabled" };
1052
+ if (mode === "zai-api") return captureZaiApiUsageSnapshot(args, phase);
1053
+ if (mode === "codexbar") return captureCodexBarUsageSnapshot(args, phase);
1054
+ const direct = await captureZaiApiUsageSnapshot(args, phase);
1055
+ if (direct.ok) return direct;
1056
+ const fallback = await captureCodexBarUsageSnapshot(args, phase);
1057
+ return fallback.ok
1058
+ ? { ...fallback, fallback_from: { source: direct.source, error_type: direct.error_type, message: direct.message } }
1059
+ : { ...fallback, fallback_from: direct };
1060
+ }
1061
+
1062
+ function deriveQuotaWindowDelta(beforeWindow, afterWindow) {
1063
+ const before = finiteNumber(beforeWindow?.used_percent);
1064
+ const after = finiteNumber(afterWindow?.used_percent);
1065
+ const beforeAuthoritative = beforeWindow?.authoritative !== false;
1066
+ const afterAuthoritative = afterWindow?.authoritative !== false;
1067
+ const authoritative = beforeAuthoritative && afterAuthoritative;
1068
+ const resetChanged = Boolean(beforeWindow?.resets_at && afterWindow?.resets_at && beforeWindow.resets_at !== afterWindow.resets_at);
1069
+ const rawDelta = before !== null && after !== null ? after - before : null;
1070
+ const unavailableReason = !authoritative
1071
+ ? beforeWindow?.quota_percent_unavailable_reason
1072
+ ?? afterWindow?.quota_percent_unavailable_reason
1073
+ ?? "quota_percent_window_non_authoritative"
1074
+ : before === null || after === null
1075
+ ? "quota_percent_window_missing_used_percent"
1076
+ : resetChanged
1077
+ ? "quota_window_reset_changed"
1078
+ : rawDelta < 0
1079
+ ? "quota_percent_delta_negative"
1080
+ : null;
1081
+ const usedDelta = rawDelta !== null && rawDelta >= 0 && !resetChanged && authoritative
1082
+ ? Number(rawDelta.toFixed(4))
1083
+ : null;
1084
+ return {
1085
+ before_used_percent: before,
1086
+ after_used_percent: after,
1087
+ before_raw_used_percent: finiteNumber(beforeWindow?.raw_used_percent),
1088
+ after_raw_used_percent: finiteNumber(afterWindow?.raw_used_percent),
1089
+ used_percent_delta: usedDelta,
1090
+ authoritative,
1091
+ quota_percent_unavailable_reason: usedDelta === null ? unavailableReason : null,
1092
+ reset_changed: resetChanged,
1093
+ before_resets_at: beforeWindow?.resets_at ?? null,
1094
+ after_resets_at: afterWindow?.resets_at ?? null,
1095
+ reset_description: afterWindow?.reset_description ?? beforeWindow?.reset_description ?? null,
1096
+ };
1097
+ }
1098
+
1099
+ function deriveQuotaUsage(beforeSnapshot, afterSnapshot) {
1100
+ const available = Boolean(beforeSnapshot?.ok && afterSnapshot?.ok);
1101
+ const sources = new Set([beforeSnapshot?.source, afterSnapshot?.source].filter(Boolean));
1102
+ const result = {
1103
+ available,
1104
+ source: available ? (sources.size === 1 ? [...sources][0] : "mixed") : null,
1105
+ provider: afterSnapshot?.provider ?? beforeSnapshot?.provider ?? null,
1106
+ quota_percent_direction: "used",
1107
+ quota_percent_before: null,
1108
+ quota_percent_after: null,
1109
+ quota_percent_used: null,
1110
+ quota_percent_status: "unavailable",
1111
+ quota_percent_unavailable_reason: available ? null : "usage_snapshot_unavailable",
1112
+ windows: {},
1113
+ };
1114
+ if (!available) return result;
1115
+ const names = new Set([
1116
+ ...Object.keys(beforeSnapshot.windows ?? {}),
1117
+ ...Object.keys(afterSnapshot.windows ?? {}),
1118
+ ]);
1119
+ for (const name of names) {
1120
+ result.windows[name] = deriveQuotaWindowDelta(
1121
+ beforeSnapshot.windows?.[name],
1122
+ afterSnapshot.windows?.[name],
1123
+ );
1124
+ }
1125
+ const primary = result.windows.primary ?? Object.values(result.windows)[0] ?? null;
1126
+ if (primary) {
1127
+ result.quota_percent_before = primary.before_used_percent;
1128
+ result.quota_percent_after = primary.after_used_percent;
1129
+ result.quota_percent_used = primary.used_percent_delta;
1130
+ result.quota_percent_status = primary.used_percent_delta !== null ? "measured" : "unavailable";
1131
+ result.quota_percent_unavailable_reason = primary.used_percent_delta !== null
1132
+ ? null
1133
+ : primary.quota_percent_unavailable_reason ?? "quota_percent_delta_unavailable";
1134
+ } else {
1135
+ result.quota_percent_unavailable_reason = "quota_percent_window_unavailable";
1136
+ }
1137
+ return result;
1138
+ }
1139
+
1140
+ function missingUsageReason(finalResult) {
1141
+ if (finalResult?.provider_error) return "provider_error_without_zcode_cli_usage";
1142
+ if (finalResult?.cli_ok === false || finalResult?.ok === false) return "zcode_cli_result_missing_usage";
1143
+ return "zcode_cli_usage_missing";
1144
+ }
1145
+
1146
+ function buildUsageAccounting(finalResult, beforeSnapshot, afterSnapshot) {
1147
+ const runUsage = normalizedZcodeUsageFromStdout(finalResult?.stdout ?? "");
1148
+ const quota = deriveQuotaUsage(beforeSnapshot, afterSnapshot);
1149
+ const usageAvailable = Boolean(runUsage.normalized);
1150
+ return {
1151
+ usage_available: usageAvailable,
1152
+ no_usage_reason: usageAvailable ? null : missingUsageReason(finalResult),
1153
+ tokens_source: runUsage.normalized ? "zcode_cli_json_usage" : null,
1154
+ tokens_used: runUsage.normalized?.total_tokens ?? null,
1155
+ tokens_total: runUsage.normalized?.total_tokens ?? null,
1156
+ input_tokens: runUsage.normalized?.input_tokens ?? null,
1157
+ output_tokens: runUsage.normalized?.output_tokens ?? null,
1158
+ cache_read_tokens: runUsage.normalized?.cache_read_tokens ?? null,
1159
+ cache_write_tokens: runUsage.normalized?.cache_write_tokens ?? null,
1160
+ quota_source: quota.source,
1161
+ quota_provider: quota.provider,
1162
+ quota_percent_direction: quota.quota_percent_direction,
1163
+ quota_percent_before: quota.quota_percent_before,
1164
+ quota_percent_after: quota.quota_percent_after,
1165
+ quota_percent_used: quota.quota_percent_used,
1166
+ quota_percent_status: quota.quota_percent_status,
1167
+ quota_percent_unavailable_reason: quota.quota_percent_unavailable_reason,
1168
+ quota_windows: quota.windows,
1169
+ };
1170
+ }
1171
+
1172
+ function mapMode(mode) {
1173
+ const mapping = {
1174
+ "Plan": "plan",
1175
+ "Auto Edit": "edit",
1176
+ "Full Access": "yolo",
1177
+ "Confirm Before Changes": "build",
1178
+ };
1179
+ return mapping[mode] ?? mode ?? "plan";
1180
+ }
1181
+
1182
+ function positiveIntOrDefault(value, fallback) {
1183
+ const parsed = Math.trunc(Number(value));
1184
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
1185
+ }
1186
+
1187
+ function nonNegativeIntOrDefault(value, fallback) {
1188
+ const parsed = Math.trunc(Number(value));
1189
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
1190
+ }
1191
+
1192
+ function resolveWorkspacePath(workspace, relativePath) {
1193
+ const root = resolve(workspace);
1194
+ const absolute = resolve(root, relativePath);
1195
+ if (absolute !== root && !absolute.startsWith(`${root}/`)) {
1196
+ throw new Error(`packet vision image escapes workspace: ${relativePath}`);
1197
+ }
1198
+ return absolute;
1199
+ }
1200
+
1201
+ function isSecretLikePath(path) {
1202
+ const lowered = String(path).toLowerCase();
1203
+ return SECRET_PATH_NEEDLES.some((needle) => lowered.includes(needle));
1204
+ }
1205
+
1206
+ function resolveVisionAttachment(workspace, relativePath) {
1207
+ if (!relativePath || typeof relativePath !== "string") {
1208
+ throw new Error("packet vision image path must be a non-empty string");
1209
+ }
1210
+ if (isSecretLikePath(relativePath)) {
1211
+ throw new Error(`secret-like vision attachment is not allowed: ${relativePath}`);
1212
+ }
1213
+ const absolute = resolveWorkspacePath(workspace, relativePath);
1214
+ if (isSecretLikePath(absolute)) {
1215
+ throw new Error(`secret-like vision attachment is not allowed: ${relativePath}`);
1216
+ }
1217
+ if (!IMAGE_EXTENSIONS.has(extname(absolute).toLowerCase())) {
1218
+ throw new Error(`packet vision attachment must be an image file: ${relativePath}`);
1219
+ }
1220
+ return absolute;
1221
+ }
1222
+
1223
+ function packetVision(packet, workspace) {
1224
+ const rawVision = isRecord(packet.vision) ? packet.vision : {};
1225
+ const imageFiles = Array.isArray(rawVision.image_files)
1226
+ ? rawVision.image_files.filter((item) => typeof item === "string")
1227
+ : [];
1228
+ const service = typeof rawVision.service === "string" && rawVision.service.trim()
1229
+ ? rawVision.service.trim()
1230
+ : DEFAULT_VISION_SERVICE;
1231
+ return {
1232
+ required: Boolean(rawVision.required),
1233
+ service,
1234
+ image_files: imageFiles,
1235
+ attached_files: imageFiles.map((item) => resolveVisionAttachment(workspace, item)),
1236
+ model_limit: rawVision.model_limit ?? null,
1237
+ };
1238
+ }
1239
+
1240
+ async function missingVisionAttachment(vision) {
1241
+ for (const path of vision.attached_files) {
1242
+ if (!(await pathExists(path))) return path;
1243
+ }
1244
+ return null;
1245
+ }
1246
+
1247
+ async function normalizeVisionAttachmentTargets(vision, workspace) {
1248
+ const realWorkspace = await realpath(workspace);
1249
+ const attachedFiles = [];
1250
+ for (let index = 0; index < vision.attached_files.length; index += 1) {
1251
+ const path = vision.attached_files[index];
1252
+ const rel = vision.image_files[index] ?? path;
1253
+ const target = await realpath(path);
1254
+ if (target !== realWorkspace && !target.startsWith(`${realWorkspace}/`)) {
1255
+ throw new Error(`packet vision attachment target escapes workspace: ${rel}`);
1256
+ }
1257
+ if (isSecretLikePath(target)) {
1258
+ throw new Error(`secret-like vision attachment target is not allowed: ${rel}`);
1259
+ }
1260
+ if (!IMAGE_EXTENSIONS.has(extname(target).toLowerCase())) {
1261
+ throw new Error(`packet vision attachment target must be an image file: ${rel}`);
1262
+ }
1263
+ attachedFiles.push(target);
1264
+ }
1265
+ return {
1266
+ ...vision,
1267
+ attached_files: attachedFiles,
1268
+ };
1269
+ }
1270
+
1271
+ async function readFileHeader(path, byteCount = 16) {
1272
+ const handle = await open(path, "r");
1273
+ try {
1274
+ const buffer = Buffer.alloc(byteCount);
1275
+ const { bytesRead } = await handle.read(buffer, 0, byteCount, 0);
1276
+ return buffer.subarray(0, bytesRead);
1277
+ } finally {
1278
+ await handle.close();
1279
+ }
1280
+ }
1281
+
1282
+ function hasImageSignature(path, header) {
1283
+ const extension = extname(path).toLowerCase();
1284
+ if (extension === ".png") {
1285
+ return header.length >= 8 && header.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
1286
+ }
1287
+ if (extension === ".jpg" || extension === ".jpeg") {
1288
+ return header.length >= 3 && header[0] === 0xFF && header[1] === 0xD8 && header[2] === 0xFF;
1289
+ }
1290
+ if (extension === ".gif") {
1291
+ const marker = header.subarray(0, 6).toString("ascii");
1292
+ return marker === "GIF87a" || marker === "GIF89a";
1293
+ }
1294
+ if (extension === ".webp") {
1295
+ return header.length >= 12
1296
+ && header.subarray(0, 4).toString("ascii") === "RIFF"
1297
+ && header.subarray(8, 12).toString("ascii") === "WEBP";
1298
+ }
1299
+ if (extension === ".bmp") {
1300
+ return header.length >= 2 && header[0] === 0x42 && header[1] === 0x4D;
1301
+ }
1302
+ return false;
1303
+ }
1304
+
1305
+ async function invalidVisionAttachmentContent(vision) {
1306
+ for (let index = 0; index < vision.attached_files.length; index += 1) {
1307
+ const path = vision.attached_files[index];
1308
+ const header = await readFileHeader(path);
1309
+ if (!hasImageSignature(path, header)) {
1310
+ return `packet vision attachment is not a valid image file: ${vision.image_files[index] ?? path}`;
1311
+ }
1312
+ }
1313
+ return null;
1314
+ }
1315
+
1316
+ function shouldRunVisionPreflight(mode, vision) {
1317
+ if (mode === "off") return false;
1318
+ if (mode === "required") return true;
1319
+ return vision.required;
1320
+ }
1321
+
1322
+ async function buildVisionRunEnv(args, vision, visionPreflight) {
1323
+ if (!vision.required || !visionPreflight?.ok) return { env: {}, credential_source: null };
1324
+ if (process.env.Z_AI_API_KEY || process.env.ZAI_API_KEY) {
1325
+ const legacyKey = process.env.Z_AI_API_KEY ? null : process.env.ZAI_API_KEY;
1326
+ return {
1327
+ env: {
1328
+ Z_AI_MODE: process.env.Z_AI_MODE ?? "ZAI",
1329
+ ...(legacyKey ? { Z_AI_API_KEY: legacyKey } : {}),
1330
+ },
1331
+ credential_source: process.env.Z_AI_API_KEY ? "env:Z_AI_API_KEY" : "env:ZAI_API_KEY",
1332
+ };
1333
+ }
1334
+ const credential = await resolveZaiQuotaApiKey(args);
1335
+ if (!credential.ok || !credential.api_key) {
1336
+ return { env: {}, credential_source: null, error: credential.error ?? "Z_AI_API_KEY not found" };
1337
+ }
1338
+ return {
1339
+ env: {
1340
+ Z_AI_MODE: process.env.Z_AI_MODE ?? "ZAI",
1341
+ Z_AI_API_KEY: credential.api_key,
1342
+ },
1343
+ credential_source: credential.source,
1344
+ };
1345
+ }
1346
+
1347
+ async function printJsonPayload(payload, out) {
1348
+ const textPayload = JSON.stringify(payload, null, 2);
1349
+ if (out) {
1350
+ const outputPath = resolve(out);
1351
+ await mkdir(dirname(outputPath), { recursive: true });
1352
+ await writeFile(outputPath, `${textPayload}\n`);
1353
+ }
1354
+ console.log(textPayload);
1355
+ if (!payload.ok) process.exitCode = payload.exit_code ?? 1;
1356
+ }
1357
+
1358
+ function buildPromptArgs(args, promptText, workspace) {
1359
+ const cliArgs = ["--cwd", workspace, "--prompt", promptText, "--mode", mapMode(args.mode), "--no-color"];
1360
+ if (args.json) cliArgs.push("--json");
1361
+ if (args.continue) cliArgs.push("--continue");
1362
+ if (args.resume) cliArgs.push("--resume", args.resume);
1363
+ if (args.target) cliArgs.push("--target", args.target);
1364
+ if (args.targetReplace) cliArgs.push("--target-replace");
1365
+ for (const item of args.attach ?? []) cliArgs.push("--attach", resolve(item));
1366
+ return cliArgs;
1367
+ }
1368
+
1369
+ async function runSupervisorJson(supervisorArgs, options = {}) {
1370
+ try {
1371
+ const { stdout, stderr } = await execFileAsync("python3", [SUPERVISOR_SCRIPT, ...supervisorArgs], {
1372
+ cwd: options.cwd ?? process.cwd(),
1373
+ maxBuffer: 25 * 1024 * 1024,
1374
+ timeout: options.timeoutMs ?? undefined,
1375
+ });
1376
+ const payload = parseJsonOrText(stdout);
1377
+ return isRecord(payload) ? { ...payload, command_exit_code: 0, command_stderr: stderr } : payload;
1378
+ } catch (error) {
1379
+ const payload = parseJsonOrText(error.stdout ?? "");
1380
+ if (isRecord(payload)) {
1381
+ return {
1382
+ ...payload,
1383
+ command_exit_code: typeof error.code === "number" ? error.code : 1,
1384
+ command_stderr: error.stderr ?? "",
1385
+ };
1386
+ }
1387
+ throw error;
1388
+ }
1389
+ }
1390
+
1391
+ async function createRunPacketSnapshot(workspace) {
1392
+ const snapshotPath = join(tmpdir(), `zcode-run-packet-${process.pid}-${randomUUID()}.json`);
1393
+ await runSupervisorJson(["snapshot", "--workspace", workspace, "--out", snapshotPath], { cwd: workspace });
1394
+ return snapshotPath;
1395
+ }
1396
+
1397
+ async function auditRunPacketAttempt({ workspace, packetPath, snapshotPath, validationTimeout }) {
1398
+ return runSupervisorJson(
1399
+ [
1400
+ "audit",
1401
+ "--workspace",
1402
+ workspace,
1403
+ "--snapshot",
1404
+ snapshotPath,
1405
+ "--packet",
1406
+ packetPath,
1407
+ "--validation-timeout",
1408
+ String(validationTimeout ?? 60),
1409
+ ],
1410
+ { cwd: workspace },
1411
+ );
1412
+ }
1413
+
1414
+ function compactAttemptRecord(attempt, index) {
1415
+ const runUsage = normalizedZcodeUsageFromStdout(attempt.stdout ?? "");
1416
+ const usageAvailable = Boolean(runUsage.normalized);
1417
+ return {
1418
+ attempt: index + 1,
1419
+ cli_ok: attempt.cli_ok,
1420
+ exit_code: attempt.exit_code,
1421
+ provider_error: attempt.provider_error,
1422
+ provider_code: attempt.provider_code,
1423
+ provider_message: attempt.provider_message,
1424
+ provider_id: attempt.provider_id,
1425
+ provider_kind: attempt.provider_kind,
1426
+ retryable_provider_error: attempt.retryable_provider_error,
1427
+ usage_available: usageAvailable,
1428
+ no_usage_reason: usageAvailable ? null : missingUsageReason(attempt),
1429
+ tokens_total: runUsage.normalized?.total_tokens ?? null,
1430
+ supervisor_state: attempt.supervisor_state,
1431
+ changed_count: attempt.audit?.changed_count ?? null,
1432
+ validation_ok: attempt.audit?.validation?.ok ?? null,
1433
+ audit_ok: attempt.audit?.ok ?? null,
1434
+ };
1435
+ }
1436
+
1437
+ function sleep(ms) {
1438
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
1439
+ }
1440
+
1441
+ async function cliPrompt(args) {
1442
+ await ensureCliPromptReady(args);
1443
+ const promptText = await readPrompt(args);
1444
+ const workspace = resolve(args.workspace ?? process.cwd());
1445
+ const cliArgs = buildPromptArgs(args, promptText, workspace);
1446
+ const result = await runZcodeCli(cliArgs, {
1447
+ cwd: workspace,
1448
+ timeoutMs: args.timeoutMs ?? PROMPT_TIMEOUT_MS,
1449
+ });
1450
+ await printCliResult(result, args.out);
1451
+ }
1452
+
1453
+ async function visionPreflightCommand(args) {
1454
+ const workspace = resolve(args.workspace ?? process.cwd());
1455
+ const serviceName = args.visionService ?? DEFAULT_VISION_SERVICE;
1456
+ const payload = await inspectVisionServices(args, workspace, serviceName);
1457
+ await printJsonPayload(payload, args.out);
1458
+ }
1459
+
1460
+ async function runPacket(args) {
1461
+ if (!args.packet) throw new Error("--packet is required");
1462
+ const packetPath = resolve(args.packet);
1463
+ const packet = JSON.parse(await readFile(packetPath, "utf8"));
1464
+ if (!packet.prompt) throw new Error(`packet is missing prompt: ${packetPath}`);
1465
+ const workspace = resolve(packet.workspace);
1466
+ let vision;
1467
+ try {
1468
+ vision = packetVision(packet, workspace);
1469
+ } catch (error) {
1470
+ await printJsonPayload(
1471
+ {
1472
+ ok: false,
1473
+ exit_code: 1,
1474
+ status: "vision_attachment_invalid",
1475
+ supervisor_state: "vision_attachment_invalid",
1476
+ packet: packetPath,
1477
+ workspace,
1478
+ error: error.message,
1479
+ },
1480
+ args.out,
1481
+ );
1482
+ return;
1483
+ }
1484
+ const missingAttachment = await missingVisionAttachment(vision);
1485
+ if (missingAttachment) {
1486
+ await printJsonPayload(
1487
+ {
1488
+ ok: false,
1489
+ exit_code: 1,
1490
+ status: "vision_attachment_missing",
1491
+ supervisor_state: "vision_attachment_missing",
1492
+ packet: packetPath,
1493
+ workspace,
1494
+ vision,
1495
+ error: `vision attachment does not exist: ${missingAttachment}`,
1496
+ },
1497
+ args.out,
1498
+ );
1499
+ return;
1500
+ }
1501
+ try {
1502
+ vision = await normalizeVisionAttachmentTargets(vision, workspace);
1503
+ } catch (error) {
1504
+ await printJsonPayload(
1505
+ {
1506
+ ok: false,
1507
+ exit_code: 1,
1508
+ status: "vision_attachment_invalid",
1509
+ supervisor_state: "vision_attachment_invalid",
1510
+ packet: packetPath,
1511
+ workspace,
1512
+ vision,
1513
+ error: error.message,
1514
+ },
1515
+ args.out,
1516
+ );
1517
+ return;
1518
+ }
1519
+ const invalidAttachment = await invalidVisionAttachmentContent(vision);
1520
+ if (invalidAttachment) {
1521
+ await printJsonPayload(
1522
+ {
1523
+ ok: false,
1524
+ exit_code: 1,
1525
+ status: "vision_attachment_invalid",
1526
+ supervisor_state: "vision_attachment_invalid",
1527
+ packet: packetPath,
1528
+ workspace,
1529
+ vision,
1530
+ error: invalidAttachment,
1531
+ },
1532
+ args.out,
1533
+ );
1534
+ return;
1535
+ }
1536
+ const visionPreflightMode = args.visionPreflight ?? "auto";
1537
+ if (!["auto", "required", "off"].includes(visionPreflightMode)) {
1538
+ throw new Error("--vision-preflight must be auto, required, or off");
1539
+ }
1540
+ const visionPreflight = shouldRunVisionPreflight(visionPreflightMode, vision)
1541
+ ? await inspectVisionServices(args, workspace, vision.service)
1542
+ : null;
1543
+ const visionPreflightRequired = vision.required || visionPreflightMode === "required";
1544
+ if (visionPreflightRequired && visionPreflightMode !== "off" && visionPreflight && !visionPreflight.ok) {
1545
+ await printJsonPayload(
1546
+ {
1547
+ ok: false,
1548
+ cli_ok: false,
1549
+ exit_code: 1,
1550
+ status: "vision_service_unavailable",
1551
+ supervisor_state: "vision_service_unavailable",
1552
+ packet: packetPath,
1553
+ workspace,
1554
+ vision,
1555
+ vision_preflight: visionPreflight,
1556
+ next_action: visionPreflight.next_action,
1557
+ },
1558
+ args.out,
1559
+ );
1560
+ return;
1561
+ }
1562
+ const visionRunEnv = await buildVisionRunEnv(args, vision, visionPreflight);
1563
+ if (vision.required && visionPreflight?.ok && visionRunEnv.error) {
1564
+ await printJsonPayload(
1565
+ {
1566
+ ok: false,
1567
+ cli_ok: false,
1568
+ exit_code: 1,
1569
+ status: "vision_service_credentials_unavailable",
1570
+ supervisor_state: "vision_service_credentials_unavailable",
1571
+ packet: packetPath,
1572
+ workspace,
1573
+ vision,
1574
+ vision_preflight: visionPreflight,
1575
+ error: visionRunEnv.error,
1576
+ next_action: "Set Z_AI_API_KEY or configure the Z.AI API key in the ZCode CLI config before required image-understanding tasks.",
1577
+ },
1578
+ args.out,
1579
+ );
1580
+ return;
1581
+ }
1582
+ await ensureCliPromptReady(args);
1583
+ const cliArgs = buildPromptArgs(
1584
+ {
1585
+ ...args,
1586
+ mode: args.mode ?? packet.mode,
1587
+ text: packet.prompt,
1588
+ textFile: undefined,
1589
+ attach: [...(args.attach ?? []), ...vision.attached_files],
1590
+ },
1591
+ packet.prompt,
1592
+ workspace,
1593
+ );
1594
+ const maxAttempts = positiveIntOrDefault(args.maxAttempts, DEFAULT_PROVIDER_MAX_ATTEMPTS);
1595
+ const retryDelayMs = nonNegativeIntOrDefault(args.retryDelayMs, DEFAULT_PROVIDER_RETRY_DELAY_MS);
1596
+ const snapshotPath = await createRunPacketSnapshot(workspace);
1597
+ const usageBefore = await captureUsageSnapshot(args, "before");
1598
+ const attempts = [];
1599
+ const retryDelaysMs = [];
1600
+ let finalResult = null;
1601
+
1602
+ try {
1603
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
1604
+ const result = await runZcodeCli(cliArgs, {
1605
+ cwd: workspace,
1606
+ timeoutMs: args.timeoutMs ?? PROMPT_TIMEOUT_MS,
1607
+ env: visionRunEnv.env,
1608
+ });
1609
+ const audit = await auditRunPacketAttempt({
1610
+ workspace,
1611
+ packetPath,
1612
+ snapshotPath,
1613
+ validationTimeout: args.validationTimeout,
1614
+ });
1615
+ const state = classifyProviderRunState({ cliOk: result.cli_ok, provider: result, audit });
1616
+ const attemptResult = { ...result, audit, ...state };
1617
+ attempts.push(attemptResult);
1618
+ const shouldRetry = (
1619
+ state.supervisor_state === "retryable_provider_error" &&
1620
+ attempt < maxAttempts
1621
+ );
1622
+ if (shouldRetry) {
1623
+ retryDelaysMs.push(retryDelayMs);
1624
+ if (retryDelayMs > 0) await sleep(retryDelayMs);
1625
+ continue;
1626
+ }
1627
+ finalResult = attemptResult;
1628
+ break;
1629
+ }
1630
+
1631
+ const usageAfter = await captureUsageSnapshot(args, "after");
1632
+ const runUsage = normalizedZcodeUsageFromStdout(finalResult.stdout ?? "");
1633
+ const usageAccounting = buildUsageAccounting(finalResult, usageBefore, usageAfter);
1634
+ const finalOk = ["success", "partial_success"].includes(finalResult.supervisor_state);
1635
+ await printCliResult(
1636
+ {
1637
+ ...finalResult,
1638
+ ok: finalOk,
1639
+ packet: packetPath,
1640
+ workspace,
1641
+ mode: mapMode(args.mode ?? packet.mode),
1642
+ prompt_chars: packet.prompt.length,
1643
+ vision,
1644
+ vision_preflight: visionPreflight,
1645
+ vision_service_credential_source: visionRunEnv.credential_source,
1646
+ status: finalResult.supervisor_state,
1647
+ attempts: attempts.length,
1648
+ attempt_count: attempts.length,
1649
+ retry_count: Math.max(0, attempts.length - 1),
1650
+ retry_delays_ms: retryDelaysMs,
1651
+ max_attempts: maxAttempts,
1652
+ safe_to_retry_later: finalResult.safe_to_retry_later && attempts.length >= maxAttempts,
1653
+ attempt_results: attempts.map(compactAttemptRecord),
1654
+ usage_available: usageAccounting.usage_available,
1655
+ no_usage_reason: usageAccounting.no_usage_reason,
1656
+ response: runUsage.response,
1657
+ audit: finalResult.audit,
1658
+ validation: finalResult.audit?.validation ?? null,
1659
+ validation_ok: finalResult.audit?.validation?.ok ?? null,
1660
+ usage: runUsage.usage,
1661
+ usage_normalized: runUsage.normalized,
1662
+ projection: runUsage.projection,
1663
+ usage_snapshots: {
1664
+ before: usageBefore,
1665
+ after: usageAfter,
1666
+ },
1667
+ usage_accounting: usageAccounting,
1668
+ quota_percent_status: usageAccounting.quota_percent_status,
1669
+ quota_percent_unavailable_reason: usageAccounting.quota_percent_unavailable_reason,
1670
+ },
1671
+ args.out,
1672
+ );
1673
+ } finally {
1674
+ await unlink(snapshotPath).catch(() => {});
1675
+ }
1676
+ }
1677
+
1678
+ async function doctor() {
1679
+ const payload = await runJson("python3", [
1680
+ "tools/zcode_eval/zcode_eval.py",
1681
+ "doctor",
1682
+ "--json",
1683
+ ]);
1684
+ console.log(JSON.stringify(payload, null, 2));
1685
+ }
1686
+
1687
+ async function launch(port, options = {}) {
1688
+ const payload = await runJson("cua-driver", [
1689
+ "call",
1690
+ "launch_app",
1691
+ JSON.stringify({
1692
+ bundle_id: DEFAULT_BUNDLE_ID,
1693
+ electron_debugging_port: port,
1694
+ creates_new_application_instance: Boolean(options.newInstance),
1695
+ }),
1696
+ ]);
1697
+ payload.cdp = await probeCdp(port);
1698
+ console.log(JSON.stringify(payload, null, 2));
1699
+ }
1700
+
1701
+ async function fetchTargets(port) {
1702
+ return getJson(port, "/json/list");
1703
+ }
1704
+
1705
+ async function probeCdp(port) {
1706
+ let lastError = null;
1707
+ for (let attempt = 0; attempt < 3; attempt += 1) {
1708
+ try {
1709
+ const version = await getJson(port, "/json/version");
1710
+ return { ok: true, browser: version.Browser ?? null };
1711
+ } catch (error) {
1712
+ lastError = error;
1713
+ await new Promise((resolveWait) => setTimeout(resolveWait, 500));
1714
+ }
1715
+ }
1716
+ return { ok: false, error: lastError?.message ?? "unknown CDP error" };
1717
+ }
1718
+
1719
+ function getJson(port, path) {
1720
+ return new Promise((resolveJson, rejectJson) => {
1721
+ const request = http.get(
1722
+ {
1723
+ host: "127.0.0.1",
1724
+ port,
1725
+ path,
1726
+ timeout: 5000,
1727
+ },
1728
+ (response) => {
1729
+ let body = "";
1730
+ response.setEncoding("utf8");
1731
+ response.on("data", (chunk) => {
1732
+ body += chunk;
1733
+ });
1734
+ response.on("end", () => {
1735
+ if ((response.statusCode ?? 500) >= 400) {
1736
+ rejectJson(new Error(`CDP HTTP ${response.statusCode}: ${body.slice(0, 500)}`));
1737
+ return;
1738
+ }
1739
+ try {
1740
+ resolveJson(JSON.parse(body));
1741
+ } catch (error) {
1742
+ rejectJson(error);
1743
+ }
1744
+ });
1745
+ },
1746
+ );
1747
+ request.on("timeout", () => {
1748
+ request.destroy(new Error(`CDP HTTP timeout on port ${port}`));
1749
+ });
1750
+ request.on("error", rejectJson);
1751
+ });
1752
+ }
1753
+
1754
+ function pickMainPage(targets) {
1755
+ const page = targets.find(
1756
+ (target) =>
1757
+ target.type === "page" &&
1758
+ target.title === "ZCode" &&
1759
+ target.webSocketDebuggerUrl,
1760
+ );
1761
+ if (!page) {
1762
+ throw new Error("No ZCode page target found. Run launch first.");
1763
+ }
1764
+ return page;
1765
+ }
1766
+
1767
+ async function printTargets(port) {
1768
+ const targets = await fetchTargets(port);
1769
+ const slim = targets.map((target) => ({
1770
+ id: target.id,
1771
+ type: target.type,
1772
+ title: target.title,
1773
+ url: target.url,
1774
+ has_websocket: Boolean(target.webSocketDebuggerUrl),
1775
+ }));
1776
+ console.log(JSON.stringify(slim, null, 2));
1777
+ }
1778
+
1779
+ async function cdpCommand(wsUrl, method, params = {}) {
1780
+ const socket = new WebSocket(wsUrl);
1781
+ const id = Math.floor(Math.random() * 1_000_000);
1782
+ await new Promise((resolveOpen, rejectOpen) => {
1783
+ socket.addEventListener("open", resolveOpen, { once: true });
1784
+ socket.addEventListener("error", rejectOpen, { once: true });
1785
+ });
1786
+
1787
+ const result = await new Promise((resolveMessage, rejectMessage) => {
1788
+ const timeout = setTimeout(() => rejectMessage(new Error(`CDP timeout: ${method}`)), 15000);
1789
+ socket.addEventListener("message", (event) => {
1790
+ const message = JSON.parse(event.data);
1791
+ if (message.id !== id) return;
1792
+ clearTimeout(timeout);
1793
+ if (message.error) rejectMessage(new Error(JSON.stringify(message.error)));
1794
+ else resolveMessage(message.result);
1795
+ });
1796
+ socket.send(JSON.stringify({ id, method, params }));
1797
+ });
1798
+ socket.close();
1799
+ return result;
1800
+ }
1801
+
1802
+ async function evaluate(port, expression) {
1803
+ const result = await runtimeEvaluate(port, expression);
1804
+ console.log(JSON.stringify(result, null, 2));
1805
+ }
1806
+
1807
+ async function runtimeEvaluate(port, expression) {
1808
+ const target = pickMainPage(await fetchTargets(port));
1809
+ const result = await cdpCommand(target.webSocketDebuggerUrl, "Runtime.evaluate", {
1810
+ expression,
1811
+ awaitPromise: true,
1812
+ returnByValue: true,
1813
+ });
1814
+ return result.result;
1815
+ }
1816
+
1817
+ async function text(port, max = 4000) {
1818
+ const expression = `(() => {
1819
+ const text = document.body ? document.body.innerText : "";
1820
+ return text.slice(0, ${JSON.stringify(max)});
1821
+ })()`;
1822
+ await evaluate(port, expression);
1823
+ }
1824
+
1825
+ async function textboxes(port) {
1826
+ const expression = `(() => Array.from(document.querySelectorAll('textarea,input,[contenteditable=true],[role=textbox]')).map((el, i) => ({
1827
+ i,
1828
+ tag: el.tagName,
1829
+ role: el.getAttribute('role'),
1830
+ contenteditable: el.getAttribute('contenteditable'),
1831
+ placeholder: el.getAttribute('placeholder'),
1832
+ aria: el.getAttribute('aria-label'),
1833
+ text: (el.innerText || el.value || el.textContent || '').slice(0, 160),
1834
+ rect: (() => {
1835
+ const r = el.getBoundingClientRect();
1836
+ return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
1837
+ })(),
1838
+ })))()`;
1839
+ await evaluate(port, expression);
1840
+ }
1841
+
1842
+ async function buttons(port) {
1843
+ const expression = `(() => Array.from(document.querySelectorAll('button,[role=button]')).map((el, i) => ({
1844
+ i,
1845
+ text: (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim().slice(0, 120),
1846
+ aria: el.getAttribute('aria-label'),
1847
+ disabled: Boolean(el.disabled) || el.getAttribute('aria-disabled') === 'true',
1848
+ rect: (() => {
1849
+ const r = el.getBoundingClientRect();
1850
+ return { x: Math.round(r.x), y: Math.round(r.y), w: Math.round(r.width), h: Math.round(r.height) };
1851
+ })(),
1852
+ })))()`;
1853
+ await evaluate(port, expression);
1854
+ }
1855
+
1856
+ async function summary(port) {
1857
+ await evaluate(port, summaryExpression());
1858
+ }
1859
+
1860
+ async function openUsage(port) {
1861
+ await evaluate(port, openUsageExpression());
1862
+ }
1863
+
1864
+ async function usageSnapshot(port, out) {
1865
+ const result = await runtimeEvaluate(port, usageSnapshotExpression());
1866
+ const payload = result.value ?? {};
1867
+ const textPayload = JSON.stringify(payload, null, 2);
1868
+ if (out) {
1869
+ const outputPath = resolve(out);
1870
+ await mkdir(dirname(outputPath), { recursive: true });
1871
+ await writeFile(outputPath, `${textPayload}\n`);
1872
+ }
1873
+ console.log(textPayload);
1874
+ }
1875
+
1876
+ async function newTask(port, workspace) {
1877
+ if (!workspace) throw new Error("--workspace is required");
1878
+ const expression = `(() => {
1879
+ const workspace = ${JSON.stringify(workspace)};
1880
+ const buttons = Array.from(document.querySelectorAll('button,[role=button]'));
1881
+ const workspaceRows = buttons
1882
+ .filter((node) => (node.innerText || node.textContent || '').trim() === workspace)
1883
+ .map((node) => node.getBoundingClientRect())
1884
+ .filter((rect) => rect.width > 0 && rect.height > 0)
1885
+ .sort((a, b) => a.y - b.y);
1886
+ if (!workspaceRows.length) return { ok: false, reason: 'workspace not found', workspace };
1887
+ const row = workspaceRows[workspaceRows.length - 1];
1888
+ const newTaskButton = buttons.find((node) => {
1889
+ const r = node.getBoundingClientRect();
1890
+ const text = (node.innerText || node.textContent || node.getAttribute('aria-label') || '').trim();
1891
+ return text === 'New task' && Math.abs((r.y + r.height / 2) - (row.y + row.height / 2)) < 6;
1892
+ });
1893
+ if (!newTaskButton) return { ok: false, reason: 'workspace new task button not found', workspace };
1894
+ const r = newTaskButton.getBoundingClientRect();
1895
+ const x = Math.round(r.x + r.width / 2);
1896
+ const y = Math.round(r.y + r.height / 2);
1897
+ for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
1898
+ newTaskButton.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y }));
1899
+ }
1900
+ return { ok: true, workspace, x, y };
1901
+ })()`;
1902
+ await evaluate(port, expression);
1903
+ }
1904
+
1905
+ async function setMode(port, mode) {
1906
+ if (!mode) throw new Error("--mode is required");
1907
+ const opened = await runtimeEvaluate(port, `(() => {
1908
+ const el = Array.from(document.querySelectorAll('button,[role=button]')).find((node) => node.getAttribute('aria-label') === 'Switch mode');
1909
+ if (!el) return { ok: false, reason: 'mode switch not found' };
1910
+ const r = el.getBoundingClientRect();
1911
+ const x = Math.round(r.x + r.width / 2);
1912
+ const y = Math.round(r.y + r.height / 2);
1913
+ for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
1914
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y }));
1915
+ }
1916
+ return { ok: true, current: (el.innerText || el.textContent || '').trim(), x, y };
1917
+ })()`);
1918
+ if (!opened.value?.ok) {
1919
+ console.log(JSON.stringify(opened, null, 2));
1920
+ return;
1921
+ }
1922
+ await new Promise((resolveWait) => setTimeout(resolveWait, 300));
1923
+ const expression = `(() => {
1924
+ const mode = ${JSON.stringify(mode)};
1925
+ const el = Array.from(document.querySelectorAll('[role=option],button,[role=button]'))
1926
+ .find((node) => (node.innerText || node.textContent || '').trim().includes(mode));
1927
+ if (!el) return { ok: false, reason: 'mode option not found', mode };
1928
+ const r = el.getBoundingClientRect();
1929
+ const x = Math.round(r.x + r.width / 2);
1930
+ const y = Math.round(r.y + r.height / 2);
1931
+ for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
1932
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y }));
1933
+ }
1934
+ return { ok: true, mode, x, y, text: (el.innerText || el.textContent || '').trim() };
1935
+ })()`;
1936
+ await evaluate(port, expression);
1937
+ }
1938
+
1939
+ async function setComposer(port, promptText) {
1940
+ if (!promptText) throw new Error("--text is required");
1941
+ const expression = `(() => {
1942
+ const text = ${JSON.stringify(promptText)};
1943
+ const el = Array.from(document.querySelectorAll('[contenteditable=true],[role=textbox],textarea,input'))
1944
+ .find((node) => node.getBoundingClientRect().width > 100 && node.getBoundingClientRect().height > 10);
1945
+ if (!el) return { ok: false, reason: 'composer not found' };
1946
+ el.focus();
1947
+ if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
1948
+ el.value = text;
1949
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
1950
+ } else {
1951
+ document.execCommand('selectAll', false, null);
1952
+ document.execCommand('insertText', false, text);
1953
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
1954
+ }
1955
+ return {
1956
+ ok: true,
1957
+ text: (el.innerText || el.value || el.textContent || '').slice(0, 500),
1958
+ };
1959
+ })()`;
1960
+ await evaluate(port, expression);
1961
+ }
1962
+
1963
+ async function submitTask(port, promptText) {
1964
+ if (!promptText) throw new Error("--text is required");
1965
+ await setComposer(port, promptText);
1966
+ await clickByText(port, "Send");
1967
+ }
1968
+
1969
+ async function submitGoal(port, promptText) {
1970
+ const text = promptText.trimStart().startsWith("/goal")
1971
+ ? promptText
1972
+ : `/goal ${promptText}`;
1973
+ await submitTask(port, text);
1974
+ }
1975
+
1976
+ async function waitIdle(port, timeoutMs = 300_000, intervalMs = 2_000) {
1977
+ const deadline = Date.now() + timeoutMs;
1978
+ let lastSummary = null;
1979
+ while (Date.now() <= deadline) {
1980
+ const result = await runtimeEvaluate(port, `(() => {
1981
+ const bodyText = document.body?.innerText || "";
1982
+ const buttons = Array.from(document.querySelectorAll('button,[role=button]')).map((el) => ({
1983
+ text: (el.innerText || el.textContent || el.getAttribute('aria-label') || '').trim(),
1984
+ aria: el.getAttribute('aria-label'),
1985
+ rect: (() => { const r = el.getBoundingClientRect(); return { w: Math.round(r.width), h: Math.round(r.height) }; })(),
1986
+ })).filter((button) => button.rect.w > 0 && button.rect.h > 0);
1987
+ return {
1988
+ running: /\\bWorking for\\b/.test(bodyText) || buttons.some((button) => button.text === 'Stop' || button.aria === 'Stop'),
1989
+ awaitingApproval: /Awaiting approval|Permission required/.test(bodyText),
1990
+ workedFor: (bodyText.match(/Worked for\\s+([^\\n]+)/) || [])[1] || null,
1991
+ lastText: bodyText.slice(-3000),
1992
+ };
1993
+ })()`);
1994
+ lastSummary = result.value;
1995
+ if (lastSummary?.awaitingApproval) {
1996
+ console.log(JSON.stringify({ ok: false, reason: "awaiting_approval", summary: lastSummary }, null, 2));
1997
+ return;
1998
+ }
1999
+ if (lastSummary && !lastSummary.running && lastSummary.workedFor) {
2000
+ console.log(JSON.stringify({ ok: true, summary: lastSummary }, null, 2));
2001
+ return;
2002
+ }
2003
+ await new Promise((resolveWait) => setTimeout(resolveWait, intervalMs));
2004
+ }
2005
+ console.log(JSON.stringify({ ok: false, reason: "timeout", summary: lastSummary }, null, 2));
2006
+ }
2007
+
2008
+ async function clickByText(port, label) {
2009
+ if (!label) throw new Error("--text is required");
2010
+ await clickMatchingText(port, label, false);
2011
+ }
2012
+
2013
+ async function clickByContainedText(port, needle) {
2014
+ if (!needle) throw new Error("--text is required");
2015
+ await clickMatchingText(port, needle, true);
2016
+ }
2017
+
2018
+ async function clickMatchingText(port, label, contains) {
2019
+ const expression = `(() => {
2020
+ const label = ${JSON.stringify(label)};
2021
+ const contains = ${JSON.stringify(contains)};
2022
+ const candidates = Array.from(document.querySelectorAll('button,[role=button]'));
2023
+ const el = candidates.find((node) => {
2024
+ const text = (node.innerText || node.textContent || node.getAttribute('aria-label') || '').trim();
2025
+ const aria = node.getAttribute('aria-label') || '';
2026
+ return contains ? text.includes(label) || aria.includes(label) : text === label || aria === label;
2027
+ });
2028
+ if (!el) return { ok: false, reason: 'button not found', label };
2029
+ const disabled = Boolean(el.disabled) || el.getAttribute('aria-disabled') === 'true';
2030
+ if (disabled) return { ok: false, reason: 'button disabled', label };
2031
+ const r = el.getBoundingClientRect();
2032
+ const x = Math.round(r.x + r.width / 2);
2033
+ const y = Math.round(r.y + r.height / 2);
2034
+ for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
2035
+ el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window, clientX: x, clientY: y }));
2036
+ }
2037
+ return { ok: true, label, contains, x, y };
2038
+ })()`;
2039
+ await evaluate(port, expression);
2040
+ }
2041
+
2042
+ async function screenshot(port, out) {
2043
+ if (!out) throw new Error("--out is required");
2044
+ const target = pickMainPage(await fetchTargets(port));
2045
+ await cdpCommand(target.webSocketDebuggerUrl, "Page.enable");
2046
+ const result = await cdpCommand(target.webSocketDebuggerUrl, "Page.captureScreenshot", {
2047
+ format: "png",
2048
+ fromSurface: true,
2049
+ });
2050
+ const outputPath = resolve(out);
2051
+ await mkdir(dirname(outputPath), { recursive: true });
2052
+ await writeFile(outputPath, Buffer.from(result.data, "base64"));
2053
+ console.log(outputPath);
2054
+ }
2055
+
2056
+ async function main() {
2057
+ const args = parseArgs(process.argv.slice(2));
2058
+ if (!args.command || args.command === "help" || args.command === "--help") {
2059
+ usage();
2060
+ return;
2061
+ }
2062
+ if (args.command === "doctor") await doctor();
2063
+ else if (args.command === "cli-path") await cliPath();
2064
+ else if (args.command === "cli-doctor") await cliDoctor(args.out);
2065
+ else if (args.command === "cli-preflight") await cliPreflight(args);
2066
+ else if (args.command === "cli-version") await cliVersion(args.out);
2067
+ else if (args.command === "bootstrap-cli-config") await bootstrapCliConfig(args);
2068
+ else if (args.command === "vision-preflight") await visionPreflightCommand(args);
2069
+ else if (args.command === "cli-prompt") await cliPrompt(args);
2070
+ else if (args.command === "run-packet") await runPacket(args);
2071
+ else if (args.command === "launch") await launch(args.port, { newInstance: args.newInstance });
2072
+ else if (args.command === "targets") await printTargets(args.port);
2073
+ else if (args.command === "text") await text(args.port, args.max ?? 4000);
2074
+ else if (args.command === "eval") {
2075
+ if (!args.expr) throw new Error("--expr is required");
2076
+ await evaluate(args.port, args.expr);
2077
+ } else if (args.command === "textboxes") await textboxes(args.port);
2078
+ else if (args.command === "buttons") await buttons(args.port);
2079
+ else if (args.command === "summary") await summary(args.port);
2080
+ else if (args.command === "open-usage") await openUsage(args.port);
2081
+ else if (args.command === "usage") await usageSnapshot(args.port, args.out);
2082
+ else if (args.command === "new-task") await newTask(args.port, args.workspace);
2083
+ else if (args.command === "set-mode") await setMode(args.port, args.mode);
2084
+ else if (args.command === "set-composer") await setComposer(args.port, await readPrompt(args));
2085
+ else if (args.command === "submit-task") await submitTask(args.port, await readPrompt(args));
2086
+ else if (args.command === "goal") await submitGoal(args.port, await readPrompt(args));
2087
+ else if (args.command === "wait-idle") await waitIdle(args.port, args.timeoutMs ?? 300_000, args.intervalMs ?? 2_000);
2088
+ else if (args.command === "click") await clickByText(args.port, args.text);
2089
+ else if (args.command === "click-contains") await clickByContainedText(args.port, args.text);
2090
+ else if (args.command === "screenshot") await screenshot(args.port, args.out);
2091
+ else throw new Error(`Unknown command: ${args.command}`);
2092
+ }
2093
+
2094
+ main().catch((error) => {
2095
+ console.error(error.message);
2096
+ process.exit(1);
2097
+ });