@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.
@@ -35,7 +35,7 @@ describe('flag mapping & defaults lock', () => {
35
35
  it('plan command maps defaults correctly', async () => {
36
36
  mockPlanImpl.mockReset();
37
37
  const ctx = fakeCtx();
38
- await runCli(ctx, ['plan', 'do something']);
38
+ await runCli(ctx, ['plan', '--model', 'xai-grok/grok-4.5', '--out', 'plan.md', 'do something']);
39
39
  expect(mockPlanImpl).toHaveBeenCalledTimes(1);
40
40
  const [call] = mockPlanImpl.mock.calls;
41
41
  expect(call).toBeDefined();
@@ -43,6 +43,8 @@ describe('flag mapping & defaults lock', () => {
43
43
  const [flags, prompt] = call;
44
44
  expect(prompt).toBe('do something');
45
45
  expect(flags).toEqual({
46
+ model: 'xai-grok/grok-4.5',
47
+ out: 'plan.md',
46
48
  preflight: true,
47
49
  });
48
50
  });
@@ -50,7 +52,17 @@ describe('flag mapping & defaults lock', () => {
50
52
  it('plan command handles --no-preflight and --timeout', async () => {
51
53
  mockPlanImpl.mockReset();
52
54
  const ctx = fakeCtx();
53
- await runCli(ctx, ['plan', '--no-preflight', '--timeout', '120', 'task']);
55
+ await runCli(ctx, [
56
+ 'plan',
57
+ '--model',
58
+ 'xai-grok/grok-4.5',
59
+ '--out',
60
+ 'plan.md',
61
+ '--no-preflight',
62
+ '--timeout',
63
+ '120',
64
+ 'task',
65
+ ]);
54
66
  expect(mockPlanImpl).toHaveBeenCalledTimes(1);
55
67
  const [call] = mockPlanImpl.mock.calls;
56
68
  expect(call).toBeDefined();
@@ -58,6 +70,8 @@ describe('flag mapping & defaults lock', () => {
58
70
  const [flags, prompt] = call;
59
71
  expect(prompt).toBe('task');
60
72
  expect(flags).toEqual({
73
+ model: 'xai-grok/grok-4.5',
74
+ out: 'plan.md',
61
75
  preflight: false,
62
76
  timeout: 120,
63
77
  });
@@ -66,7 +80,7 @@ describe('flag mapping & defaults lock', () => {
66
80
  it('subagent command maps defaults correctly', async () => {
67
81
  mockSubagentImpl.mockReset();
68
82
  const ctx = fakeCtx();
69
- await runCli(ctx, ['subagent', 'hello agent']);
83
+ await runCli(ctx, ['subagent', '--model', 'xai-grok/grok-4.5', 'hello agent']);
70
84
  expect(mockSubagentImpl).toHaveBeenCalledTimes(1);
71
85
  const [call] = mockSubagentImpl.mock.calls;
72
86
  expect(call).toBeDefined();
@@ -74,6 +88,7 @@ describe('flag mapping & defaults lock', () => {
74
88
  const [flags, prompt] = call;
75
89
  expect(prompt).toBe('hello agent');
76
90
  expect(flags).toEqual({
91
+ model: 'xai-grok/grok-4.5',
77
92
  tools: true,
78
93
  preflight: true,
79
94
  json: false,
@@ -83,7 +98,14 @@ describe('flag mapping & defaults lock', () => {
83
98
  it('subagent command handles --no-tools and --no-preflight', async () => {
84
99
  mockSubagentImpl.mockReset();
85
100
  const ctx = fakeCtx();
86
- await runCli(ctx, ['subagent', '--no-tools', '--no-preflight', 'hello agent']);
101
+ await runCli(ctx, [
102
+ 'subagent',
103
+ '--model',
104
+ 'xai-grok/grok-4.5',
105
+ '--no-tools',
106
+ '--no-preflight',
107
+ 'hello agent',
108
+ ]);
87
109
  expect(mockSubagentImpl).toHaveBeenCalledTimes(1);
88
110
  const [call] = mockSubagentImpl.mock.calls;
89
111
  expect(call).toBeDefined();
@@ -91,6 +113,7 @@ describe('flag mapping & defaults lock', () => {
91
113
  const [flags, prompt] = call;
92
114
  expect(prompt).toBe('hello agent');
93
115
  expect(flags).toEqual({
116
+ model: 'xai-grok/grok-4.5',
94
117
  tools: false,
95
118
  preflight: false,
96
119
  json: false,
package/src/index.ts CHANGED
@@ -6,9 +6,6 @@ export { getDriver } from './drivers.ts';
6
6
  export {
7
7
  type Backend,
8
8
  backendModelId,
9
- DEFAULT_IMAGE_GEN,
10
- DEFAULT_IMPLEMENTER,
11
- DEFAULT_MODEL,
12
9
  type Effort,
13
10
  formatImageGenModelError,
14
11
  formatUnknownModelError,
@@ -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 ['grok', 'gemini', 'codex', 'sonnet', 'opus', 'gemini-3.6', 'gpt-oss']) {
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 only codex and grok seats as image-gen capable', () => {
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
- if (!codex || !grok || !gemini) throw new Error('resolution failed');
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(false);
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).not.toContain('google-antigravity/gemini-3.6-flash');
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 gemini = resolveModel('google-antigravity/gemini-3.6-flash');
98
- if (!gemini) throw new Error('gemini resolution failed');
99
- const err = formatImageGenModelError('google-antigravity/gemini-3.6-flash', gemini);
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 "agy"');
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('google-antigravity/gemini-3.6-flash');
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 | undefined; // undefined = CLI default
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: 'Google Gemini 3.6 Flash via agy — default for implement; own Antigravity login',
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,31 +70,45 @@ 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
 
87
- export const DEFAULT_MODEL = 'xai-grok/grok-4.5';
88
- export const DEFAULT_IMPLEMENTER = 'google-antigravity/gemini-3.6-flash';
89
- export const DEFAULT_IMAGE_GEN = 'openai-codex/gpt-5.6-sol';
98
+ export type ImageFormat = 'jpg' | 'png';
90
99
 
91
- const IMAGE_GEN_BACKENDS: ReadonlySet<Backend> = new Set(['codex', 'grok']);
100
+ const IMAGE_GEN_FORMATS: ReadonlyMap<Backend, ImageFormat> = new Map([
101
+ ['agy', 'jpg'],
102
+ ['codex', 'png'],
103
+ ['grok', 'jpg'],
104
+ ]);
92
105
 
93
106
  export function supportsImageGen(resolved: ResolvedModel): boolean {
94
- return IMAGE_GEN_BACKENDS.has(resolved.spec.backend);
107
+ return IMAGE_GEN_FORMATS.has(resolved.spec.backend);
108
+ }
109
+
110
+ export function imageFormatFor(resolved: ResolvedModel): ImageFormat | undefined {
111
+ return IMAGE_GEN_FORMATS.get(resolved.spec.backend);
95
112
  }
96
113
 
97
114
  const EFFORTS_SET: ReadonlySet<string> = new Set<Effort>(['low', 'medium', 'high', 'xhigh', 'max']);
@@ -122,10 +139,7 @@ export function resolveModel(input: string): ResolvedModel | undefined {
122
139
  return undefined;
123
140
  }
124
141
 
125
- export function backendModelId(resolved: ResolvedModel): string | undefined {
126
- if (!resolved.spec.backendModel) {
127
- return undefined;
128
- }
142
+ export function backendModelId(resolved: ResolvedModel): string {
129
143
  if (resolved.spec.backend === 'agy') {
130
144
  const effort = resolved.effort ?? resolved.spec.defaultEffort;
131
145
  if (effort) {
@@ -138,7 +152,7 @@ export function backendModelId(resolved: ResolvedModel): string | undefined {
138
152
  export function listModelHelpLines(opts: { readonly imageOnly?: boolean } = {}): string[] {
139
153
  const lines: string[] = [];
140
154
  for (const [slug, spec] of Object.entries(MODELS)) {
141
- if (opts.imageOnly && !IMAGE_GEN_BACKENDS.has(spec.backend)) continue;
155
+ if (opts.imageOnly && !IMAGE_GEN_FORMATS.has(spec.backend)) continue;
142
156
  lines.push(` ${slug}`);
143
157
  lines.push(` ${spec.brief}`);
144
158
  }
@@ -62,8 +62,7 @@ export async function preflightModel(resolved: ResolvedModel): Promise<Preflight
62
62
 
63
63
  try {
64
64
  const snapshot = await fetchAgyQuota();
65
- const modelId = backendModelId(resolved) ?? '';
66
- return evaluateAgyPreflight(snapshot, modelId);
65
+ return evaluateAgyPreflight(snapshot, backendModelId(resolved));
67
66
  } catch (err) {
68
67
  return {
69
68
  ok: true,