@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,184 @@
1
+ import { CommandContext } from "@stricli/core";
2
+ import { AgyQuotaSnapshot } from "@aibridge/agy";
3
+ import { ClaudeQuotaSnapshot } from "@aibridge/claude";
4
+ import { CodexQuotaSnapshot } from "@aibridge/codex";
5
+ //#region src/context.d.ts
6
+ interface LocalContext extends CommandContext {
7
+ /** Full Node process — satisfies stricli WritableStreams + exitCode/env/cwd used by impls. */
8
+ readonly process: NodeJS.Process;
9
+ }
10
+ declare function buildContext(process: NodeJS.Process): LocalContext;
11
+ //#endregion
12
+ //#region src/app.d.ts
13
+ declare const app: import("@stricli/core").Application<LocalContext>;
14
+ /** Public entry used by cli.ts and index.ts — preserves runCli(ctx, argv) surface. */
15
+ declare function runCli(ctx: LocalContext, argv: readonly string[]): Promise<void>;
16
+ //#endregion
17
+ //#region src/models.d.ts
18
+ /**
19
+ * Canonical model registry and resolution for aibridge.
20
+ *
21
+ * Models are registered by canonical, provider-qualified slug —
22
+ * `<vendor>-<cli>/<model>[-<effort>]`, e.g. `openai-codex/gpt-5.6-sol-high`.
23
+ * Canonical slugs only — no short aliases, by design.
24
+ */
25
+ type Backend = 'agy' | 'claude' | 'codex' | 'grok';
26
+ type Effort = 'low' | 'medium' | 'high' | 'xhigh' | 'max';
27
+ interface ModelSpec {
28
+ readonly slug: string;
29
+ readonly backend: Backend;
30
+ readonly backendModel: string | undefined;
31
+ readonly efforts: readonly Effort[] | null;
32
+ readonly defaultEffort?: Effort;
33
+ readonly brief: string;
34
+ }
35
+ interface ResolvedModel {
36
+ readonly spec: ModelSpec;
37
+ readonly effort: Effort | undefined;
38
+ }
39
+ declare const MODELS: Record<string, ModelSpec>;
40
+ declare const DEFAULT_MODEL = "xai-grok/grok-4.5";
41
+ declare const DEFAULT_IMPLEMENTER = "google-antigravity/gemini-3.6-flash";
42
+ declare const DEFAULT_IMAGE_GEN = "openai-codex/gpt-5.6-sol";
43
+ declare function supportsImageGen(resolved: ResolvedModel): boolean;
44
+ declare function resolveModel(input: string): ResolvedModel | undefined;
45
+ declare function backendModelId(resolved: ResolvedModel): string | undefined;
46
+ declare function listModelHelpLines(opts?: {
47
+ readonly imageOnly?: boolean;
48
+ }): string[];
49
+ declare function formatUnknownModelError(input: string): string;
50
+ declare function formatImageGenModelError(input: string, resolved: ResolvedModel): string;
51
+ //#endregion
52
+ //#region src/driver.d.ts
53
+ type Availability = {
54
+ readonly ok: true;
55
+ readonly version: string;
56
+ } | {
57
+ readonly ok: false;
58
+ readonly error: string;
59
+ };
60
+ interface DelegationTask {
61
+ readonly prompt: string;
62
+ readonly tools: boolean;
63
+ readonly timeoutSec: number;
64
+ readonly cwd: string;
65
+ readonly backendModel: string | undefined;
66
+ readonly effort: Effort | undefined;
67
+ readonly onStdout?: (chunk: string) => void;
68
+ readonly onStderr?: (chunk: string) => void;
69
+ readonly onSpawn?: (pid: number) => void;
70
+ }
71
+ type DelegationResult = {
72
+ readonly ok: true;
73
+ readonly response: string;
74
+ readonly exitCode: number;
75
+ } | {
76
+ readonly ok: false;
77
+ readonly kind: 'not-found' | 'spawn' | 'timeout' | 'no-answer';
78
+ readonly message: string;
79
+ readonly exitCode: number | null;
80
+ };
81
+ type QuotaSnapshot = AgyQuotaSnapshot | CodexQuotaSnapshot | ClaudeQuotaSnapshot;
82
+ interface ImageGenRequest {
83
+ readonly prompt: string;
84
+ readonly workDir: string;
85
+ readonly backendModel: string | undefined;
86
+ readonly effort: Effort | undefined;
87
+ readonly quality: string;
88
+ readonly size: {
89
+ readonly w: number;
90
+ readonly h: number;
91
+ } | undefined;
92
+ readonly imagePaths: readonly string[];
93
+ readonly timeoutSec: number;
94
+ readonly forceful: boolean;
95
+ readonly minBytes: number;
96
+ }
97
+ type ImageResult = {
98
+ readonly kind: 'ok';
99
+ readonly path: string;
100
+ readonly bytes: number;
101
+ } | {
102
+ readonly kind: 'suspect';
103
+ } | {
104
+ readonly kind: 'error';
105
+ readonly reason: string;
106
+ };
107
+ interface AgentCliDriver {
108
+ probe(): Promise<Availability>;
109
+ run(task: DelegationTask): Promise<DelegationResult>;
110
+ quota?(): Promise<QuotaSnapshot>;
111
+ generateImage?(req: ImageGenRequest): Promise<ImageResult>;
112
+ }
113
+ //#endregion
114
+ //#region src/runlog.d.ts
115
+ interface RunMeta {
116
+ readonly id: string;
117
+ readonly command: string;
118
+ readonly detail: string;
119
+ pid: number | null;
120
+ readonly startedAt: string;
121
+ endedAt: string | null;
122
+ status: 'running' | 'done' | 'error' | 'timeout' | 'stale';
123
+ exitCode: number | null;
124
+ }
125
+ interface RunLog {
126
+ readonly id: string;
127
+ readonly dir: string;
128
+ setPid(pid: number): void;
129
+ stdout(chunk: string): void;
130
+ stderr(chunk: string): void;
131
+ finish(status: 'done' | 'error' | 'timeout', exitCode: number | null): void;
132
+ }
133
+ declare function startRun(command: string, detail: string): RunLog;
134
+ declare function listRuns(): RunMeta[];
135
+ declare function readRunLogs(id: string): {
136
+ meta: RunMeta;
137
+ stdout: string;
138
+ stderr: string;
139
+ } | null;
140
+ //#endregion
141
+ //#region src/delegate.d.ts
142
+ interface DelegateOptions {
143
+ readonly model: ResolvedModel;
144
+ readonly prompt: string;
145
+ readonly tools: boolean;
146
+ readonly timeoutSec: number;
147
+ readonly cwd: string;
148
+ readonly run: RunLog;
149
+ }
150
+ type DelegateOutcome = DelegationResult;
151
+ declare function delegate(opts: DelegateOptions, driver?: AgentCliDriver): Promise<DelegateOutcome>;
152
+ //#endregion
153
+ //#region src/drivers.d.ts
154
+ declare function getDriver(backend: Backend): AgentCliDriver;
155
+ //#endregion
156
+ //#region src/parsers.d.ts
157
+ /**
158
+ * stricli `parse` functions (string -> T). Throwing inside one makes stricli
159
+ * reject the argument up-front with a clean, flag-named error — instead of
160
+ * silently letting a bad value (NaN, Infinity, "") flow into an impl where it
161
+ * gets masked or, worse, breaks `setTimeout`.
162
+ */
163
+ declare function positiveIntSeconds(input: string): number;
164
+ declare function nonEmptyPrompt(input: string): string;
165
+ //#endregion
166
+ //#region src/quotaPreflight.d.ts
167
+ type PreflightVerdict = {
168
+ readonly ok: true;
169
+ readonly warning?: string;
170
+ } | {
171
+ readonly ok: false;
172
+ readonly message: string;
173
+ readonly resetAt: string | undefined;
174
+ };
175
+ declare function evaluateAgyPreflight(snapshot: AgyQuotaSnapshot, backendModel: string): PreflightVerdict;
176
+ declare function evaluateCodexPreflight(snapshot: CodexQuotaSnapshot): PreflightVerdict;
177
+ declare function preflightModel(resolved: ResolvedModel): Promise<PreflightVerdict>;
178
+ declare function preflightCodex(): Promise<PreflightVerdict>;
179
+ declare function renderPreflightRefusal(cmd: string, verdict: {
180
+ message: string;
181
+ resetAt: string | undefined;
182
+ }): string;
183
+ //#endregion
184
+ export { type AgentCliDriver, type Backend, DEFAULT_IMAGE_GEN, DEFAULT_IMPLEMENTER, DEFAULT_MODEL, type DelegateOptions, type DelegateOutcome, type DelegationResult, type DelegationTask, type Effort, type LocalContext, MODELS, type ModelSpec, type PreflightVerdict, type ResolvedModel, type RunLog, type RunMeta, app, backendModelId, buildContext, delegate, evaluateAgyPreflight, evaluateCodexPreflight, formatImageGenModelError, formatUnknownModelError, getDriver, listModelHelpLines, listRuns, nonEmptyPrompt, positiveIntSeconds, preflightCodex, preflightModel, readRunLogs, renderPreflightRefusal, resolveModel, runCli, startRun, supportsImageGen };
package/dist/index.mjs ADDED
@@ -0,0 +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-BLjTHa41.mjs";
2
+ export { DEFAULT_IMAGE_GEN, DEFAULT_IMPLEMENTER, DEFAULT_MODEL, MODELS, app, backendModelId, buildContext, delegate, evaluateAgyPreflight, evaluateCodexPreflight, formatImageGenModelError, formatUnknownModelError, getDriver, listModelHelpLines, listRuns, nonEmptyPrompt, positiveIntSeconds, preflightCodex, preflightModel, readRunLogs, renderPreflightRefusal, resolveModel, runCli, startRun, supportsImageGen };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@aibridge/cli",
3
+ "version": "0.0.1",
4
+ "description": "CLI that bridges tasks to non-Claude AI CLIs (plan / implement / review / subagent / image-gen)",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=24.11"
9
+ },
10
+ "keywords": [
11
+ "aibridge",
12
+ "cli"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/fishballapp/aibridge.git",
17
+ "directory": "packages/cli"
18
+ },
19
+ "homepage": "https://github.com/fishballapp/aibridge#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/fishballapp/aibridge/issues"
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.mts",
26
+ "import": "./dist/index.mjs"
27
+ }
28
+ },
29
+ "bin": {
30
+ "aibridge": "./dist/cli.mjs"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "src"
35
+ ],
36
+ "dependencies": {
37
+ "@stricli/core": "1.3.0",
38
+ "@aibridge/proc": "0.0.1",
39
+ "@aibridge/codex": "0.0.1",
40
+ "@aibridge/claude": "0.0.1",
41
+ "@aibridge/grok": "0.0.1",
42
+ "@aibridge/agy": "0.0.1"
43
+ },
44
+ "devDependencies": {
45
+ "tsdown": "0.22.14"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "scripts": {
51
+ "build": "tsdown"
52
+ }
53
+ }
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { runCli } from './app.ts';
3
+ import type { LocalContext } from './context.ts';
4
+
5
+ function fakeCtx(): LocalContext & { _stdout: string[]; _stderr: string[] } {
6
+ const _stdout: string[] = [];
7
+ const _stderr: string[] = [];
8
+ const processLike = {
9
+ stdout: {
10
+ write: (s: string) => {
11
+ _stdout.push(s);
12
+ return true;
13
+ },
14
+ },
15
+ stderr: {
16
+ write: (s: string) => {
17
+ _stderr.push(s);
18
+ return true;
19
+ },
20
+ },
21
+ exitCode: undefined as number | undefined,
22
+ env: { ...process.env, NO_COLOR: '1', STRICLI_NO_COLOR: '1' },
23
+ cwd: () => process.cwd(),
24
+ };
25
+ return { process: processLike as unknown as NodeJS.Process, _stdout, _stderr };
26
+ }
27
+
28
+ describe('stricli exit-code lock & routing', () => {
29
+ it('unknown command → 2', async () => {
30
+ const ctx = fakeCtx();
31
+ await runCli(ctx, ['nonsense']);
32
+ expect(ctx.process.exitCode).toBe(2);
33
+ });
34
+
35
+ it('unknown flag → 2', async () => {
36
+ const ctx = fakeCtx();
37
+ await runCli(ctx, ['plan', '--not-a-flag', 'x']);
38
+ expect(ctx.process.exitCode).toBe(2);
39
+ });
40
+
41
+ it('missing required arg → 2', async () => {
42
+ const ctx = fakeCtx();
43
+ await runCli(ctx, ['plan']);
44
+ expect(ctx.process.exitCode).toBe(2);
45
+ });
46
+
47
+ it('empty prompt → 2', async () => {
48
+ const ctx = fakeCtx();
49
+ await runCli(ctx, ['plan', '']);
50
+ expect(ctx.process.exitCode).toBe(2);
51
+ });
52
+
53
+ it('review with stray positional → 2', async () => {
54
+ const ctx = fakeCtx();
55
+ await runCli(ctx, ['review', 'stray']);
56
+ expect(ctx.process.exitCode).toBe(2);
57
+ });
58
+
59
+ it('runs --watch with idPrefix → 2', async () => {
60
+ const ctx = fakeCtx();
61
+ await runCli(ctx, ['runs', '--watch', 'someid']);
62
+ expect(ctx.process.exitCode).toBe(2);
63
+ });
64
+
65
+ it('runs --watch with --json → 2', async () => {
66
+ const ctx = fakeCtx();
67
+ await runCli(ctx, ['runs', '--watch', '--json']);
68
+ expect(ctx.process.exitCode).toBe(2);
69
+ });
70
+
71
+ it('runs --no-json → 2', async () => {
72
+ const ctx = fakeCtx();
73
+ await runCli(ctx, ['runs', '--no-json']);
74
+ expect(ctx.process.exitCode).toBe(2);
75
+ });
76
+
77
+ it('runs --no-watch → 2', async () => {
78
+ const ctx = fakeCtx();
79
+ await runCli(ctx, ['runs', '--no-watch']);
80
+ expect(ctx.process.exitCode).toBe(2);
81
+ });
82
+
83
+ it('root --help lists all commands', async () => {
84
+ const ctx = fakeCtx();
85
+ await runCli(ctx, ['--help']);
86
+ const output = ctx._stdout.join('');
87
+ for (const cmd of ['plan', 'implement', 'review', 'subagent', 'image-gen', 'runs', 'quota']) {
88
+ expect(output).toContain(cmd);
89
+ }
90
+ });
91
+ });
package/src/app.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { createRequire } from 'node:module';
2
+ import { buildApplication, buildRouteMap, run } from '@stricli/core';
3
+ import { imageGen } from './commands/image-gen/command.ts';
4
+ import { implement } from './commands/implement/command.ts';
5
+ import { plan } from './commands/plan/command.ts';
6
+ import { quota } from './commands/quota/command.ts';
7
+ import { review } from './commands/review/command.ts';
8
+ import { runs } from './commands/runs/command.ts';
9
+ import { subagent } from './commands/subagent/command.ts';
10
+ import type { LocalContext } from './context.ts';
11
+ import { normalizeExitCode } from './exitCode.ts';
12
+
13
+ const require = createRequire(import.meta.url);
14
+ const { version } = require('../package.json') as { version: string };
15
+
16
+ const BRIEF =
17
+ 'Bridge tasks to non-Claude AI CLIs — a plan → implement → review workflow, task delegation, and image generation (codex gpt-image-2 / grok Imagine).';
18
+
19
+ const routes = buildRouteMap({
20
+ routes: {
21
+ plan,
22
+ implement,
23
+ review,
24
+ subagent,
25
+ 'image-gen': imageGen,
26
+ runs,
27
+ quota,
28
+ },
29
+ docs: {
30
+ brief: BRIEF,
31
+ },
32
+ });
33
+
34
+ export const app = buildApplication(routes, {
35
+ name: 'aibridge',
36
+ versionInfo: {
37
+ currentVersion: version,
38
+ },
39
+ scanner: {
40
+ // Accept --no-preflight / --no-tools while flag keys stay camelCase in TS
41
+ caseStyle: 'allow-kebab-for-camel',
42
+ },
43
+ });
44
+
45
+ /** Public entry used by cli.ts and index.ts — preserves runCli(ctx, argv) surface. */
46
+ export async function runCli(ctx: LocalContext, argv: readonly string[]): Promise<void> {
47
+ await run(app, argv, ctx);
48
+ normalizeExitCode(ctx);
49
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from './app.ts';
3
+ import { buildContext } from './context.ts';
4
+
5
+ await runCli(buildContext(process), process.argv.slice(2));
@@ -0,0 +1,77 @@
1
+ import { buildCommand } from '@stricli/core';
2
+ import { DEFAULT_IMAGE_GEN, listModelHelpLines } from '../../models.ts';
3
+ import { nonEmptyPrompt, positiveIntSeconds } from '../../parsers.ts';
4
+ import imageGenImpl from './impl.ts';
5
+
6
+ const fullDescription = [
7
+ "Renders an image by driving the seat's CLI (codex → gpt-image-2, grok →",
8
+ 'Imagine), then verifies the result is a real render before returning it.',
9
+ '',
10
+ 'Image-gen seats (canonical slug):',
11
+ ...listModelHelpLines({ imageOnly: true }),
12
+ `Default: ${DEFAULT_IMAGE_GEN} (gpt-image-2 via codex; historical default).`,
13
+ ].join('\n');
14
+
15
+ export const imageGen = buildCommand({
16
+ func: imageGenImpl,
17
+ parameters: {
18
+ flags: {
19
+ model: {
20
+ kind: 'parsed',
21
+ parse: String,
22
+ optional: true,
23
+ brief: `Model slug (default: ${DEFAULT_IMAGE_GEN})`,
24
+ },
25
+ out: {
26
+ kind: 'parsed',
27
+ parse: String,
28
+ optional: true,
29
+ brief: 'Path to write the image (default: ./aibridge-image.png)',
30
+ },
31
+ size: {
32
+ kind: 'parsed',
33
+ parse: String,
34
+ optional: true,
35
+ brief:
36
+ 'WIDTHxHEIGHT (codex: each edge ÷16; grok: mapped to aspect_ratio, then optionally resized)',
37
+ },
38
+ image: {
39
+ kind: 'parsed',
40
+ parse: String,
41
+ optional: true,
42
+ brief: 'Reference image path(s), comma-separated — visual reference',
43
+ },
44
+ quality: {
45
+ kind: 'parsed',
46
+ parse: String,
47
+ optional: true,
48
+ brief: 'low | medium | high (codex/gpt-image-2; default high)',
49
+ },
50
+ timeout: {
51
+ kind: 'parsed',
52
+ parse: positiveIntSeconds,
53
+ optional: true,
54
+ brief: 'Max seconds to wait for the render (default: 600)',
55
+ },
56
+ json: {
57
+ kind: 'boolean',
58
+ withNegated: false,
59
+ brief: 'Emit a machine-readable JSON result instead of prose',
60
+ },
61
+ },
62
+ positional: {
63
+ kind: 'tuple',
64
+ parameters: [
65
+ {
66
+ brief: 'Description of the image to generate',
67
+ parse: nonEmptyPrompt,
68
+ placeholder: 'prompt',
69
+ },
70
+ ],
71
+ },
72
+ },
73
+ docs: {
74
+ brief: 'Generate a raster image via a model seat',
75
+ fullDescription,
76
+ },
77
+ });