@aibridge/cli 0.2.0 → 0.4.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.
@@ -1,5 +1,5 @@
1
1
  import { buildCommand } from '@stricli/core';
2
- import { DEFAULT_IMAGE_GEN, listModelHelpLines } from '../../models.ts';
2
+ import { listModelHelpLines } from '../../models.ts';
3
3
  import { nonEmptyPrompt, positiveIntSeconds } from '../../parsers.ts';
4
4
  import imageGenImpl from './impl.ts';
5
5
 
@@ -9,7 +9,7 @@ const fullDescription = [
9
9
  '',
10
10
  'Image-gen seats (canonical slug):',
11
11
  ...listModelHelpLines({ imageOnly: true }),
12
- `Default: ${DEFAULT_IMAGE_GEN}.`,
12
+ 'Recommended seat: openai-codex/gpt-5.6-sol.',
13
13
  ].join('\n');
14
14
 
15
15
  export const imageGen = buildCommand({
@@ -19,8 +19,7 @@ export const imageGen = buildCommand({
19
19
  model: {
20
20
  kind: 'parsed',
21
21
  parse: String,
22
- optional: true,
23
- brief: `Model slug (default: ${DEFAULT_IMAGE_GEN})`,
22
+ brief: 'Model slug (required) — see the seat list above',
24
23
  },
25
24
  out: {
26
25
  kind: 'parsed',
@@ -15,7 +15,6 @@ import type { ImageResult } from '../../driver.ts';
15
15
  import { getDriver } from '../../drivers.ts';
16
16
  import {
17
17
  backendModelId,
18
- DEFAULT_IMAGE_GEN,
19
18
  formatImageGenModelError,
20
19
  formatUnknownModelError,
21
20
  imageFormatFor,
@@ -24,7 +23,7 @@ import {
24
23
  } from '../../models.ts';
25
24
 
26
25
  export interface ImageGenFlags {
27
- readonly model?: string;
26
+ readonly model: string;
28
27
  readonly out: string;
29
28
  readonly aspectRatio?: string;
30
29
  readonly image?: string;
@@ -45,14 +44,14 @@ export default async function imageGen(
45
44
  this.process.exitCode = 1;
46
45
  };
47
46
 
48
- const inputSlug = flags.model ?? DEFAULT_IMAGE_GEN;
47
+ const inputSlug = flags.model;
49
48
  const model = resolveModel(inputSlug);
50
49
  if (!model) return fail(formatUnknownModelError(inputSlug));
51
50
  if (!supportsImageGen(model)) return fail(formatImageGenModelError(inputSlug, model));
52
51
 
53
52
  if (model.spec.backend === 'codex' && model.effort) {
54
53
  return fail(
55
- `effort "-${model.effort}" has no effect on image-gen (the image tool renders, not the seat model); use ${DEFAULT_IMAGE_GEN}.`,
54
+ `effort "-${model.effort}" has no effect on image-gen (the image tool renders, not the seat model); pass the un-suffixed slug "${model.spec.slug}" instead.`,
56
55
  );
57
56
  }
58
57
 
@@ -1,5 +1,5 @@
1
1
  import { buildCommand } from '@stricli/core';
2
- import { DEFAULT_IMPLEMENTER, listModelHelpLines } from '../../models.ts';
2
+ import { listModelHelpLines } from '../../models.ts';
3
3
  import { positiveIntSeconds } from '../../parsers.ts';
4
4
  import implementImpl from './impl.ts';
5
5
 
@@ -17,8 +17,7 @@ export const implement = buildCommand({
17
17
  model: {
18
18
  kind: 'parsed',
19
19
  parse: String,
20
- optional: true,
21
- brief: `Model slug (default: ${DEFAULT_IMPLEMENTER})`,
20
+ brief: 'Model slug (required) — see the seat list above',
22
21
  },
23
22
  timeout: {
24
23
  kind: 'parsed',
@@ -3,12 +3,12 @@ import { isAbsolute, resolve } from 'node:path';
3
3
  import { runCaptured } from '@aibridge/proc';
4
4
  import type { LocalContext } from '../../context.ts';
5
5
  import { delegate } from '../../delegate.ts';
6
- import { DEFAULT_IMPLEMENTER, formatUnknownModelError, resolveModel } from '../../models.ts';
6
+ import { formatUnknownModelError, resolveModel } from '../../models.ts';
7
7
  import { preflightModel, renderPreflightRefusal } from '../../quotaPreflight.ts';
8
8
  import { startRun } from '../../runlog.ts';
9
9
 
10
10
  export interface ImplementFlags {
11
- readonly model?: string;
11
+ readonly model: string;
12
12
  readonly timeout?: number;
13
13
  readonly preflight: boolean;
14
14
  }
@@ -18,7 +18,7 @@ export default async function implement(
18
18
  flags: ImplementFlags,
19
19
  planFile: string,
20
20
  ): Promise<void> {
21
- const inputSlug = flags.model ?? DEFAULT_IMPLEMENTER;
21
+ const inputSlug = flags.model;
22
22
  const model = resolveModel(inputSlug);
23
23
  if (!model) {
24
24
  this.process.stderr.write(`${formatUnknownModelError(inputSlug)}\n`);
@@ -0,0 +1,18 @@
1
+ import { buildCommand } from '@stricli/core';
2
+ import modelsImpl from './impl.ts';
3
+
4
+ export const models = buildCommand({
5
+ func: modelsImpl,
6
+ parameters: {
7
+ flags: {
8
+ json: {
9
+ kind: 'boolean',
10
+ withNegated: false,
11
+ brief: 'Emit the registry as JSON',
12
+ },
13
+ },
14
+ },
15
+ docs: {
16
+ brief: 'List every model seat in the registry (slug, efforts, image format)',
17
+ },
18
+ });
@@ -0,0 +1,89 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { LocalContext } from '../../context.ts';
3
+ import { MODELS } from '../../models.ts';
4
+ import modelsImpl from './impl.ts';
5
+
6
+ function createTestContext() {
7
+ let stdoutText = '';
8
+ const fakeProcess = {
9
+ stdout: {
10
+ write(chunk: string | Uint8Array) {
11
+ stdoutText += chunk.toString();
12
+ return true;
13
+ },
14
+ },
15
+ stderr: {
16
+ write() {
17
+ return true;
18
+ },
19
+ },
20
+ exitCode: 0,
21
+ } as unknown as NodeJS.Process;
22
+
23
+ return {
24
+ ctx: { process: fakeProcess } as LocalContext,
25
+ getStdout: () => stdoutText,
26
+ };
27
+ }
28
+
29
+ describe('modelsImpl', () => {
30
+ it('--json emits parseable JSON with one entry per key of MODELS, with seven documented fields', () => {
31
+ const { ctx, getStdout } = createTestContext();
32
+ modelsImpl.call(ctx, { json: true });
33
+
34
+ const raw = getStdout();
35
+ const data = JSON.parse(raw);
36
+
37
+ expect(Array.isArray(data)).toBe(true);
38
+ expect(data.length).toBe(Object.keys(MODELS).length);
39
+
40
+ for (const item of data) {
41
+ expect(item).toHaveProperty('slug');
42
+ expect(item).toHaveProperty('backend');
43
+ expect(item).toHaveProperty('backendModel');
44
+ expect(item).toHaveProperty('efforts');
45
+ expect(item).toHaveProperty('defaultEffort');
46
+ expect(item).toHaveProperty('image');
47
+ expect(item).toHaveProperty('brief');
48
+ expect(Object.keys(item)).toHaveLength(7);
49
+ }
50
+ });
51
+
52
+ it('reports efforts: ["low", "high"] and defaultEffort: "high" for gemini-3.1-pro in JSON', () => {
53
+ const { ctx, getStdout } = createTestContext();
54
+ modelsImpl.call(ctx, { json: true });
55
+
56
+ const data = JSON.parse(getStdout());
57
+ const geminiPro = data.find(
58
+ (item: { slug: string }) => item.slug === 'google-antigravity/gemini-3.1-pro',
59
+ );
60
+ expect(geminiPro).toBeDefined();
61
+ expect(geminiPro.efforts).toEqual(['low', 'high']);
62
+ expect(geminiPro.defaultEffort).toBe('high');
63
+ });
64
+
65
+ it('reports backendModel and image correctly for opus-5 and gpt-5.6-sol in JSON', () => {
66
+ const { ctx, getStdout } = createTestContext();
67
+ modelsImpl.call(ctx, { json: true });
68
+
69
+ const data = JSON.parse(getStdout());
70
+ const opus = data.find((item: { slug: string }) => item.slug === 'anthropic-claude/opus-5');
71
+ expect(opus).toBeDefined();
72
+ expect(opus.backendModel).toBe('claude-opus-5[1m]');
73
+ expect(opus.image).toBeNull();
74
+
75
+ const sol = data.find((item: { slug: string }) => item.slug === 'openai-codex/gpt-5.6-sol');
76
+ expect(sol).toBeDefined();
77
+ expect(sol.image).toBe('png');
78
+ });
79
+
80
+ it('human output (no --json) contains every slug in MODELS', () => {
81
+ const { ctx, getStdout } = createTestContext();
82
+ modelsImpl.call(ctx, { json: false });
83
+
84
+ const output = getStdout();
85
+ for (const slug of Object.keys(MODELS)) {
86
+ expect(output).toContain(slug);
87
+ }
88
+ });
89
+ });
@@ -0,0 +1,71 @@
1
+ import type { LocalContext } from '../../context.ts';
2
+ import { type Backend, imageFormatFor, MODELS } from '../../models.ts';
3
+
4
+ export interface ModelsFlags {
5
+ readonly json: boolean;
6
+ }
7
+
8
+ const BACKEND_DISPLAY_NAMES: Record<Backend, string> = {
9
+ grok: 'grok (Grok CLI)',
10
+ agy: 'agy (Antigravity)',
11
+ codex: 'codex (Codex CLI)',
12
+ claude: 'claude (Claude Code CLI)',
13
+ };
14
+
15
+ export default function modelsImpl(this: LocalContext, flags: ModelsFlags): void {
16
+ const specs = Object.values(MODELS);
17
+
18
+ if (flags.json) {
19
+ const jsonOutput = specs.map(spec => ({
20
+ slug: spec.slug,
21
+ backend: spec.backend,
22
+ backendModel: spec.backendModel,
23
+ efforts: spec.efforts ? [...spec.efforts] : [],
24
+ defaultEffort: spec.defaultEffort ?? null,
25
+ image: imageFormatFor({ spec, effort: undefined }) ?? null,
26
+ brief: spec.brief,
27
+ }));
28
+ this.process.stdout.write(`${JSON.stringify(jsonOutput)}\n`);
29
+ return;
30
+ }
31
+
32
+ const backends: Backend[] = [];
33
+ for (const spec of specs) {
34
+ if (!backends.includes(spec.backend)) {
35
+ backends.push(spec.backend);
36
+ }
37
+ }
38
+
39
+ let firstBackend = true;
40
+ for (const backend of backends) {
41
+ if (!firstBackend) {
42
+ this.process.stdout.write('\n');
43
+ }
44
+ firstBackend = false;
45
+
46
+ this.process.stdout.write(`=== ${BACKEND_DISPLAY_NAMES[backend]} ===\n`);
47
+ const backendSpecs = specs.filter(spec => spec.backend === backend);
48
+ for (const spec of backendSpecs) {
49
+ this.process.stdout.write(` ${spec.slug}\n`);
50
+
51
+ const segments: string[] = [];
52
+ if (spec.efforts) {
53
+ const formattedEfforts = spec.efforts
54
+ .map(e => (e === spec.defaultEffort ? `${e}*` : e))
55
+ .join(' | ');
56
+ segments.push(`efforts: ${formattedEfforts}`);
57
+ }
58
+ const img = imageFormatFor({ spec, effort: undefined });
59
+ segments.push(`image: ${img ?? '—'}`);
60
+ segments.push(`id: ${spec.backendModel}`);
61
+
62
+ this.process.stdout.write(` ${segments.join(' · ')}\n`);
63
+ this.process.stdout.write(` ${spec.brief}\n`);
64
+ }
65
+ }
66
+
67
+ const hasDefaultEffort = specs.some(spec => spec.defaultEffort !== undefined);
68
+ if (hasDefaultEffort) {
69
+ this.process.stdout.write('\n* = effort used when the slug has no -<effort> suffix\n');
70
+ }
71
+ }
@@ -1,5 +1,5 @@
1
1
  import { buildCommand } from '@stricli/core';
2
- import { DEFAULT_MODEL, listModelHelpLines } from '../../models.ts';
2
+ import { listModelHelpLines } from '../../models.ts';
3
3
  import { nonEmptyPrompt, positiveIntSeconds } from '../../parsers.ts';
4
4
  import planImpl from './impl.ts';
5
5
 
@@ -17,14 +17,12 @@ export const plan = buildCommand({
17
17
  model: {
18
18
  kind: 'parsed',
19
19
  parse: String,
20
- optional: true,
21
- brief: `Model slug (default: ${DEFAULT_MODEL})`,
20
+ brief: 'Model slug (required) — see the seat list above',
22
21
  },
23
22
  out: {
24
23
  kind: 'parsed',
25
24
  parse: String,
26
- optional: true,
27
- brief: 'Where to write the plan (default: <run.dir>/plan.md)',
25
+ brief: 'Where to write the plan file (required)',
28
26
  },
29
27
  timeout: {
30
28
  kind: 'parsed',
@@ -3,13 +3,13 @@ import { isAbsolute, resolve } from 'node:path';
3
3
  import { runCaptured } from '@aibridge/proc';
4
4
  import type { LocalContext } from '../../context.ts';
5
5
  import { delegate } from '../../delegate.ts';
6
- import { DEFAULT_MODEL, formatUnknownModelError, resolveModel } from '../../models.ts';
6
+ import { formatUnknownModelError, resolveModel } from '../../models.ts';
7
7
  import { preflightModel, renderPreflightRefusal } from '../../quotaPreflight.ts';
8
8
  import { startRun } from '../../runlog.ts';
9
9
 
10
10
  export interface PlanFlags {
11
- readonly model?: string;
12
- readonly out?: string;
11
+ readonly model: string;
12
+ readonly out: string;
13
13
  readonly timeout?: number;
14
14
  readonly preflight: boolean;
15
15
  }
@@ -63,7 +63,7 @@ export default async function plan(
63
63
  flags: PlanFlags,
64
64
  taskPrompt: string,
65
65
  ): Promise<void> {
66
- const inputSlug = flags.model ?? DEFAULT_MODEL;
66
+ const inputSlug = flags.model;
67
67
  const model = resolveModel(inputSlug);
68
68
  if (!model) {
69
69
  this.process.stderr.write(`${formatUnknownModelError(inputSlug)}\n`);
@@ -86,11 +86,7 @@ export default async function plan(
86
86
  const promptSnippet = taskPrompt.replace(/\r?\n/g, ' ').slice(0, 80);
87
87
  const run = startRun('plan', `${model.spec.slug}: ${promptSnippet}`);
88
88
 
89
- const absOutPath = flags.out
90
- ? isAbsolute(flags.out)
91
- ? flags.out
92
- : resolve(cwd, flags.out)
93
- : resolve(run.dir, 'plan.md');
89
+ const absOutPath = isAbsolute(flags.out) ? flags.out : resolve(cwd, flags.out);
94
90
 
95
91
  const beforePorcelain = await getPorcelainStatus(cwd);
96
92
 
@@ -2,6 +2,8 @@ import { buildCommand } from '@stricli/core';
2
2
  import quotaImpl from './impl.ts';
3
3
 
4
4
  const fullDescription = [
5
+ 'grok: reads ~/.grok/auth.json and asks the xAI billing endpoint for the',
6
+ 'weekly credit usage percentage and per-product split.',
5
7
  'agy: reads its cached OAuth token (~/.gemini/antigravity-cli/) and asks the',
6
8
  'Cloud Code API for per-model remaining quota. EXHAUSTED means agy turns on',
7
9
  'that model fail with an empty answer until the reset time.',
@@ -24,7 +26,7 @@ export const quota = buildCommand({
24
26
  },
25
27
  },
26
28
  docs: {
27
- brief: 'Show agy / codex / claude quota with reset times',
29
+ brief: 'Show grok / agy / codex / claude quota with reset times',
28
30
  fullDescription,
29
31
  },
30
32
  });
@@ -1,6 +1,7 @@
1
1
  import { type AgyQuotaSnapshot, fetchAgyQuota } from '@aibridge/driver-agy';
2
2
  import { type ClaudeQuotaSnapshot, fetchClaudeQuota } from '@aibridge/driver-claude';
3
3
  import { type CodexQuotaSnapshot, fetchCodexQuota } from '@aibridge/driver-codex';
4
+ import { fetchGrokQuota, type GrokQuotaSnapshot } from '@aibridge/driver-grok';
4
5
  import type { LocalContext } from '../../context.ts';
5
6
 
6
7
  export interface QuotaFlags {
@@ -17,6 +18,20 @@ function formatReset(resetTime: string | undefined): string {
17
18
  return `${new Date(resetTime).toLocaleTimeString()} (in ${rel})`;
18
19
  }
19
20
 
21
+ function renderGrok(ctx: LocalContext, snapshot: GrokQuotaSnapshot): void {
22
+ ctx.process.stdout.write('=== grok (xAI) — used this period ===\n');
23
+ ctx.process.stdout.write(`${'PERIOD'.padEnd(10)} ${'USED'.padEnd(10)} RESET\n`);
24
+ const usedPctStr = snapshot.usedPercent !== undefined ? `${snapshot.usedPercent}%` : '?';
25
+ const periodStr = snapshot.periodType ?? '-';
26
+ ctx.process.stdout.write(
27
+ `${periodStr.padEnd(10)} ${usedPctStr.padEnd(10)} ${formatReset(snapshot.periodEnd)}\n`,
28
+ );
29
+ if (snapshot.products.length > 0) {
30
+ const prods = snapshot.products.map(p => `${p.product} ${p.usedPercent}%`).join(' · ');
31
+ ctx.process.stdout.write(` ${prods}\n`);
32
+ }
33
+ }
34
+
20
35
  function renderAgy(ctx: LocalContext, snapshot: AgyQuotaSnapshot): void {
21
36
  ctx.process.stdout.write('=== agy (Antigravity) — remaining per model group ===\n');
22
37
  for (const group of snapshot.groups) {
@@ -75,19 +90,24 @@ function renderSection<T>(
75
90
  }
76
91
 
77
92
  export default async function quotaImpl(this: LocalContext, flags: QuotaFlags): Promise<void> {
78
- const [agy, codex, claude] = await Promise.allSettled([
93
+ const [grok, agy, codex, claude] = await Promise.allSettled([
94
+ fetchGrokQuota(),
79
95
  fetchAgyQuota(),
80
96
  fetchCodexQuota(),
81
97
  fetchClaudeQuota(),
82
98
  ]);
83
99
 
84
100
  const allFailed =
85
- agy.status === 'rejected' && codex.status === 'rejected' && claude.status === 'rejected';
101
+ grok.status === 'rejected' &&
102
+ agy.status === 'rejected' &&
103
+ codex.status === 'rejected' &&
104
+ claude.status === 'rejected';
86
105
 
87
106
  if (flags.json) {
88
107
  this.process.stdout.write(
89
108
  `${JSON.stringify(
90
109
  {
110
+ grok: grok.status === 'fulfilled' ? grok.value : { error: String(grok.reason) },
91
111
  agy: agy.status === 'fulfilled' ? agy.value : { error: String(agy.reason) },
92
112
  codex: codex.status === 'fulfilled' ? codex.value : { error: String(codex.reason) },
93
113
  claude: claude.status === 'fulfilled' ? claude.value : { error: String(claude.reason) },
@@ -100,6 +120,8 @@ export default async function quotaImpl(this: LocalContext, flags: QuotaFlags):
100
120
  return;
101
121
  }
102
122
 
123
+ renderSection(this, grok, 'grok (xAI)', renderGrok);
124
+ this.process.stdout.write('\n');
103
125
  renderSection(this, agy, 'agy (Antigravity)', renderAgy);
104
126
  this.process.stdout.write('\n');
105
127
  renderSection(this, codex, 'codex (ChatGPT)', renderCodex);
@@ -1,5 +1,5 @@
1
1
  import { buildCommand } from '@stricli/core';
2
- import { DEFAULT_MODEL, listModelHelpLines } from '../../models.ts';
2
+ import { listModelHelpLines } from '../../models.ts';
3
3
  import { positiveIntSeconds } from '../../parsers.ts';
4
4
  import reviewImpl from './impl.ts';
5
5
 
@@ -17,8 +17,7 @@ export const review = buildCommand({
17
17
  model: {
18
18
  kind: 'parsed',
19
19
  parse: String,
20
- optional: true,
21
- brief: `Model slug (default: ${DEFAULT_MODEL})`,
20
+ brief: 'Model slug (required) — see the seat list above',
22
21
  },
23
22
  plan: {
24
23
  kind: 'parsed',
@@ -35,8 +34,7 @@ export const review = buildCommand({
35
34
  out: {
36
35
  kind: 'parsed',
37
36
  parse: String,
38
- optional: true,
39
- brief: 'Where to write the review report (default: <run.dir>/review.md)',
37
+ brief: 'Where to write the review report (required)',
40
38
  },
41
39
  timeout: {
42
40
  kind: 'parsed',
@@ -3,15 +3,15 @@ import { isAbsolute, resolve } from 'node:path';
3
3
  import { runCaptured } from '@aibridge/proc';
4
4
  import type { LocalContext } from '../../context.ts';
5
5
  import { delegate } from '../../delegate.ts';
6
- import { DEFAULT_MODEL, formatUnknownModelError, resolveModel } from '../../models.ts';
6
+ import { formatUnknownModelError, resolveModel } from '../../models.ts';
7
7
  import { preflightModel, renderPreflightRefusal } from '../../quotaPreflight.ts';
8
8
  import { startRun } from '../../runlog.ts';
9
9
 
10
10
  export interface ReviewFlags {
11
- readonly model?: string;
11
+ readonly model: string;
12
12
  readonly plan?: string;
13
13
  readonly base?: string;
14
- readonly out?: string;
14
+ readonly out: string;
15
15
  readonly timeout?: number;
16
16
  readonly preflight: boolean;
17
17
  }
@@ -78,7 +78,7 @@ export function parseReviewVerdict(response: string): ReviewVerdictResult {
78
78
  }
79
79
 
80
80
  export default async function review(this: LocalContext, flags: ReviewFlags): Promise<void> {
81
- const inputSlug = flags.model ?? DEFAULT_MODEL;
81
+ const inputSlug = flags.model;
82
82
  const model = resolveModel(inputSlug);
83
83
  if (!model) {
84
84
  this.process.stderr.write(`${formatUnknownModelError(inputSlug)}\n`);
@@ -140,11 +140,7 @@ export default async function review(this: LocalContext, flags: ReviewFlags): Pr
140
140
 
141
141
  const run = startRun('review', `${model.spec.slug}: ${modeDetail}`);
142
142
 
143
- const absOutPath = flags.out
144
- ? isAbsolute(flags.out)
145
- ? flags.out
146
- : resolve(cwd, flags.out)
147
- : resolve(run.dir, 'review.md');
143
+ const absOutPath = isAbsolute(flags.out) ? flags.out : resolve(cwd, flags.out);
148
144
 
149
145
  let reviewPrompt: string;
150
146
  if (isDirty) {
@@ -1,5 +1,5 @@
1
1
  import { buildCommand } from '@stricli/core';
2
- import { DEFAULT_MODEL, listModelHelpLines } from '../../models.ts';
2
+ import { listModelHelpLines } from '../../models.ts';
3
3
  import { nonEmptyPrompt, positiveIntSeconds } from '../../parsers.ts';
4
4
  import subagentImpl from './impl.ts';
5
5
 
@@ -8,8 +8,8 @@ const fullDescription = [
8
8
  '',
9
9
  'Available models (canonical slug):',
10
10
  ...listModelHelpLines(),
11
- `Default: ${DEFAULT_MODEL} (off-budget). The claude-backend slugs are FALLBACKS for`,
12
- 'when the off-budget CLIs are quota-exhaustedthey bill your Claude subscription.',
11
+ 'Recommended first choice: xai-grok/grok-4.5. Whichever seat runs on the same provider as the',
12
+ 'agent you orchestrate from is your last resort it spends the pool you are already burning.',
13
13
  ].join('\n');
14
14
 
15
15
  export const subagent = buildCommand({
@@ -19,8 +19,7 @@ export const subagent = buildCommand({
19
19
  model: {
20
20
  kind: 'parsed',
21
21
  parse: String,
22
- optional: true,
23
- brief: `Model slug to delegate to (default: ${DEFAULT_MODEL})`,
22
+ brief: 'Model slug (required) — see the seat list above',
24
23
  },
25
24
  timeout: {
26
25
  kind: 'parsed',
@@ -1,16 +1,11 @@
1
1
  import type { LocalContext } from '../../context.ts';
2
2
  import { delegate } from '../../delegate.ts';
3
- import {
4
- backendModelId,
5
- DEFAULT_MODEL,
6
- formatUnknownModelError,
7
- resolveModel,
8
- } from '../../models.ts';
3
+ import { backendModelId, formatUnknownModelError, resolveModel } from '../../models.ts';
9
4
  import { preflightModel, renderPreflightRefusal } from '../../quotaPreflight.ts';
10
5
  import { startRun } from '../../runlog.ts';
11
6
 
12
7
  export interface SubagentFlags {
13
- readonly model?: string;
8
+ readonly model: string;
14
9
  readonly timeout?: number;
15
10
  readonly tools: boolean;
16
11
  readonly preflight: boolean;
@@ -22,7 +17,7 @@ export default async function subagent(
22
17
  flags: SubagentFlags,
23
18
  prompt: string,
24
19
  ): Promise<void> {
25
- const inputSlug = flags.model ?? DEFAULT_MODEL;
20
+ const inputSlug = flags.model;
26
21
  const model = resolveModel(inputSlug);
27
22
  if (!model) {
28
23
  this.process.stderr.write(`${formatUnknownModelError(inputSlug)}\n`);
package/src/driver.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { AgyQuotaSnapshot } from '@aibridge/driver-agy';
2
2
  import type { ClaudeQuotaSnapshot } from '@aibridge/driver-claude';
3
3
  import type { CodexQuotaSnapshot } from '@aibridge/driver-codex';
4
+ import type { GrokQuotaSnapshot } from '@aibridge/driver-grok';
4
5
  import type { Effort } from './models.ts';
5
6
 
6
7
  export type Availability =
@@ -28,7 +29,11 @@ export type DelegationResult =
28
29
  readonly exitCode: number | null;
29
30
  };
30
31
 
31
- export type QuotaSnapshot = AgyQuotaSnapshot | CodexQuotaSnapshot | ClaudeQuotaSnapshot;
32
+ export type QuotaSnapshot =
33
+ | AgyQuotaSnapshot
34
+ | CodexQuotaSnapshot
35
+ | ClaudeQuotaSnapshot
36
+ | GrokQuotaSnapshot;
32
37
 
33
38
  export interface ImageGenRequest {
34
39
  readonly prompt: string;
package/src/drivers.ts CHANGED
@@ -15,6 +15,7 @@ const agyDriver: AgentCliDriver = {
15
15
  const grokDriver: AgentCliDriver = {
16
16
  probe: () => grok.probe(),
17
17
  run: task => grok.run(task),
18
+ quota: () => grok.fetchGrokQuota(),
18
19
  generateImage: req => grok.generateImage(req),
19
20
  };
20
21
 
@@ -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', 'do something']);
38
+ await runCli(ctx, ['plan', '--model', 'xai-grok/grok-4.5', '--out', 'plan.md', 'do something']);
39
39
  expect(mockPlanImpl).toHaveBeenCalledTimes(1);
40
40
  const [call] = mockPlanImpl.mock.calls;
41
41
  expect(call).toBeDefined();
@@ -43,6 +43,8 @@ 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',
47
+ out: 'plan.md',
46
48
  preflight: true,
47
49
  });
48
50
  });
@@ -50,7 +52,17 @@ describe('flag mapping & defaults lock', () => {
50
52
  it('plan command handles --no-preflight and --timeout', async () => {
51
53
  mockPlanImpl.mockReset();
52
54
  const ctx = fakeCtx();
53
- await runCli(ctx, ['plan', '--no-preflight', '--timeout', '120', 'task']);
55
+ await runCli(ctx, [
56
+ 'plan',
57
+ '--model',
58
+ 'xai-grok/grok-4.5',
59
+ '--out',
60
+ 'plan.md',
61
+ '--no-preflight',
62
+ '--timeout',
63
+ '120',
64
+ 'task',
65
+ ]);
54
66
  expect(mockPlanImpl).toHaveBeenCalledTimes(1);
55
67
  const [call] = mockPlanImpl.mock.calls;
56
68
  expect(call).toBeDefined();
@@ -58,6 +70,8 @@ describe('flag mapping & defaults lock', () => {
58
70
  const [flags, prompt] = call;
59
71
  expect(prompt).toBe('task');
60
72
  expect(flags).toEqual({
73
+ model: 'xai-grok/grok-4.5',
74
+ out: 'plan.md',
61
75
  preflight: false,
62
76
  timeout: 120,
63
77
  });
@@ -66,7 +80,7 @@ describe('flag mapping & defaults lock', () => {
66
80
  it('subagent command maps defaults correctly', async () => {
67
81
  mockSubagentImpl.mockReset();
68
82
  const ctx = fakeCtx();
69
- await runCli(ctx, ['subagent', 'hello agent']);
83
+ await runCli(ctx, ['subagent', '--model', 'xai-grok/grok-4.5', 'hello agent']);
70
84
  expect(mockSubagentImpl).toHaveBeenCalledTimes(1);
71
85
  const [call] = mockSubagentImpl.mock.calls;
72
86
  expect(call).toBeDefined();
@@ -74,6 +88,7 @@ describe('flag mapping & defaults lock', () => {
74
88
  const [flags, prompt] = call;
75
89
  expect(prompt).toBe('hello agent');
76
90
  expect(flags).toEqual({
91
+ model: 'xai-grok/grok-4.5',
77
92
  tools: true,
78
93
  preflight: true,
79
94
  json: false,
@@ -83,7 +98,14 @@ describe('flag mapping & defaults lock', () => {
83
98
  it('subagent command handles --no-tools and --no-preflight', async () => {
84
99
  mockSubagentImpl.mockReset();
85
100
  const ctx = fakeCtx();
86
- await runCli(ctx, ['subagent', '--no-tools', '--no-preflight', 'hello agent']);
101
+ await runCli(ctx, [
102
+ 'subagent',
103
+ '--model',
104
+ 'xai-grok/grok-4.5',
105
+ '--no-tools',
106
+ '--no-preflight',
107
+ 'hello agent',
108
+ ]);
87
109
  expect(mockSubagentImpl).toHaveBeenCalledTimes(1);
88
110
  const [call] = mockSubagentImpl.mock.calls;
89
111
  expect(call).toBeDefined();
@@ -91,6 +113,7 @@ describe('flag mapping & defaults lock', () => {
91
113
  const [flags, prompt] = call;
92
114
  expect(prompt).toBe('hello agent');
93
115
  expect(flags).toEqual({
116
+ model: 'xai-grok/grok-4.5',
94
117
  tools: false,
95
118
  preflight: false,
96
119
  json: false,