@aibridge/cli 0.0.1

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cli.d.mts +1 -0
  3. package/dist/cli.mjs +6 -0
  4. package/dist/context-BLjTHa41.mjs +1529 -0
  5. package/dist/index.d.mts +184 -0
  6. package/dist/index.mjs +2 -0
  7. package/package.json +53 -0
  8. package/src/app.exit-code.test.ts +91 -0
  9. package/src/app.ts +49 -0
  10. package/src/cli.ts +5 -0
  11. package/src/commands/image-gen/command.ts +77 -0
  12. package/src/commands/image-gen/impl.ts +268 -0
  13. package/src/commands/implement/command.ts +50 -0
  14. package/src/commands/implement/impl.ts +99 -0
  15. package/src/commands/plan/command.ts +56 -0
  16. package/src/commands/plan/impl.ts +172 -0
  17. package/src/commands/plan/plan.test.ts +19 -0
  18. package/src/commands/quota/command.ts +30 -0
  19. package/src/commands/quota/impl.ts +109 -0
  20. package/src/commands/review/command.ts +58 -0
  21. package/src/commands/review/impl.ts +211 -0
  22. package/src/commands/review/review.test.ts +54 -0
  23. package/src/commands/runs/command.ts +53 -0
  24. package/src/commands/runs/impl.ts +171 -0
  25. package/src/commands/subagent/command.ts +62 -0
  26. package/src/commands/subagent/impl.ts +87 -0
  27. package/src/context.ts +10 -0
  28. package/src/delegate.test.ts +180 -0
  29. package/src/delegate.ts +46 -0
  30. package/src/driver.ts +56 -0
  31. package/src/drivers.ts +44 -0
  32. package/src/exitCode.test.ts +44 -0
  33. package/src/exitCode.ts +24 -0
  34. package/src/flagMapping.test.ts +99 -0
  35. package/src/index.ts +37 -0
  36. package/src/models.test.ts +107 -0
  37. package/src/models.ts +159 -0
  38. package/src/parsers.ts +24 -0
  39. package/src/quotaPreflight.test.ts +178 -0
  40. package/src/quotaPreflight.ts +103 -0
  41. package/src/runlog.ts +195 -0
@@ -0,0 +1,62 @@
1
+ import { buildCommand } from '@stricli/core';
2
+ import { DEFAULT_MODEL, listModelHelpLines } from '../../models.ts';
3
+ import { nonEmptyPrompt, positiveIntSeconds } from '../../parsers.ts';
4
+ import subagentImpl from './impl.ts';
5
+
6
+ const fullDescription = [
7
+ 'Hands a self-contained prompt to another model and returns its answer.',
8
+ '',
9
+ 'Available models (canonical slug):',
10
+ ...listModelHelpLines(),
11
+ `Default: ${DEFAULT_MODEL} (off-budget). The claude-backend slugs are FALLBACKS for`,
12
+ 'when the off-budget CLIs are quota-exhausted — they bill your Claude subscription.',
13
+ ].join('\n');
14
+
15
+ export const subagent = buildCommand({
16
+ func: subagentImpl,
17
+ parameters: {
18
+ flags: {
19
+ model: {
20
+ kind: 'parsed',
21
+ parse: String,
22
+ optional: true,
23
+ brief: `Model slug to delegate to (default: ${DEFAULT_MODEL})`,
24
+ },
25
+ timeout: {
26
+ kind: 'parsed',
27
+ parse: positiveIntSeconds,
28
+ optional: true,
29
+ brief: 'Max seconds to wait for the backend (default: 600)',
30
+ },
31
+ tools: {
32
+ kind: 'boolean',
33
+ default: true,
34
+ brief: 'Allow delegate model to use tools (use --no-tools to restrict to reasoning only)',
35
+ },
36
+ preflight: {
37
+ kind: 'boolean',
38
+ default: true,
39
+ brief: 'Check model quota before running (use --no-preflight to skip)',
40
+ },
41
+ json: {
42
+ kind: 'boolean',
43
+ withNegated: false,
44
+ brief: 'Emit a machine-readable JSON result (using canonical slug) instead of prose',
45
+ },
46
+ },
47
+ positional: {
48
+ kind: 'tuple',
49
+ parameters: [
50
+ {
51
+ brief: 'Self-contained task prompt for the delegate model',
52
+ parse: nonEmptyPrompt,
53
+ placeholder: 'prompt',
54
+ },
55
+ ],
56
+ },
57
+ },
58
+ docs: {
59
+ brief: 'Delegate a self-contained task to another model',
60
+ fullDescription,
61
+ },
62
+ });
@@ -0,0 +1,87 @@
1
+ import type { LocalContext } from '../../context.ts';
2
+ import { delegate } from '../../delegate.ts';
3
+ import {
4
+ backendModelId,
5
+ DEFAULT_MODEL,
6
+ formatUnknownModelError,
7
+ resolveModel,
8
+ } from '../../models.ts';
9
+ import { preflightModel, renderPreflightRefusal } from '../../quotaPreflight.ts';
10
+ import { startRun } from '../../runlog.ts';
11
+
12
+ export interface SubagentFlags {
13
+ readonly model?: string;
14
+ readonly timeout?: number;
15
+ readonly tools: boolean;
16
+ readonly preflight: boolean;
17
+ readonly json: boolean;
18
+ }
19
+
20
+ export default async function subagent(
21
+ this: LocalContext,
22
+ flags: SubagentFlags,
23
+ prompt: string,
24
+ ): Promise<void> {
25
+ const inputSlug = flags.model ?? DEFAULT_MODEL;
26
+ const model = resolveModel(inputSlug);
27
+ if (!model) {
28
+ this.process.stderr.write(`${formatUnknownModelError(inputSlug)}\n`);
29
+ this.process.exitCode = 2;
30
+ return;
31
+ }
32
+
33
+ if (flags.preflight) {
34
+ const verdict = await preflightModel(model);
35
+ if (!verdict.ok) {
36
+ if (flags.json) {
37
+ this.process.stdout.write(
38
+ `${JSON.stringify({
39
+ error: 'quota_exhausted',
40
+ message: verdict.message,
41
+ resetAt: verdict.resetAt ?? null,
42
+ slug: model.spec.slug,
43
+ })}\n`,
44
+ );
45
+ } else {
46
+ this.process.stderr.write(`${renderPreflightRefusal('subagent', verdict)}\n`);
47
+ }
48
+ this.process.exitCode = 3;
49
+ return;
50
+ }
51
+ if (verdict.warning) this.process.stderr.write(`aibridge subagent: ${verdict.warning}\n`);
52
+ }
53
+
54
+ const timeoutSec = flags.timeout ?? 600;
55
+ const workDir = this.process.cwd();
56
+ const promptSnippet = prompt.replace(/\r?\n/g, ' ').slice(0, 80);
57
+ const run = startRun('subagent', `${model.spec.slug}: ${promptSnippet}`);
58
+
59
+ const outcome = await delegate({
60
+ model,
61
+ prompt,
62
+ tools: flags.tools,
63
+ timeoutSec,
64
+ cwd: workDir,
65
+ run,
66
+ });
67
+
68
+ if (!outcome.ok) {
69
+ this.process.stderr.write(`${outcome.message}\n`);
70
+ this.process.exitCode = 1;
71
+ return;
72
+ }
73
+
74
+ if (flags.json) {
75
+ const modelId = backendModelId(model) ?? null;
76
+ this.process.stdout.write(
77
+ `${JSON.stringify({
78
+ model: modelId,
79
+ slug: model.spec.slug,
80
+ response: outcome.response,
81
+ exitCode: outcome.exitCode,
82
+ })}\n`,
83
+ );
84
+ } else {
85
+ this.process.stdout.write(`${outcome.response}\n`);
86
+ }
87
+ }
package/src/context.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { CommandContext } from '@stricli/core';
2
+
3
+ export interface LocalContext extends CommandContext {
4
+ /** Full Node process — satisfies stricli WritableStreams + exitCode/env/cwd used by impls. */
5
+ readonly process: NodeJS.Process;
6
+ }
7
+
8
+ export function buildContext(process: NodeJS.Process): LocalContext {
9
+ return { process };
10
+ }
@@ -0,0 +1,180 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { delegate } from './delegate.ts';
3
+ import type { AgentCliDriver, DelegationResult, DelegationTask } from './driver.ts';
4
+ import { resolveModel } from './models.ts';
5
+ import type { RunLog } from './runlog.ts';
6
+
7
+ const PREAMBLE_PIN =
8
+ 'You are the sole executing agent for this task: do it yourself with your tools, now. ' +
9
+ 'Never defer to, wait for, or claim to hand off to another agent or process — no one ' +
10
+ 'else will act, and work not done in this run does not happen.\n\n';
11
+
12
+ class StubDriver implements AgentCliDriver {
13
+ lastTask?: DelegationTask;
14
+ private readonly result: DelegationResult;
15
+ private readonly callbacks?: { stdout?: string; stderr?: string; pid?: number };
16
+
17
+ constructor(
18
+ result: DelegationResult,
19
+ callbacks?: { stdout?: string; stderr?: string; pid?: number },
20
+ ) {
21
+ this.result = result;
22
+ this.callbacks = callbacks;
23
+ }
24
+
25
+ async probe() {
26
+ return { ok: true, version: '1.0' } as const;
27
+ }
28
+
29
+ async run(task: DelegationTask): Promise<DelegationResult> {
30
+ this.lastTask = task;
31
+ if (this.callbacks) {
32
+ if (this.callbacks.pid !== undefined) task.onSpawn?.(this.callbacks.pid);
33
+ if (this.callbacks.stdout !== undefined) task.onStdout?.(this.callbacks.stdout);
34
+ if (this.callbacks.stderr !== undefined) task.onStderr?.(this.callbacks.stderr);
35
+ }
36
+ return this.result;
37
+ }
38
+ }
39
+
40
+ function createRecordingRunLog() {
41
+ const calls = {
42
+ pid: null as number | null,
43
+ stdout: [] as string[],
44
+ stderr: [] as string[],
45
+ finish: null as { status: string; exitCode: number | null } | null,
46
+ };
47
+ const runLog: RunLog = {
48
+ id: 'test-id',
49
+ dir: '/test/dir',
50
+ setPid(pid) {
51
+ calls.pid = pid;
52
+ },
53
+ stdout(chunk) {
54
+ calls.stdout.push(chunk);
55
+ },
56
+ stderr(chunk) {
57
+ calls.stderr.push(chunk);
58
+ },
59
+ finish(status, exitCode) {
60
+ calls.finish = { status, exitCode };
61
+ },
62
+ };
63
+ return { runLog, calls };
64
+ }
65
+
66
+ describe('delegate stub-driver tests', () => {
67
+ const model = resolveModel('xai-grok/grok-4.5');
68
+ if (!model) throw new Error('model resolution failed');
69
+
70
+ it('prepends preamble when tools: true, passes untouched when tools: false', async () => {
71
+ const stubTrue = new StubDriver({ ok: true, response: 'ok', exitCode: 0 });
72
+ const { runLog: runLogTrue } = createRecordingRunLog();
73
+ await delegate(
74
+ {
75
+ model,
76
+ prompt: 'do work',
77
+ tools: true,
78
+ timeoutSec: 60,
79
+ cwd: '/test',
80
+ run: runLogTrue,
81
+ },
82
+ stubTrue,
83
+ );
84
+ expect(stubTrue.lastTask?.prompt).toBe(`${PREAMBLE_PIN}do work`);
85
+
86
+ const stubFalse = new StubDriver({ ok: true, response: 'ok', exitCode: 0 });
87
+ const { runLog: runLogFalse } = createRecordingRunLog();
88
+ await delegate(
89
+ {
90
+ model,
91
+ prompt: 'do work',
92
+ tools: false,
93
+ timeoutSec: 60,
94
+ cwd: '/test',
95
+ run: runLogFalse,
96
+ },
97
+ stubFalse,
98
+ );
99
+ expect(stubFalse.lastTask?.prompt).toBe('do work');
100
+ });
101
+
102
+ it('forwards stdout/stderr/spawn callbacks to RunLog', async () => {
103
+ const stub = new StubDriver(
104
+ { ok: true, response: 'ok', exitCode: 0 },
105
+ { pid: 999, stdout: 'out chunk', stderr: 'err chunk' },
106
+ );
107
+ const { runLog, calls } = createRecordingRunLog();
108
+
109
+ await delegate(
110
+ {
111
+ model,
112
+ prompt: 'test callbacks',
113
+ tools: true,
114
+ timeoutSec: 60,
115
+ cwd: '/test',
116
+ run: runLog,
117
+ },
118
+ stub,
119
+ );
120
+
121
+ expect(calls.pid).toBe(999);
122
+ expect(calls.stdout).toEqual(['out chunk']);
123
+ expect(calls.stderr).toEqual(['err chunk']);
124
+ });
125
+
126
+ it('maps finish status and exitCode correctly for all outcome kinds', async () => {
127
+ const outcomes: Array<{
128
+ result: DelegationResult;
129
+ expectedStatus: string;
130
+ expectedExitCode: number | null;
131
+ }> = [
132
+ {
133
+ result: { ok: true, response: 'all good', exitCode: 0 },
134
+ expectedStatus: 'done',
135
+ expectedExitCode: 0,
136
+ },
137
+ {
138
+ result: { ok: false, kind: 'timeout', message: 'Timed out', exitCode: null },
139
+ expectedStatus: 'timeout',
140
+ expectedExitCode: null,
141
+ },
142
+ {
143
+ result: { ok: false, kind: 'no-answer', message: 'Nonzero exit', exitCode: 1 },
144
+ expectedStatus: 'error',
145
+ expectedExitCode: 1,
146
+ },
147
+ {
148
+ result: { ok: false, kind: 'not-found', message: 'CLI not found', exitCode: null },
149
+ expectedStatus: 'error',
150
+ expectedExitCode: null,
151
+ },
152
+ {
153
+ result: { ok: false, kind: 'spawn', message: 'Spawn error', exitCode: null },
154
+ expectedStatus: 'error',
155
+ expectedExitCode: null,
156
+ },
157
+ ];
158
+
159
+ for (const { result, expectedStatus, expectedExitCode } of outcomes) {
160
+ const stub = new StubDriver(result);
161
+ const { runLog, calls } = createRecordingRunLog();
162
+ await delegate(
163
+ {
164
+ model,
165
+ prompt: 'test finish mapping',
166
+ tools: true,
167
+ timeoutSec: 60,
168
+ cwd: '/test',
169
+ run: runLog,
170
+ },
171
+ stub,
172
+ );
173
+
174
+ expect(calls.finish).toEqual({
175
+ status: expectedStatus,
176
+ exitCode: expectedExitCode,
177
+ });
178
+ }
179
+ });
180
+ });
@@ -0,0 +1,46 @@
1
+ import type { AgentCliDriver, DelegationResult } from './driver.ts';
2
+ import { getDriver } from './drivers.ts';
3
+ import { backendModelId, type ResolvedModel } from './models.ts';
4
+ import type { RunLog } from './runlog.ts';
5
+
6
+ export interface DelegateOptions {
7
+ readonly model: ResolvedModel;
8
+ readonly prompt: string;
9
+ readonly tools: boolean;
10
+ readonly timeoutSec: number;
11
+ readonly cwd: string;
12
+ readonly run: RunLog;
13
+ }
14
+
15
+ export type DelegateOutcome = DelegationResult;
16
+
17
+ const PREAMBLE =
18
+ 'You are the sole executing agent for this task: do it yourself with your tools, now. ' +
19
+ 'Never defer to, wait for, or claim to hand off to another agent or process — no one ' +
20
+ 'else will act, and work not done in this run does not happen.\n\n';
21
+
22
+ export async function delegate(
23
+ opts: DelegateOptions,
24
+ driver: AgentCliDriver = getDriver(opts.model.spec.backend),
25
+ ): Promise<DelegateOutcome> {
26
+ const effectivePrompt = opts.tools ? PREAMBLE + opts.prompt : opts.prompt;
27
+ const result = await driver.run({
28
+ prompt: effectivePrompt,
29
+ tools: opts.tools,
30
+ timeoutSec: opts.timeoutSec,
31
+ cwd: opts.cwd,
32
+ backendModel: backendModelId(opts.model) ?? opts.model.spec.backendModel,
33
+ effort: opts.model.effort,
34
+ onStdout: c => opts.run.stdout(c),
35
+ onStderr: c => opts.run.stderr(c),
36
+ onSpawn: pid => opts.run.setPid(pid),
37
+ });
38
+
39
+ if (result.ok) {
40
+ opts.run.finish('done', result.exitCode);
41
+ } else {
42
+ opts.run.finish(result.kind === 'timeout' ? 'timeout' : 'error', result.exitCode);
43
+ }
44
+
45
+ return result;
46
+ }
package/src/driver.ts ADDED
@@ -0,0 +1,56 @@
1
+ import type { AgyQuotaSnapshot } from '@aibridge/agy';
2
+ import type { ClaudeQuotaSnapshot } from '@aibridge/claude';
3
+ import type { CodexQuotaSnapshot } from '@aibridge/codex';
4
+ import type { Effort } from './models.ts';
5
+
6
+ export type Availability =
7
+ | { readonly ok: true; readonly version: string }
8
+ | { readonly ok: false; readonly error: string };
9
+
10
+ export interface DelegationTask {
11
+ readonly prompt: string;
12
+ readonly tools: boolean;
13
+ readonly timeoutSec: number;
14
+ readonly cwd: string;
15
+ readonly backendModel: string | undefined;
16
+ readonly effort: Effort | undefined;
17
+ readonly onStdout?: (chunk: string) => void;
18
+ readonly onStderr?: (chunk: string) => void;
19
+ readonly onSpawn?: (pid: number) => void;
20
+ }
21
+
22
+ export type DelegationResult =
23
+ | { readonly ok: true; readonly response: string; readonly exitCode: number }
24
+ | {
25
+ readonly ok: false;
26
+ readonly kind: 'not-found' | 'spawn' | 'timeout' | 'no-answer';
27
+ readonly message: string;
28
+ readonly exitCode: number | null;
29
+ };
30
+
31
+ export type QuotaSnapshot = AgyQuotaSnapshot | CodexQuotaSnapshot | ClaudeQuotaSnapshot;
32
+
33
+ export interface ImageGenRequest {
34
+ readonly prompt: string;
35
+ readonly workDir: string;
36
+ readonly backendModel: string | undefined;
37
+ readonly effort: Effort | undefined;
38
+ readonly quality: string;
39
+ readonly size: { readonly w: number; readonly h: number } | undefined;
40
+ readonly imagePaths: readonly string[];
41
+ readonly timeoutSec: number;
42
+ readonly forceful: boolean;
43
+ readonly minBytes: number;
44
+ }
45
+
46
+ export type ImageResult =
47
+ | { readonly kind: 'ok'; readonly path: string; readonly bytes: number }
48
+ | { readonly kind: 'suspect' }
49
+ | { readonly kind: 'error'; readonly reason: string };
50
+
51
+ export interface AgentCliDriver {
52
+ probe(): Promise<Availability>;
53
+ run(task: DelegationTask): Promise<DelegationResult>;
54
+ quota?(): Promise<QuotaSnapshot>;
55
+ generateImage?(req: ImageGenRequest): Promise<ImageResult>;
56
+ }
package/src/drivers.ts ADDED
@@ -0,0 +1,44 @@
1
+ import * as agy from '@aibridge/agy';
2
+ import * as claude from '@aibridge/claude';
3
+ import * as codex from '@aibridge/codex';
4
+ import * as grok from '@aibridge/grok';
5
+ import type { AgentCliDriver } from './driver.ts';
6
+ import type { Backend } from './models.ts';
7
+
8
+ const agyDriver: AgentCliDriver = {
9
+ probe: () => agy.probe(),
10
+ run: task => agy.run(task),
11
+ quota: () => agy.fetchAgyQuota(),
12
+ };
13
+
14
+ const grokDriver: AgentCliDriver = {
15
+ probe: () => grok.probe(),
16
+ run: task => grok.run(task),
17
+ generateImage: req => grok.generateImage(req),
18
+ };
19
+
20
+ const codexDriver: AgentCliDriver = {
21
+ probe: () => codex.probe(),
22
+ run: task => codex.run(task),
23
+ quota: () => codex.fetchCodexQuota(),
24
+ generateImage: req => codex.generateImage(req),
25
+ };
26
+
27
+ const claudeDriver: AgentCliDriver = {
28
+ probe: () => claude.probe(),
29
+ run: task => claude.run(task),
30
+ quota: () => claude.fetchClaudeQuota(),
31
+ };
32
+
33
+ const DRIVERS: Record<Backend, AgentCliDriver> = {
34
+ agy: agyDriver,
35
+ grok: grokDriver,
36
+ codex: codexDriver,
37
+ claude: claudeDriver,
38
+ };
39
+
40
+ export function getDriver(backend: Backend): AgentCliDriver {
41
+ const driver = DRIVERS[backend];
42
+ if (!driver) throw new Error(`Unknown backend "${backend}"`);
43
+ return driver;
44
+ }
@@ -0,0 +1,44 @@
1
+ import { ExitCode } from '@stricli/core';
2
+ import { describe, expect, it } from 'vitest';
3
+ import type { LocalContext } from './context.ts';
4
+ import { normalizeExitCode } from './exitCode.ts';
5
+
6
+ function createMockCtx(exitCode?: number): LocalContext {
7
+ return {
8
+ process: {
9
+ exitCode,
10
+ } as unknown as NodeJS.Process,
11
+ };
12
+ }
13
+
14
+ describe('normalizeExitCode', () => {
15
+ it('leaves undefined exitCode untouched', () => {
16
+ const ctx = createMockCtx(undefined);
17
+ normalizeExitCode(ctx);
18
+ expect(ctx.process.exitCode).toBeUndefined();
19
+ });
20
+
21
+ it.each([0, 1, 2, 3])('preserves valid contract exit code %i', code => {
22
+ const ctx = createMockCtx(code);
23
+ normalizeExitCode(ctx);
24
+ expect(ctx.process.exitCode).toBe(code);
25
+ });
26
+
27
+ it('maps ExitCode.InvalidArgument (-4) to 2', () => {
28
+ const ctx = createMockCtx(ExitCode.InvalidArgument);
29
+ normalizeExitCode(ctx);
30
+ expect(ctx.process.exitCode).toBe(2);
31
+ });
32
+
33
+ it('maps ExitCode.UnknownCommand (-5) to 2', () => {
34
+ const ctx = createMockCtx(ExitCode.UnknownCommand);
35
+ normalizeExitCode(ctx);
36
+ expect(ctx.process.exitCode).toBe(2);
37
+ });
38
+
39
+ it.each([-1, -2, -3, -6, -99, 4, 127])('maps other non-contract code %i to 1', code => {
40
+ const ctx = createMockCtx(code);
41
+ normalizeExitCode(ctx);
42
+ expect(ctx.process.exitCode).toBe(1);
43
+ });
44
+ });
@@ -0,0 +1,24 @@
1
+ import { ExitCode } from '@stricli/core';
2
+ import type { LocalContext } from './context.ts';
3
+
4
+ /**
5
+ * Stricli uses negative ExitCode values for parse/route failures.
6
+ * Our public contract is Unix-style: 0 ok, 1 op fail, 2 bad args, 3 quota refuse.
7
+ * Call after `run()`; never overwrite a code already set by an impl (run uses ??=).
8
+ */
9
+ export function normalizeExitCode(ctx: LocalContext): void {
10
+ const code = ctx.process.exitCode;
11
+ if (typeof code !== 'number') return;
12
+ if (code === ExitCode.InvalidArgument || code === ExitCode.UnknownCommand) {
13
+ ctx.process.exitCode = 2;
14
+ return;
15
+ }
16
+ // Contract-space fallback: impls only ever set 0/1/2/3. Anything else on
17
+ // the process at this point is a stricli framework code (whatever its
18
+ // actual numeric value in this stricli version) → operational failure.
19
+ // Do NOT assume framework codes are negative — compare against the
20
+ // ExitCode constants and the contract space only.
21
+ if (code !== 0 && code !== 1 && code !== 2 && code !== 3) {
22
+ ctx.process.exitCode = 1;
23
+ }
24
+ }
@@ -0,0 +1,99 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import type { LocalContext } from './context.ts';
3
+
4
+ const mockPlanImpl = vi.fn();
5
+ const mockSubagentImpl = vi.fn();
6
+
7
+ vi.mock('./commands/plan/impl.ts', () => ({
8
+ default: function (this: LocalContext, ...args: unknown[]) {
9
+ return mockPlanImpl.call(this, ...args);
10
+ },
11
+ }));
12
+
13
+ vi.mock('./commands/subagent/impl.ts', () => ({
14
+ default: function (this: LocalContext, ...args: unknown[]) {
15
+ return mockSubagentImpl.call(this, ...args);
16
+ },
17
+ }));
18
+
19
+ // Must import app AFTER mocks
20
+ const { runCli } = await import('./app.ts');
21
+
22
+ function fakeCtx(): LocalContext {
23
+ return {
24
+ process: {
25
+ stdout: { write: () => true },
26
+ stderr: { write: () => true },
27
+ exitCode: undefined as number | undefined,
28
+ env: { ...process.env, NO_COLOR: '1' },
29
+ cwd: () => process.cwd(),
30
+ } as unknown as NodeJS.Process,
31
+ };
32
+ }
33
+
34
+ describe('flag mapping & defaults lock', () => {
35
+ it('plan command maps defaults correctly', async () => {
36
+ mockPlanImpl.mockReset();
37
+ const ctx = fakeCtx();
38
+ await runCli(ctx, ['plan', 'do something']);
39
+ expect(mockPlanImpl).toHaveBeenCalledTimes(1);
40
+ const [call] = mockPlanImpl.mock.calls;
41
+ expect(call).toBeDefined();
42
+ if (!call) return;
43
+ const [flags, prompt] = call;
44
+ expect(prompt).toBe('do something');
45
+ expect(flags).toEqual({
46
+ preflight: true,
47
+ });
48
+ });
49
+
50
+ it('plan command handles --no-preflight and --timeout', async () => {
51
+ mockPlanImpl.mockReset();
52
+ const ctx = fakeCtx();
53
+ await runCli(ctx, ['plan', '--no-preflight', '--timeout', '120', 'task']);
54
+ expect(mockPlanImpl).toHaveBeenCalledTimes(1);
55
+ const [call] = mockPlanImpl.mock.calls;
56
+ expect(call).toBeDefined();
57
+ if (!call) return;
58
+ const [flags, prompt] = call;
59
+ expect(prompt).toBe('task');
60
+ expect(flags).toEqual({
61
+ preflight: false,
62
+ timeout: 120,
63
+ });
64
+ });
65
+
66
+ it('subagent command maps defaults correctly', async () => {
67
+ mockSubagentImpl.mockReset();
68
+ const ctx = fakeCtx();
69
+ await runCli(ctx, ['subagent', 'hello agent']);
70
+ expect(mockSubagentImpl).toHaveBeenCalledTimes(1);
71
+ const [call] = mockSubagentImpl.mock.calls;
72
+ expect(call).toBeDefined();
73
+ if (!call) return;
74
+ const [flags, prompt] = call;
75
+ expect(prompt).toBe('hello agent');
76
+ expect(flags).toEqual({
77
+ tools: true,
78
+ preflight: true,
79
+ json: false,
80
+ });
81
+ });
82
+
83
+ it('subagent command handles --no-tools and --no-preflight', async () => {
84
+ mockSubagentImpl.mockReset();
85
+ const ctx = fakeCtx();
86
+ await runCli(ctx, ['subagent', '--no-tools', '--no-preflight', 'hello agent']);
87
+ expect(mockSubagentImpl).toHaveBeenCalledTimes(1);
88
+ const [call] = mockSubagentImpl.mock.calls;
89
+ expect(call).toBeDefined();
90
+ if (!call) return;
91
+ const [flags, prompt] = call;
92
+ expect(prompt).toBe('hello agent');
93
+ expect(flags).toEqual({
94
+ tools: false,
95
+ preflight: false,
96
+ json: false,
97
+ });
98
+ });
99
+ });
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ export { app, runCli } from './app.ts';
2
+ export { buildContext, type LocalContext } from './context.ts';
3
+ export { type DelegateOptions, type DelegateOutcome, delegate } from './delegate.ts';
4
+ export type { AgentCliDriver, DelegationResult, DelegationTask } from './driver.ts';
5
+ export { getDriver } from './drivers.ts';
6
+ export {
7
+ type Backend,
8
+ backendModelId,
9
+ DEFAULT_IMAGE_GEN,
10
+ DEFAULT_IMPLEMENTER,
11
+ DEFAULT_MODEL,
12
+ type Effort,
13
+ formatImageGenModelError,
14
+ formatUnknownModelError,
15
+ listModelHelpLines,
16
+ MODELS,
17
+ type ModelSpec,
18
+ type ResolvedModel,
19
+ resolveModel,
20
+ supportsImageGen,
21
+ } from './models.ts';
22
+ export { nonEmptyPrompt, positiveIntSeconds } from './parsers.ts';
23
+ export {
24
+ evaluateAgyPreflight,
25
+ evaluateCodexPreflight,
26
+ type PreflightVerdict,
27
+ preflightCodex,
28
+ preflightModel,
29
+ renderPreflightRefusal,
30
+ } from './quotaPreflight.ts';
31
+ export {
32
+ listRuns,
33
+ type RunLog,
34
+ type RunMeta,
35
+ readRunLogs,
36
+ startRun,
37
+ } from './runlog.ts';