@aibridge/cli 0.1.1 → 0.2.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 +7 -3
- package/dist/cli.mjs +1 -1
- package/dist/{context-8JjDzZBG.mjs → context-DQxvMUd2.mjs} +76 -108
- package/dist/index.d.mts +8 -10
- package/dist/index.mjs +1 -1
- package/package.json +6 -6
- package/src/app.ts +1 -1
- package/src/commands/image-gen/command.ts +7 -14
- package/src/commands/image-gen/impl.ts +44 -84
- package/src/commands/subagent/impl.ts +1 -2
- package/src/delegate.ts +1 -1
- package/src/driver.ts +3 -4
- package/src/drivers.ts +1 -0
- package/src/models.test.ts +28 -14
- package/src/models.ts +36 -18
- package/src/quotaPreflight.ts +1 -2
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
---
|
|
17
17
|
|
|
18
|
-
Your agent is one model, from one provider. Your machine probably has several more sitting behind CLIs you already use — `grok`, `agy` (Antigravity), `codex`, `claude`. **aibridge** turns them into seats your agent can drive: a planner that studies your repo, an implementer that edits it and runs your real tests, a reviewer from a *different* model family that cross-checks the diff against the plan, concurrent one-shot delegates — and capabilities your agent's own provider may not offer at all, like real image generation (
|
|
18
|
+
Your agent is one model, from one provider. Your machine probably has several more sitting behind CLIs you already use — `grok`, `agy` (Antigravity), `codex`, `claude`. **aibridge** turns them into seats your agent can drive: a planner that studies your repo, an implementer that edits it and runs your real tests, a reviewer from a *different* model family that cross-checks the diff against the plan, concurrent one-shot delegates — and capabilities your agent's own provider may not offer at all, like real image generation (Codex, Antigravity, or Grok seats).
|
|
19
19
|
|
|
20
20
|
## Install
|
|
21
21
|
|
|
@@ -41,7 +41,7 @@ npx -y @aibridge/cli subagent "summarize the architecture of this repo"
|
|
|
41
41
|
| `aibridge implement <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
42
|
| `aibridge review [--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
43
|
| `aibridge subagent "<task>"` | A self-contained task deserves a concurrent delegate, a cross-model second opinion, or a red-team pass |
|
|
44
|
-
| `aibridge image-gen "<prompt>"` | You need a real raster image —
|
|
44
|
+
| `aibridge image-gen "<prompt>" --out out.png` | You need a real raster image — on a Codex (default), Antigravity, or Grok seat, with render verification |
|
|
45
45
|
| `aibridge quota` | Two-second check of every backend's remaining quota before you pipeline work |
|
|
46
46
|
| `aibridge runs` | Inspect or watch past delegation runs (`~/.aibridge/runs`) |
|
|
47
47
|
|
|
@@ -61,7 +61,7 @@ Plan files — not their contents — travel between stages, so the loop is near
|
|
|
61
61
|
- **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.
|
|
62
62
|
- **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.
|
|
63
63
|
- **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.)
|
|
64
|
-
- **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`. No aliases
|
|
64
|
+
- **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
65
|
|
|
66
66
|
## Requirements
|
|
67
67
|
|
|
@@ -72,6 +72,10 @@ Plan files — not their contents — travel between stages, so the loop is near
|
|
|
72
72
|
|
|
73
73
|
Everything is published under the [`@aibridge`](https://www.npmjs.com/org/aibridge) scope: [`@aibridge/cli`](https://www.npmjs.com/package/@aibridge/cli) (the command), `@aibridge/proc` (spawn/capture), and one driver per backing CLI — `@aibridge/driver-agy`, `@aibridge/driver-grok`, `@aibridge/driver-codex`, `@aibridge/driver-claude` — reusable if you want to drive a single CLI from your own code.
|
|
74
74
|
|
|
75
|
+
## Security
|
|
76
|
+
|
|
77
|
+
aibridge executes real delegation — that's the product, and security scanners rightly notice: backing CLIs read/write files and run shell in tools mode, **at the same trust level as the agent you already run**. Nothing gains more access than you granted your agent and those CLIs when you installed them. Task content goes to the delegate's provider (use `--no-tools` for untrusted input — reasoning only, no file/shell access). All packages publish from this public repo via OIDC with [SLSA provenance](https://www.npmjs.com/package/@aibridge/cli), with no install-time scripts.
|
|
78
|
+
|
|
75
79
|
## Contributing & development
|
|
76
80
|
|
|
77
81
|
Dev docs, architecture, and the working agreements for coding agents live in [`AGENTS.md`](AGENTS.md); design history in [`docs/`](docs/). Quick loop:
|
package/dist/cli.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import { ExitCode, buildApplication, buildCommand, buildRouteMap, run } from "@s
|
|
|
3
3
|
import { appendFileSync, closeSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
-
import { isNotFound, runCaptured } from "@aibridge/proc";
|
|
7
6
|
import * as agy from "@aibridge/driver-agy";
|
|
8
7
|
import { fetchAgyQuota, findModelQuota } from "@aibridge/driver-agy";
|
|
9
8
|
import * as claude from "@aibridge/driver-claude";
|
|
@@ -11,6 +10,7 @@ import { fetchClaudeQuota } from "@aibridge/driver-claude";
|
|
|
11
10
|
import * as codex from "@aibridge/driver-codex";
|
|
12
11
|
import { fetchCodexQuota } from "@aibridge/driver-codex";
|
|
13
12
|
import * as grok from "@aibridge/driver-grok";
|
|
13
|
+
import { runCaptured } from "@aibridge/proc";
|
|
14
14
|
import { randomBytes } from "node:crypto";
|
|
15
15
|
//#region src/models.ts
|
|
16
16
|
const MODELS = {
|
|
@@ -35,7 +35,7 @@ const MODELS = {
|
|
|
35
35
|
"high"
|
|
36
36
|
],
|
|
37
37
|
defaultEffort: "high",
|
|
38
|
-
brief: "Google Gemini 3.6 Flash via agy — default for implement; own Antigravity login"
|
|
38
|
+
brief: "Google Gemini 3.6 Flash via agy — default for implement, also image-gen; own Antigravity login"
|
|
39
39
|
},
|
|
40
40
|
"google-antigravity/claude-sonnet-4-6": {
|
|
41
41
|
slug: "google-antigravity/claude-sonnet-4-6",
|
|
@@ -70,10 +70,10 @@ const MODELS = {
|
|
|
70
70
|
],
|
|
71
71
|
brief: "OpenAI Codex gpt-5.6-sol via codex CLI"
|
|
72
72
|
},
|
|
73
|
-
"anthropic-claude/sonnet": {
|
|
74
|
-
slug: "anthropic-claude/sonnet",
|
|
73
|
+
"anthropic-claude/sonnet-5": {
|
|
74
|
+
slug: "anthropic-claude/sonnet-5",
|
|
75
75
|
backend: "claude",
|
|
76
|
-
backendModel: "sonnet",
|
|
76
|
+
backendModel: "claude-sonnet-5",
|
|
77
77
|
efforts: [
|
|
78
78
|
"low",
|
|
79
79
|
"medium",
|
|
@@ -81,12 +81,12 @@ const MODELS = {
|
|
|
81
81
|
"xhigh",
|
|
82
82
|
"max"
|
|
83
83
|
],
|
|
84
|
-
brief: "Claude Sonnet via claude CLI — bills your Claude subscription"
|
|
84
|
+
brief: "Claude Sonnet 5 via claude CLI — bills your Claude subscription"
|
|
85
85
|
},
|
|
86
|
-
"anthropic-claude/opus": {
|
|
87
|
-
slug: "anthropic-claude/opus",
|
|
86
|
+
"anthropic-claude/opus-5": {
|
|
87
|
+
slug: "anthropic-claude/opus-5",
|
|
88
88
|
backend: "claude",
|
|
89
|
-
backendModel: "opus",
|
|
89
|
+
backendModel: "claude-opus-5",
|
|
90
90
|
efforts: [
|
|
91
91
|
"low",
|
|
92
92
|
"medium",
|
|
@@ -95,15 +95,36 @@ const MODELS = {
|
|
|
95
95
|
"max"
|
|
96
96
|
],
|
|
97
97
|
defaultEffort: "high",
|
|
98
|
-
brief: "Claude Opus via claude CLI (default effort: high) — bills subscription"
|
|
98
|
+
brief: "Claude Opus 5 via claude CLI (default effort: high) — bills subscription"
|
|
99
|
+
},
|
|
100
|
+
"anthropic-claude/opus-5-1m": {
|
|
101
|
+
slug: "anthropic-claude/opus-5-1m",
|
|
102
|
+
backend: "claude",
|
|
103
|
+
backendModel: "claude-opus-5[1m]",
|
|
104
|
+
efforts: [
|
|
105
|
+
"low",
|
|
106
|
+
"medium",
|
|
107
|
+
"high",
|
|
108
|
+
"xhigh",
|
|
109
|
+
"max"
|
|
110
|
+
],
|
|
111
|
+
defaultEffort: "high",
|
|
112
|
+
brief: "Claude Opus 5, 1M context via claude CLI — long-context work; bills subscription"
|
|
99
113
|
}
|
|
100
114
|
};
|
|
101
115
|
const DEFAULT_MODEL = "xai-grok/grok-4.5";
|
|
102
116
|
const DEFAULT_IMPLEMENTER = "google-antigravity/gemini-3.6-flash";
|
|
103
117
|
const DEFAULT_IMAGE_GEN = "openai-codex/gpt-5.6-sol";
|
|
104
|
-
const
|
|
118
|
+
const IMAGE_GEN_FORMATS = /* @__PURE__ */ new Map([
|
|
119
|
+
["agy", "jpg"],
|
|
120
|
+
["codex", "png"],
|
|
121
|
+
["grok", "jpg"]
|
|
122
|
+
]);
|
|
105
123
|
function supportsImageGen(resolved) {
|
|
106
|
-
return
|
|
124
|
+
return IMAGE_GEN_FORMATS.has(resolved.spec.backend);
|
|
125
|
+
}
|
|
126
|
+
function imageFormatFor(resolved) {
|
|
127
|
+
return IMAGE_GEN_FORMATS.get(resolved.spec.backend);
|
|
107
128
|
}
|
|
108
129
|
const EFFORTS_SET = /* @__PURE__ */ new Set([
|
|
109
130
|
"low",
|
|
@@ -132,7 +153,6 @@ function resolveModel(input) {
|
|
|
132
153
|
}
|
|
133
154
|
}
|
|
134
155
|
function backendModelId(resolved) {
|
|
135
|
-
if (!resolved.spec.backendModel) return;
|
|
136
156
|
if (resolved.spec.backend === "agy") {
|
|
137
157
|
const effort = resolved.effort ?? resolved.spec.defaultEffort;
|
|
138
158
|
if (effort) return `${resolved.spec.backendModel}-${effort}`;
|
|
@@ -142,7 +162,7 @@ function backendModelId(resolved) {
|
|
|
142
162
|
function listModelHelpLines(opts = {}) {
|
|
143
163
|
const lines = [];
|
|
144
164
|
for (const [slug, spec] of Object.entries(MODELS)) {
|
|
145
|
-
if (opts.imageOnly && !
|
|
165
|
+
if (opts.imageOnly && !IMAGE_GEN_FORMATS.has(spec.backend)) continue;
|
|
146
166
|
lines.push(` ${slug}`);
|
|
147
167
|
lines.push(` ${spec.brief}`);
|
|
148
168
|
}
|
|
@@ -186,7 +206,8 @@ const DRIVERS = {
|
|
|
186
206
|
agy: {
|
|
187
207
|
probe: () => agy.probe(),
|
|
188
208
|
run: (task) => agy.run(task),
|
|
189
|
-
quota: () => agy.fetchAgyQuota()
|
|
209
|
+
quota: () => agy.fetchAgyQuota(),
|
|
210
|
+
generateImage: (req) => agy.generateImage(req)
|
|
190
211
|
},
|
|
191
212
|
grok: {
|
|
192
213
|
probe: () => grok.probe(),
|
|
@@ -213,7 +234,7 @@ function getDriver(backend) {
|
|
|
213
234
|
//#endregion
|
|
214
235
|
//#region src/commands/image-gen/impl.ts
|
|
215
236
|
const MIN_REAL_BYTES_CODEX = 1e5;
|
|
216
|
-
const
|
|
237
|
+
const MIN_REAL_BYTES_TOOL = 1e4;
|
|
217
238
|
async function imageGen$1(flags, prompt) {
|
|
218
239
|
const fail = (msg) => {
|
|
219
240
|
this.process.stderr.write(`aibridge image-gen: ${msg}\n`);
|
|
@@ -223,28 +244,19 @@ async function imageGen$1(flags, prompt) {
|
|
|
223
244
|
const model = resolveModel(inputSlug);
|
|
224
245
|
if (!model) return fail(formatUnknownModelError(inputSlug));
|
|
225
246
|
if (!supportsImageGen(model)) return fail(formatImageGenModelError(inputSlug, model));
|
|
226
|
-
if (model.spec.backend === "codex" && model.effort) return fail(`effort "-${model.effort}" has no effect on image-gen (
|
|
227
|
-
const
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (!m) return fail(`invalid --size "${flags.size}" (expected e.g. 1024x1024)`);
|
|
237
|
-
size = {
|
|
238
|
-
w: Number(m[1]),
|
|
239
|
-
h: Number(m[2])
|
|
240
|
-
};
|
|
241
|
-
if (model.spec.backend === "codex") {
|
|
242
|
-
const constraint = sizeConstraintError(size.w, size.h);
|
|
243
|
-
if (constraint) return fail(`invalid --size ${size.w}x${size.h}: ${constraint}`);
|
|
244
|
-
} else if (size.w < 1 || size.h < 1) return fail(`invalid --size ${size.w}x${size.h}: dimensions must be positive`);
|
|
247
|
+
if (model.spec.backend === "codex" && model.effort) return fail(`effort "-${model.effort}" has no effect on image-gen (the image tool renders, not the seat model); use ${DEFAULT_IMAGE_GEN}.`);
|
|
248
|
+
const expected = imageFormatFor(model);
|
|
249
|
+
if (expected === void 0) return fail(formatImageGenModelError(inputSlug, model));
|
|
250
|
+
const label = expected === "png" ? "PNG" : "JPEG";
|
|
251
|
+
if (!(expected === "png" ? /\.png$/i.test(flags.out) : /\.jpe?g$/i.test(flags.out))) return fail(`--out "${flags.out}" must end in ${expected === "png" ? ".png" : ".jpg or .jpeg"} — the ${model.spec.slug} seat renders ${label} and aibridge does not convert.`);
|
|
252
|
+
let aspectRatio;
|
|
253
|
+
if (flags.aspectRatio !== void 0) {
|
|
254
|
+
const m = flags.aspectRatio.match(/^(\d+)\s*:\s*(\d+)$/);
|
|
255
|
+
if (!m || Number(m[1]) < 1 || Number(m[2]) < 1) return fail(`invalid --aspect-ratio "${flags.aspectRatio}" (expected e.g. 16:9)`);
|
|
256
|
+
aspectRatio = `${Number(m[1])}:${Number(m[2])}`;
|
|
245
257
|
}
|
|
246
258
|
const timeoutSec = flags.timeout ?? 600;
|
|
247
|
-
const outPath = resolve(this.process.cwd(), flags.out
|
|
259
|
+
const outPath = resolve(this.process.cwd(), flags.out);
|
|
248
260
|
const imagePaths = [];
|
|
249
261
|
if (flags.image !== void 0) for (const raw of flags.image.split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
250
262
|
const abs = resolve(this.process.cwd(), raw);
|
|
@@ -253,16 +265,15 @@ async function imageGen$1(flags, prompt) {
|
|
|
253
265
|
}
|
|
254
266
|
const driver = getDriver(model.spec.backend);
|
|
255
267
|
if (!driver.generateImage) return fail(formatImageGenModelError(inputSlug, model));
|
|
256
|
-
const minBytes = model.spec.backend === "codex" ? MIN_REAL_BYTES_CODEX :
|
|
268
|
+
const minBytes = model.spec.backend === "codex" ? MIN_REAL_BYTES_CODEX : MIN_REAL_BYTES_TOOL;
|
|
257
269
|
const work = mkdtempSync(join(tmpdir(), "aibridge-imagegen-"));
|
|
258
270
|
try {
|
|
259
271
|
let outcome = await driver.generateImage({
|
|
260
272
|
prompt,
|
|
261
273
|
workDir: work,
|
|
262
|
-
backendModel: model
|
|
274
|
+
backendModel: backendModelId(model),
|
|
263
275
|
effort: model.effort,
|
|
264
|
-
|
|
265
|
-
size,
|
|
276
|
+
aspectRatio,
|
|
266
277
|
imagePaths,
|
|
267
278
|
timeoutSec,
|
|
268
279
|
forceful: false,
|
|
@@ -271,10 +282,9 @@ async function imageGen$1(flags, prompt) {
|
|
|
271
282
|
if (model.spec.backend === "codex" && outcome.kind === "suspect") outcome = await driver.generateImage({
|
|
272
283
|
prompt,
|
|
273
284
|
workDir: work,
|
|
274
|
-
backendModel: model
|
|
285
|
+
backendModel: backendModelId(model),
|
|
275
286
|
effort: model.effort,
|
|
276
|
-
|
|
277
|
-
size,
|
|
287
|
+
aspectRatio,
|
|
278
288
|
imagePaths,
|
|
279
289
|
timeoutSec,
|
|
280
290
|
forceful: true,
|
|
@@ -282,34 +292,20 @@ async function imageGen$1(flags, prompt) {
|
|
|
282
292
|
});
|
|
283
293
|
if (outcome.kind === "ok" && outcome.bytes < minBytes) outcome = { kind: "suspect" };
|
|
284
294
|
if (outcome.kind === "error") return fail(outcome.reason);
|
|
285
|
-
if (outcome.kind === "suspect") return fail(model.spec.backend === "grok" ? "grok produced no usable image. Check SuperGrok image access and re-run with a simpler prompt." : "codex produced only a tiny/code-drawn image, not a real
|
|
295
|
+
if (outcome.kind === "suspect") return fail(model.spec.backend === "agy" ? "agy produced no usable image. Re-run with a simpler prompt, or check Antigravity image access." : model.spec.backend === "grok" ? "grok produced no usable image. Check SuperGrok image access and re-run with a simpler prompt." : "codex produced only a tiny/code-drawn image, not a real render. Try a clearer, simpler prompt.");
|
|
286
296
|
const local = join(work, "result.bin");
|
|
287
297
|
copyFileSync(outcome.path, local);
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
`${size.w}x${size.h}!`,
|
|
293
|
-
local
|
|
294
|
-
])) dims = imageSize(local) ?? dims;
|
|
295
|
-
else this.process.stderr.write(`aibridge image-gen: rendered ${dims.width}x${dims.height}, wanted ${size.w}x${size.h}, and ImageMagick (magick/convert) is unavailable to resize.
|
|
296
|
-
`);
|
|
297
|
-
const outExt = /\.png$/i.test(outPath) ? "png" : /\.jpe?g$/i.test(outPath) ? "jpg" : null;
|
|
298
|
-
const actualFmt = pngSize(local) ? "png" : jpegSize(local) ? "jpg" : null;
|
|
299
|
-
const needsConvert = outExt !== null && actualFmt !== null && outExt !== actualFmt;
|
|
300
|
-
if (!needsConvert || !await magick([local, outPath])) {
|
|
301
|
-
if (needsConvert) this.process.stderr.write(`aibridge image-gen: render is ${actualFmt.toUpperCase()} but out path wants ${outExt.toUpperCase()}, and ImageMagick (magick/convert) is unavailable to convert; writing the raw bytes as-is.
|
|
302
|
-
`);
|
|
303
|
-
copyFileSync(local, outPath);
|
|
304
|
-
}
|
|
298
|
+
const dims = imageSize(local);
|
|
299
|
+
const actual = pngSize(local) ? "png" : jpegSize(local) ? "jpg" : null;
|
|
300
|
+
if (actual !== null && actual !== expected) this.process.stderr.write(`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`);
|
|
301
|
+
copyFileSync(local, outPath);
|
|
305
302
|
const bytes = statSync(outPath).size;
|
|
306
303
|
if (flags.json) this.process.stdout.write(`${JSON.stringify({
|
|
307
304
|
out: outPath,
|
|
308
305
|
bytes,
|
|
309
306
|
width: dims?.width ?? null,
|
|
310
307
|
height: dims?.height ?? null,
|
|
311
|
-
|
|
312
|
-
quality: model.spec.backend === "codex" ? quality : null,
|
|
308
|
+
aspectRatio: flags.aspectRatio ?? null,
|
|
313
309
|
model: model.spec.slug,
|
|
314
310
|
backend: model.spec.backend,
|
|
315
311
|
real: true
|
|
@@ -317,8 +313,7 @@ async function imageGen$1(flags, prompt) {
|
|
|
317
313
|
else {
|
|
318
314
|
const kb = Math.round(bytes / 1024);
|
|
319
315
|
const dimStr = dims ? `${dims.width}x${dims.height}, ` : "";
|
|
320
|
-
|
|
321
|
-
this.process.stdout.write(`✓ Wrote ${outPath} (${dimStr}${kb} KB${qualityStr}, ${model.spec.slug})\n`);
|
|
316
|
+
this.process.stdout.write(`✓ Wrote ${outPath} (${dimStr}${kb} KB, ${model.spec.slug})\n`);
|
|
322
317
|
}
|
|
323
318
|
} finally {
|
|
324
319
|
rmSync(work, {
|
|
@@ -327,15 +322,6 @@ async function imageGen$1(flags, prompt) {
|
|
|
327
322
|
});
|
|
328
323
|
}
|
|
329
324
|
}
|
|
330
|
-
function sizeConstraintError(w, h) {
|
|
331
|
-
if (w % 16 !== 0 || h % 16 !== 0) return "each edge must be divisible by 16";
|
|
332
|
-
const long = Math.max(w, h);
|
|
333
|
-
if (long / Math.min(w, h) > 3) return "aspect ratio must be within 1:3–3:1";
|
|
334
|
-
if (long > 3840) return "longest edge must be <= 3840px";
|
|
335
|
-
const px = w * h;
|
|
336
|
-
if (px < 655360 || px > 8294400) return "total pixels must be 655,360–8,294,400";
|
|
337
|
-
return null;
|
|
338
|
-
}
|
|
339
325
|
function imageSize(path) {
|
|
340
326
|
return pngSize(path) ?? jpegSize(path);
|
|
341
327
|
}
|
|
@@ -384,24 +370,15 @@ function jpegSize(path) {
|
|
|
384
370
|
return null;
|
|
385
371
|
}
|
|
386
372
|
}
|
|
387
|
-
async function magick(args) {
|
|
388
|
-
for (const tool of ["magick", "convert"]) try {
|
|
389
|
-
const r = await runCaptured(tool, [...args], { timeoutMs: 6e4 });
|
|
390
|
-
if (!r.timedOut && r.code === 0) return true;
|
|
391
|
-
} catch (err) {
|
|
392
|
-
if (!isNotFound(err)) throw err;
|
|
393
|
-
}
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
373
|
//#endregion
|
|
397
374
|
//#region src/commands/image-gen/command.ts
|
|
398
375
|
const fullDescription$6 = [
|
|
399
|
-
"Renders an image by driving the seat's CLI
|
|
400
|
-
"
|
|
376
|
+
"Renders an image by driving the seat's own CLI, then verifies the result is",
|
|
377
|
+
"a real render before returning it.",
|
|
401
378
|
"",
|
|
402
379
|
"Image-gen seats (canonical slug):",
|
|
403
380
|
...listModelHelpLines({ imageOnly: true }),
|
|
404
|
-
`Default: ${DEFAULT_IMAGE_GEN}
|
|
381
|
+
`Default: ${DEFAULT_IMAGE_GEN}.`
|
|
405
382
|
].join("\n");
|
|
406
383
|
const imageGen = buildCommand({
|
|
407
384
|
func: imageGen$1,
|
|
@@ -416,14 +393,13 @@ const imageGen = buildCommand({
|
|
|
416
393
|
out: {
|
|
417
394
|
kind: "parsed",
|
|
418
395
|
parse: String,
|
|
419
|
-
|
|
420
|
-
brief: "Path to write the image (default: ./aibridge-image.png)"
|
|
396
|
+
brief: "Path to write the image — extension must match the seat format (.png for codex, .jpg for agy/grok)"
|
|
421
397
|
},
|
|
422
|
-
|
|
398
|
+
aspectRatio: {
|
|
423
399
|
kind: "parsed",
|
|
424
400
|
parse: String,
|
|
425
401
|
optional: true,
|
|
426
|
-
brief: "
|
|
402
|
+
brief: "Aspect ratio N:M, e.g. 16:9 (agy/grok: real tool param; codex: prompt hint)"
|
|
427
403
|
},
|
|
428
404
|
image: {
|
|
429
405
|
kind: "parsed",
|
|
@@ -431,12 +407,6 @@ const imageGen = buildCommand({
|
|
|
431
407
|
optional: true,
|
|
432
408
|
brief: "Reference image path(s), comma-separated — visual reference"
|
|
433
409
|
},
|
|
434
|
-
quality: {
|
|
435
|
-
kind: "parsed",
|
|
436
|
-
parse: String,
|
|
437
|
-
optional: true,
|
|
438
|
-
brief: "low | medium | high (codex/gpt-image-2; default high)"
|
|
439
|
-
},
|
|
440
410
|
timeout: {
|
|
441
411
|
kind: "parsed",
|
|
442
412
|
parse: positiveIntSeconds,
|
|
@@ -473,7 +443,7 @@ async function delegate(opts, driver = getDriver(opts.model.spec.backend)) {
|
|
|
473
443
|
tools: opts.tools,
|
|
474
444
|
timeoutSec: opts.timeoutSec,
|
|
475
445
|
cwd: opts.cwd,
|
|
476
|
-
backendModel: backendModelId(opts.model)
|
|
446
|
+
backendModel: backendModelId(opts.model),
|
|
477
447
|
effort: opts.model.effort,
|
|
478
448
|
onStdout: (c) => opts.run.stdout(c),
|
|
479
449
|
onStderr: (c) => opts.run.stderr(c),
|
|
@@ -526,7 +496,7 @@ async function preflightModel(resolved) {
|
|
|
526
496
|
if (resolved.spec.backend === "codex") return preflightCodex();
|
|
527
497
|
if (resolved.spec.backend !== "agy") return { ok: true };
|
|
528
498
|
try {
|
|
529
|
-
return evaluateAgyPreflight(await fetchAgyQuota(), backendModelId(resolved)
|
|
499
|
+
return evaluateAgyPreflight(await fetchAgyQuota(), backendModelId(resolved));
|
|
530
500
|
} catch (err) {
|
|
531
501
|
return {
|
|
532
502
|
ok: true,
|
|
@@ -1414,15 +1384,13 @@ async function subagent$1(flags, prompt) {
|
|
|
1414
1384
|
this.process.exitCode = 1;
|
|
1415
1385
|
return;
|
|
1416
1386
|
}
|
|
1417
|
-
if (flags.json) {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
})}\n`);
|
|
1425
|
-
} else this.process.stdout.write(`${outcome.response}\n`);
|
|
1387
|
+
if (flags.json) this.process.stdout.write(`${JSON.stringify({
|
|
1388
|
+
model: backendModelId(model),
|
|
1389
|
+
slug: model.spec.slug,
|
|
1390
|
+
response: outcome.response,
|
|
1391
|
+
exitCode: outcome.exitCode
|
|
1392
|
+
})}\n`);
|
|
1393
|
+
else this.process.stdout.write(`${outcome.response}\n`);
|
|
1426
1394
|
}
|
|
1427
1395
|
//#endregion
|
|
1428
1396
|
//#region src/commands/subagent/command.ts
|
|
@@ -1509,7 +1477,7 @@ const app = buildApplication(buildRouteMap({
|
|
|
1509
1477
|
runs,
|
|
1510
1478
|
quota
|
|
1511
1479
|
},
|
|
1512
|
-
docs: { brief: "Bridge tasks to non-Claude AI CLIs — a plan → implement → review workflow, task delegation, and image generation (codex
|
|
1480
|
+
docs: { brief: "Bridge tasks to non-Claude AI CLIs — a plan → implement → review workflow, task delegation, and image generation (codex / agy / grok seats)." }
|
|
1513
1481
|
}), {
|
|
1514
1482
|
name: "aibridge",
|
|
1515
1483
|
versionInfo: { currentVersion: version },
|
package/dist/index.d.mts
CHANGED
|
@@ -20,14 +20,16 @@ declare function runCli(ctx: LocalContext, argv: readonly string[]): Promise<voi
|
|
|
20
20
|
*
|
|
21
21
|
* Models are registered by canonical, provider-qualified slug —
|
|
22
22
|
* `<vendor>-<cli>/<model>[-<effort>]`, e.g. `openai-codex/gpt-5.6-sol-high`.
|
|
23
|
-
* Canonical slugs only — no short aliases, by design.
|
|
23
|
+
* Canonical slugs only — no short aliases, by design. That holds on both sides:
|
|
24
|
+
* `backendModel` is a pinned model id (`claude-opus-5`), never a moving vendor
|
|
25
|
+
* alias (`opus`), so a seat never silently changes model under you.
|
|
24
26
|
*/
|
|
25
27
|
type Backend = 'agy' | 'claude' | 'codex' | 'grok';
|
|
26
28
|
type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
27
29
|
interface ModelSpec {
|
|
28
30
|
readonly slug: string;
|
|
29
31
|
readonly backend: Backend;
|
|
30
|
-
readonly backendModel: string
|
|
32
|
+
readonly backendModel: string;
|
|
31
33
|
readonly efforts: readonly Effort[] | null;
|
|
32
34
|
readonly defaultEffort?: Effort;
|
|
33
35
|
readonly brief: string;
|
|
@@ -42,7 +44,7 @@ declare const DEFAULT_IMPLEMENTER = "google-antigravity/gemini-3.6-flash";
|
|
|
42
44
|
declare const DEFAULT_IMAGE_GEN = "openai-codex/gpt-5.6-sol";
|
|
43
45
|
declare function supportsImageGen(resolved: ResolvedModel): boolean;
|
|
44
46
|
declare function resolveModel(input: string): ResolvedModel | undefined;
|
|
45
|
-
declare function backendModelId(resolved: ResolvedModel): string
|
|
47
|
+
declare function backendModelId(resolved: ResolvedModel): string;
|
|
46
48
|
declare function listModelHelpLines(opts?: {
|
|
47
49
|
readonly imageOnly?: boolean;
|
|
48
50
|
}): string[];
|
|
@@ -62,7 +64,7 @@ interface DelegationTask {
|
|
|
62
64
|
readonly tools: boolean;
|
|
63
65
|
readonly timeoutSec: number;
|
|
64
66
|
readonly cwd: string;
|
|
65
|
-
readonly backendModel: string
|
|
67
|
+
readonly backendModel: string;
|
|
66
68
|
readonly effort: Effort | undefined;
|
|
67
69
|
readonly onStdout?: (chunk: string) => void;
|
|
68
70
|
readonly onStderr?: (chunk: string) => void;
|
|
@@ -82,13 +84,9 @@ type QuotaSnapshot = AgyQuotaSnapshot | CodexQuotaSnapshot | ClaudeQuotaSnapshot
|
|
|
82
84
|
interface ImageGenRequest {
|
|
83
85
|
readonly prompt: string;
|
|
84
86
|
readonly workDir: string;
|
|
85
|
-
readonly backendModel: string
|
|
87
|
+
readonly backendModel: string;
|
|
86
88
|
readonly effort: Effort | undefined;
|
|
87
|
-
readonly
|
|
88
|
-
readonly size: {
|
|
89
|
-
readonly w: number;
|
|
90
|
-
readonly h: number;
|
|
91
|
-
} | undefined;
|
|
89
|
+
readonly aspectRatio: string | undefined;
|
|
92
90
|
readonly imagePaths: readonly string[];
|
|
93
91
|
readonly timeoutSec: number;
|
|
94
92
|
readonly forceful: boolean;
|
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-
|
|
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-DQxvMUd2.mjs";
|
|
2
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aibridge/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.
|
|
39
|
-
"@aibridge/driver-
|
|
40
|
-
"@aibridge/driver-grok": "0.
|
|
41
|
-
"@aibridge/driver-
|
|
42
|
-
"@aibridge/driver-
|
|
38
|
+
"@aibridge/proc": "0.2.0",
|
|
39
|
+
"@aibridge/driver-codex": "0.2.0",
|
|
40
|
+
"@aibridge/driver-grok": "0.2.0",
|
|
41
|
+
"@aibridge/driver-claude": "0.2.0",
|
|
42
|
+
"@aibridge/driver-agy": "0.2.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
|
|
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: {
|
|
@@ -4,12 +4,12 @@ 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
|
|
8
|
-
'
|
|
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}
|
|
12
|
+
`Default: ${DEFAULT_IMAGE_GEN}.`,
|
|
13
13
|
].join('\n');
|
|
14
14
|
|
|
15
15
|
export const imageGen = buildCommand({
|
|
@@ -25,15 +25,14 @@ export const imageGen = buildCommand({
|
|
|
25
25
|
out: {
|
|
26
26
|
kind: 'parsed',
|
|
27
27
|
parse: String,
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
brief:
|
|
29
|
+
'Path to write the image — extension must match the seat format (.png for codex, .jpg for agy/grok)',
|
|
30
30
|
},
|
|
31
|
-
|
|
31
|
+
aspectRatio: {
|
|
32
32
|
kind: 'parsed',
|
|
33
33
|
parse: String,
|
|
34
34
|
optional: true,
|
|
35
|
-
brief:
|
|
36
|
-
'WIDTHxHEIGHT (codex: each edge ÷16; grok: mapped to aspect_ratio, then optionally resized)',
|
|
35
|
+
brief: 'Aspect ratio N:M, e.g. 16:9 (agy/grok: real tool param; codex: prompt hint)',
|
|
37
36
|
},
|
|
38
37
|
image: {
|
|
39
38
|
kind: 'parsed',
|
|
@@ -41,12 +40,6 @@ export const imageGen = buildCommand({
|
|
|
41
40
|
optional: true,
|
|
42
41
|
brief: 'Reference image path(s), comma-separated — visual reference',
|
|
43
42
|
},
|
|
44
|
-
quality: {
|
|
45
|
-
kind: 'parsed',
|
|
46
|
-
parse: String,
|
|
47
|
-
optional: true,
|
|
48
|
-
brief: 'low | medium | high (codex/gpt-image-2; default high)',
|
|
49
|
-
},
|
|
50
43
|
timeout: {
|
|
51
44
|
kind: 'parsed',
|
|
52
45
|
parse: positiveIntSeconds,
|
|
@@ -10,30 +10,30 @@ 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 {
|
|
17
|
+
backendModelId,
|
|
18
18
|
DEFAULT_IMAGE_GEN,
|
|
19
19
|
formatImageGenModelError,
|
|
20
20
|
formatUnknownModelError,
|
|
21
|
+
imageFormatFor,
|
|
21
22
|
resolveModel,
|
|
22
23
|
supportsImageGen,
|
|
23
24
|
} from '../../models.ts';
|
|
24
25
|
|
|
25
26
|
export interface ImageGenFlags {
|
|
26
27
|
readonly model?: string;
|
|
27
|
-
readonly out
|
|
28
|
-
readonly
|
|
28
|
+
readonly out: string;
|
|
29
|
+
readonly aspectRatio?: string;
|
|
29
30
|
readonly image?: string;
|
|
30
|
-
readonly quality?: string;
|
|
31
31
|
readonly timeout?: number;
|
|
32
32
|
readonly json: boolean;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const MIN_REAL_BYTES_CODEX = 100_000;
|
|
36
|
-
const
|
|
36
|
+
const MIN_REAL_BYTES_TOOL = 10_000;
|
|
37
37
|
|
|
38
38
|
export default async function imageGen(
|
|
39
39
|
this: LocalContext,
|
|
@@ -52,30 +52,32 @@ export default async function imageGen(
|
|
|
52
52
|
|
|
53
53
|
if (model.spec.backend === 'codex' && model.effort) {
|
|
54
54
|
return fail(
|
|
55
|
-
`effort "-${model.effort}" has no effect on image-gen (
|
|
55
|
+
`effort "-${model.effort}" has no effect on image-gen (the image tool renders, not the seat model); use ${DEFAULT_IMAGE_GEN}.`,
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
59
|
+
const expected = imageFormatFor(model);
|
|
60
|
+
if (expected === undefined) return fail(formatImageGenModelError(inputSlug, model));
|
|
61
|
+
|
|
62
|
+
const label = expected === 'png' ? 'PNG' : 'JPEG';
|
|
63
|
+
const extValid = expected === 'png' ? /\.png$/i.test(flags.out) : /\.jpe?g$/i.test(flags.out);
|
|
64
|
+
if (!extValid) {
|
|
65
|
+
return fail(
|
|
66
|
+
`--out "${flags.out}" must end in ${expected === 'png' ? '.png' : '.jpg or .jpeg'} — the ${model.spec.slug} seat renders ${label} and aibridge does not convert.`,
|
|
67
|
+
);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
let
|
|
65
|
-
if (flags.
|
|
66
|
-
const m = flags.
|
|
67
|
-
if (!m
|
|
68
|
-
|
|
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`);
|
|
70
|
+
let aspectRatio: string | undefined;
|
|
71
|
+
if (flags.aspectRatio !== undefined) {
|
|
72
|
+
const m = flags.aspectRatio.match(/^(\d+)\s*:\s*(\d+)$/);
|
|
73
|
+
if (!m || Number(m[1]) < 1 || Number(m[2]) < 1) {
|
|
74
|
+
return fail(`invalid --aspect-ratio "${flags.aspectRatio}" (expected e.g. 16:9)`);
|
|
74
75
|
}
|
|
76
|
+
aspectRatio = `${Number(m[1])}:${Number(m[2])}`;
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
const timeoutSec = flags.timeout ?? 600;
|
|
78
|
-
const outPath = resolve(this.process.cwd(), flags.out
|
|
80
|
+
const outPath = resolve(this.process.cwd(), flags.out);
|
|
79
81
|
|
|
80
82
|
const imagePaths: string[] = [];
|
|
81
83
|
if (flags.image !== undefined) {
|
|
@@ -94,17 +96,16 @@ export default async function imageGen(
|
|
|
94
96
|
return fail(formatImageGenModelError(inputSlug, model));
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
const minBytes = model.spec.backend === 'codex' ? MIN_REAL_BYTES_CODEX :
|
|
99
|
+
const minBytes = model.spec.backend === 'codex' ? MIN_REAL_BYTES_CODEX : MIN_REAL_BYTES_TOOL;
|
|
98
100
|
const work = mkdtempSync(join(tmpdir(), 'aibridge-imagegen-'));
|
|
99
101
|
|
|
100
102
|
try {
|
|
101
103
|
let outcome: ImageResult = await driver.generateImage({
|
|
102
104
|
prompt,
|
|
103
105
|
workDir: work,
|
|
104
|
-
backendModel: model
|
|
106
|
+
backendModel: backendModelId(model),
|
|
105
107
|
effort: model.effort,
|
|
106
|
-
|
|
107
|
-
size,
|
|
108
|
+
aspectRatio,
|
|
108
109
|
imagePaths,
|
|
109
110
|
timeoutSec,
|
|
110
111
|
forceful: false,
|
|
@@ -115,10 +116,9 @@ export default async function imageGen(
|
|
|
115
116
|
outcome = await driver.generateImage({
|
|
116
117
|
prompt,
|
|
117
118
|
workDir: work,
|
|
118
|
-
backendModel: model
|
|
119
|
+
backendModel: backendModelId(model),
|
|
119
120
|
effort: model.effort,
|
|
120
|
-
|
|
121
|
-
size,
|
|
121
|
+
aspectRatio,
|
|
122
122
|
imagePaths,
|
|
123
123
|
timeoutSec,
|
|
124
124
|
forceful: true,
|
|
@@ -133,42 +133,29 @@ export default async function imageGen(
|
|
|
133
133
|
if (outcome.kind === 'error') return fail(outcome.reason);
|
|
134
134
|
if (outcome.kind === 'suspect') {
|
|
135
135
|
return fail(
|
|
136
|
-
model.spec.backend === '
|
|
137
|
-
? '
|
|
138
|
-
:
|
|
139
|
-
|
|
136
|
+
model.spec.backend === 'agy'
|
|
137
|
+
? 'agy produced no usable image. Re-run with a simpler prompt, or check Antigravity image access.'
|
|
138
|
+
: model.spec.backend === 'grok'
|
|
139
|
+
? 'grok produced no usable image. Check SuperGrok image access and re-run with a simpler prompt.'
|
|
140
|
+
: 'codex produced only a tiny/code-drawn image, not a real render. ' +
|
|
141
|
+
'Try a clearer, simpler prompt.',
|
|
140
142
|
);
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
const local = join(work, 'result.bin');
|
|
144
146
|
copyFileSync(outcome.path, local);
|
|
145
147
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
}
|
|
148
|
+
const dims = imageSize(local);
|
|
149
|
+
const actual = pngSize(local) ? 'png' : jpegSize(local) ? 'jpg' : null;
|
|
158
150
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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);
|
|
151
|
+
// ponytail: guard for a backend changing formats in the future without throwing away a paid render
|
|
152
|
+
if (actual !== null && actual !== expected) {
|
|
153
|
+
this.process.stderr.write(
|
|
154
|
+
`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`,
|
|
155
|
+
);
|
|
171
156
|
}
|
|
157
|
+
|
|
158
|
+
copyFileSync(local, outPath);
|
|
172
159
|
const bytes = statSync(outPath).size;
|
|
173
160
|
|
|
174
161
|
if (flags.json) {
|
|
@@ -178,8 +165,7 @@ export default async function imageGen(
|
|
|
178
165
|
bytes,
|
|
179
166
|
width: dims?.width ?? null,
|
|
180
167
|
height: dims?.height ?? null,
|
|
181
|
-
|
|
182
|
-
quality: model.spec.backend === 'codex' ? quality : null,
|
|
168
|
+
aspectRatio: flags.aspectRatio ?? null,
|
|
183
169
|
model: model.spec.slug,
|
|
184
170
|
backend: model.spec.backend,
|
|
185
171
|
real: true,
|
|
@@ -188,27 +174,13 @@ export default async function imageGen(
|
|
|
188
174
|
} else {
|
|
189
175
|
const kb = Math.round(bytes / 1024);
|
|
190
176
|
const dimStr = dims ? `${dims.width}x${dims.height}, ` : '';
|
|
191
|
-
|
|
192
|
-
this.process.stdout.write(
|
|
193
|
-
`✓ Wrote ${outPath} (${dimStr}${kb} KB${qualityStr}, ${model.spec.slug})\n`,
|
|
194
|
-
);
|
|
177
|
+
this.process.stdout.write(`✓ Wrote ${outPath} (${dimStr}${kb} KB, ${model.spec.slug})\n`);
|
|
195
178
|
}
|
|
196
179
|
} finally {
|
|
197
180
|
rmSync(work, { recursive: true, force: true });
|
|
198
181
|
}
|
|
199
182
|
}
|
|
200
183
|
|
|
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
184
|
function imageSize(path: string): { width: number; height: number } | null {
|
|
213
185
|
return pngSize(path) ?? jpegSize(path);
|
|
214
186
|
}
|
|
@@ -254,15 +226,3 @@ function jpegSize(path: string): { width: number; height: number } | null {
|
|
|
254
226
|
return null;
|
|
255
227
|
}
|
|
256
228
|
}
|
|
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
|
-
}
|
|
@@ -72,10 +72,9 @@ export default async function subagent(
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
if (flags.json) {
|
|
75
|
-
const modelId = backendModelId(model) ?? null;
|
|
76
75
|
this.process.stdout.write(
|
|
77
76
|
`${JSON.stringify({
|
|
78
|
-
model:
|
|
77
|
+
model: backendModelId(model),
|
|
79
78
|
slug: model.spec.slug,
|
|
80
79
|
response: outcome.response,
|
|
81
80
|
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)
|
|
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
|
|
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
|
|
36
|
+
readonly backendModel: string;
|
|
37
37
|
readonly effort: Effort | undefined;
|
|
38
|
-
readonly
|
|
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
package/src/models.test.ts
CHANGED
|
@@ -22,7 +22,17 @@ describe('models registry', () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it('rejects short aliases — canonical slugs only', () => {
|
|
25
|
-
for (const alias of [
|
|
25
|
+
for (const alias of [
|
|
26
|
+
'grok',
|
|
27
|
+
'gemini',
|
|
28
|
+
'codex',
|
|
29
|
+
'sonnet',
|
|
30
|
+
'opus',
|
|
31
|
+
'gemini-3.6',
|
|
32
|
+
'gpt-oss',
|
|
33
|
+
'anthropic-claude/sonnet',
|
|
34
|
+
'anthropic-claude/opus',
|
|
35
|
+
]) {
|
|
26
36
|
expect(resolveModel(alias)).toBeUndefined();
|
|
27
37
|
}
|
|
28
38
|
});
|
|
@@ -32,8 +42,8 @@ describe('models registry', () => {
|
|
|
32
42
|
expect(grokMedium?.spec.slug).toBe('xai-grok/grok-4.5');
|
|
33
43
|
expect(grokMedium?.effort).toBe('medium');
|
|
34
44
|
|
|
35
|
-
const sonnetMax = resolveModel('anthropic-claude/sonnet-max');
|
|
36
|
-
expect(sonnetMax?.spec.slug).toBe('anthropic-claude/sonnet');
|
|
45
|
+
const sonnetMax = resolveModel('anthropic-claude/sonnet-5-max');
|
|
46
|
+
expect(sonnetMax?.spec.slug).toBe('anthropic-claude/sonnet-5');
|
|
37
47
|
expect(sonnetMax?.effort).toBe('max');
|
|
38
48
|
});
|
|
39
49
|
|
|
@@ -71,37 +81,41 @@ describe('models registry', () => {
|
|
|
71
81
|
if (!grok) throw new Error('grok resolution failed');
|
|
72
82
|
expect(backendModelId(grok)).toBe('grok-4.5');
|
|
73
83
|
|
|
74
|
-
const sonnet = resolveModel('anthropic-claude/sonnet');
|
|
84
|
+
const sonnet = resolveModel('anthropic-claude/sonnet-5');
|
|
75
85
|
if (!sonnet) throw new Error('sonnet resolution failed');
|
|
76
|
-
expect(backendModelId(sonnet)).toBe('sonnet');
|
|
86
|
+
expect(backendModelId(sonnet)).toBe('claude-sonnet-5');
|
|
77
87
|
});
|
|
78
88
|
|
|
79
|
-
it('marks
|
|
89
|
+
it('marks codex, grok, and gemini-3.6-flash seats as image-gen capable', () => {
|
|
80
90
|
const codex = resolveModel('openai-codex/gpt-5.6-sol');
|
|
81
91
|
const grok = resolveModel('xai-grok/grok-4.5');
|
|
82
92
|
const gemini = resolveModel('google-antigravity/gemini-3.6-flash');
|
|
83
|
-
|
|
93
|
+
const claudeSonnet = resolveModel('anthropic-claude/sonnet-5');
|
|
94
|
+
if (!codex || !grok || !gemini || !claudeSonnet) throw new Error('resolution failed');
|
|
84
95
|
expect(supportsImageGen(codex)).toBe(true);
|
|
85
96
|
expect(supportsImageGen(grok)).toBe(true);
|
|
86
|
-
expect(supportsImageGen(gemini)).toBe(
|
|
97
|
+
expect(supportsImageGen(gemini)).toBe(true);
|
|
98
|
+
expect(supportsImageGen(claudeSonnet)).toBe(false);
|
|
87
99
|
});
|
|
88
100
|
|
|
89
101
|
it('lists only image-capable seats when imageOnly', () => {
|
|
90
102
|
const lines = listModelHelpLines({ imageOnly: true }).join('\n');
|
|
91
103
|
expect(lines).toContain('xai-grok/grok-4.5');
|
|
92
104
|
expect(lines).toContain('openai-codex/gpt-5.6-sol');
|
|
93
|
-
expect(lines).
|
|
105
|
+
expect(lines).toContain('google-antigravity/gemini-3.6-flash');
|
|
106
|
+
expect(lines).not.toContain('anthropic-claude/sonnet-5');
|
|
94
107
|
});
|
|
95
108
|
|
|
96
109
|
it('formats image-gen model errors with capable seats only', () => {
|
|
97
|
-
const
|
|
98
|
-
if (!
|
|
99
|
-
const err = formatImageGenModelError('
|
|
110
|
+
const claudeSonnet = resolveModel('anthropic-claude/sonnet-5');
|
|
111
|
+
if (!claudeSonnet) throw new Error('claudeSonnet resolution failed');
|
|
112
|
+
const err = formatImageGenModelError('anthropic-claude/sonnet-5', claudeSonnet);
|
|
100
113
|
expect(err).toContain('cannot generate images');
|
|
101
|
-
expect(err).toContain('backend "
|
|
114
|
+
expect(err).toContain('backend "claude"');
|
|
102
115
|
expect(err).toContain('xai-grok/grok-4.5');
|
|
103
116
|
expect(err).toContain('openai-codex/gpt-5.6-sol');
|
|
117
|
+
expect(err).toContain('google-antigravity/gemini-3.6-flash');
|
|
104
118
|
const seatsSection = err.slice(err.indexOf('Image-gen seats'));
|
|
105
|
-
expect(seatsSection).not.toContain('
|
|
119
|
+
expect(seatsSection).not.toContain('anthropic-claude/sonnet-5');
|
|
106
120
|
});
|
|
107
121
|
});
|
package/src/models.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Models are registered by canonical, provider-qualified slug —
|
|
5
5
|
* `<vendor>-<cli>/<model>[-<effort>]`, e.g. `openai-codex/gpt-5.6-sol-high`.
|
|
6
|
-
* Canonical slugs only — no short aliases, by design.
|
|
6
|
+
* Canonical slugs only — no short aliases, by design. That holds on both sides:
|
|
7
|
+
* `backendModel` is a pinned model id (`claude-opus-5`), never a moving vendor
|
|
8
|
+
* alias (`opus`), so a seat never silently changes model under you.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
export type Backend = 'agy' | 'claude' | 'codex' | 'grok';
|
|
@@ -12,7 +14,7 @@ export type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
|
12
14
|
export interface ModelSpec {
|
|
13
15
|
readonly slug: string; // canonical, effort-less
|
|
14
16
|
readonly backend: Backend;
|
|
15
|
-
readonly backendModel: string
|
|
17
|
+
readonly backendModel: string; // always explicit — never "whatever the CLI defaults to"
|
|
16
18
|
readonly efforts: readonly Effort[] | null;
|
|
17
19
|
readonly defaultEffort?: Effort; // only when backend REQUIRES one (agy gemini)
|
|
18
20
|
readonly brief: string;
|
|
@@ -37,7 +39,8 @@ export const MODELS: Record<string, ModelSpec> = {
|
|
|
37
39
|
backendModel: 'gemini-3.6-flash',
|
|
38
40
|
efforts: ['low', 'medium', 'high'],
|
|
39
41
|
defaultEffort: 'high',
|
|
40
|
-
brief:
|
|
42
|
+
brief:
|
|
43
|
+
'Google Gemini 3.6 Flash via agy — default for implement, also image-gen; own Antigravity login',
|
|
41
44
|
},
|
|
42
45
|
'google-antigravity/claude-sonnet-4-6': {
|
|
43
46
|
slug: 'google-antigravity/claude-sonnet-4-6',
|
|
@@ -67,20 +70,28 @@ export const MODELS: Record<string, ModelSpec> = {
|
|
|
67
70
|
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
68
71
|
brief: 'OpenAI Codex gpt-5.6-sol via codex CLI',
|
|
69
72
|
},
|
|
70
|
-
'anthropic-claude/sonnet': {
|
|
71
|
-
slug: 'anthropic-claude/sonnet',
|
|
73
|
+
'anthropic-claude/sonnet-5': {
|
|
74
|
+
slug: 'anthropic-claude/sonnet-5',
|
|
72
75
|
backend: 'claude',
|
|
73
|
-
backendModel: 'sonnet',
|
|
76
|
+
backendModel: 'claude-sonnet-5',
|
|
74
77
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
75
|
-
brief: 'Claude Sonnet via claude CLI — bills your Claude subscription',
|
|
78
|
+
brief: 'Claude Sonnet 5 via claude CLI — bills your Claude subscription',
|
|
76
79
|
},
|
|
77
|
-
'anthropic-claude/opus': {
|
|
78
|
-
slug: 'anthropic-claude/opus',
|
|
80
|
+
'anthropic-claude/opus-5': {
|
|
81
|
+
slug: 'anthropic-claude/opus-5',
|
|
79
82
|
backend: 'claude',
|
|
80
|
-
backendModel: 'opus',
|
|
83
|
+
backendModel: 'claude-opus-5',
|
|
81
84
|
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
82
85
|
defaultEffort: 'high',
|
|
83
|
-
brief: 'Claude Opus via claude CLI (default effort: high) — bills subscription',
|
|
86
|
+
brief: 'Claude Opus 5 via claude CLI (default effort: high) — bills subscription',
|
|
87
|
+
},
|
|
88
|
+
'anthropic-claude/opus-5-1m': {
|
|
89
|
+
slug: 'anthropic-claude/opus-5-1m',
|
|
90
|
+
backend: 'claude',
|
|
91
|
+
backendModel: 'claude-opus-5[1m]',
|
|
92
|
+
efforts: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
93
|
+
defaultEffort: 'high',
|
|
94
|
+
brief: 'Claude Opus 5, 1M context via claude CLI — long-context work; bills subscription',
|
|
84
95
|
},
|
|
85
96
|
} as const satisfies Record<string, ModelSpec>;
|
|
86
97
|
|
|
@@ -88,10 +99,20 @@ export const DEFAULT_MODEL = 'xai-grok/grok-4.5';
|
|
|
88
99
|
export const DEFAULT_IMPLEMENTER = 'google-antigravity/gemini-3.6-flash';
|
|
89
100
|
export const DEFAULT_IMAGE_GEN = 'openai-codex/gpt-5.6-sol';
|
|
90
101
|
|
|
91
|
-
|
|
102
|
+
export type ImageFormat = 'jpg' | 'png';
|
|
103
|
+
|
|
104
|
+
const IMAGE_GEN_FORMATS: ReadonlyMap<Backend, ImageFormat> = new Map([
|
|
105
|
+
['agy', 'jpg'],
|
|
106
|
+
['codex', 'png'],
|
|
107
|
+
['grok', 'jpg'],
|
|
108
|
+
]);
|
|
92
109
|
|
|
93
110
|
export function supportsImageGen(resolved: ResolvedModel): boolean {
|
|
94
|
-
return
|
|
111
|
+
return IMAGE_GEN_FORMATS.has(resolved.spec.backend);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function imageFormatFor(resolved: ResolvedModel): ImageFormat | undefined {
|
|
115
|
+
return IMAGE_GEN_FORMATS.get(resolved.spec.backend);
|
|
95
116
|
}
|
|
96
117
|
|
|
97
118
|
const EFFORTS_SET: ReadonlySet<string> = new Set<Effort>(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
@@ -122,10 +143,7 @@ export function resolveModel(input: string): ResolvedModel | undefined {
|
|
|
122
143
|
return undefined;
|
|
123
144
|
}
|
|
124
145
|
|
|
125
|
-
export function backendModelId(resolved: ResolvedModel): string
|
|
126
|
-
if (!resolved.spec.backendModel) {
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
146
|
+
export function backendModelId(resolved: ResolvedModel): string {
|
|
129
147
|
if (resolved.spec.backend === 'agy') {
|
|
130
148
|
const effort = resolved.effort ?? resolved.spec.defaultEffort;
|
|
131
149
|
if (effort) {
|
|
@@ -138,7 +156,7 @@ export function backendModelId(resolved: ResolvedModel): string | undefined {
|
|
|
138
156
|
export function listModelHelpLines(opts: { readonly imageOnly?: boolean } = {}): string[] {
|
|
139
157
|
const lines: string[] = [];
|
|
140
158
|
for (const [slug, spec] of Object.entries(MODELS)) {
|
|
141
|
-
if (opts.imageOnly && !
|
|
159
|
+
if (opts.imageOnly && !IMAGE_GEN_FORMATS.has(spec.backend)) continue;
|
|
142
160
|
lines.push(` ${slug}`);
|
|
143
161
|
lines.push(` ${spec.brief}`);
|
|
144
162
|
}
|
package/src/quotaPreflight.ts
CHANGED
|
@@ -62,8 +62,7 @@ export async function preflightModel(resolved: ResolvedModel): Promise<Preflight
|
|
|
62
62
|
|
|
63
63
|
try {
|
|
64
64
|
const snapshot = await fetchAgyQuota();
|
|
65
|
-
|
|
66
|
-
return evaluateAgyPreflight(snapshot, modelId);
|
|
65
|
+
return evaluateAgyPreflight(snapshot, backendModelId(resolved));
|
|
67
66
|
} catch (err) {
|
|
68
67
|
return {
|
|
69
68
|
ok: true,
|