@aibridge/cli 0.4.0 → 0.5.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 CHANGED
@@ -28,7 +28,7 @@ npx skills add ycmjason/aibridge
28
28
  That's it. The skill runs the CLI on demand via `npx -y @aibridge/cli` — nothing else to install. Ask your agent to "use aibridge", or try it yourself:
29
29
 
30
30
  ```bash
31
- npx -y @aibridge/cli subagent --model xai-grok/grok-4.5 "summarize the architecture of this repo"
31
+ npx -y @aibridge/cli subagent --model xai-grok/grok-4.6 "summarize the architecture of this repo"
32
32
  ```
33
33
 
34
34
  <sup>Want the `aibridge` command on your PATH for manual use? `npm i -g @aibridge/cli` (optional).</sup>
@@ -37,10 +37,10 @@ npx -y @aibridge/cli subagent --model xai-grok/grok-4.5 "summarize the architect
37
37
 
38
38
  | Command | Use when |
39
39
  |---|---|
40
- | `aibridge plan --model xai-grok/grok-4.5 --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
- | `aibridge implement --model google-antigravity/gemini-3.6-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.5 --out review.md [--plan <plan.md>]` | You want a **different model** to pressure-test the working-tree diff against the plan contract (over-reach is a finding) — or to review the plan itself before implementing |
43
- | `aibridge subagent --model xai-grok/grok-4.5 "<task>"` | A self-contained task deserves a concurrent delegate, a cross-model second opinion, or a red-team pass |
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
+ | `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 the working-tree diff against the plan contract (over-reach is a finding) — or to review the plan itself before implementing |
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) |
46
46
  | `aibridge quota` | Two-second check of every backend's remaining quota before you pipeline work |
@@ -49,10 +49,10 @@ npx -y @aibridge/cli subagent --model xai-grok/grok-4.5 "summarize the architect
49
49
  The three verbs compose into an orchestrator-driven loop your agent stays in charge of:
50
50
 
51
51
  ```
52
- aibridge plan --model xai-grok/grok-4.5 --out plan.md "add rate limiting to the API" # delegate writes plan.md
52
+ aibridge plan --model xai-grok/grok-4.6 --out plan.md "add rate limiting to the API" # delegate writes plan.md
53
53
  # → your agent reads, edits, approves the plan
54
- aibridge implement --model google-antigravity/gemini-3.6-flash plan.md # another model executes it, runs your gates
55
- aibridge review --model xai-grok/grok-4.5 --out review.md --plan plan.md # a third seat cross-checks the diff
54
+ aibridge implement --model google-antigravity/gemini-3.7-flash plan.md # another model executes it, runs your gates
55
+ aibridge review --model xai-grok/grok-4.6 --out review.md --plan plan.md # a third seat cross-checks the diff
56
56
  ```
57
57
 
58
58
  Plan files — not their contents — travel between stages, so the loop is nearly free on your agent's context.
@@ -62,7 +62,7 @@ Plan files — not their contents — travel between stages, so the loop is near
62
62
  - **The skill carries judgment; the CLI owns execution.** The skill teaches your agent prompt-craft, seat selection, and when to gate; the CLI deterministically drives the backing CLIs, captures their output, verifies results (a "generated image" under 100 KB is a code-drawn fake, an empty answer is a quota death), and logs every run.
63
63
  - **Seats stay cross-model by default.** Grok plans and reviews, Gemini implements — a model never reviews its own diff, and independent eyes catch what shared blind spots miss.
64
64
  - **No API keys.** Delegation runs on the backing CLIs' existing logins, each spending its own quota. (The skill treats a backend that shares your agent's own quota pool as a last resort.)
65
- - **Models are canonical slugs**: `<vendor>-<cli>/<model>[-<effort>]` — e.g. `xai-grok/grok-4.5`, `google-antigravity/gemini-3.6-flash`, `openai-codex/gpt-5.6-sol-high`, `anthropic-claude/opus-5`. No aliases — not short ones, and not moving vendor aliases like `opus`: every seat pins an exact model version. `aibridge <command> --help` lists every seat.
65
+ - **Models are canonical slugs**: `<vendor>-<cli>/<model>[-<effort>]` — e.g. `xai-grok/grok-4.6`, `google-antigravity/gemini-3.7-flash`, `openai-codex/gpt-5.6-sol-high`, `anthropic-claude/opus-5`. No aliases — not short ones, and not moving vendor aliases like `opus`: every seat pins an exact model version. `aibridge <command> --help` lists every seat.
66
66
 
67
67
  ## Tell your agent when to reach for it
68
68
 
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as runCli, t as buildContext } from "./context-v_ns4n_W.mjs";
2
+ import { r as runCli, t as buildContext } from "./context-F8WLXzPv.mjs";
3
3
  //#region src/cli.ts
4
4
  await runCli(buildContext(process), process.argv.slice(2));
5
5
  //#endregion
@@ -11,10 +11,21 @@ import * as codex from "@aibridge/driver-codex";
11
11
  import { fetchCodexQuota } from "@aibridge/driver-codex";
12
12
  import * as grok from "@aibridge/driver-grok";
13
13
  import { fetchGrokQuota } from "@aibridge/driver-grok";
14
- import { runCaptured } from "@aibridge/proc";
14
+ import { isAuthExpired, runCaptured } from "@aibridge/proc";
15
15
  import { randomBytes } from "node:crypto";
16
16
  //#region src/models.ts
17
17
  const MODELS = {
18
+ "xai-grok/grok-4.6": {
19
+ slug: "xai-grok/grok-4.6",
20
+ backend: "grok",
21
+ backendModel: "grok-4.6",
22
+ efforts: [
23
+ "low",
24
+ "medium",
25
+ "high"
26
+ ],
27
+ brief: "xAI Grok 4.6 via grok CLI — own xAI login; ~30 req/min, ~1k msgs/day, single-flight"
28
+ },
18
29
  "xai-grok/grok-4.5": {
19
30
  slug: "xai-grok/grok-4.5",
20
31
  backend: "grok",
@@ -26,6 +37,18 @@ const MODELS = {
26
37
  ],
27
38
  brief: "xAI Grok 4.5 via grok CLI — own xAI login; ~30 req/min, ~1k msgs/day, single-flight"
28
39
  },
40
+ "google-antigravity/gemini-3.7-flash": {
41
+ slug: "google-antigravity/gemini-3.7-flash",
42
+ backend: "agy",
43
+ backendModel: "gemini-3.7-flash",
44
+ efforts: [
45
+ "low",
46
+ "medium",
47
+ "high"
48
+ ],
49
+ defaultEffort: "high",
50
+ brief: "Google Gemini 3.7 Flash via agy — own Antigravity login; quota shared across all Gemini tiers (not yet itemised per-model, so no exhaustion preflight)"
51
+ },
29
52
  "google-antigravity/gemini-3.6-flash": {
30
53
  slug: "google-antigravity/gemini-3.6-flash",
31
54
  backend: "agy",
@@ -515,6 +538,7 @@ function evaluateAgyPreflight(snapshot, backendModel) {
515
538
  }
516
539
  return {
517
540
  ok: false,
541
+ kind: "quota",
518
542
  message: `agy model "${backendModel}" is quota-exhausted`,
519
543
  resetAt
520
544
  };
@@ -524,12 +548,14 @@ function evaluateAgyPreflight(snapshot, backendModel) {
524
548
  function evaluateCodexPreflight(snapshot) {
525
549
  if (snapshot.limitReached) return {
526
550
  ok: false,
551
+ kind: "quota",
527
552
  message: "codex quota limit reached",
528
553
  resetAt: snapshot.windows.find((w) => w.resetAt)?.resetAt
529
554
  };
530
555
  const exhaustedWindow = snapshot.windows.find((w) => w.usedPercent >= 100);
531
556
  if (exhaustedWindow) return {
532
557
  ok: false,
558
+ kind: "quota",
533
559
  message: "codex quota limit reached",
534
560
  resetAt: exhaustedWindow.resetAt
535
561
  };
@@ -538,6 +564,7 @@ function evaluateCodexPreflight(snapshot) {
538
564
  function evaluateGrokPreflight(snapshot) {
539
565
  if (snapshot.usedPercent !== void 0 && snapshot.usedPercent >= 100) return {
540
566
  ok: false,
567
+ kind: "quota",
541
568
  message: "grok credit quota exhausted",
542
569
  resetAt: snapshot.periodEnd
543
570
  };
@@ -550,6 +577,12 @@ async function preflightModel(resolved) {
550
577
  try {
551
578
  return evaluateAgyPreflight(await fetchAgyQuota(), backendModelId(resolved));
552
579
  } catch (err) {
580
+ if (isAuthExpired(err)) return {
581
+ ok: false,
582
+ kind: "auth",
583
+ message: err.message,
584
+ resetAt: void 0
585
+ };
553
586
  return {
554
587
  ok: true,
555
588
  warning: `quota preflight failed (${err.message}); proceeding`
@@ -560,6 +593,12 @@ async function preflightCodex() {
560
593
  try {
561
594
  return evaluateCodexPreflight(await fetchCodexQuota());
562
595
  } catch (err) {
596
+ if (isAuthExpired(err)) return {
597
+ ok: false,
598
+ kind: "auth",
599
+ message: err.message,
600
+ resetAt: void 0
601
+ };
563
602
  return {
564
603
  ok: true,
565
604
  warning: `quota preflight failed (${err.message}); proceeding`
@@ -570,6 +609,12 @@ async function preflightGrok() {
570
609
  try {
571
610
  return evaluateGrokPreflight(await fetchGrokQuota());
572
611
  } catch (err) {
612
+ if (isAuthExpired(err)) return {
613
+ ok: false,
614
+ kind: "auth",
615
+ message: err.message,
616
+ resetAt: void 0
617
+ };
573
618
  return {
574
619
  ok: true,
575
620
  warning: `quota preflight failed (${err.message}); proceeding`
@@ -586,6 +631,7 @@ function formatReset$1(resetTime) {
586
631
  return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
587
632
  }
588
633
  function renderPreflightRefusal(cmd, verdict) {
634
+ 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.`;
589
635
  const resetClause = verdict.resetAt ? ` Resets ${formatReset$1(verdict.resetAt)}.` : "";
590
636
  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).`;
591
637
  }
@@ -1537,7 +1583,7 @@ const fullDescription = [
1537
1583
  "",
1538
1584
  "Available models (canonical slug):",
1539
1585
  ...listModelHelpLines(),
1540
- "Recommended first choice: xai-grok/grok-4.5. Whichever seat runs on the same provider as the",
1586
+ "Recommended first choice: xai-grok/grok-4.6. Whichever seat runs on the same provider as the",
1541
1587
  "agent you orchestrate from is your last resort — it spends the pool you are already burning."
1542
1588
  ].join("\n");
1543
1589
  const subagent = buildCommand({
package/dist/index.d.mts CHANGED
@@ -165,6 +165,7 @@ type PreflightVerdict = {
165
165
  readonly warning?: string;
166
166
  } | {
167
167
  readonly ok: false;
168
+ readonly kind: 'auth' | 'quota';
168
169
  readonly message: string;
169
170
  readonly resetAt: string | undefined;
170
171
  };
@@ -173,6 +174,7 @@ declare function evaluateCodexPreflight(snapshot: CodexQuotaSnapshot): Preflight
173
174
  declare function preflightModel(resolved: ResolvedModel): Promise<PreflightVerdict>;
174
175
  declare function preflightCodex(): Promise<PreflightVerdict>;
175
176
  declare function renderPreflightRefusal(cmd: string, verdict: {
177
+ kind: 'auth' | 'quota';
176
178
  message: string;
177
179
  resetAt: string | undefined;
178
180
  }): string;
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { S as supportsImageGen, _ as backendModelId, a as readRunLogs, b as listModelHelpLines, c as evaluateCodexPreflight, d as renderPreflightRefusal, f as delegate, g as MODELS, h as positiveIntSeconds, i as listRuns, l as preflightCodex, m as nonEmptyPrompt, n as app, o as startRun, p as getDriver, r as runCli, s as evaluateAgyPreflight, t as buildContext, u as preflightModel, v as formatImageGenModelError, x as resolveModel, y as formatUnknownModelError } from "./context-v_ns4n_W.mjs";
1
+ import { S as supportsImageGen, _ as backendModelId, a as readRunLogs, b as listModelHelpLines, c as evaluateCodexPreflight, d as renderPreflightRefusal, f as delegate, g as MODELS, h as positiveIntSeconds, i as listRuns, l as preflightCodex, m as nonEmptyPrompt, n as app, o as startRun, p as getDriver, r as runCli, s as evaluateAgyPreflight, t as buildContext, u as preflightModel, v as formatImageGenModelError, x as resolveModel, y as formatUnknownModelError } from "./context-F8WLXzPv.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.4.0",
3
+ "version": "0.5.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",
@@ -35,11 +35,11 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@stricli/core": "1.3.0",
38
- "@aibridge/proc": "0.4.0",
39
- "@aibridge/driver-grok": "0.4.0",
40
- "@aibridge/driver-agy": "0.4.0",
41
- "@aibridge/driver-codex": "0.4.0",
42
- "@aibridge/driver-claude": "0.4.0"
38
+ "@aibridge/proc": "0.5.0",
39
+ "@aibridge/driver-codex": "0.5.0",
40
+ "@aibridge/driver-claude": "0.5.0",
41
+ "@aibridge/driver-agy": "0.5.0",
42
+ "@aibridge/driver-grok": "0.5.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "tsdown": "0.22.14"
@@ -8,7 +8,7 @@ const fullDescription = [
8
8
  '',
9
9
  'Available models (canonical slug):',
10
10
  ...listModelHelpLines(),
11
- 'Recommended first choice: xai-grok/grok-4.5. Whichever seat runs on the same provider as the',
11
+ 'Recommended first choice: xai-grok/grok-4.6. Whichever seat runs on the same provider as the',
12
12
  'agent you orchestrate from is your last resort — it spends the pool you are already burning.',
13
13
  ].join('\n');
14
14
 
@@ -64,7 +64,7 @@ function createRecordingRunLog() {
64
64
  }
65
65
 
66
66
  describe('delegate stub-driver tests', () => {
67
- const model = resolveModel('xai-grok/grok-4.5');
67
+ const model = resolveModel('xai-grok/grok-4.6');
68
68
  if (!model) throw new Error('model resolution failed');
69
69
 
70
70
  it('prepends preamble when tools: true, passes untouched when tools: false', async () => {
@@ -35,7 +35,7 @@ describe('flag mapping & defaults lock', () => {
35
35
  it('plan command maps defaults correctly', async () => {
36
36
  mockPlanImpl.mockReset();
37
37
  const ctx = fakeCtx();
38
- await runCli(ctx, ['plan', '--model', 'xai-grok/grok-4.5', '--out', 'plan.md', 'do something']);
38
+ await runCli(ctx, ['plan', '--model', 'xai-grok/grok-4.6', '--out', 'plan.md', 'do something']);
39
39
  expect(mockPlanImpl).toHaveBeenCalledTimes(1);
40
40
  const [call] = mockPlanImpl.mock.calls;
41
41
  expect(call).toBeDefined();
@@ -43,7 +43,7 @@ describe('flag mapping & defaults lock', () => {
43
43
  const [flags, prompt] = call;
44
44
  expect(prompt).toBe('do something');
45
45
  expect(flags).toEqual({
46
- model: 'xai-grok/grok-4.5',
46
+ model: 'xai-grok/grok-4.6',
47
47
  out: 'plan.md',
48
48
  preflight: true,
49
49
  });
@@ -55,7 +55,7 @@ describe('flag mapping & defaults lock', () => {
55
55
  await runCli(ctx, [
56
56
  'plan',
57
57
  '--model',
58
- 'xai-grok/grok-4.5',
58
+ 'xai-grok/grok-4.6',
59
59
  '--out',
60
60
  'plan.md',
61
61
  '--no-preflight',
@@ -70,7 +70,7 @@ describe('flag mapping & defaults lock', () => {
70
70
  const [flags, prompt] = call;
71
71
  expect(prompt).toBe('task');
72
72
  expect(flags).toEqual({
73
- model: 'xai-grok/grok-4.5',
73
+ model: 'xai-grok/grok-4.6',
74
74
  out: 'plan.md',
75
75
  preflight: false,
76
76
  timeout: 120,
@@ -80,7 +80,7 @@ describe('flag mapping & defaults lock', () => {
80
80
  it('subagent command maps defaults correctly', async () => {
81
81
  mockSubagentImpl.mockReset();
82
82
  const ctx = fakeCtx();
83
- await runCli(ctx, ['subagent', '--model', 'xai-grok/grok-4.5', 'hello agent']);
83
+ await runCli(ctx, ['subagent', '--model', 'xai-grok/grok-4.6', 'hello agent']);
84
84
  expect(mockSubagentImpl).toHaveBeenCalledTimes(1);
85
85
  const [call] = mockSubagentImpl.mock.calls;
86
86
  expect(call).toBeDefined();
@@ -88,7 +88,7 @@ describe('flag mapping & defaults lock', () => {
88
88
  const [flags, prompt] = call;
89
89
  expect(prompt).toBe('hello agent');
90
90
  expect(flags).toEqual({
91
- model: 'xai-grok/grok-4.5',
91
+ model: 'xai-grok/grok-4.6',
92
92
  tools: true,
93
93
  preflight: true,
94
94
  json: false,
@@ -101,7 +101,7 @@ describe('flag mapping & defaults lock', () => {
101
101
  await runCli(ctx, [
102
102
  'subagent',
103
103
  '--model',
104
- 'xai-grok/grok-4.5',
104
+ 'xai-grok/grok-4.6',
105
105
  '--no-tools',
106
106
  '--no-preflight',
107
107
  'hello agent',
@@ -113,7 +113,7 @@ describe('flag mapping & defaults lock', () => {
113
113
  const [flags, prompt] = call;
114
114
  expect(prompt).toBe('hello agent');
115
115
  expect(flags).toEqual({
116
- model: 'xai-grok/grok-4.5',
116
+ model: 'xai-grok/grok-4.6',
117
117
  tools: false,
118
118
  preflight: false,
119
119
  json: false,
@@ -10,14 +10,14 @@ import {
10
10
 
11
11
  describe('models registry', () => {
12
12
  it('resolves canonical slugs', () => {
13
- const grok = resolveModel('xai-grok/grok-4.5');
13
+ const grok = resolveModel('xai-grok/grok-4.6');
14
14
  expect(grok).toBeDefined();
15
- expect(grok?.spec.slug).toBe('xai-grok/grok-4.5');
15
+ expect(grok?.spec.slug).toBe('xai-grok/grok-4.6');
16
16
  expect(grok?.effort).toBeUndefined();
17
17
 
18
- const gemini = resolveModel('google-antigravity/gemini-3.6-flash');
18
+ const gemini = resolveModel('google-antigravity/gemini-3.7-flash');
19
19
  expect(gemini).toBeDefined();
20
- expect(gemini?.spec.slug).toBe('google-antigravity/gemini-3.6-flash');
20
+ expect(gemini?.spec.slug).toBe('google-antigravity/gemini-3.7-flash');
21
21
  expect(gemini?.effort).toBe('high');
22
22
  });
23
23
 
@@ -38,8 +38,8 @@ describe('models registry', () => {
38
38
  });
39
39
 
40
40
  it('resolves effort suffixes', () => {
41
- const grokMedium = resolveModel('xai-grok/grok-4.5-medium');
42
- expect(grokMedium?.spec.slug).toBe('xai-grok/grok-4.5');
41
+ const grokMedium = resolveModel('xai-grok/grok-4.6-medium');
42
+ expect(grokMedium?.spec.slug).toBe('xai-grok/grok-4.6');
43
43
  expect(grokMedium?.effort).toBe('medium');
44
44
 
45
45
  const sonnetMax = resolveModel('anthropic-claude/sonnet-5-max');
@@ -48,7 +48,7 @@ describe('models registry', () => {
48
48
  });
49
49
 
50
50
  it('rejects unsupported efforts', () => {
51
- const grokXhigh = resolveModel('xai-grok/grok-4.5-xhigh');
51
+ const grokXhigh = resolveModel('xai-grok/grok-4.6-xhigh');
52
52
  expect(grokXhigh).toBeUndefined();
53
53
 
54
54
  const gptOssHigh = resolveModel('google-antigravity/gpt-oss-120b-medium-high');
@@ -69,27 +69,27 @@ describe('models registry', () => {
69
69
  });
70
70
 
71
71
  it('computes backendModelId correctly for agy vs others', () => {
72
- const gemini = resolveModel('google-antigravity/gemini-3.6-flash');
72
+ const gemini = resolveModel('google-antigravity/gemini-3.7-flash');
73
73
  if (!gemini) throw new Error('gemini resolution failed');
74
- expect(backendModelId(gemini)).toBe('gemini-3.6-flash-high');
74
+ expect(backendModelId(gemini)).toBe('gemini-3.7-flash-high');
75
75
 
76
- const geminiLow = resolveModel('google-antigravity/gemini-3.6-flash-low');
76
+ const geminiLow = resolveModel('google-antigravity/gemini-3.7-flash-low');
77
77
  if (!geminiLow) throw new Error('geminiLow resolution failed');
78
- expect(backendModelId(geminiLow)).toBe('gemini-3.6-flash-low');
78
+ expect(backendModelId(geminiLow)).toBe('gemini-3.7-flash-low');
79
79
 
80
- const grok = resolveModel('xai-grok/grok-4.5');
80
+ const grok = resolveModel('xai-grok/grok-4.6');
81
81
  if (!grok) throw new Error('grok resolution failed');
82
- expect(backendModelId(grok)).toBe('grok-4.5');
82
+ expect(backendModelId(grok)).toBe('grok-4.6');
83
83
 
84
84
  const sonnet = resolveModel('anthropic-claude/sonnet-5');
85
85
  if (!sonnet) throw new Error('sonnet resolution failed');
86
86
  expect(backendModelId(sonnet)).toBe('claude-sonnet-5');
87
87
  });
88
88
 
89
- it('marks codex, grok, and gemini-3.6-flash seats as image-gen capable', () => {
89
+ it('marks codex, grok, and gemini-3.7-flash seats as image-gen capable', () => {
90
90
  const codex = resolveModel('openai-codex/gpt-5.6-sol');
91
- const grok = resolveModel('xai-grok/grok-4.5');
92
- const gemini = resolveModel('google-antigravity/gemini-3.6-flash');
91
+ const grok = resolveModel('xai-grok/grok-4.6');
92
+ const gemini = resolveModel('google-antigravity/gemini-3.7-flash');
93
93
  const claudeSonnet = resolveModel('anthropic-claude/sonnet-5');
94
94
  if (!codex || !grok || !gemini || !claudeSonnet) throw new Error('resolution failed');
95
95
  expect(supportsImageGen(codex)).toBe(true);
@@ -100,9 +100,9 @@ describe('models registry', () => {
100
100
 
101
101
  it('lists only image-capable seats when imageOnly', () => {
102
102
  const lines = listModelHelpLines({ imageOnly: true }).join('\n');
103
- expect(lines).toContain('xai-grok/grok-4.5');
103
+ expect(lines).toContain('xai-grok/grok-4.6');
104
104
  expect(lines).toContain('openai-codex/gpt-5.6-sol');
105
- expect(lines).toContain('google-antigravity/gemini-3.6-flash');
105
+ expect(lines).toContain('google-antigravity/gemini-3.7-flash');
106
106
  expect(lines).not.toContain('anthropic-claude/sonnet-5');
107
107
  });
108
108
 
@@ -112,9 +112,9 @@ describe('models registry', () => {
112
112
  const err = formatImageGenModelError('anthropic-claude/sonnet-5', claudeSonnet);
113
113
  expect(err).toContain('cannot generate images');
114
114
  expect(err).toContain('backend "claude"');
115
- expect(err).toContain('xai-grok/grok-4.5');
115
+ expect(err).toContain('xai-grok/grok-4.6');
116
116
  expect(err).toContain('openai-codex/gpt-5.6-sol');
117
- expect(err).toContain('google-antigravity/gemini-3.6-flash');
117
+ expect(err).toContain('google-antigravity/gemini-3.7-flash');
118
118
  const seatsSection = err.slice(err.indexOf('Image-gen seats'));
119
119
  expect(seatsSection).not.toContain('anthropic-claude/sonnet-5');
120
120
  });
package/src/models.ts CHANGED
@@ -26,6 +26,15 @@ export interface ResolvedModel {
26
26
  }
27
27
 
28
28
  export const MODELS: Record<string, ModelSpec> = {
29
+ 'xai-grok/grok-4.6': {
30
+ slug: 'xai-grok/grok-4.6',
31
+ backend: 'grok',
32
+ backendModel: 'grok-4.6',
33
+ efforts: ['low', 'medium', 'high'],
34
+ brief: 'xAI Grok 4.6 via grok CLI — own xAI login; ~30 req/min, ~1k msgs/day, single-flight',
35
+ },
36
+ // Both grok tiers stay registered: 4.6 and 4.5 differ in character, not just
37
+ // recency, so this is two seats of one class rather than a superseded pin.
29
38
  'xai-grok/grok-4.5': {
30
39
  slug: 'xai-grok/grok-4.5',
31
40
  backend: 'grok',
@@ -33,6 +42,17 @@ export const MODELS: Record<string, ModelSpec> = {
33
42
  efforts: ['low', 'medium', 'high'],
34
43
  brief: 'xAI Grok 4.5 via grok CLI — own xAI login; ~30 req/min, ~1k msgs/day, single-flight',
35
44
  },
45
+ 'google-antigravity/gemini-3.7-flash': {
46
+ slug: 'google-antigravity/gemini-3.7-flash',
47
+ backend: 'agy',
48
+ backendModel: 'gemini-3.7-flash',
49
+ efforts: ['low', 'medium', 'high'],
50
+ defaultEffort: 'high',
51
+ brief:
52
+ 'Google Gemini 3.7 Flash via agy — own Antigravity login; quota shared across all Gemini tiers (not yet itemised per-model, so no exhaustion preflight)',
53
+ },
54
+ // 3.6 stays registered while agy's quota endpoint still stops at 3.6: it is
55
+ // the newest flash tier the exhaustion preflight can actually guard.
36
56
  'google-antigravity/gemini-3.6-flash': {
37
57
  slug: 'google-antigravity/gemini-3.6-flash',
38
58
  backend: 'agy',
@@ -7,6 +7,7 @@ import {
7
7
  evaluateAgyPreflight,
8
8
  evaluateCodexPreflight,
9
9
  evaluateGrokPreflight,
10
+ renderPreflightRefusal,
10
11
  } from './quotaPreflight.ts';
11
12
 
12
13
  test('evaluateAgyPreflight: exhausted model returns ok:false with resetTime', () => {
@@ -28,6 +29,7 @@ test('evaluateAgyPreflight: exhausted model returns ok:false with resetTime', ()
28
29
 
29
30
  assert.deepEqual(result, {
30
31
  ok: false,
32
+ kind: 'quota',
31
33
  message: 'agy model "Gemini 3.5 Flash (High)" is quota-exhausted',
32
34
  resetAt: '2024-01-02T12:00:00Z',
33
35
  });
@@ -127,6 +129,7 @@ test('evaluateCodexPreflight: limitReached returns ok:false', () => {
127
129
 
128
130
  assert.deepEqual(result, {
129
131
  ok: false,
132
+ kind: 'quota',
130
133
  message: 'codex quota limit reached',
131
134
  resetAt: '2024-01-01T17:00:00Z',
132
135
  });
@@ -199,6 +202,7 @@ test('evaluateGrokPreflight: usedPercent at 100 refuses with resetAt', () => {
199
202
 
200
203
  assert.deepEqual(result, {
201
204
  ok: false,
205
+ kind: 'quota',
202
206
  message: 'grok credit quota exhausted',
203
207
  resetAt: '2024-01-08T00:00:00Z',
204
208
  });
@@ -221,3 +225,27 @@ test('evaluateGrokPreflight: healthy (17% used) returns ok:true', () => {
221
225
 
222
226
  assert.deepEqual(result, { ok: true });
223
227
  });
228
+
229
+ test('renderPreflightRefusal: auth kind uses unauthenticated wording', () => {
230
+ const msg = renderPreflightRefusal('plan', {
231
+ kind: 'auth',
232
+ message: 'grok session expired (401) — run `grok login`, then retry',
233
+ resetAt: undefined,
234
+ });
235
+ assert.strictEqual(
236
+ msg,
237
+ 'aibridge plan: refusing — grok session expired (401) — run `grok login`, then retry. Running with --no-preflight would only send the delegate in unauthenticated. Or use a different --model.',
238
+ );
239
+ });
240
+
241
+ test('renderPreflightRefusal: quota kind keeps override wording', () => {
242
+ const msg = renderPreflightRefusal('subagent', {
243
+ kind: 'quota',
244
+ message: 'grok credit quota exhausted',
245
+ resetAt: undefined,
246
+ });
247
+ assert.strictEqual(
248
+ msg,
249
+ 'aibridge subagent: refusing — grok credit quota exhausted. Use --no-preflight to override, or a claude-backend fallback (subagent --model sonnet|opus — bills the Claude subscription).',
250
+ );
251
+ });
@@ -1,11 +1,17 @@
1
1
  import { type AgyQuotaSnapshot, fetchAgyQuota, findModelQuota } from '@aibridge/driver-agy';
2
2
  import { type CodexQuotaSnapshot, fetchCodexQuota } from '@aibridge/driver-codex';
3
3
  import { fetchGrokQuota, type GrokQuotaSnapshot } from '@aibridge/driver-grok';
4
+ import { isAuthExpired } from '@aibridge/proc';
4
5
  import { backendModelId, type ResolvedModel } from './models.ts';
5
6
 
6
7
  export type PreflightVerdict =
7
8
  | { readonly ok: true; readonly warning?: string }
8
- | { readonly ok: false; readonly message: string; readonly resetAt: string | undefined };
9
+ | {
10
+ readonly ok: false;
11
+ readonly kind: 'auth' | 'quota';
12
+ readonly message: string;
13
+ readonly resetAt: string | undefined;
14
+ };
9
15
 
10
16
  export function evaluateAgyPreflight(
11
17
  snapshot: AgyQuotaSnapshot,
@@ -32,7 +38,12 @@ export function evaluateAgyPreflight(
32
38
  }
33
39
  }
34
40
  }
35
- return { ok: false, message: `agy model "${backendModel}" is quota-exhausted`, resetAt };
41
+ return {
42
+ ok: false,
43
+ kind: 'quota',
44
+ message: `agy model "${backendModel}" is quota-exhausted`,
45
+ resetAt,
46
+ };
36
47
  }
37
48
 
38
49
  return { ok: true };
@@ -41,12 +52,17 @@ export function evaluateAgyPreflight(
41
52
  export function evaluateCodexPreflight(snapshot: CodexQuotaSnapshot): PreflightVerdict {
42
53
  if (snapshot.limitReached) {
43
54
  const resetAt = snapshot.windows.find(w => w.resetAt)?.resetAt;
44
- return { ok: false, message: 'codex quota limit reached', resetAt };
55
+ return { ok: false, kind: 'quota', message: 'codex quota limit reached', resetAt };
45
56
  }
46
57
 
47
58
  const exhaustedWindow = snapshot.windows.find(w => w.usedPercent >= 100);
48
59
  if (exhaustedWindow) {
49
- return { ok: false, message: 'codex quota limit reached', resetAt: exhaustedWindow.resetAt };
60
+ return {
61
+ ok: false,
62
+ kind: 'quota',
63
+ message: 'codex quota limit reached',
64
+ resetAt: exhaustedWindow.resetAt,
65
+ };
50
66
  }
51
67
 
52
68
  return { ok: true };
@@ -56,6 +72,7 @@ export function evaluateGrokPreflight(snapshot: GrokQuotaSnapshot): PreflightVer
56
72
  if (snapshot.usedPercent !== undefined && snapshot.usedPercent >= 100) {
57
73
  return {
58
74
  ok: false,
75
+ kind: 'quota',
59
76
  message: 'grok credit quota exhausted',
60
77
  resetAt: snapshot.periodEnd,
61
78
  };
@@ -81,10 +98,9 @@ export async function preflightModel(resolved: ResolvedModel): Promise<Preflight
81
98
  const snapshot = await fetchAgyQuota();
82
99
  return evaluateAgyPreflight(snapshot, backendModelId(resolved));
83
100
  } catch (err) {
84
- return {
85
- ok: true,
86
- warning: `quota preflight failed (${(err as Error).message}); proceeding`,
87
- };
101
+ if (isAuthExpired(err))
102
+ return { ok: false, kind: 'auth', message: err.message, resetAt: undefined };
103
+ return { ok: true, warning: `quota preflight failed (${(err as Error).message}); proceeding` };
88
104
  }
89
105
  }
90
106
 
@@ -93,10 +109,9 @@ export async function preflightCodex(): Promise<PreflightVerdict> {
93
109
  const snapshot = await fetchCodexQuota();
94
110
  return evaluateCodexPreflight(snapshot);
95
111
  } catch (err) {
96
- return {
97
- ok: true,
98
- warning: `quota preflight failed (${(err as Error).message}); proceeding`,
99
- };
112
+ if (isAuthExpired(err))
113
+ return { ok: false, kind: 'auth', message: err.message, resetAt: undefined };
114
+ return { ok: true, warning: `quota preflight failed (${(err as Error).message}); proceeding` };
100
115
  }
101
116
  }
102
117
 
@@ -105,10 +120,9 @@ export async function preflightGrok(): Promise<PreflightVerdict> {
105
120
  const snapshot = await fetchGrokQuota();
106
121
  return evaluateGrokPreflight(snapshot);
107
122
  } catch (err) {
108
- return {
109
- ok: true,
110
- warning: `quota preflight failed (${(err as Error).message}); proceeding`,
111
- };
123
+ if (isAuthExpired(err))
124
+ return { ok: false, kind: 'auth', message: err.message, resetAt: undefined };
125
+ return { ok: true, warning: `quota preflight failed (${(err as Error).message}); proceeding` };
112
126
  }
113
127
  }
114
128
 
@@ -124,8 +138,11 @@ function formatReset(resetTime: string | undefined): string {
124
138
 
125
139
  export function renderPreflightRefusal(
126
140
  cmd: string,
127
- verdict: { message: string; resetAt: string | undefined },
141
+ verdict: { kind: 'auth' | 'quota'; message: string; resetAt: string | undefined },
128
142
  ): string {
143
+ if (verdict.kind === 'auth') {
144
+ return `aibridge ${cmd}: refusing — ${verdict.message}. Running with --no-preflight would only send the delegate in unauthenticated. Or use a different --model.`;
145
+ }
129
146
  const resetClause = verdict.resetAt ? ` Resets ${formatReset(verdict.resetAt)}.` : '';
130
147
  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).`;
131
148
  }