@aibridge/cli 0.1.1 → 0.3.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/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { C as listModelHelpLines, S as formatUnknownModelError, T as supportsImageGen, _ as DEFAULT_IMPLEMENTER, a as readRunLogs, b as backendModelId, c as evaluateCodexPreflight, d as renderPreflightRefusal, f as delegate, g as DEFAULT_IMAGE_GEN, 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 DEFAULT_MODEL, w as resolveModel, x as formatImageGenModelError, y as MODELS } from "./context-8JjDzZBG.mjs";
2
- export { DEFAULT_IMAGE_GEN, DEFAULT_IMPLEMENTER, DEFAULT_MODEL, MODELS, app, backendModelId, buildContext, delegate, evaluateAgyPreflight, evaluateCodexPreflight, formatImageGenModelError, formatUnknownModelError, getDriver, listModelHelpLines, listRuns, nonEmptyPrompt, positiveIntSeconds, preflightCodex, preflightModel, readRunLogs, renderPreflightRefusal, resolveModel, runCli, startRun, supportsImageGen };
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-B6I4QI9z.mjs";
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.1.1",
3
+ "version": "0.3.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.1.1",
39
- "@aibridge/driver-agy": "0.1.1",
40
- "@aibridge/driver-grok": "0.1.1",
41
- "@aibridge/driver-codex": "0.1.1",
42
- "@aibridge/driver-claude": "0.1.1"
38
+ "@aibridge/proc": "0.3.0",
39
+ "@aibridge/driver-agy": "0.3.0",
40
+ "@aibridge/driver-grok": "0.3.0",
41
+ "@aibridge/driver-codex": "0.3.0",
42
+ "@aibridge/driver-claude": "0.3.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "tsdown": "0.22.14"
package/src/app.ts CHANGED
@@ -14,7 +14,7 @@ const require = createRequire(import.meta.url);
14
14
  const { version } = require('../package.json') as { version: string };
15
15
 
16
16
  const BRIEF =
17
- 'Bridge tasks to non-Claude AI CLIs — a plan → implement → review workflow, task delegation, and image generation (codex gpt-image-2 / grok Imagine).';
17
+ 'Bridge tasks to non-Claude AI CLIs — a plan → implement → review workflow, task delegation, and image generation (codex / agy / grok seats).';
18
18
 
19
19
  const routes = buildRouteMap({
20
20
  routes: {
@@ -1,15 +1,15 @@
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
 
6
6
  const fullDescription = [
7
- "Renders an image by driving the seat's CLI (codex gpt-image-2, grok ",
8
- 'Imagine), then verifies the result is a real render before returning it.',
7
+ "Renders an image by driving the seat's own CLI, then verifies the result is",
8
+ 'a real render before returning it.',
9
9
  '',
10
10
  'Image-gen seats (canonical slug):',
11
11
  ...listModelHelpLines({ imageOnly: true }),
12
- `Default: ${DEFAULT_IMAGE_GEN} (gpt-image-2 via codex; historical default).`,
12
+ 'Recommended seat: openai-codex/gpt-5.6-sol.',
13
13
  ].join('\n');
14
14
 
15
15
  export const imageGen = buildCommand({
@@ -19,21 +19,19 @@ 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',
27
26
  parse: String,
28
- optional: true,
29
- brief: 'Path to write the image (default: ./aibridge-image.png)',
27
+ brief:
28
+ 'Path to write the image — extension must match the seat format (.png for codex, .jpg for agy/grok)',
30
29
  },
31
- size: {
30
+ aspectRatio: {
32
31
  kind: 'parsed',
33
32
  parse: String,
34
33
  optional: true,
35
- brief:
36
- 'WIDTHxHEIGHT (codex: each edge ÷16; grok: mapped to aspect_ratio, then optionally resized)',
34
+ brief: 'Aspect ratio N:M, e.g. 16:9 (agy/grok: real tool param; codex: prompt hint)',
37
35
  },
38
36
  image: {
39
37
  kind: 'parsed',
@@ -41,12 +39,6 @@ export const imageGen = buildCommand({
41
39
  optional: true,
42
40
  brief: 'Reference image path(s), comma-separated — visual reference',
43
41
  },
44
- quality: {
45
- kind: 'parsed',
46
- parse: String,
47
- optional: true,
48
- brief: 'low | medium | high (codex/gpt-image-2; default high)',
49
- },
50
42
  timeout: {
51
43
  kind: 'parsed',
52
44
  parse: positiveIntSeconds,
@@ -10,30 +10,29 @@ import {
10
10
  } from 'node:fs';
11
11
  import { tmpdir } from 'node:os';
12
12
  import { join, resolve } from 'node:path';
13
- import { isNotFound, runCaptured } from '@aibridge/proc';
14
13
  import type { LocalContext } from '../../context.ts';
15
14
  import type { ImageResult } from '../../driver.ts';
16
15
  import { getDriver } from '../../drivers.ts';
17
16
  import {
18
- DEFAULT_IMAGE_GEN,
17
+ backendModelId,
19
18
  formatImageGenModelError,
20
19
  formatUnknownModelError,
20
+ imageFormatFor,
21
21
  resolveModel,
22
22
  supportsImageGen,
23
23
  } from '../../models.ts';
24
24
 
25
25
  export interface ImageGenFlags {
26
- readonly model?: string;
27
- readonly out?: string;
28
- readonly size?: string;
26
+ readonly model: string;
27
+ readonly out: string;
28
+ readonly aspectRatio?: string;
29
29
  readonly image?: string;
30
- readonly quality?: string;
31
30
  readonly timeout?: number;
32
31
  readonly json: boolean;
33
32
  }
34
33
 
35
34
  const MIN_REAL_BYTES_CODEX = 100_000;
36
- const MIN_REAL_BYTES_GROK = 10_000;
35
+ const MIN_REAL_BYTES_TOOL = 10_000;
37
36
 
38
37
  export default async function imageGen(
39
38
  this: LocalContext,
@@ -45,37 +44,39 @@ 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 (gpt-image-2 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
 
59
- const quality = (flags.quality ?? 'high').toLowerCase();
60
- if (!['low', 'medium', 'high'].includes(quality)) {
61
- return fail(`invalid --quality "${flags.quality}" (use low | medium | high)`);
58
+ const expected = imageFormatFor(model);
59
+ if (expected === undefined) return fail(formatImageGenModelError(inputSlug, model));
60
+
61
+ const label = expected === 'png' ? 'PNG' : 'JPEG';
62
+ const extValid = expected === 'png' ? /\.png$/i.test(flags.out) : /\.jpe?g$/i.test(flags.out);
63
+ if (!extValid) {
64
+ return fail(
65
+ `--out "${flags.out}" must end in ${expected === 'png' ? '.png' : '.jpg or .jpeg'} — the ${model.spec.slug} seat renders ${label} and aibridge does not convert.`,
66
+ );
62
67
  }
63
68
 
64
- let size: { w: number; h: number } | undefined;
65
- if (flags.size !== undefined) {
66
- const m = flags.size.match(/^(\d+)\s*x\s*(\d+)$/i);
67
- if (!m) return fail(`invalid --size "${flags.size}" (expected e.g. 1024x1024)`);
68
- size = { w: Number(m[1]), h: Number(m[2]) };
69
- if (model.spec.backend === 'codex') {
70
- const constraint = sizeConstraintError(size.w, size.h);
71
- if (constraint) return fail(`invalid --size ${size.w}x${size.h}: ${constraint}`);
72
- } else if (size.w < 1 || size.h < 1) {
73
- return fail(`invalid --size ${size.w}x${size.h}: dimensions must be positive`);
69
+ let aspectRatio: string | undefined;
70
+ if (flags.aspectRatio !== undefined) {
71
+ const m = flags.aspectRatio.match(/^(\d+)\s*:\s*(\d+)$/);
72
+ if (!m || Number(m[1]) < 1 || Number(m[2]) < 1) {
73
+ return fail(`invalid --aspect-ratio "${flags.aspectRatio}" (expected e.g. 16:9)`);
74
74
  }
75
+ aspectRatio = `${Number(m[1])}:${Number(m[2])}`;
75
76
  }
76
77
 
77
78
  const timeoutSec = flags.timeout ?? 600;
78
- const outPath = resolve(this.process.cwd(), flags.out ?? './aibridge-image.png');
79
+ const outPath = resolve(this.process.cwd(), flags.out);
79
80
 
80
81
  const imagePaths: string[] = [];
81
82
  if (flags.image !== undefined) {
@@ -94,17 +95,16 @@ export default async function imageGen(
94
95
  return fail(formatImageGenModelError(inputSlug, model));
95
96
  }
96
97
 
97
- const minBytes = model.spec.backend === 'codex' ? MIN_REAL_BYTES_CODEX : MIN_REAL_BYTES_GROK;
98
+ const minBytes = model.spec.backend === 'codex' ? MIN_REAL_BYTES_CODEX : MIN_REAL_BYTES_TOOL;
98
99
  const work = mkdtempSync(join(tmpdir(), 'aibridge-imagegen-'));
99
100
 
100
101
  try {
101
102
  let outcome: ImageResult = await driver.generateImage({
102
103
  prompt,
103
104
  workDir: work,
104
- backendModel: model.spec.backendModel,
105
+ backendModel: backendModelId(model),
105
106
  effort: model.effort,
106
- quality,
107
- size,
107
+ aspectRatio,
108
108
  imagePaths,
109
109
  timeoutSec,
110
110
  forceful: false,
@@ -115,10 +115,9 @@ export default async function imageGen(
115
115
  outcome = await driver.generateImage({
116
116
  prompt,
117
117
  workDir: work,
118
- backendModel: model.spec.backendModel,
118
+ backendModel: backendModelId(model),
119
119
  effort: model.effort,
120
- quality,
121
- size,
120
+ aspectRatio,
122
121
  imagePaths,
123
122
  timeoutSec,
124
123
  forceful: true,
@@ -133,42 +132,29 @@ export default async function imageGen(
133
132
  if (outcome.kind === 'error') return fail(outcome.reason);
134
133
  if (outcome.kind === 'suspect') {
135
134
  return fail(
136
- model.spec.backend === 'grok'
137
- ? 'grok produced no usable image. Check SuperGrok image access and re-run with a simpler prompt.'
138
- : 'codex produced only a tiny/code-drawn image, not a real gpt-image-2 render. ' +
139
- 'Try --quality high or a clearer, simpler prompt.',
135
+ model.spec.backend === 'agy'
136
+ ? 'agy produced no usable image. Re-run with a simpler prompt, or check Antigravity image access.'
137
+ : model.spec.backend === 'grok'
138
+ ? 'grok produced no usable image. Check SuperGrok image access and re-run with a simpler prompt.'
139
+ : 'codex produced only a tiny/code-drawn image, not a real render. ' +
140
+ 'Try a clearer, simpler prompt.',
140
141
  );
141
142
  }
142
143
 
143
144
  const local = join(work, 'result.bin');
144
145
  copyFileSync(outcome.path, local);
145
146
 
146
- let dims = imageSize(local);
147
- if (size && dims && (dims.width !== size.w || dims.height !== size.h)) {
148
- const resized = await magick([local, '-resize', `${size.w}x${size.h}!`, local]);
149
- if (resized) {
150
- dims = imageSize(local) ?? dims;
151
- } else {
152
- this.process.stderr.write(
153
- `aibridge image-gen: rendered ${dims.width}x${dims.height}, wanted ${size.w}x${size.h}, ` +
154
- 'and ImageMagick (magick/convert) is unavailable to resize.\n',
155
- );
156
- }
157
- }
147
+ const dims = imageSize(local);
148
+ const actual = pngSize(local) ? 'png' : jpegSize(local) ? 'jpg' : null;
158
149
 
159
- const outExt = /\.png$/i.test(outPath) ? 'png' : /\.jpe?g$/i.test(outPath) ? 'jpg' : null;
160
- const actualFmt = pngSize(local) ? 'png' : jpegSize(local) ? 'jpg' : null;
161
- const needsConvert = outExt !== null && actualFmt !== null && outExt !== actualFmt;
162
- if (!needsConvert || !(await magick([local, outPath]))) {
163
- if (needsConvert) {
164
- this.process.stderr.write(
165
- `aibridge image-gen: render is ${actualFmt.toUpperCase()} but out path wants ` +
166
- `${outExt.toUpperCase()}, and ImageMagick (magick/convert) is unavailable to convert; ` +
167
- 'writing the raw bytes as-is.\n',
168
- );
169
- }
170
- copyFileSync(local, outPath);
150
+ // ponytail: guard for a backend changing formats in the future without throwing away a paid render
151
+ if (actual !== null && actual !== expected) {
152
+ this.process.stderr.write(
153
+ `aibridge image-gen: expected a ${label} render from this seat but got ${actual === 'png' ? 'PNG' : 'JPEG'}; wrote the raw bytes to ${outPath} anyway — the extension does not match the contents.\n`,
154
+ );
171
155
  }
156
+
157
+ copyFileSync(local, outPath);
172
158
  const bytes = statSync(outPath).size;
173
159
 
174
160
  if (flags.json) {
@@ -178,8 +164,7 @@ export default async function imageGen(
178
164
  bytes,
179
165
  width: dims?.width ?? null,
180
166
  height: dims?.height ?? null,
181
- sizeRequested: flags.size ?? null,
182
- quality: model.spec.backend === 'codex' ? quality : null,
167
+ aspectRatio: flags.aspectRatio ?? null,
183
168
  model: model.spec.slug,
184
169
  backend: model.spec.backend,
185
170
  real: true,
@@ -188,27 +173,13 @@ export default async function imageGen(
188
173
  } else {
189
174
  const kb = Math.round(bytes / 1024);
190
175
  const dimStr = dims ? `${dims.width}x${dims.height}, ` : '';
191
- const qualityStr = model.spec.backend === 'codex' ? `, ${quality} quality` : '';
192
- this.process.stdout.write(
193
- `✓ Wrote ${outPath} (${dimStr}${kb} KB${qualityStr}, ${model.spec.slug})\n`,
194
- );
176
+ this.process.stdout.write(`✓ Wrote ${outPath} (${dimStr}${kb} KB, ${model.spec.slug})\n`);
195
177
  }
196
178
  } finally {
197
179
  rmSync(work, { recursive: true, force: true });
198
180
  }
199
181
  }
200
182
 
201
- function sizeConstraintError(w: number, h: number): string | null {
202
- if (w % 16 !== 0 || h % 16 !== 0) return 'each edge must be divisible by 16';
203
- const long = Math.max(w, h);
204
- const short = Math.min(w, h);
205
- if (long / short > 3) return 'aspect ratio must be within 1:3–3:1';
206
- if (long > 3840) return 'longest edge must be <= 3840px';
207
- const px = w * h;
208
- if (px < 655_360 || px > 8_294_400) return 'total pixels must be 655,360–8,294,400';
209
- return null;
210
- }
211
-
212
183
  function imageSize(path: string): { width: number; height: number } | null {
213
184
  return pngSize(path) ?? jpegSize(path);
214
185
  }
@@ -254,15 +225,3 @@ function jpegSize(path: string): { width: number; height: number } | null {
254
225
  return null;
255
226
  }
256
227
  }
257
-
258
- async function magick(args: readonly string[]): Promise<boolean> {
259
- for (const tool of ['magick', 'convert']) {
260
- try {
261
- const r = await runCaptured(tool, [...args], { timeoutMs: 60_000 });
262
- if (!r.timedOut && r.code === 0) return true;
263
- } catch (err) {
264
- if (!isNotFound(err)) throw err;
265
- }
266
- }
267
- return false;
268
- }
@@ -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`);
@@ -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
 
@@ -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,7 +8,7 @@ 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`,
11
+ 'Recommended first choice: xai-grok/grok-4.5 (off-budget). The claude-backend slugs are FALLBACKS for',
12
12
  'when the off-budget CLIs are quota-exhausted — they bill your Claude subscription.',
13
13
  ].join('\n');
14
14
 
@@ -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`);
@@ -72,10 +67,9 @@ export default async function subagent(
72
67
  }
73
68
 
74
69
  if (flags.json) {
75
- const modelId = backendModelId(model) ?? null;
76
70
  this.process.stdout.write(
77
71
  `${JSON.stringify({
78
- model: modelId,
72
+ model: backendModelId(model),
79
73
  slug: model.spec.slug,
80
74
  response: outcome.response,
81
75
  exitCode: outcome.exitCode,
package/src/delegate.ts CHANGED
@@ -29,7 +29,7 @@ export async function delegate(
29
29
  tools: opts.tools,
30
30
  timeoutSec: opts.timeoutSec,
31
31
  cwd: opts.cwd,
32
- backendModel: backendModelId(opts.model) ?? opts.model.spec.backendModel,
32
+ backendModel: backendModelId(opts.model),
33
33
  effort: opts.model.effort,
34
34
  onStdout: c => opts.run.stdout(c),
35
35
  onStderr: c => opts.run.stderr(c),
package/src/driver.ts CHANGED
@@ -12,7 +12,7 @@ export interface DelegationTask {
12
12
  readonly tools: boolean;
13
13
  readonly timeoutSec: number;
14
14
  readonly cwd: string;
15
- readonly backendModel: string | undefined;
15
+ readonly backendModel: string;
16
16
  readonly effort: Effort | undefined;
17
17
  readonly onStdout?: (chunk: string) => void;
18
18
  readonly onStderr?: (chunk: string) => void;
@@ -33,10 +33,9 @@ export type QuotaSnapshot = AgyQuotaSnapshot | CodexQuotaSnapshot | ClaudeQuotaS
33
33
  export interface ImageGenRequest {
34
34
  readonly prompt: string;
35
35
  readonly workDir: string;
36
- readonly backendModel: string | undefined;
36
+ readonly backendModel: string;
37
37
  readonly effort: Effort | undefined;
38
- readonly quality: string;
39
- readonly size: { readonly w: number; readonly h: number } | undefined;
38
+ readonly aspectRatio: string | undefined;
40
39
  readonly imagePaths: readonly string[];
41
40
  readonly timeoutSec: number;
42
41
  readonly forceful: boolean;
package/src/drivers.ts CHANGED
@@ -9,6 +9,7 @@ const agyDriver: AgentCliDriver = {
9
9
  probe: () => agy.probe(),
10
10
  run: task => agy.run(task),
11
11
  quota: () => agy.fetchAgyQuota(),
12
+ generateImage: req => agy.generateImage(req),
12
13
  };
13
14
 
14
15
  const grokDriver: AgentCliDriver = {