@aibridge/cli 0.0.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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.d.mts +1 -0
  3. package/dist/cli.mjs +6 -0
  4. package/dist/context-BLjTHa41.mjs +1529 -0
  5. package/dist/index.d.mts +184 -0
  6. package/dist/index.mjs +2 -0
  7. package/package.json +53 -0
  8. package/src/app.exit-code.test.ts +91 -0
  9. package/src/app.ts +49 -0
  10. package/src/cli.ts +5 -0
  11. package/src/commands/image-gen/command.ts +77 -0
  12. package/src/commands/image-gen/impl.ts +268 -0
  13. package/src/commands/implement/command.ts +50 -0
  14. package/src/commands/implement/impl.ts +99 -0
  15. package/src/commands/plan/command.ts +56 -0
  16. package/src/commands/plan/impl.ts +172 -0
  17. package/src/commands/plan/plan.test.ts +19 -0
  18. package/src/commands/quota/command.ts +30 -0
  19. package/src/commands/quota/impl.ts +109 -0
  20. package/src/commands/review/command.ts +58 -0
  21. package/src/commands/review/impl.ts +211 -0
  22. package/src/commands/review/review.test.ts +54 -0
  23. package/src/commands/runs/command.ts +53 -0
  24. package/src/commands/runs/impl.ts +171 -0
  25. package/src/commands/subagent/command.ts +62 -0
  26. package/src/commands/subagent/impl.ts +87 -0
  27. package/src/context.ts +10 -0
  28. package/src/delegate.test.ts +180 -0
  29. package/src/delegate.ts +46 -0
  30. package/src/driver.ts +56 -0
  31. package/src/drivers.ts +44 -0
  32. package/src/exitCode.test.ts +44 -0
  33. package/src/exitCode.ts +24 -0
  34. package/src/flagMapping.test.ts +99 -0
  35. package/src/index.ts +37 -0
  36. package/src/models.test.ts +107 -0
  37. package/src/models.ts +159 -0
  38. package/src/parsers.ts +24 -0
  39. package/src/quotaPreflight.test.ts +178 -0
  40. package/src/quotaPreflight.ts +103 -0
  41. package/src/runlog.ts +195 -0
@@ -0,0 +1,1529 @@
1
+ import { createRequire } from "node:module";
2
+ import { ExitCode, buildApplication, buildCommand, buildRouteMap, run } from "@stricli/core";
3
+ import { appendFileSync, closeSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { isAbsolute, join, resolve } from "node:path";
6
+ import { isNotFound, runCaptured } from "@aibridge/proc";
7
+ import * as agy from "@aibridge/agy";
8
+ import { fetchAgyQuota, findModelQuota } from "@aibridge/agy";
9
+ import * as claude from "@aibridge/claude";
10
+ import { fetchClaudeQuota } from "@aibridge/claude";
11
+ import * as codex from "@aibridge/codex";
12
+ import { fetchCodexQuota } from "@aibridge/codex";
13
+ import * as grok from "@aibridge/grok";
14
+ import { randomBytes } from "node:crypto";
15
+ //#region src/models.ts
16
+ const MODELS = {
17
+ "xai-grok/grok-4.5": {
18
+ slug: "xai-grok/grok-4.5",
19
+ backend: "grok",
20
+ backendModel: "grok-4.5",
21
+ efforts: [
22
+ "low",
23
+ "medium",
24
+ "high"
25
+ ],
26
+ brief: "xAI Grok 4.5 via grok CLI — default for plan & review; off-budget"
27
+ },
28
+ "google-antigravity/gemini-3.6-flash": {
29
+ slug: "google-antigravity/gemini-3.6-flash",
30
+ backend: "agy",
31
+ backendModel: "gemini-3.6-flash",
32
+ efforts: [
33
+ "low",
34
+ "medium",
35
+ "high"
36
+ ],
37
+ defaultEffort: "high",
38
+ brief: "Google Gemini 3.6 Flash via agy — default for implement; off-budget"
39
+ },
40
+ "google-antigravity/claude-sonnet-4-6": {
41
+ slug: "google-antigravity/claude-sonnet-4-6",
42
+ backend: "agy",
43
+ backendModel: "claude-sonnet-4-6",
44
+ efforts: null,
45
+ brief: "Claude Sonnet 4.6 (thinking) via agy — off-budget"
46
+ },
47
+ "google-antigravity/claude-opus-4-6-thinking": {
48
+ slug: "google-antigravity/claude-opus-4-6-thinking",
49
+ backend: "agy",
50
+ backendModel: "claude-opus-4-6-thinking",
51
+ efforts: null,
52
+ brief: "Claude Opus 4.6 (thinking) via agy — off-budget heavyweight"
53
+ },
54
+ "google-antigravity/gpt-oss-120b-medium": {
55
+ slug: "google-antigravity/gpt-oss-120b-medium",
56
+ backend: "agy",
57
+ backendModel: "gpt-oss-120b-medium",
58
+ efforts: null,
59
+ brief: "GPT-OSS 120B (medium) via agy — off-budget"
60
+ },
61
+ "openai-codex/gpt-5.6-sol": {
62
+ slug: "openai-codex/gpt-5.6-sol",
63
+ backend: "codex",
64
+ backendModel: "gpt-5.6-sol",
65
+ efforts: [
66
+ "low",
67
+ "medium",
68
+ "high",
69
+ "xhigh"
70
+ ],
71
+ brief: "OpenAI Codex gpt-5.6-sol via codex CLI"
72
+ },
73
+ "anthropic-claude/sonnet": {
74
+ slug: "anthropic-claude/sonnet",
75
+ backend: "claude",
76
+ backendModel: "sonnet",
77
+ efforts: [
78
+ "low",
79
+ "medium",
80
+ "high",
81
+ "xhigh",
82
+ "max"
83
+ ],
84
+ brief: "Claude Sonnet via claude CLI — bills your Claude subscription"
85
+ },
86
+ "anthropic-claude/opus": {
87
+ slug: "anthropic-claude/opus",
88
+ backend: "claude",
89
+ backendModel: "opus",
90
+ efforts: [
91
+ "low",
92
+ "medium",
93
+ "high",
94
+ "xhigh",
95
+ "max"
96
+ ],
97
+ defaultEffort: "high",
98
+ brief: "Claude Opus via claude CLI (default effort: high) — bills subscription"
99
+ }
100
+ };
101
+ const DEFAULT_MODEL = "xai-grok/grok-4.5";
102
+ const DEFAULT_IMPLEMENTER = "google-antigravity/gemini-3.6-flash";
103
+ const DEFAULT_IMAGE_GEN = "openai-codex/gpt-5.6-sol";
104
+ const IMAGE_GEN_BACKENDS = /* @__PURE__ */ new Set(["codex", "grok"]);
105
+ function supportsImageGen(resolved) {
106
+ return IMAGE_GEN_BACKENDS.has(resolved.spec.backend);
107
+ }
108
+ const EFFORTS_SET = /* @__PURE__ */ new Set([
109
+ "low",
110
+ "medium",
111
+ "high",
112
+ "xhigh",
113
+ "max"
114
+ ]);
115
+ function resolveModel(input) {
116
+ if (MODELS[input]) {
117
+ const spec = MODELS[input];
118
+ return {
119
+ spec,
120
+ effort: spec.defaultEffort
121
+ };
122
+ }
123
+ const lastDashIdx = input.lastIndexOf("-");
124
+ if (lastDashIdx > 0) {
125
+ const prefix = input.slice(0, lastDashIdx);
126
+ const token = input.slice(lastDashIdx + 1);
127
+ const spec = MODELS[prefix];
128
+ if (spec && EFFORTS_SET.has(token) && spec.efforts && spec.efforts.includes(token)) return {
129
+ spec,
130
+ effort: token
131
+ };
132
+ }
133
+ }
134
+ function backendModelId(resolved) {
135
+ if (!resolved.spec.backendModel) return;
136
+ if (resolved.spec.backend === "agy") {
137
+ const effort = resolved.effort ?? resolved.spec.defaultEffort;
138
+ if (effort) return `${resolved.spec.backendModel}-${effort}`;
139
+ }
140
+ return resolved.spec.backendModel;
141
+ }
142
+ function listModelHelpLines(opts = {}) {
143
+ const lines = [];
144
+ for (const [slug, spec] of Object.entries(MODELS)) {
145
+ if (opts.imageOnly && !IMAGE_GEN_BACKENDS.has(spec.backend)) continue;
146
+ lines.push(` ${slug}`);
147
+ lines.push(` ${spec.brief}`);
148
+ }
149
+ return lines;
150
+ }
151
+ function formatUnknownModelError(input) {
152
+ return [
153
+ `Unknown model "${input}".`,
154
+ "Available models:",
155
+ ...listModelHelpLines()
156
+ ].join("\n");
157
+ }
158
+ function formatImageGenModelError(input, resolved) {
159
+ return [
160
+ `Model "${input}" (${resolved.spec.slug}) cannot generate images — backend "${resolved.spec.backend}" has no image path.`,
161
+ "Image-gen seats (canonical slug):",
162
+ ...listModelHelpLines({ imageOnly: true })
163
+ ].join("\n");
164
+ }
165
+ //#endregion
166
+ //#region src/parsers.ts
167
+ /**
168
+ * stricli `parse` functions (string -> T). Throwing inside one makes stricli
169
+ * reject the argument up-front with a clean, flag-named error — instead of
170
+ * silently letting a bad value (NaN, Infinity, "") flow into an impl where it
171
+ * gets masked or, worse, breaks `setTimeout`.
172
+ */
173
+ function positiveIntSeconds(input) {
174
+ const n = Number(input);
175
+ if (!Number.isInteger(n) || n <= 0) throw new RangeError(`expected a positive whole number of seconds, got "${input}"`);
176
+ if (n > 86400) throw new RangeError(`timeout too large: ${n}s (max 86400 = 24h)`);
177
+ return n;
178
+ }
179
+ function nonEmptyPrompt(input) {
180
+ if (input.trim().length === 0) throw new Error("prompt must not be empty");
181
+ return input;
182
+ }
183
+ //#endregion
184
+ //#region src/drivers.ts
185
+ const DRIVERS = {
186
+ agy: {
187
+ probe: () => agy.probe(),
188
+ run: (task) => agy.run(task),
189
+ quota: () => agy.fetchAgyQuota()
190
+ },
191
+ grok: {
192
+ probe: () => grok.probe(),
193
+ run: (task) => grok.run(task),
194
+ generateImage: (req) => grok.generateImage(req)
195
+ },
196
+ codex: {
197
+ probe: () => codex.probe(),
198
+ run: (task) => codex.run(task),
199
+ quota: () => codex.fetchCodexQuota(),
200
+ generateImage: (req) => codex.generateImage(req)
201
+ },
202
+ claude: {
203
+ probe: () => claude.probe(),
204
+ run: (task) => claude.run(task),
205
+ quota: () => claude.fetchClaudeQuota()
206
+ }
207
+ };
208
+ function getDriver(backend) {
209
+ const driver = DRIVERS[backend];
210
+ if (!driver) throw new Error(`Unknown backend "${backend}"`);
211
+ return driver;
212
+ }
213
+ //#endregion
214
+ //#region src/commands/image-gen/impl.ts
215
+ const MIN_REAL_BYTES_CODEX = 1e5;
216
+ const MIN_REAL_BYTES_GROK = 1e4;
217
+ async function imageGen$1(flags, prompt) {
218
+ const fail = (msg) => {
219
+ this.process.stderr.write(`aibridge image-gen: ${msg}\n`);
220
+ this.process.exitCode = 1;
221
+ };
222
+ const inputSlug = flags.model ?? "openai-codex/gpt-5.6-sol";
223
+ const model = resolveModel(inputSlug);
224
+ if (!model) return fail(formatUnknownModelError(inputSlug));
225
+ if (!supportsImageGen(model)) return fail(formatImageGenModelError(inputSlug, model));
226
+ if (model.spec.backend === "codex" && model.effort) return fail(`effort "-${model.effort}" has no effect on image-gen (gpt-image-2 renders, not the seat model); use ${DEFAULT_IMAGE_GEN}.`);
227
+ const quality = (flags.quality ?? "high").toLowerCase();
228
+ if (![
229
+ "low",
230
+ "medium",
231
+ "high"
232
+ ].includes(quality)) return fail(`invalid --quality "${flags.quality}" (use low | medium | high)`);
233
+ let size;
234
+ if (flags.size !== void 0) {
235
+ const m = flags.size.match(/^(\d+)\s*x\s*(\d+)$/i);
236
+ if (!m) return fail(`invalid --size "${flags.size}" (expected e.g. 1024x1024)`);
237
+ size = {
238
+ w: Number(m[1]),
239
+ h: Number(m[2])
240
+ };
241
+ if (model.spec.backend === "codex") {
242
+ const constraint = sizeConstraintError(size.w, size.h);
243
+ if (constraint) return fail(`invalid --size ${size.w}x${size.h}: ${constraint}`);
244
+ } else if (size.w < 1 || size.h < 1) return fail(`invalid --size ${size.w}x${size.h}: dimensions must be positive`);
245
+ }
246
+ const timeoutSec = flags.timeout ?? 600;
247
+ const outPath = resolve(this.process.cwd(), flags.out ?? "./aibridge-image.png");
248
+ const imagePaths = [];
249
+ if (flags.image !== void 0) for (const raw of flags.image.split(",").map((s) => s.trim()).filter(Boolean)) {
250
+ const abs = resolve(this.process.cwd(), raw);
251
+ if (!existsSync(abs)) return fail(`reference image not found: ${raw}`);
252
+ imagePaths.push(abs);
253
+ }
254
+ const driver = getDriver(model.spec.backend);
255
+ if (!driver.generateImage) return fail(formatImageGenModelError(inputSlug, model));
256
+ const minBytes = model.spec.backend === "codex" ? MIN_REAL_BYTES_CODEX : MIN_REAL_BYTES_GROK;
257
+ const work = mkdtempSync(join(tmpdir(), "aibridge-imagegen-"));
258
+ try {
259
+ let outcome = await driver.generateImage({
260
+ prompt,
261
+ workDir: work,
262
+ backendModel: model.spec.backendModel,
263
+ effort: model.effort,
264
+ quality,
265
+ size,
266
+ imagePaths,
267
+ timeoutSec,
268
+ forceful: false,
269
+ minBytes
270
+ });
271
+ if (model.spec.backend === "codex" && outcome.kind === "suspect") outcome = await driver.generateImage({
272
+ prompt,
273
+ workDir: work,
274
+ backendModel: model.spec.backendModel,
275
+ effort: model.effort,
276
+ quality,
277
+ size,
278
+ imagePaths,
279
+ timeoutSec,
280
+ forceful: true,
281
+ minBytes
282
+ });
283
+ if (outcome.kind === "ok" && outcome.bytes < minBytes) outcome = { kind: "suspect" };
284
+ if (outcome.kind === "error") return fail(outcome.reason);
285
+ if (outcome.kind === "suspect") return fail(model.spec.backend === "grok" ? "grok produced no usable image. Check SuperGrok image access and re-run with a simpler prompt." : "codex produced only a tiny/code-drawn image, not a real gpt-image-2 render. Try --quality high or a clearer, simpler prompt.");
286
+ const local = join(work, "result.bin");
287
+ copyFileSync(outcome.path, local);
288
+ let dims = imageSize(local);
289
+ if (size && dims && (dims.width !== size.w || dims.height !== size.h)) if (await magick([
290
+ local,
291
+ "-resize",
292
+ `${size.w}x${size.h}!`,
293
+ local
294
+ ])) dims = imageSize(local) ?? dims;
295
+ else this.process.stderr.write(`aibridge image-gen: rendered ${dims.width}x${dims.height}, wanted ${size.w}x${size.h}, and ImageMagick (magick/convert) is unavailable to resize.
296
+ `);
297
+ const outExt = /\.png$/i.test(outPath) ? "png" : /\.jpe?g$/i.test(outPath) ? "jpg" : null;
298
+ const actualFmt = pngSize(local) ? "png" : jpegSize(local) ? "jpg" : null;
299
+ const needsConvert = outExt !== null && actualFmt !== null && outExt !== actualFmt;
300
+ if (!needsConvert || !await magick([local, outPath])) {
301
+ if (needsConvert) this.process.stderr.write(`aibridge image-gen: render is ${actualFmt.toUpperCase()} but out path wants ${outExt.toUpperCase()}, and ImageMagick (magick/convert) is unavailable to convert; writing the raw bytes as-is.
302
+ `);
303
+ copyFileSync(local, outPath);
304
+ }
305
+ const bytes = statSync(outPath).size;
306
+ if (flags.json) this.process.stdout.write(`${JSON.stringify({
307
+ out: outPath,
308
+ bytes,
309
+ width: dims?.width ?? null,
310
+ height: dims?.height ?? null,
311
+ sizeRequested: flags.size ?? null,
312
+ quality: model.spec.backend === "codex" ? quality : null,
313
+ model: model.spec.slug,
314
+ backend: model.spec.backend,
315
+ real: true
316
+ })}\n`);
317
+ else {
318
+ const kb = Math.round(bytes / 1024);
319
+ const dimStr = dims ? `${dims.width}x${dims.height}, ` : "";
320
+ const qualityStr = model.spec.backend === "codex" ? `, ${quality} quality` : "";
321
+ this.process.stdout.write(`✓ Wrote ${outPath} (${dimStr}${kb} KB${qualityStr}, ${model.spec.slug})\n`);
322
+ }
323
+ } finally {
324
+ rmSync(work, {
325
+ recursive: true,
326
+ force: true
327
+ });
328
+ }
329
+ }
330
+ function sizeConstraintError(w, h) {
331
+ if (w % 16 !== 0 || h % 16 !== 0) return "each edge must be divisible by 16";
332
+ const long = Math.max(w, h);
333
+ if (long / Math.min(w, h) > 3) return "aspect ratio must be within 1:3–3:1";
334
+ if (long > 3840) return "longest edge must be <= 3840px";
335
+ const px = w * h;
336
+ if (px < 655360 || px > 8294400) return "total pixels must be 655,360–8,294,400";
337
+ return null;
338
+ }
339
+ function imageSize(path) {
340
+ return pngSize(path) ?? jpegSize(path);
341
+ }
342
+ function pngSize(path) {
343
+ try {
344
+ const fd = openSync(path, "r");
345
+ const head = Buffer.alloc(24);
346
+ readSync(fd, head, 0, 24, 0);
347
+ closeSync(fd);
348
+ if (head.toString("latin1", 1, 4) !== "PNG") return null;
349
+ if (head.toString("latin1", 12, 16) !== "IHDR") return null;
350
+ return {
351
+ width: head.readUInt32BE(16),
352
+ height: head.readUInt32BE(20)
353
+ };
354
+ } catch {
355
+ return null;
356
+ }
357
+ }
358
+ function jpegSize(path) {
359
+ try {
360
+ const fd = openSync(path, "r");
361
+ const buf = Buffer.alloc(64 * 1024);
362
+ const n = readSync(fd, buf, 0, buf.length, 0);
363
+ closeSync(fd);
364
+ if (n < 4 || buf[0] !== 255 || buf[1] !== 216) return null;
365
+ let i = 2;
366
+ while (i + 9 < n) {
367
+ if (buf[i] !== 255) return null;
368
+ const marker = buf[i + 1];
369
+ if (marker === void 0) return null;
370
+ if (marker === 192 || marker === 193 || marker === 194) {
371
+ const height = buf.readUInt16BE(i + 5);
372
+ return {
373
+ width: buf.readUInt16BE(i + 7),
374
+ height
375
+ };
376
+ }
377
+ if (marker === 217 || marker === 218) return null;
378
+ const len = buf.readUInt16BE(i + 2);
379
+ if (len < 2) return null;
380
+ i += 2 + len;
381
+ }
382
+ return null;
383
+ } catch {
384
+ return null;
385
+ }
386
+ }
387
+ async function magick(args) {
388
+ for (const tool of ["magick", "convert"]) try {
389
+ const r = await runCaptured(tool, [...args], { timeoutMs: 6e4 });
390
+ if (!r.timedOut && r.code === 0) return true;
391
+ } catch (err) {
392
+ if (!isNotFound(err)) throw err;
393
+ }
394
+ return false;
395
+ }
396
+ //#endregion
397
+ //#region src/commands/image-gen/command.ts
398
+ const fullDescription$6 = [
399
+ "Renders an image by driving the seat's CLI (codex → gpt-image-2, grok →",
400
+ "Imagine), then verifies the result is a real render before returning it.",
401
+ "",
402
+ "Image-gen seats (canonical slug):",
403
+ ...listModelHelpLines({ imageOnly: true }),
404
+ `Default: ${DEFAULT_IMAGE_GEN} (gpt-image-2 via codex; historical default).`
405
+ ].join("\n");
406
+ const imageGen = buildCommand({
407
+ func: imageGen$1,
408
+ parameters: {
409
+ flags: {
410
+ model: {
411
+ kind: "parsed",
412
+ parse: String,
413
+ optional: true,
414
+ brief: `Model slug (default: ${DEFAULT_IMAGE_GEN})`
415
+ },
416
+ out: {
417
+ kind: "parsed",
418
+ parse: String,
419
+ optional: true,
420
+ brief: "Path to write the image (default: ./aibridge-image.png)"
421
+ },
422
+ size: {
423
+ kind: "parsed",
424
+ parse: String,
425
+ optional: true,
426
+ brief: "WIDTHxHEIGHT (codex: each edge ÷16; grok: mapped to aspect_ratio, then optionally resized)"
427
+ },
428
+ image: {
429
+ kind: "parsed",
430
+ parse: String,
431
+ optional: true,
432
+ brief: "Reference image path(s), comma-separated — visual reference"
433
+ },
434
+ quality: {
435
+ kind: "parsed",
436
+ parse: String,
437
+ optional: true,
438
+ brief: "low | medium | high (codex/gpt-image-2; default high)"
439
+ },
440
+ timeout: {
441
+ kind: "parsed",
442
+ parse: positiveIntSeconds,
443
+ optional: true,
444
+ brief: "Max seconds to wait for the render (default: 600)"
445
+ },
446
+ json: {
447
+ kind: "boolean",
448
+ withNegated: false,
449
+ brief: "Emit a machine-readable JSON result instead of prose"
450
+ }
451
+ },
452
+ positional: {
453
+ kind: "tuple",
454
+ parameters: [{
455
+ brief: "Description of the image to generate",
456
+ parse: nonEmptyPrompt,
457
+ placeholder: "prompt"
458
+ }]
459
+ }
460
+ },
461
+ docs: {
462
+ brief: "Generate a raster image via a model seat",
463
+ fullDescription: fullDescription$6
464
+ }
465
+ });
466
+ //#endregion
467
+ //#region src/delegate.ts
468
+ const PREAMBLE = "You are the sole executing agent for this task: do it yourself with your tools, now. Never defer to, wait for, or claim to hand off to another agent or process — no one else will act, and work not done in this run does not happen.\n\n";
469
+ async function delegate(opts, driver = getDriver(opts.model.spec.backend)) {
470
+ const effectivePrompt = opts.tools ? PREAMBLE + opts.prompt : opts.prompt;
471
+ const result = await driver.run({
472
+ prompt: effectivePrompt,
473
+ tools: opts.tools,
474
+ timeoutSec: opts.timeoutSec,
475
+ cwd: opts.cwd,
476
+ backendModel: backendModelId(opts.model) ?? opts.model.spec.backendModel,
477
+ effort: opts.model.effort,
478
+ onStdout: (c) => opts.run.stdout(c),
479
+ onStderr: (c) => opts.run.stderr(c),
480
+ onSpawn: (pid) => opts.run.setPid(pid)
481
+ });
482
+ if (result.ok) opts.run.finish("done", result.exitCode);
483
+ else opts.run.finish(result.kind === "timeout" ? "timeout" : "error", result.exitCode);
484
+ return result;
485
+ }
486
+ //#endregion
487
+ //#region src/quotaPreflight.ts
488
+ function evaluateAgyPreflight(snapshot, backendModel) {
489
+ const quota = findModelQuota(snapshot, backendModel);
490
+ if (!quota) return {
491
+ ok: true,
492
+ warning: `model "${backendModel}" not in quota snapshot; proceeding`
493
+ };
494
+ if (quota.exhausted) {
495
+ let resetAt = quota.resetTime;
496
+ if (!resetAt) {
497
+ for (const group of snapshot.groups) if (group.displayName.includes("Gemini")) {
498
+ for (const bucket of group.buckets) if (bucket.resetTime) {
499
+ if (!resetAt || new Date(bucket.resetTime).getTime() < new Date(resetAt).getTime()) resetAt = bucket.resetTime;
500
+ }
501
+ }
502
+ }
503
+ return {
504
+ ok: false,
505
+ message: `agy model "${backendModel}" is quota-exhausted`,
506
+ resetAt
507
+ };
508
+ }
509
+ return { ok: true };
510
+ }
511
+ function evaluateCodexPreflight(snapshot) {
512
+ if (snapshot.limitReached) return {
513
+ ok: false,
514
+ message: "codex quota limit reached",
515
+ resetAt: snapshot.windows.find((w) => w.resetAt)?.resetAt
516
+ };
517
+ const exhaustedWindow = snapshot.windows.find((w) => w.usedPercent >= 100);
518
+ if (exhaustedWindow) return {
519
+ ok: false,
520
+ message: "codex quota limit reached",
521
+ resetAt: exhaustedWindow.resetAt
522
+ };
523
+ return { ok: true };
524
+ }
525
+ async function preflightModel(resolved) {
526
+ if (resolved.spec.backend === "codex") return preflightCodex();
527
+ if (resolved.spec.backend !== "agy") return { ok: true };
528
+ try {
529
+ return evaluateAgyPreflight(await fetchAgyQuota(), backendModelId(resolved) ?? "");
530
+ } catch (err) {
531
+ return {
532
+ ok: true,
533
+ warning: `quota preflight failed (${err.message}); proceeding`
534
+ };
535
+ }
536
+ }
537
+ async function preflightCodex() {
538
+ try {
539
+ return evaluateCodexPreflight(await fetchCodexQuota());
540
+ } catch (err) {
541
+ return {
542
+ ok: true,
543
+ warning: `quota preflight failed (${err.message}); proceeding`
544
+ };
545
+ }
546
+ }
547
+ function formatReset$1(resetTime) {
548
+ if (!resetTime) return "-";
549
+ const ms = new Date(resetTime).getTime() - Date.now();
550
+ if (Number.isNaN(ms)) return resetTime;
551
+ if (ms <= 0) return "now";
552
+ const mins = Math.round(ms / 6e4);
553
+ const rel = mins < 60 ? `${mins}m` : `${Math.floor(mins / 60)}h${mins % 60}m`;
554
+ return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
555
+ }
556
+ function renderPreflightRefusal(cmd, verdict) {
557
+ const resetClause = verdict.resetAt ? ` Resets ${formatReset$1(verdict.resetAt)}.` : "";
558
+ return `aibridge ${cmd}: refusing — ${verdict.message}.${resetClause} Use --no-preflight to override, or a claude-backend fallback (subagent --model sonnet|opus — bills the Claude subscription).`;
559
+ }
560
+ //#endregion
561
+ //#region src/runlog.ts
562
+ function getTimestamp() {
563
+ const d = /* @__PURE__ */ new Date();
564
+ return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, "0")}${String(d.getDate()).padStart(2, "0")}-${String(d.getHours()).padStart(2, "0")}${String(d.getMinutes()).padStart(2, "0")}${String(d.getSeconds()).padStart(2, "0")}`;
565
+ }
566
+ function pruneOldRuns(runsDir) {
567
+ try {
568
+ if (!existsSync(runsDir)) return;
569
+ const dirs = readdirSync(runsDir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name).sort();
570
+ if (dirs.length > 50) {
571
+ const toDelete = dirs.slice(0, dirs.length - 50);
572
+ for (const d of toDelete) try {
573
+ rmSync(join(runsDir, d), {
574
+ recursive: true,
575
+ force: true
576
+ });
577
+ } catch {}
578
+ }
579
+ } catch {}
580
+ }
581
+ function startRun(command, detail) {
582
+ const runsDir = join(homedir(), ".aibridge", "runs");
583
+ try {
584
+ mkdirSync(runsDir, { recursive: true });
585
+ pruneOldRuns(runsDir);
586
+ const id = `${getTimestamp()}-${command}-${randomBytes(2).toString("hex")}`;
587
+ const dir = join(runsDir, id);
588
+ mkdirSync(dir, { recursive: true });
589
+ const meta = {
590
+ id,
591
+ command,
592
+ detail,
593
+ pid: null,
594
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
595
+ endedAt: null,
596
+ status: "running",
597
+ exitCode: null
598
+ };
599
+ const metaJsonPath = join(dir, "meta.json");
600
+ const stdoutLogPath = join(dir, "stdout.log");
601
+ const stderrLogPath = join(dir, "stderr.log");
602
+ writeFileSync(metaJsonPath, JSON.stringify(meta, null, 2), "utf8");
603
+ writeFileSync(stdoutLogPath, "", "utf8");
604
+ writeFileSync(stderrLogPath, "", "utf8");
605
+ return {
606
+ id,
607
+ dir,
608
+ setPid(pid) {
609
+ try {
610
+ meta.pid = pid;
611
+ writeFileSync(metaJsonPath, JSON.stringify(meta, null, 2), "utf8");
612
+ } catch {}
613
+ },
614
+ stdout(chunk) {
615
+ try {
616
+ appendFileSync(stdoutLogPath, chunk, "utf8");
617
+ } catch {}
618
+ },
619
+ stderr(chunk) {
620
+ try {
621
+ appendFileSync(stderrLogPath, chunk, "utf8");
622
+ } catch {}
623
+ },
624
+ finish(status, exitCode) {
625
+ try {
626
+ meta.status = status;
627
+ meta.exitCode = exitCode;
628
+ meta.endedAt = (/* @__PURE__ */ new Date()).toISOString();
629
+ writeFileSync(metaJsonPath, JSON.stringify(meta, null, 2), "utf8");
630
+ } catch {}
631
+ }
632
+ };
633
+ } catch {
634
+ return {
635
+ id: "",
636
+ dir: "",
637
+ setPid() {},
638
+ stdout() {},
639
+ stderr() {},
640
+ finish() {}
641
+ };
642
+ }
643
+ }
644
+ function listRuns() {
645
+ const runsDir = join(homedir(), ".aibridge", "runs");
646
+ if (!existsSync(runsDir)) return [];
647
+ try {
648
+ const entries = readdirSync(runsDir, { withFileTypes: true });
649
+ const runs = [];
650
+ for (const entry of entries) if (entry.isDirectory()) try {
651
+ const metaPath = join(runsDir, entry.name, "meta.json");
652
+ if (existsSync(metaPath)) {
653
+ const content = readFileSync(metaPath, "utf8");
654
+ const parsed = JSON.parse(content);
655
+ if (parsed && typeof parsed === "object" && parsed.id && parsed.startedAt && typeof parsed.detail === "string") runs.push(parsed);
656
+ }
657
+ } catch {}
658
+ return runs.sort((a, b) => b.startedAt.localeCompare(a.startedAt));
659
+ } catch {
660
+ return [];
661
+ }
662
+ }
663
+ function readRunLogs(id) {
664
+ const dir = join(join(homedir(), ".aibridge", "runs"), id);
665
+ const metaPath = join(dir, "meta.json");
666
+ const stdoutPath = join(dir, "stdout.log");
667
+ const stderrPath = join(dir, "stderr.log");
668
+ if (!existsSync(metaPath)) return null;
669
+ try {
670
+ return {
671
+ meta: JSON.parse(readFileSync(metaPath, "utf8")),
672
+ stdout: existsSync(stdoutPath) ? readFileSync(stdoutPath, "utf8") : "",
673
+ stderr: existsSync(stderrPath) ? readFileSync(stderrPath, "utf8") : ""
674
+ };
675
+ } catch {
676
+ return null;
677
+ }
678
+ }
679
+ //#endregion
680
+ //#region src/commands/implement/impl.ts
681
+ async function implement$1(flags, planFile) {
682
+ const inputSlug = flags.model ?? "google-antigravity/gemini-3.6-flash";
683
+ const model = resolveModel(inputSlug);
684
+ if (!model) {
685
+ this.process.stderr.write(`${formatUnknownModelError(inputSlug)}\n`);
686
+ this.process.exitCode = 2;
687
+ return;
688
+ }
689
+ const cwd = this.process.cwd();
690
+ const absPlanPath = isAbsolute(planFile) ? planFile : resolve(cwd, planFile);
691
+ if (!existsSync(absPlanPath)) {
692
+ this.process.stderr.write(`aibridge implement: plan file "${absPlanPath}" not found\n`);
693
+ this.process.exitCode = 2;
694
+ return;
695
+ }
696
+ if (flags.preflight) {
697
+ const verdict = await preflightModel(model);
698
+ if (!verdict.ok) {
699
+ this.process.stderr.write(`${renderPreflightRefusal("implement", verdict)}\n`);
700
+ this.process.exitCode = 3;
701
+ return;
702
+ }
703
+ if (verdict.warning) this.process.stderr.write(`aibridge implement: ${verdict.warning}\n`);
704
+ }
705
+ const timeoutSec = flags.timeout ?? 1800;
706
+ const run = startRun("implement", `${model.spec.slug}: ${planFile}`);
707
+ const outcome = await delegate({
708
+ model,
709
+ prompt: `Read the implementation plan file at ${absPlanPath} and implement it EXACTLY.\nEdit only the files it names. Run the project's REAL typecheck and tests and fix until green.\nDo NOT commit, push, or delete unrelated files. Reply with a short summary (files changed + final typecheck/test results).`,
710
+ tools: true,
711
+ timeoutSec,
712
+ cwd,
713
+ run
714
+ });
715
+ if (!outcome.ok) {
716
+ this.process.stderr.write(`${outcome.message}\n`);
717
+ this.process.exitCode = 1;
718
+ return;
719
+ }
720
+ const diffStat = (await runCaptured("git", ["diff", "--stat"], { cwd })).stdout.trim();
721
+ const statusRes = await runCaptured("git", ["status", "--porcelain"], { cwd });
722
+ let untrackedCount = 0;
723
+ if (statusRes.code === 0) {
724
+ for (const line of statusRes.stdout.split(/\r?\n/)) if (line.startsWith("??")) untrackedCount++;
725
+ }
726
+ if (diffStat.length === 0 && untrackedCount === 0) {
727
+ this.process.stderr.write(`aibridge implement: delegate completed but made zero working tree changes.\n`);
728
+ this.process.exitCode = 1;
729
+ return;
730
+ }
731
+ const outputParts = [outcome.response, ""];
732
+ if (diffStat.length > 0) outputParts.push(diffStat);
733
+ outputParts.push(`untracked files: ${untrackedCount}`);
734
+ outputParts.push(`run: ${run.id}`);
735
+ this.process.stdout.write(`${outputParts.join("\n")}\n`);
736
+ }
737
+ //#endregion
738
+ //#region src/commands/implement/command.ts
739
+ const fullDescription$5 = [
740
+ "Reads an implementation plan file and delegates execution to a model.",
741
+ "",
742
+ "Available models (canonical slug):",
743
+ ...listModelHelpLines()
744
+ ].join("\n");
745
+ const implement = buildCommand({
746
+ func: implement$1,
747
+ parameters: {
748
+ flags: {
749
+ model: {
750
+ kind: "parsed",
751
+ parse: String,
752
+ optional: true,
753
+ brief: `Model slug (default: ${DEFAULT_IMPLEMENTER})`
754
+ },
755
+ timeout: {
756
+ kind: "parsed",
757
+ parse: positiveIntSeconds,
758
+ optional: true,
759
+ brief: "Max seconds for implementation (default: 1800)"
760
+ },
761
+ preflight: {
762
+ kind: "boolean",
763
+ default: true,
764
+ brief: "Check model quota before running (use --no-preflight to skip)"
765
+ }
766
+ },
767
+ positional: {
768
+ kind: "tuple",
769
+ parameters: [{
770
+ brief: "Path to the plan file to implement",
771
+ parse: String,
772
+ placeholder: "plan-file"
773
+ }]
774
+ }
775
+ },
776
+ docs: {
777
+ brief: "Execute an implementation plan",
778
+ fullDescription: fullDescription$5
779
+ }
780
+ });
781
+ //#endregion
782
+ //#region src/commands/plan/impl.ts
783
+ function countOpenQuestions(markdown) {
784
+ const headingIdx = markdown.search(/^## Open questions[ \t]*$/m);
785
+ if (headingIdx === -1) return 0;
786
+ const lines = markdown.slice(headingIdx).split(/\r?\n/);
787
+ lines.shift();
788
+ let count = 0;
789
+ for (const line of lines) {
790
+ if (/^## /.test(line)) break;
791
+ const trimmed = line.trim();
792
+ if (trimmed === "None." && count === 0) return 0;
793
+ if (/^[-*] /.test(trimmed)) count++;
794
+ }
795
+ return count;
796
+ }
797
+ async function getPorcelainStatus(cwd) {
798
+ const res = await runCaptured("git", ["status", "--porcelain"], { cwd });
799
+ if (res.code !== 0) return /* @__PURE__ */ new Set();
800
+ const set = /* @__PURE__ */ new Set();
801
+ for (const line of res.stdout.split(/\r?\n/)) if (line.trim().length > 0) set.add(line);
802
+ return set;
803
+ }
804
+ function extractPathFromPorcelainLine(line) {
805
+ let content = line.slice(3).trim();
806
+ if (content.includes(" -> ")) {
807
+ const parts = content.split(" -> ");
808
+ content = (parts[parts.length - 1] ?? "").trim();
809
+ }
810
+ if (content.startsWith("\"") && content.endsWith("\"")) content = content.slice(1, -1);
811
+ return content;
812
+ }
813
+ async function plan$1(flags, taskPrompt) {
814
+ const inputSlug = flags.model ?? "xai-grok/grok-4.5";
815
+ const model = resolveModel(inputSlug);
816
+ if (!model) {
817
+ this.process.stderr.write(`${formatUnknownModelError(inputSlug)}\n`);
818
+ this.process.exitCode = 2;
819
+ return;
820
+ }
821
+ if (flags.preflight) {
822
+ const verdict = await preflightModel(model);
823
+ if (!verdict.ok) {
824
+ this.process.stderr.write(`${renderPreflightRefusal("plan", verdict)}\n`);
825
+ this.process.exitCode = 3;
826
+ return;
827
+ }
828
+ if (verdict.warning) this.process.stderr.write(`aibridge plan: ${verdict.warning}\n`);
829
+ }
830
+ const timeoutSec = flags.timeout ?? 1800;
831
+ const cwd = this.process.cwd();
832
+ const promptSnippet = taskPrompt.replace(/\r?\n/g, " ").slice(0, 80);
833
+ const run = startRun("plan", `${model.spec.slug}: ${promptSnippet}`);
834
+ const absOutPath = flags.out ? isAbsolute(flags.out) ? flags.out : resolve(cwd, flags.out) : resolve(run.dir, "plan.md");
835
+ const beforePorcelain = await getPorcelainStatus(cwd);
836
+ const outcome = await delegate({
837
+ model,
838
+ prompt: `You are a senior implementation planner. Study the real codebase with your tools at ${cwd}.\nDesign module boundaries, interfaces, and naming. Name every file to touch and describe what changes; define clear verification gates.\nWrite EXACTLY one file to the absolute path: ${absOutPath}\nTouch nothing else in the working tree.\nEnd the document with a section titled "## Open questions" (write "None." under it if you are confident and have no open questions).\nDo not commit or push.\n\nTask Prompt:\n${taskPrompt}`,
839
+ tools: true,
840
+ timeoutSec,
841
+ cwd,
842
+ run
843
+ });
844
+ if (!outcome.ok) {
845
+ this.process.stderr.write(`${outcome.message}\n`);
846
+ this.process.exitCode = 1;
847
+ return;
848
+ }
849
+ if (!existsSync(absOutPath)) {
850
+ this.process.stderr.write(`aibridge plan: plan file was not written to ${absOutPath}\n`);
851
+ this.process.exitCode = 1;
852
+ return;
853
+ }
854
+ const planContent = readFileSync(absOutPath, "utf8");
855
+ if (planContent.trim().length === 0) {
856
+ this.process.stderr.write(`aibridge plan: plan file at ${absOutPath} is empty\n`);
857
+ this.process.exitCode = 1;
858
+ return;
859
+ }
860
+ if (!/^## Open questions/m.test(planContent)) {
861
+ this.process.stderr.write(`aibridge plan: plan file at ${absOutPath} missing required "## Open questions" section\n`);
862
+ this.process.exitCode = 1;
863
+ return;
864
+ }
865
+ const afterPorcelain = await getPorcelainStatus(cwd);
866
+ const normalizedOut = resolve(absOutPath);
867
+ const normalizedCwd = resolve(cwd);
868
+ const isOutInRepo = normalizedOut.startsWith(normalizedCwd);
869
+ const unexpectedPaths = [];
870
+ for (const line of afterPorcelain) if (!beforePorcelain.has(line)) {
871
+ const relPath = extractPathFromPorcelainLine(line);
872
+ const absPath = resolve(cwd, relPath);
873
+ if (isOutInRepo && absPath === normalizedOut) continue;
874
+ unexpectedPaths.push(relPath);
875
+ }
876
+ if (unexpectedPaths.length > 0) {
877
+ this.process.stderr.write(`aibridge plan: unexpected working tree changes beyond plan file:\n${unexpectedPaths.map((p) => ` ${p}`).join("\n")}\n`);
878
+ this.process.exitCode = 1;
879
+ return;
880
+ }
881
+ const openQuestions = countOpenQuestions(planContent);
882
+ this.process.stdout.write(`plan: ${absOutPath}\nopen questions: ${openQuestions}\nrun: ${run.id}\n`);
883
+ }
884
+ //#endregion
885
+ //#region src/commands/plan/command.ts
886
+ const fullDescription$4 = [
887
+ "Produce a detailed implementation plan for a task prompt.",
888
+ "",
889
+ "Available models (canonical slug):",
890
+ ...listModelHelpLines()
891
+ ].join("\n");
892
+ const plan = buildCommand({
893
+ func: plan$1,
894
+ parameters: {
895
+ flags: {
896
+ model: {
897
+ kind: "parsed",
898
+ parse: String,
899
+ optional: true,
900
+ brief: `Model slug (default: ${DEFAULT_MODEL})`
901
+ },
902
+ out: {
903
+ kind: "parsed",
904
+ parse: String,
905
+ optional: true,
906
+ brief: "Where to write the plan (default: <run.dir>/plan.md)"
907
+ },
908
+ timeout: {
909
+ kind: "parsed",
910
+ parse: positiveIntSeconds,
911
+ optional: true,
912
+ brief: "Max seconds for planning (default: 1800)"
913
+ },
914
+ preflight: {
915
+ kind: "boolean",
916
+ default: true,
917
+ brief: "Check model quota before running (use --no-preflight to skip)"
918
+ }
919
+ },
920
+ positional: {
921
+ kind: "tuple",
922
+ parameters: [{
923
+ brief: "Task prompt to expand into a detailed implementation plan",
924
+ parse: nonEmptyPrompt,
925
+ placeholder: "task-prompt"
926
+ }]
927
+ }
928
+ },
929
+ docs: {
930
+ brief: "Produce a detailed implementation plan for a task prompt",
931
+ fullDescription: fullDescription$4
932
+ }
933
+ });
934
+ //#endregion
935
+ //#region src/commands/quota/impl.ts
936
+ function formatReset(resetTime) {
937
+ if (!resetTime) return "-";
938
+ const ms = new Date(resetTime).getTime() - Date.now();
939
+ if (Number.isNaN(ms)) return resetTime;
940
+ if (ms <= 0) return "now";
941
+ const mins = Math.round(ms / 6e4);
942
+ const rel = mins < 60 ? `${mins}m` : `${Math.floor(mins / 60)}h${mins % 60}m`;
943
+ return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
944
+ }
945
+ function renderAgy(ctx, snapshot) {
946
+ ctx.process.stdout.write("=== agy (Antigravity) — remaining per model group ===\n");
947
+ for (const group of snapshot.groups) {
948
+ ctx.process.stdout.write(`${group.displayName}\n`);
949
+ for (const b of group.buckets) {
950
+ const pct = b.remainingFraction === 0 ? "EXHAUSTED" : `${Math.round(b.remainingFraction * 100)}%`;
951
+ ctx.process.stdout.write(` ${b.displayName.padEnd(18)} ${pct.padEnd(10)} ${formatReset(b.resetTime)}\n`);
952
+ }
953
+ }
954
+ const exhausted = snapshot.models.filter((m) => m.exhausted);
955
+ if (exhausted.length > 0) ctx.process.stdout.write(`Exhausted models: ${[...new Set(exhausted.map((m) => m.label))].join(", ")}\n`);
956
+ }
957
+ function renderCodex(ctx, snapshot) {
958
+ const plan = snapshot.planType ? ` — plan: ${snapshot.planType}` : "";
959
+ const reached = snapshot.limitReached ? " [LIMIT REACHED]" : "";
960
+ ctx.process.stdout.write(`=== codex (ChatGPT)${plan}${reached} — used per window ===\n`);
961
+ ctx.process.stdout.write(`${"WINDOW".padEnd(10)} ${"USED".padEnd(10)} RESET\n`);
962
+ for (const w of snapshot.windows) ctx.process.stdout.write(`${w.window.padEnd(10)} ${`${w.usedPercent}%`.padEnd(10)} ${formatReset(w.resetAt)}\n`);
963
+ }
964
+ function renderClaude(ctx, snapshot) {
965
+ ctx.process.stdout.write("=== claude (Claude Code subscription) — used per window ===\n");
966
+ ctx.process.stdout.write(`${"WINDOW".padEnd(20)} ${"USED".padEnd(10)} RESET\n`);
967
+ for (const w of snapshot.windows) ctx.process.stdout.write(`${w.window.padEnd(20)} ${`${w.usedPercent}%`.padEnd(10)} ${w.resetsText || "-"}\n`);
968
+ }
969
+ function renderSection(ctx, result, title, render) {
970
+ if (result.status === "fulfilled") render(ctx, result.value);
971
+ else ctx.process.stdout.write(`=== ${title} ===\nunavailable: ${result.reason.message}\n`);
972
+ }
973
+ async function quotaImpl(flags) {
974
+ const [agy, codex, claude] = await Promise.allSettled([
975
+ fetchAgyQuota(),
976
+ fetchCodexQuota(),
977
+ fetchClaudeQuota()
978
+ ]);
979
+ const allFailed = agy.status === "rejected" && codex.status === "rejected" && claude.status === "rejected";
980
+ if (flags.json) {
981
+ this.process.stdout.write(`${JSON.stringify({
982
+ agy: agy.status === "fulfilled" ? agy.value : { error: String(agy.reason) },
983
+ codex: codex.status === "fulfilled" ? codex.value : { error: String(codex.reason) },
984
+ claude: claude.status === "fulfilled" ? claude.value : { error: String(claude.reason) }
985
+ }, null, 2)}\n`);
986
+ if (allFailed) this.process.exitCode = 1;
987
+ return;
988
+ }
989
+ renderSection(this, agy, "agy (Antigravity)", renderAgy);
990
+ this.process.stdout.write("\n");
991
+ renderSection(this, codex, "codex (ChatGPT)", renderCodex);
992
+ this.process.stdout.write("\n");
993
+ renderSection(this, claude, "claude (Claude Code subscription)", renderClaude);
994
+ if (allFailed) this.process.exitCode = 1;
995
+ }
996
+ const quota = buildCommand({
997
+ func: quotaImpl,
998
+ parameters: { flags: { json: {
999
+ kind: "boolean",
1000
+ withNegated: false,
1001
+ brief: "Emit the raw snapshot as JSON"
1002
+ } } },
1003
+ docs: {
1004
+ brief: "Show agy / codex / claude quota with reset times",
1005
+ fullDescription: [
1006
+ "agy: reads its cached OAuth token (~/.gemini/antigravity-cli/) and asks the",
1007
+ "Cloud Code API for per-model remaining quota. EXHAUSTED means agy turns on",
1008
+ "that model fail with an empty answer until the reset time.",
1009
+ "codex: reads ~/.codex/auth.json and asks the ChatGPT usage endpoint for the",
1010
+ "5-hour and weekly windows (used % + reset). No separate logins for either.",
1011
+ "claude: shells out to `claude -p \"/usage\"` (the slow leg, ~5-10s) and parses",
1012
+ "the session + weekly windows — no HTTP endpoint exists and we never touch",
1013
+ "the Keychain; the claude CLI uses its own credentials."
1014
+ ].join("\n")
1015
+ }
1016
+ });
1017
+ //#endregion
1018
+ //#region src/commands/review/impl.ts
1019
+ function matchVerdictLine(line) {
1020
+ if (/^PASS\b/i.test(line)) return { kind: "pass" };
1021
+ const match = line.match(/^FINDINGS:\s*(?:(\d+)\s*critical,?\s*)?(?:(\d+)\s*major,?\s*)?(?:(\d+)\s*minor)?/i);
1022
+ if (match) {
1023
+ const critical = match[1] ? Number.parseInt(match[1], 10) : 0;
1024
+ const major = match[2] ? Number.parseInt(match[2], 10) : 0;
1025
+ const minor = match[3] ? Number.parseInt(match[3], 10) : 0;
1026
+ return {
1027
+ kind: "findings",
1028
+ critical,
1029
+ major,
1030
+ minor,
1031
+ formattedLine: `FINDINGS: ${critical} critical, ${major} major, ${minor} minor`
1032
+ };
1033
+ }
1034
+ return null;
1035
+ }
1036
+ function parseReviewVerdict(response) {
1037
+ const lines = response.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
1038
+ if (lines.length === 0) return {
1039
+ kind: "unparseable",
1040
+ rawLine: ""
1041
+ };
1042
+ const first = matchVerdictLine(lines[0]);
1043
+ if (first) return first;
1044
+ const last = matchVerdictLine(lines[lines.length - 1]);
1045
+ if (last) return last;
1046
+ const embedded = [...response.matchAll(/FINDINGS:\s*(\d+)\s*critical,?\s*(\d+)\s*major,?\s*(\d+)\s*minor/gi)].at(-1);
1047
+ if (embedded) {
1048
+ const critical = Number.parseInt(embedded[1], 10);
1049
+ const major = Number.parseInt(embedded[2], 10);
1050
+ const minor = Number.parseInt(embedded[3], 10);
1051
+ return {
1052
+ kind: "findings",
1053
+ critical,
1054
+ major,
1055
+ minor,
1056
+ formattedLine: `FINDINGS: ${critical} critical, ${major} major, ${minor} minor`
1057
+ };
1058
+ }
1059
+ return {
1060
+ kind: "unparseable",
1061
+ rawLine: lines[0]
1062
+ };
1063
+ }
1064
+ async function review$1(flags) {
1065
+ const inputSlug = flags.model ?? "xai-grok/grok-4.5";
1066
+ const model = resolveModel(inputSlug);
1067
+ if (!model) {
1068
+ this.process.stderr.write(`${formatUnknownModelError(inputSlug)}\n`);
1069
+ this.process.exitCode = 2;
1070
+ return;
1071
+ }
1072
+ const cwd = this.process.cwd();
1073
+ const baseRef = flags.base ?? "HEAD";
1074
+ let absPlanPath;
1075
+ if (flags.plan) {
1076
+ absPlanPath = isAbsolute(flags.plan) ? flags.plan : resolve(cwd, flags.plan);
1077
+ if (!existsSync(absPlanPath)) {
1078
+ this.process.stderr.write(`aibridge review: plan file "${absPlanPath}" not found\n`);
1079
+ this.process.exitCode = 2;
1080
+ return;
1081
+ }
1082
+ }
1083
+ const diffRes = await runCaptured("git", [
1084
+ "diff",
1085
+ "--quiet",
1086
+ baseRef
1087
+ ], { cwd });
1088
+ if (diffRes.code !== 0 && diffRes.code !== 1) {
1089
+ const detail = diffRes.stderr.trim().split("\n")[0] ?? `exit code ${diffRes.code}`;
1090
+ this.process.stderr.write(`aibridge review: git diff failed for base "${baseRef}": ${detail}\n`);
1091
+ this.process.exitCode = 2;
1092
+ return;
1093
+ }
1094
+ const hasDiff = diffRes.code === 1;
1095
+ const statusRes = await runCaptured("git", ["status", "--porcelain"], { cwd });
1096
+ const hasPorcelain = statusRes.code === 0 && statusRes.stdout.trim().length > 0;
1097
+ const isDirty = hasDiff || hasPorcelain;
1098
+ if (!isDirty && !absPlanPath) {
1099
+ this.process.stderr.write(`aibridge review: nothing to review\n`);
1100
+ this.process.exitCode = 2;
1101
+ return;
1102
+ }
1103
+ if (flags.preflight) {
1104
+ const verdict = await preflightModel(model);
1105
+ if (!verdict.ok) {
1106
+ this.process.stderr.write(`${renderPreflightRefusal("review", verdict)}\n`);
1107
+ this.process.exitCode = 3;
1108
+ return;
1109
+ }
1110
+ if (verdict.warning) this.process.stderr.write(`aibridge review: ${verdict.warning}\n`);
1111
+ }
1112
+ const timeoutSec = flags.timeout ?? 1200;
1113
+ const modeDetail = isDirty ? absPlanPath ? `diff + plan (${absPlanPath})` : `diff (${baseRef})` : `plan-only (${absPlanPath})`;
1114
+ const run = startRun("review", `${model.spec.slug}: ${modeDetail}`);
1115
+ const absOutPath = flags.out ? isAbsolute(flags.out) ? flags.out : resolve(cwd, flags.out) : resolve(run.dir, "review.md");
1116
+ let reviewPrompt;
1117
+ if (isDirty) reviewPrompt = `You are an expert code reviewer. Inspect the working tree diff against base '${baseRef}' and untracked files at ${cwd}.\n` + (absPlanPath ? `Compare the implementation against the plan contract at ${absPlanPath}. Any file modified or feature added outside the plan contract counts as over-reach (severity: major unless harmful, then critical).\n` : "") + `Write your detailed review report to the file ${absOutPath}. For each finding, include file:line, severity (critical|major|minor), and rationale.\nYour final answer (last message) must consist of EXACTLY ONE VERDICT LINE:\nEither: "PASS"\nOr: "FINDINGS: <c> critical, <m> major, <n> minor"`;
1118
+ else reviewPrompt = `You are an expert architecture reviewer. Inspect the plan contract file at ${absPlanPath}.\nReview the plan for soundness, missing edge cases, safety, and feasibility.\nWrite your detailed review report to the file ${absOutPath}. For each finding, include severity (critical|major|minor) and rationale.\nYour final answer (last message) must consist of EXACTLY ONE VERDICT LINE:\nEither: "PASS"\nOr: "FINDINGS: <c> critical, <m> major, <n> minor"`;
1119
+ const outcome = await delegate({
1120
+ model,
1121
+ prompt: reviewPrompt,
1122
+ tools: true,
1123
+ timeoutSec,
1124
+ cwd,
1125
+ run
1126
+ });
1127
+ if (!outcome.ok) {
1128
+ this.process.stderr.write(`${outcome.message}\n`);
1129
+ this.process.exitCode = 1;
1130
+ return;
1131
+ }
1132
+ if (!existsSync(absOutPath) || readFileSync(absOutPath, "utf8").trim().length === 0) {
1133
+ this.process.stderr.write(`aibridge review: review file was not written to ${absOutPath}\n`);
1134
+ this.process.exitCode = 1;
1135
+ return;
1136
+ }
1137
+ const verdictResult = parseReviewVerdict(outcome.response);
1138
+ if (verdictResult.kind === "unparseable") {
1139
+ this.process.stderr.write(`aibridge review: could not parse a verdict line from the answer.\n`);
1140
+ this.process.stdout.write(`${outcome.response}\nreview: ${absOutPath}\nrun: ${run.id}\n`);
1141
+ this.process.exitCode = 1;
1142
+ return;
1143
+ }
1144
+ if (verdictResult.kind === "pass") {
1145
+ this.process.stdout.write(`PASS\nreview: ${absOutPath}\nrun: ${run.id}\n`);
1146
+ this.process.exitCode = 0;
1147
+ return;
1148
+ }
1149
+ this.process.stdout.write(`${verdictResult.formattedLine}\nreview: ${absOutPath}\nrun: ${run.id}\n`);
1150
+ const isPassing = verdictResult.critical === 0 && verdictResult.major === 0;
1151
+ this.process.exitCode = isPassing ? 0 : 1;
1152
+ }
1153
+ //#endregion
1154
+ //#region src/commands/review/command.ts
1155
+ const fullDescription$2 = [
1156
+ "Inspects code diffs or plan contracts and writes a review report.",
1157
+ "",
1158
+ "Available models (canonical slug):",
1159
+ ...listModelHelpLines()
1160
+ ].join("\n");
1161
+ const review = buildCommand({
1162
+ func: review$1,
1163
+ parameters: { flags: {
1164
+ model: {
1165
+ kind: "parsed",
1166
+ parse: String,
1167
+ optional: true,
1168
+ brief: `Model slug (default: ${DEFAULT_MODEL})`
1169
+ },
1170
+ plan: {
1171
+ kind: "parsed",
1172
+ parse: String,
1173
+ optional: true,
1174
+ brief: "Plan file for contract / over-reach check"
1175
+ },
1176
+ base: {
1177
+ kind: "parsed",
1178
+ parse: String,
1179
+ optional: true,
1180
+ brief: "Base git ref to diff against (default: HEAD)"
1181
+ },
1182
+ out: {
1183
+ kind: "parsed",
1184
+ parse: String,
1185
+ optional: true,
1186
+ brief: "Where to write the review report (default: <run.dir>/review.md)"
1187
+ },
1188
+ timeout: {
1189
+ kind: "parsed",
1190
+ parse: positiveIntSeconds,
1191
+ optional: true,
1192
+ brief: "Max seconds for review (default: 1200)"
1193
+ },
1194
+ preflight: {
1195
+ kind: "boolean",
1196
+ default: true,
1197
+ brief: "Check model quota before running (use --no-preflight to skip)"
1198
+ }
1199
+ } },
1200
+ docs: {
1201
+ brief: "Review working tree diff or plan contract",
1202
+ fullDescription: fullDescription$2
1203
+ }
1204
+ });
1205
+ //#endregion
1206
+ //#region src/commands/runs/impl.ts
1207
+ function formatElapsed(startedAtStr, endedAtStr) {
1208
+ const start = new Date(startedAtStr).getTime();
1209
+ const end = endedAtStr ? new Date(endedAtStr).getTime() : Date.now();
1210
+ const diffSec = Math.max(0, Math.floor((end - start) / 1e3));
1211
+ if (diffSec < 60) return `${diffSec}s`;
1212
+ return `${Math.floor(diffSec / 60)}m${diffSec % 60}s`;
1213
+ }
1214
+ function getStatus(run) {
1215
+ if (run.status === "running" && run.pid !== null) try {
1216
+ process.kill(run.pid, 0);
1217
+ } catch {
1218
+ return "stale";
1219
+ }
1220
+ return run.status;
1221
+ }
1222
+ async function runs$1(flags, idPrefix) {
1223
+ if (idPrefix !== void 0) {
1224
+ const matches = listRuns().filter((r) => r.id.startsWith(idPrefix));
1225
+ if (matches.length === 0) {
1226
+ this.process.stderr.write(`aibridge runs: no run matches prefix "${idPrefix}"\n`);
1227
+ this.process.exitCode = 1;
1228
+ return;
1229
+ }
1230
+ if (matches.length > 1) {
1231
+ this.process.stderr.write(`aibridge runs: ambiguous prefix "${idPrefix}" matches:\n${matches.map((m) => ` ${m.id}`).join("\n")}\n`);
1232
+ this.process.exitCode = 1;
1233
+ return;
1234
+ }
1235
+ const target = matches[0];
1236
+ if (target === void 0) return;
1237
+ const logs = readRunLogs(target.id);
1238
+ if (!logs) {
1239
+ this.process.stderr.write(`aibridge runs: failed to read logs for run "${target.id}"\n`);
1240
+ this.process.exitCode = 1;
1241
+ return;
1242
+ }
1243
+ const status = getStatus(logs.meta).toUpperCase();
1244
+ const elapsed = formatElapsed(logs.meta.startedAt, logs.meta.endedAt);
1245
+ const summaryLines = [
1246
+ `ID: ${logs.meta.id}`,
1247
+ `COMMAND: ${logs.meta.command}`,
1248
+ `STATUS: ${status}`,
1249
+ `ELAPSED: ${elapsed}`,
1250
+ `DETAIL: ${logs.meta.detail}`
1251
+ ];
1252
+ if (logs.meta.pid !== null) summaryLines.push(`PID: ${logs.meta.pid}`);
1253
+ if (logs.meta.exitCode !== null) summaryLines.push(`EXIT: ${logs.meta.exitCode}`);
1254
+ this.process.stdout.write(`${summaryLines.join("\n")}\n\n`);
1255
+ const stdoutLines = logs.stdout.split("\n");
1256
+ if (stdoutLines.length > 1 && stdoutLines[stdoutLines.length - 1] === "") stdoutLines.pop();
1257
+ const lastStdout = stdoutLines.slice(-40).join("\n");
1258
+ this.process.stdout.write(`${lastStdout}\n`);
1259
+ if (logs.stderr.trim().length > 0) {
1260
+ const stderrLines = logs.stderr.split("\n");
1261
+ if (stderrLines.length > 1 && stderrLines[stderrLines.length - 1] === "") stderrLines.pop();
1262
+ const lastStderr = stderrLines.slice(-10).join("\n");
1263
+ this.process.stdout.write(`\n--- stderr (last 10 lines) ---\n${lastStderr}\n`);
1264
+ }
1265
+ return;
1266
+ }
1267
+ if (flags.watch) {
1268
+ const update = () => {
1269
+ this.process.stdout.write("\x1B[2J\x1B[H");
1270
+ const timeStr = (/* @__PURE__ */ new Date()).toLocaleTimeString();
1271
+ this.process.stdout.write(`aibridge runs — ${timeStr} (ctrl-c to quit)\n\n`);
1272
+ const runs = listRuns();
1273
+ if (runs.length === 0) {
1274
+ this.process.stdout.write("no runs yet\n");
1275
+ return;
1276
+ }
1277
+ const limit = runs.slice(0, 10);
1278
+ this.process.stdout.write(`${"STATUS".padEnd(10)} ${"ID".padEnd(35)} ${"ELAPSED".padEnd(10)} DETAIL\n`);
1279
+ for (const r of limit) {
1280
+ const status = getStatus(r).toUpperCase();
1281
+ const elapsed = formatElapsed(r.startedAt, r.endedAt);
1282
+ const detail = r.detail.replace(/\r?\n/g, " ");
1283
+ const truncatedDetail = detail.length > 60 ? `${detail.slice(0, 57)}...` : detail;
1284
+ this.process.stdout.write(`${status.padEnd(10)} ${r.id.padEnd(35)} ${elapsed.padEnd(10)} ${truncatedDetail}\n`);
1285
+ }
1286
+ const runningRuns = runs.filter((r) => getStatus(r) === "running");
1287
+ for (const r of runningRuns) {
1288
+ const logs = readRunLogs(r.id);
1289
+ if (logs) {
1290
+ this.process.stdout.write(`\n--- stdout: ${r.id} ---\n`);
1291
+ const lines = logs.stdout.split("\n");
1292
+ if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
1293
+ const lastSix = lines.slice(-6).join("\n");
1294
+ this.process.stdout.write(`${lastSix}\n`);
1295
+ }
1296
+ }
1297
+ };
1298
+ update();
1299
+ setInterval(update, 2e3);
1300
+ return new Promise(() => {});
1301
+ }
1302
+ const runs = listRuns();
1303
+ if (runs.length === 0) {
1304
+ this.process.stdout.write("no runs yet\n");
1305
+ return;
1306
+ }
1307
+ if (flags.json) {
1308
+ const limit = runs.slice(0, 20);
1309
+ for (const r of limit) {
1310
+ const status = getStatus(r);
1311
+ const withStatus = {
1312
+ ...r,
1313
+ status
1314
+ };
1315
+ this.process.stdout.write(`${JSON.stringify(withStatus)}\n`);
1316
+ }
1317
+ return;
1318
+ }
1319
+ const limit = runs.slice(0, 20);
1320
+ this.process.stdout.write(`${"STATUS".padEnd(10)} ${"ID".padEnd(35)} ${"ELAPSED".padEnd(10)} DETAIL\n`);
1321
+ for (const r of limit) {
1322
+ const status = getStatus(r).toUpperCase();
1323
+ const elapsed = formatElapsed(r.startedAt, r.endedAt);
1324
+ const detail = r.detail.replace(/\r?\n/g, " ");
1325
+ const truncatedDetail = detail.length > 60 ? `${detail.slice(0, 57)}...` : detail;
1326
+ this.process.stdout.write(`${status.padEnd(10)} ${r.id.padEnd(35)} ${elapsed.padEnd(10)} ${truncatedDetail}\n`);
1327
+ }
1328
+ }
1329
+ //#endregion
1330
+ //#region src/commands/runs/command.ts
1331
+ const fullDescription$1 = "Lists recent runs, watches active runs, or displays logs for a specific run.";
1332
+ async function runsCommand(flags, idPrefix) {
1333
+ if (flags.watch && idPrefix !== void 0) {
1334
+ this.process.stderr.write("aibridge runs: cannot specify <id> when using --watch\n");
1335
+ this.process.exitCode = 2;
1336
+ return;
1337
+ }
1338
+ if (flags.watch && flags.json) {
1339
+ this.process.stderr.write("aibridge runs: cannot specify --json when using --watch\n");
1340
+ this.process.exitCode = 2;
1341
+ return;
1342
+ }
1343
+ await runs$1.call(this, flags, idPrefix);
1344
+ }
1345
+ const runs = buildCommand({
1346
+ func: runsCommand,
1347
+ parameters: {
1348
+ flags: {
1349
+ watch: {
1350
+ kind: "boolean",
1351
+ withNegated: false,
1352
+ brief: "Watch running runs in real time (refresh every 2s)"
1353
+ },
1354
+ json: {
1355
+ kind: "boolean",
1356
+ withNegated: false,
1357
+ brief: "Emit output in JSON Lines format (list mode only)"
1358
+ }
1359
+ },
1360
+ positional: {
1361
+ kind: "tuple",
1362
+ parameters: [{
1363
+ brief: "Run id prefix to inspect (defaults to listing recent runs)",
1364
+ parse: String,
1365
+ placeholder: "id-prefix",
1366
+ optional: true
1367
+ }]
1368
+ }
1369
+ },
1370
+ docs: {
1371
+ brief: "Monitor and inspect execution runs",
1372
+ fullDescription: fullDescription$1
1373
+ }
1374
+ });
1375
+ //#endregion
1376
+ //#region src/commands/subagent/impl.ts
1377
+ async function subagent$1(flags, prompt) {
1378
+ const inputSlug = flags.model ?? "xai-grok/grok-4.5";
1379
+ const model = resolveModel(inputSlug);
1380
+ if (!model) {
1381
+ this.process.stderr.write(`${formatUnknownModelError(inputSlug)}\n`);
1382
+ this.process.exitCode = 2;
1383
+ return;
1384
+ }
1385
+ if (flags.preflight) {
1386
+ const verdict = await preflightModel(model);
1387
+ if (!verdict.ok) {
1388
+ if (flags.json) this.process.stdout.write(`${JSON.stringify({
1389
+ error: "quota_exhausted",
1390
+ message: verdict.message,
1391
+ resetAt: verdict.resetAt ?? null,
1392
+ slug: model.spec.slug
1393
+ })}\n`);
1394
+ else this.process.stderr.write(`${renderPreflightRefusal("subagent", verdict)}\n`);
1395
+ this.process.exitCode = 3;
1396
+ return;
1397
+ }
1398
+ if (verdict.warning) this.process.stderr.write(`aibridge subagent: ${verdict.warning}\n`);
1399
+ }
1400
+ const timeoutSec = flags.timeout ?? 600;
1401
+ const workDir = this.process.cwd();
1402
+ const promptSnippet = prompt.replace(/\r?\n/g, " ").slice(0, 80);
1403
+ const run = startRun("subagent", `${model.spec.slug}: ${promptSnippet}`);
1404
+ const outcome = await delegate({
1405
+ model,
1406
+ prompt,
1407
+ tools: flags.tools,
1408
+ timeoutSec,
1409
+ cwd: workDir,
1410
+ run
1411
+ });
1412
+ if (!outcome.ok) {
1413
+ this.process.stderr.write(`${outcome.message}\n`);
1414
+ this.process.exitCode = 1;
1415
+ return;
1416
+ }
1417
+ if (flags.json) {
1418
+ const modelId = backendModelId(model) ?? null;
1419
+ this.process.stdout.write(`${JSON.stringify({
1420
+ model: modelId,
1421
+ slug: model.spec.slug,
1422
+ response: outcome.response,
1423
+ exitCode: outcome.exitCode
1424
+ })}\n`);
1425
+ } else this.process.stdout.write(`${outcome.response}\n`);
1426
+ }
1427
+ //#endregion
1428
+ //#region src/commands/subagent/command.ts
1429
+ const fullDescription = [
1430
+ "Hands a self-contained prompt to another model and returns its answer.",
1431
+ "",
1432
+ "Available models (canonical slug):",
1433
+ ...listModelHelpLines(),
1434
+ `Default: ${DEFAULT_MODEL} (off-budget). The claude-backend slugs are FALLBACKS for`,
1435
+ "when the off-budget CLIs are quota-exhausted — they bill your Claude subscription."
1436
+ ].join("\n");
1437
+ const subagent = buildCommand({
1438
+ func: subagent$1,
1439
+ parameters: {
1440
+ flags: {
1441
+ model: {
1442
+ kind: "parsed",
1443
+ parse: String,
1444
+ optional: true,
1445
+ brief: `Model slug to delegate to (default: ${DEFAULT_MODEL})`
1446
+ },
1447
+ timeout: {
1448
+ kind: "parsed",
1449
+ parse: positiveIntSeconds,
1450
+ optional: true,
1451
+ brief: "Max seconds to wait for the backend (default: 600)"
1452
+ },
1453
+ tools: {
1454
+ kind: "boolean",
1455
+ default: true,
1456
+ brief: "Allow delegate model to use tools (use --no-tools to restrict to reasoning only)"
1457
+ },
1458
+ preflight: {
1459
+ kind: "boolean",
1460
+ default: true,
1461
+ brief: "Check model quota before running (use --no-preflight to skip)"
1462
+ },
1463
+ json: {
1464
+ kind: "boolean",
1465
+ withNegated: false,
1466
+ brief: "Emit a machine-readable JSON result (using canonical slug) instead of prose"
1467
+ }
1468
+ },
1469
+ positional: {
1470
+ kind: "tuple",
1471
+ parameters: [{
1472
+ brief: "Self-contained task prompt for the delegate model",
1473
+ parse: nonEmptyPrompt,
1474
+ placeholder: "prompt"
1475
+ }]
1476
+ }
1477
+ },
1478
+ docs: {
1479
+ brief: "Delegate a self-contained task to another model",
1480
+ fullDescription
1481
+ }
1482
+ });
1483
+ //#endregion
1484
+ //#region src/exitCode.ts
1485
+ /**
1486
+ * Stricli uses negative ExitCode values for parse/route failures.
1487
+ * Our public contract is Unix-style: 0 ok, 1 op fail, 2 bad args, 3 quota refuse.
1488
+ * Call after `run()`; never overwrite a code already set by an impl (run uses ??=).
1489
+ */
1490
+ function normalizeExitCode(ctx) {
1491
+ const code = ctx.process.exitCode;
1492
+ if (typeof code !== "number") return;
1493
+ if (code === ExitCode.InvalidArgument || code === ExitCode.UnknownCommand) {
1494
+ ctx.process.exitCode = 2;
1495
+ return;
1496
+ }
1497
+ if (code !== 0 && code !== 1 && code !== 2 && code !== 3) ctx.process.exitCode = 1;
1498
+ }
1499
+ //#endregion
1500
+ //#region src/app.ts
1501
+ const { version } = createRequire(import.meta.url)("../package.json");
1502
+ const app = buildApplication(buildRouteMap({
1503
+ routes: {
1504
+ plan,
1505
+ implement,
1506
+ review,
1507
+ subagent,
1508
+ "image-gen": imageGen,
1509
+ runs,
1510
+ quota
1511
+ },
1512
+ docs: { brief: "Bridge tasks to non-Claude AI CLIs — a plan → implement → review workflow, task delegation, and image generation (codex gpt-image-2 / grok Imagine)." }
1513
+ }), {
1514
+ name: "aibridge",
1515
+ versionInfo: { currentVersion: version },
1516
+ scanner: { caseStyle: "allow-kebab-for-camel" }
1517
+ });
1518
+ /** Public entry used by cli.ts and index.ts — preserves runCli(ctx, argv) surface. */
1519
+ async function runCli(ctx, argv) {
1520
+ await run(app, argv, ctx);
1521
+ normalizeExitCode(ctx);
1522
+ }
1523
+ //#endregion
1524
+ //#region src/context.ts
1525
+ function buildContext(process) {
1526
+ return { process };
1527
+ }
1528
+ //#endregion
1529
+ export { listModelHelpLines as C, formatUnknownModelError as S, supportsImageGen as T, DEFAULT_IMPLEMENTER as _, readRunLogs as a, backendModelId as b, evaluateCodexPreflight as c, renderPreflightRefusal as d, delegate as f, DEFAULT_IMAGE_GEN as g, positiveIntSeconds as h, listRuns as i, preflightCodex as l, nonEmptyPrompt as m, app as n, startRun as o, getDriver as p, runCli as r, evaluateAgyPreflight as s, buildContext as t, preflightModel as u, DEFAULT_MODEL as v, resolveModel as w, formatImageGenModelError as x, MODELS as y };