@aibridge/cli 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{context-DJtEcg6f.mjs → context-BQgclUqy.mjs} +135 -120
- package/dist/index.mjs +1 -1
- package/package.json +6 -6
- package/src/commands/image-gen/command.ts +5 -0
- package/src/commands/image-gen/impl.test.ts +1 -0
- package/src/commands/image-gen/impl.ts +14 -0
- package/src/commands/review/command.ts +2 -2
- package/src/commands/review/impl.ts +1 -1
- package/src/quotaPreflight.test.ts +13 -1
- package/src/quotaPreflight.ts +10 -2
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ npx -y @aibridge/cli subagent --model xai-grok/grok-4.6 "summarize the architect
|
|
|
39
39
|
|---|---|
|
|
40
40
|
| `aibridge plan --model xai-grok/grok-4.6 --out plan.md "<task>"` | You want a delegate model to study the repo and expand a task into a detailed, reviewable **plan file** before any code is written |
|
|
41
41
|
| `aibridge implement --model google-antigravity/gemini-3.7-flash <plan.md>` | You have an approved plan file and want it executed in place — with your project's **real typecheck and tests** run until green |
|
|
42
|
-
| `aibridge review --model xai-grok/grok-4.6 --out review.md [--plan <plan.md>]` | You want a **different model** to pressure-test
|
|
42
|
+
| `aibridge review --model xai-grok/grok-4.6 --out review.md [--plan <plan.md>]` | You want a **different model** to pressure-test a diff against the plan contract, where over-reach is a finding. `--base <ref>` reviews any commit range, not just the working tree. On a clean tree it reviews the plan itself |
|
|
43
43
|
| `aibridge subagent --model xai-grok/grok-4.6 "<task>"` | A self-contained task deserves a concurrent delegate, a cross-model second opinion, or a red-team pass |
|
|
44
44
|
| `aibridge image-gen --model openai-codex/gpt-5.6-sol --out out.png "<prompt>"` | You need a real raster image — on a Codex, Antigravity, or Grok seat, with render verification |
|
|
45
45
|
| `aibridge models [--json]` | You need the exact facts for every registered model seat (accepted efforts, image format, pinned model ID) |
|
package/dist/cli.mjs
CHANGED
|
@@ -312,6 +312,123 @@ function getDriver(backend) {
|
|
|
312
312
|
return driver;
|
|
313
313
|
}
|
|
314
314
|
//#endregion
|
|
315
|
+
//#region src/quotaPreflight.ts
|
|
316
|
+
function evaluateAgyPreflight(snapshot, backendModel) {
|
|
317
|
+
const quota = findModelQuota(snapshot, backendModel);
|
|
318
|
+
if (!quota) return {
|
|
319
|
+
ok: true,
|
|
320
|
+
warning: `model "${backendModel}" not in quota snapshot; proceeding`
|
|
321
|
+
};
|
|
322
|
+
if (quota.exhausted) {
|
|
323
|
+
let resetAt = quota.resetTime;
|
|
324
|
+
if (!resetAt) {
|
|
325
|
+
for (const group of snapshot.groups) if (group.displayName.includes("Gemini")) {
|
|
326
|
+
for (const bucket of group.buckets) if (bucket.resetTime) {
|
|
327
|
+
if (!resetAt || new Date(bucket.resetTime).getTime() < new Date(resetAt).getTime()) resetAt = bucket.resetTime;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
kind: "quota",
|
|
334
|
+
message: `agy model "${backendModel}" is quota-exhausted`,
|
|
335
|
+
resetAt
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return { ok: true };
|
|
339
|
+
}
|
|
340
|
+
function evaluateCodexPreflight(snapshot) {
|
|
341
|
+
if (snapshot.limitReached) return {
|
|
342
|
+
ok: false,
|
|
343
|
+
kind: "quota",
|
|
344
|
+
message: "codex quota limit reached",
|
|
345
|
+
resetAt: snapshot.windows.find((w) => w.resetAt)?.resetAt
|
|
346
|
+
};
|
|
347
|
+
const exhaustedWindow = snapshot.windows.find((w) => w.usedPercent >= 100);
|
|
348
|
+
if (exhaustedWindow) return {
|
|
349
|
+
ok: false,
|
|
350
|
+
kind: "quota",
|
|
351
|
+
message: "codex quota limit reached",
|
|
352
|
+
resetAt: exhaustedWindow.resetAt
|
|
353
|
+
};
|
|
354
|
+
return { ok: true };
|
|
355
|
+
}
|
|
356
|
+
function evaluateGrokPreflight(snapshot) {
|
|
357
|
+
if (snapshot.usedPercent !== void 0 && snapshot.usedPercent >= 100) return {
|
|
358
|
+
ok: false,
|
|
359
|
+
kind: "quota",
|
|
360
|
+
message: "grok credit quota exhausted",
|
|
361
|
+
resetAt: snapshot.periodEnd
|
|
362
|
+
};
|
|
363
|
+
return { ok: true };
|
|
364
|
+
}
|
|
365
|
+
async function preflightModel(resolved) {
|
|
366
|
+
if (resolved.spec.backend === "codex") return preflightCodex();
|
|
367
|
+
if (resolved.spec.backend === "grok") return preflightGrok();
|
|
368
|
+
if (resolved.spec.backend !== "agy") return { ok: true };
|
|
369
|
+
try {
|
|
370
|
+
return evaluateAgyPreflight(await fetchAgyQuota(), backendModelId(resolved));
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (isAuthExpired(err)) return {
|
|
373
|
+
ok: false,
|
|
374
|
+
kind: "auth",
|
|
375
|
+
message: err.message,
|
|
376
|
+
resetAt: void 0
|
|
377
|
+
};
|
|
378
|
+
return {
|
|
379
|
+
ok: true,
|
|
380
|
+
warning: `quota preflight failed (${err.message}); proceeding`
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function preflightCodex() {
|
|
385
|
+
try {
|
|
386
|
+
return evaluateCodexPreflight(await fetchCodexQuota());
|
|
387
|
+
} catch (err) {
|
|
388
|
+
if (isAuthExpired(err)) return {
|
|
389
|
+
ok: false,
|
|
390
|
+
kind: "auth",
|
|
391
|
+
message: err.message,
|
|
392
|
+
resetAt: void 0
|
|
393
|
+
};
|
|
394
|
+
return {
|
|
395
|
+
ok: true,
|
|
396
|
+
warning: `quota preflight failed (${err.message}); proceeding`
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async function preflightGrok() {
|
|
401
|
+
try {
|
|
402
|
+
return evaluateGrokPreflight(await fetchGrokQuota());
|
|
403
|
+
} catch (err) {
|
|
404
|
+
if (isAuthExpired(err)) return {
|
|
405
|
+
ok: false,
|
|
406
|
+
kind: "auth",
|
|
407
|
+
message: err.message,
|
|
408
|
+
resetAt: void 0
|
|
409
|
+
};
|
|
410
|
+
return {
|
|
411
|
+
ok: true,
|
|
412
|
+
warning: `quota preflight failed (${err.message}); proceeding`
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
function formatReset$1(resetTime) {
|
|
417
|
+
if (!resetTime) return "-";
|
|
418
|
+
const ms = new Date(resetTime).getTime() - Date.now();
|
|
419
|
+
if (Number.isNaN(ms)) return resetTime;
|
|
420
|
+
if (ms <= 0) return "now";
|
|
421
|
+
const mins = Math.round(ms / 6e4);
|
|
422
|
+
const rel = mins < 60 ? `${mins}m` : `${Math.floor(mins / 60)}h${mins % 60}m`;
|
|
423
|
+
return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
|
|
424
|
+
}
|
|
425
|
+
function renderPreflightRefusal(cmd, verdict) {
|
|
426
|
+
if (verdict.kind === "auth") return `aibridge ${cmd}: refusing — ${verdict.message}. Running with --no-preflight would only fail unauthenticated later. Or use a different --model.`;
|
|
427
|
+
const resetClause = verdict.resetAt ? ` Resets ${formatReset$1(verdict.resetAt)}.` : "";
|
|
428
|
+
const fallback = cmd === "image-gen" ? "Use --no-preflight to override, or another image seat (--model openai-codex/gpt-5.6-sol | google-antigravity/gemini-3.7-flash | xai-grok/grok-4.6)." : "Use --no-preflight to override, or a claude-backend fallback (subagent --model sonnet|opus — bills the Claude subscription).";
|
|
429
|
+
return `aibridge ${cmd}: refusing — ${verdict.message}.${resetClause} ${fallback}`;
|
|
430
|
+
}
|
|
431
|
+
//#endregion
|
|
315
432
|
//#region src/transparency.ts
|
|
316
433
|
const CHROMA_CLAUSE = "The entire background must be a perfectly flat solid #00ff00 chroma-key green. The background must be one uniform colour with no shadows, gradients, texture, reflections, or lighting variation. Keep the subject fully separated from the background with crisp edges. Do not use #00ff00 or any similar green anywhere on the subject. No cast shadow, no contact shadow, no reflection.";
|
|
317
434
|
const NATIVE_ALPHA_CLAUSE = "Render the subject on a fully transparent background — PNG with a real alpha channel, no backdrop, no canvas colour, no cast shadow.";
|
|
@@ -419,6 +536,15 @@ async function imageGen$1(flags, prompt) {
|
|
|
419
536
|
}
|
|
420
537
|
const driver = getDriver(model.spec.backend);
|
|
421
538
|
if (!driver.generateImage) return fail(formatImageGenModelError(inputSlug, model));
|
|
539
|
+
if (flags.preflight) {
|
|
540
|
+
const verdict = await preflightModel(model);
|
|
541
|
+
if (!verdict.ok) {
|
|
542
|
+
this.process.stderr.write(`${renderPreflightRefusal("image-gen", verdict)}\n`);
|
|
543
|
+
this.process.exitCode = 3;
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
if (verdict.warning) this.process.stderr.write(`aibridge image-gen: ${verdict.warning}\n`);
|
|
547
|
+
}
|
|
422
548
|
const minBytes = model.spec.backend === "codex" ? MIN_REAL_BYTES_CODEX : MIN_REAL_BYTES_TOOL;
|
|
423
549
|
const work = mkdtempSync(join(tmpdir(), "aibridge-imagegen-"));
|
|
424
550
|
const effectivePrompt = flags.transparent ? `${prompt} ${alpha === "chroma" ? CHROMA_CLAUSE : NATIVE_ALPHA_CLAUSE}` : prompt;
|
|
@@ -585,6 +711,11 @@ const imageGen = buildCommand({
|
|
|
585
711
|
optional: true,
|
|
586
712
|
brief: "Max seconds to wait for the render (default: 600)"
|
|
587
713
|
},
|
|
714
|
+
preflight: {
|
|
715
|
+
kind: "boolean",
|
|
716
|
+
default: true,
|
|
717
|
+
brief: "Check model quota before rendering (use --no-preflight to skip)"
|
|
718
|
+
},
|
|
588
719
|
json: {
|
|
589
720
|
kind: "boolean",
|
|
590
721
|
withNegated: false,
|
|
@@ -626,122 +757,6 @@ async function delegate(opts, driver = getDriver(opts.model.spec.backend)) {
|
|
|
626
757
|
return result;
|
|
627
758
|
}
|
|
628
759
|
//#endregion
|
|
629
|
-
//#region src/quotaPreflight.ts
|
|
630
|
-
function evaluateAgyPreflight(snapshot, backendModel) {
|
|
631
|
-
const quota = findModelQuota(snapshot, backendModel);
|
|
632
|
-
if (!quota) return {
|
|
633
|
-
ok: true,
|
|
634
|
-
warning: `model "${backendModel}" not in quota snapshot; proceeding`
|
|
635
|
-
};
|
|
636
|
-
if (quota.exhausted) {
|
|
637
|
-
let resetAt = quota.resetTime;
|
|
638
|
-
if (!resetAt) {
|
|
639
|
-
for (const group of snapshot.groups) if (group.displayName.includes("Gemini")) {
|
|
640
|
-
for (const bucket of group.buckets) if (bucket.resetTime) {
|
|
641
|
-
if (!resetAt || new Date(bucket.resetTime).getTime() < new Date(resetAt).getTime()) resetAt = bucket.resetTime;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
return {
|
|
646
|
-
ok: false,
|
|
647
|
-
kind: "quota",
|
|
648
|
-
message: `agy model "${backendModel}" is quota-exhausted`,
|
|
649
|
-
resetAt
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
return { ok: true };
|
|
653
|
-
}
|
|
654
|
-
function evaluateCodexPreflight(snapshot) {
|
|
655
|
-
if (snapshot.limitReached) return {
|
|
656
|
-
ok: false,
|
|
657
|
-
kind: "quota",
|
|
658
|
-
message: "codex quota limit reached",
|
|
659
|
-
resetAt: snapshot.windows.find((w) => w.resetAt)?.resetAt
|
|
660
|
-
};
|
|
661
|
-
const exhaustedWindow = snapshot.windows.find((w) => w.usedPercent >= 100);
|
|
662
|
-
if (exhaustedWindow) return {
|
|
663
|
-
ok: false,
|
|
664
|
-
kind: "quota",
|
|
665
|
-
message: "codex quota limit reached",
|
|
666
|
-
resetAt: exhaustedWindow.resetAt
|
|
667
|
-
};
|
|
668
|
-
return { ok: true };
|
|
669
|
-
}
|
|
670
|
-
function evaluateGrokPreflight(snapshot) {
|
|
671
|
-
if (snapshot.usedPercent !== void 0 && snapshot.usedPercent >= 100) return {
|
|
672
|
-
ok: false,
|
|
673
|
-
kind: "quota",
|
|
674
|
-
message: "grok credit quota exhausted",
|
|
675
|
-
resetAt: snapshot.periodEnd
|
|
676
|
-
};
|
|
677
|
-
return { ok: true };
|
|
678
|
-
}
|
|
679
|
-
async function preflightModel(resolved) {
|
|
680
|
-
if (resolved.spec.backend === "codex") return preflightCodex();
|
|
681
|
-
if (resolved.spec.backend === "grok") return preflightGrok();
|
|
682
|
-
if (resolved.spec.backend !== "agy") return { ok: true };
|
|
683
|
-
try {
|
|
684
|
-
return evaluateAgyPreflight(await fetchAgyQuota(), backendModelId(resolved));
|
|
685
|
-
} catch (err) {
|
|
686
|
-
if (isAuthExpired(err)) return {
|
|
687
|
-
ok: false,
|
|
688
|
-
kind: "auth",
|
|
689
|
-
message: err.message,
|
|
690
|
-
resetAt: void 0
|
|
691
|
-
};
|
|
692
|
-
return {
|
|
693
|
-
ok: true,
|
|
694
|
-
warning: `quota preflight failed (${err.message}); proceeding`
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
async function preflightCodex() {
|
|
699
|
-
try {
|
|
700
|
-
return evaluateCodexPreflight(await fetchCodexQuota());
|
|
701
|
-
} catch (err) {
|
|
702
|
-
if (isAuthExpired(err)) return {
|
|
703
|
-
ok: false,
|
|
704
|
-
kind: "auth",
|
|
705
|
-
message: err.message,
|
|
706
|
-
resetAt: void 0
|
|
707
|
-
};
|
|
708
|
-
return {
|
|
709
|
-
ok: true,
|
|
710
|
-
warning: `quota preflight failed (${err.message}); proceeding`
|
|
711
|
-
};
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
async function preflightGrok() {
|
|
715
|
-
try {
|
|
716
|
-
return evaluateGrokPreflight(await fetchGrokQuota());
|
|
717
|
-
} catch (err) {
|
|
718
|
-
if (isAuthExpired(err)) return {
|
|
719
|
-
ok: false,
|
|
720
|
-
kind: "auth",
|
|
721
|
-
message: err.message,
|
|
722
|
-
resetAt: void 0
|
|
723
|
-
};
|
|
724
|
-
return {
|
|
725
|
-
ok: true,
|
|
726
|
-
warning: `quota preflight failed (${err.message}); proceeding`
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
function formatReset$1(resetTime) {
|
|
731
|
-
if (!resetTime) return "-";
|
|
732
|
-
const ms = new Date(resetTime).getTime() - Date.now();
|
|
733
|
-
if (Number.isNaN(ms)) return resetTime;
|
|
734
|
-
if (ms <= 0) return "now";
|
|
735
|
-
const mins = Math.round(ms / 6e4);
|
|
736
|
-
const rel = mins < 60 ? `${mins}m` : `${Math.floor(mins / 60)}h${mins % 60}m`;
|
|
737
|
-
return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
|
|
738
|
-
}
|
|
739
|
-
function renderPreflightRefusal(cmd, verdict) {
|
|
740
|
-
if (verdict.kind === "auth") return `aibridge ${cmd}: refusing — ${verdict.message}. Running with --no-preflight would only send the delegate in unauthenticated. Or use a different --model.`;
|
|
741
|
-
const resetClause = verdict.resetAt ? ` Resets ${formatReset$1(verdict.resetAt)}.` : "";
|
|
742
|
-
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).`;
|
|
743
|
-
}
|
|
744
|
-
//#endregion
|
|
745
760
|
//#region src/runlog.ts
|
|
746
761
|
function getTimestamp() {
|
|
747
762
|
const d = /* @__PURE__ */ new Date();
|
|
@@ -1385,7 +1400,7 @@ async function review$1(flags) {
|
|
|
1385
1400
|
const run = startRun("review", `${model.spec.slug}: ${modeDetail}`);
|
|
1386
1401
|
const absOutPath = isAbsolute(flags.out) ? flags.out : resolve(cwd, flags.out);
|
|
1387
1402
|
let reviewPrompt;
|
|
1388
|
-
if (isDirty) reviewPrompt = `You are an expert code reviewer. Inspect the
|
|
1403
|
+
if (isDirty) reviewPrompt = `You are an expert code reviewer. Inspect the diff produced by \`git diff ${baseRef}\` (this covers committed and uncommitted changes) plus any 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"`;
|
|
1389
1404
|
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"`;
|
|
1390
1405
|
const outcome = await delegate({
|
|
1391
1406
|
model,
|
|
@@ -1447,7 +1462,7 @@ const review = buildCommand({
|
|
|
1447
1462
|
kind: "parsed",
|
|
1448
1463
|
parse: String,
|
|
1449
1464
|
optional: true,
|
|
1450
|
-
brief: "Base git ref to diff against (default: HEAD)"
|
|
1465
|
+
brief: "Base git ref or range to diff against, e.g. HEAD~3 or main (default: HEAD)"
|
|
1451
1466
|
},
|
|
1452
1467
|
out: {
|
|
1453
1468
|
kind: "parsed",
|
|
@@ -1467,7 +1482,7 @@ const review = buildCommand({
|
|
|
1467
1482
|
}
|
|
1468
1483
|
} },
|
|
1469
1484
|
docs: {
|
|
1470
|
-
brief: "Review working tree
|
|
1485
|
+
brief: "Review a diff (working tree, or any commit range via --base) or a plan contract",
|
|
1471
1486
|
fullDescription: fullDescription$2
|
|
1472
1487
|
}
|
|
1473
1488
|
});
|
|
@@ -1793,4 +1808,4 @@ function buildContext(process) {
|
|
|
1793
1808
|
return { process };
|
|
1794
1809
|
}
|
|
1795
1810
|
//#endregion
|
|
1796
|
-
export { supportsImageGen as S, backendModelId as _, readRunLogs as a, listModelHelpLines as b,
|
|
1811
|
+
export { supportsImageGen as S, backendModelId as _, readRunLogs as a, listModelHelpLines as b, evaluateAgyPreflight as c, preflightModel as d, renderPreflightRefusal as f, MODELS as g, positiveIntSeconds as h, listRuns as i, evaluateCodexPreflight as l, nonEmptyPrompt as m, app as n, startRun as o, getDriver as p, runCli as r, delegate as s, buildContext as t, preflightCodex as u, formatImageGenModelError as v, resolveModel as x, formatUnknownModelError as y };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { S as supportsImageGen, _ as backendModelId, a as readRunLogs, b as listModelHelpLines, c as
|
|
1
|
+
import { S as supportsImageGen, _ as backendModelId, a as readRunLogs, b as listModelHelpLines, c as evaluateAgyPreflight, d as preflightModel, f as renderPreflightRefusal, g as MODELS, h as positiveIntSeconds, i as listRuns, l as evaluateCodexPreflight, m as nonEmptyPrompt, n as app, o as startRun, p as getDriver, r as runCli, s as delegate, t as buildContext, u as preflightCodex, v as formatImageGenModelError, x as resolveModel, y as formatUnknownModelError } from "./context-BQgclUqy.mjs";
|
|
2
2
|
export { MODELS, app, backendModelId, buildContext, delegate, evaluateAgyPreflight, evaluateCodexPreflight, formatImageGenModelError, formatUnknownModelError, getDriver, listModelHelpLines, listRuns, nonEmptyPrompt, positiveIntSeconds, preflightCodex, preflightModel, readRunLogs, renderPreflightRefusal, resolveModel, runCli, startRun, supportsImageGen };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibridge/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "CLI that bridges tasks to AI CLIs on your machine (plan / implement / review / subagent / image-gen)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@stricli/core": "1.3.0",
|
|
38
38
|
"sharp": "^0.35.3",
|
|
39
|
-
"@aibridge/
|
|
40
|
-
"@aibridge/
|
|
41
|
-
"@aibridge/driver-
|
|
42
|
-
"@aibridge/driver-
|
|
43
|
-
"@aibridge/driver-claude": "0.
|
|
39
|
+
"@aibridge/proc": "0.9.0",
|
|
40
|
+
"@aibridge/driver-grok": "0.9.0",
|
|
41
|
+
"@aibridge/driver-codex": "0.9.0",
|
|
42
|
+
"@aibridge/driver-agy": "0.9.0",
|
|
43
|
+
"@aibridge/driver-claude": "0.9.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"tsdown": "0.22.14"
|
|
@@ -51,6 +51,11 @@ export const imageGen = buildCommand({
|
|
|
51
51
|
optional: true,
|
|
52
52
|
brief: 'Max seconds to wait for the render (default: 600)',
|
|
53
53
|
},
|
|
54
|
+
preflight: {
|
|
55
|
+
kind: 'boolean',
|
|
56
|
+
default: true,
|
|
57
|
+
brief: 'Check model quota before rendering (use --no-preflight to skip)',
|
|
58
|
+
},
|
|
54
59
|
json: {
|
|
55
60
|
kind: 'boolean',
|
|
56
61
|
withNegated: false,
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
resolveModel,
|
|
26
26
|
supportsImageGen,
|
|
27
27
|
} from '../../models.ts';
|
|
28
|
+
import { preflightModel, renderPreflightRefusal } from '../../quotaPreflight.ts';
|
|
28
29
|
import {
|
|
29
30
|
CHROMA_CLAUSE,
|
|
30
31
|
chromaKeyToPng,
|
|
@@ -38,6 +39,7 @@ export interface ImageGenFlags {
|
|
|
38
39
|
readonly aspectRatio?: string;
|
|
39
40
|
readonly image?: string;
|
|
40
41
|
readonly timeout?: number;
|
|
42
|
+
readonly preflight: boolean;
|
|
41
43
|
readonly json: boolean;
|
|
42
44
|
readonly transparent: boolean;
|
|
43
45
|
}
|
|
@@ -122,6 +124,18 @@ export default async function imageGen(
|
|
|
122
124
|
return fail(formatImageGenModelError(inputSlug, model));
|
|
123
125
|
}
|
|
124
126
|
|
|
127
|
+
// Last gate before a paid render. Every check above is local and must stay
|
|
128
|
+
// above it, so a bad --out or aspect ratio still fails without a network call.
|
|
129
|
+
if (flags.preflight) {
|
|
130
|
+
const verdict = await preflightModel(model);
|
|
131
|
+
if (!verdict.ok) {
|
|
132
|
+
this.process.stderr.write(`${renderPreflightRefusal('image-gen', verdict)}\n`);
|
|
133
|
+
this.process.exitCode = 3;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (verdict.warning) this.process.stderr.write(`aibridge image-gen: ${verdict.warning}\n`);
|
|
137
|
+
}
|
|
138
|
+
|
|
125
139
|
const minBytes = model.spec.backend === 'codex' ? MIN_REAL_BYTES_CODEX : MIN_REAL_BYTES_TOOL;
|
|
126
140
|
const work = mkdtempSync(join(tmpdir(), 'aibridge-imagegen-'));
|
|
127
141
|
|
|
@@ -29,7 +29,7 @@ export const review = buildCommand({
|
|
|
29
29
|
kind: 'parsed',
|
|
30
30
|
parse: String,
|
|
31
31
|
optional: true,
|
|
32
|
-
brief: 'Base git ref to diff against (default: HEAD)',
|
|
32
|
+
brief: 'Base git ref or range to diff against, e.g. HEAD~3 or main (default: HEAD)',
|
|
33
33
|
},
|
|
34
34
|
out: {
|
|
35
35
|
kind: 'parsed',
|
|
@@ -50,7 +50,7 @@ export const review = buildCommand({
|
|
|
50
50
|
},
|
|
51
51
|
},
|
|
52
52
|
docs: {
|
|
53
|
-
brief: 'Review working tree
|
|
53
|
+
brief: 'Review a diff (working tree, or any commit range via --base) or a plan contract',
|
|
54
54
|
fullDescription,
|
|
55
55
|
},
|
|
56
56
|
});
|
|
@@ -145,7 +145,7 @@ export default async function review(this: LocalContext, flags: ReviewFlags): Pr
|
|
|
145
145
|
let reviewPrompt: string;
|
|
146
146
|
if (isDirty) {
|
|
147
147
|
reviewPrompt =
|
|
148
|
-
`You are an expert code reviewer. Inspect the
|
|
148
|
+
`You are an expert code reviewer. Inspect the diff produced by \`git diff ${baseRef}\` (this covers committed and uncommitted changes) plus any untracked files at ${cwd}.\n` +
|
|
149
149
|
(absPlanPath
|
|
150
150
|
? `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`
|
|
151
151
|
: '') +
|
|
@@ -234,10 +234,22 @@ test('renderPreflightRefusal: auth kind uses unauthenticated wording', () => {
|
|
|
234
234
|
});
|
|
235
235
|
assert.strictEqual(
|
|
236
236
|
msg,
|
|
237
|
-
'aibridge plan: refusing — grok session expired (401) — run `grok login`, then retry. Running with --no-preflight would only
|
|
237
|
+
'aibridge plan: refusing — grok session expired (401) — run `grok login`, then retry. Running with --no-preflight would only fail unauthenticated later. Or use a different --model.',
|
|
238
238
|
);
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
+
test('renderPreflightRefusal: image-gen quota refusal points at other image seats', () => {
|
|
242
|
+
const msg = renderPreflightRefusal('image-gen', {
|
|
243
|
+
kind: 'quota',
|
|
244
|
+
message: 'grok credit quota exhausted',
|
|
245
|
+
resetAt: undefined,
|
|
246
|
+
});
|
|
247
|
+
// No claude seat renders images, so the delegation fallback would be dead advice.
|
|
248
|
+
assert.ok(!msg.includes('claude-backend fallback'));
|
|
249
|
+
assert.ok(msg.includes('another image seat'));
|
|
250
|
+
assert.ok(msg.includes('openai-codex/gpt-5.6-sol'));
|
|
251
|
+
});
|
|
252
|
+
|
|
241
253
|
test('renderPreflightRefusal: quota kind keeps override wording', () => {
|
|
242
254
|
const msg = renderPreflightRefusal('subagent', {
|
|
243
255
|
kind: 'quota',
|
package/src/quotaPreflight.ts
CHANGED
|
@@ -141,8 +141,16 @@ export function renderPreflightRefusal(
|
|
|
141
141
|
verdict: { kind: 'auth' | 'quota'; message: string; resetAt: string | undefined },
|
|
142
142
|
): string {
|
|
143
143
|
if (verdict.kind === 'auth') {
|
|
144
|
-
|
|
144
|
+
// "the delegate" was wrong for image-gen, which has no delegate — the grok
|
|
145
|
+
// seat is a direct API call. Verified: --no-preflight there just fails at
|
|
146
|
+
// the render with exit 1.
|
|
147
|
+
return `aibridge ${cmd}: refusing — ${verdict.message}. Running with --no-preflight would only fail unauthenticated later. Or use a different --model.`;
|
|
145
148
|
}
|
|
146
149
|
const resetClause = verdict.resetAt ? ` Resets ${formatReset(verdict.resetAt)}.` : '';
|
|
147
|
-
|
|
150
|
+
// The claude fallback is delegation-only advice: no claude seat renders images.
|
|
151
|
+
const fallback =
|
|
152
|
+
cmd === 'image-gen'
|
|
153
|
+
? 'Use --no-preflight to override, or another image seat (--model openai-codex/gpt-5.6-sol | google-antigravity/gemini-3.7-flash | xai-grok/grok-4.6).'
|
|
154
|
+
: 'Use --no-preflight to override, or a claude-backend fallback (subagent --model sonnet|opus — bills the Claude subscription).';
|
|
155
|
+
return `aibridge ${cmd}: refusing — ${verdict.message}.${resetClause} ${fallback}`;
|
|
148
156
|
}
|