@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.
- package/README.md +40 -9
- package/dist/cli.mjs +1 -1
- package/dist/{context-DQxvMUd2.mjs → context-v_ns4n_W.mjs} +186 -48
- package/dist/index.d.mts +4 -6
- package/dist/index.mjs +2 -2
- package/package.json +6 -6
- package/src/app.ts +3 -1
- package/src/commands/image-gen/command.ts +3 -4
- package/src/commands/image-gen/impl.ts +3 -4
- package/src/commands/implement/command.ts +2 -3
- package/src/commands/implement/impl.ts +3 -3
- package/src/commands/models/command.ts +18 -0
- package/src/commands/models/impl.test.ts +89 -0
- package/src/commands/models/impl.ts +71 -0
- package/src/commands/plan/command.ts +3 -5
- package/src/commands/plan/impl.ts +5 -9
- package/src/commands/quota/command.ts +3 -1
- package/src/commands/quota/impl.ts +24 -2
- package/src/commands/review/command.ts +3 -5
- package/src/commands/review/impl.ts +5 -9
- package/src/commands/subagent/command.ts +4 -5
- package/src/commands/subagent/impl.ts +3 -8
- package/src/driver.ts +6 -1
- package/src/drivers.ts +1 -0
- package/src/flagMapping.test.ts +27 -4
- package/src/index.ts +0 -3
- package/src/models.ts +48 -19
- package/src/quotaPreflight.test.ts +46 -1
- package/src/quotaPreflight.ts +29 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildCommand } from '@stricli/core';
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
12
|
-
readonly out
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
11
|
+
readonly model: string;
|
|
12
12
|
readonly plan?: string;
|
|
13
13
|
readonly base?: string;
|
|
14
|
-
readonly out
|
|
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
|
|
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 {
|
|
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
|
-
|
|
12
|
-
'
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
package/src/flagMapping.test.ts
CHANGED
|
@@ -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, [
|
|
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, [
|
|
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,
|