ai-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 +29 -2
- package/package.json +1 -1
- package/src/cli.test.ts +1 -26
- package/src/commands/image.ts +58 -5
- package/src/commands/models.ts +36 -47
- package/src/commands/text.ts +3 -2
- package/src/commands/video.ts +3 -2
- package/src/index.ts +0 -2
- package/src/lib/models.test.ts +236 -126
- package/src/lib/models.ts +128 -118
- package/src/commands/completions.ts +0 -296
package/README.md
CHANGED
|
@@ -40,6 +40,13 @@ All commands support:
|
|
|
40
40
|
--json Output metadata as JSON
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
Model IDs can be specified as `creator/model-name` or just `model-name` (resolved against models fetched from the gateway):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
ai text -m gpt-5.5 "hello" # resolves to openai/gpt-5.5
|
|
47
|
+
ai image -m flux-2-pro "a sunset" # resolves to bfl/flux-2-pro
|
|
48
|
+
```
|
|
49
|
+
|
|
43
50
|
### image
|
|
44
51
|
|
|
45
52
|
```
|
|
@@ -71,11 +78,11 @@ All commands support:
|
|
|
71
78
|
|
|
72
79
|
```
|
|
73
80
|
--type <type> Filter by type: text, image, video
|
|
74
|
-
--
|
|
81
|
+
--creator <name> Filter by creator (e.g. openai, google)
|
|
75
82
|
--json Output as JSON (includes descriptions)
|
|
76
83
|
```
|
|
77
84
|
|
|
78
|
-
All model types (text, image, video) are fetched live from the AI Gateway.
|
|
85
|
+
All model types (text, image, video) are fetched live from the AI Gateway.
|
|
79
86
|
|
|
80
87
|
### Multi-Model Comparison
|
|
81
88
|
|
|
@@ -112,9 +119,29 @@ When running in a terminal that supports the [Kitty graphics protocol](https://s
|
|
|
112
119
|
| `AI_CLI_VIDEO_MODEL` | Default video model (overrides `bytedance/seedance-2.0`) |
|
|
113
120
|
| `AI_CLI_OUTPUT_DIR` | Default output directory for generated files |
|
|
114
121
|
| `AI_CLI_PREVIEW` | Set to `1` to force inline image preview, `0` to disable |
|
|
122
|
+
| `NO_COLOR` | Disable ANSI color output |
|
|
123
|
+
| `FORCE_COLOR` | Force color output even when not a TTY |
|
|
115
124
|
|
|
116
125
|
The `-m` flag always takes priority over `AI_CLI_*_MODEL` env vars. The `-o` flag always takes priority over `AI_CLI_OUTPUT_DIR`.
|
|
117
126
|
|
|
127
|
+
### Timeouts
|
|
128
|
+
|
|
129
|
+
Requests that exceed the timeout are aborted automatically:
|
|
130
|
+
|
|
131
|
+
| Command | Timeout |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `text` | 120 seconds |
|
|
134
|
+
| `image` | 120 seconds |
|
|
135
|
+
| `video` | 300 seconds |
|
|
136
|
+
|
|
137
|
+
### Exit Codes
|
|
138
|
+
|
|
139
|
+
| Code | Meaning |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `0` | Success |
|
|
142
|
+
| `1` | All generations failed |
|
|
143
|
+
| `2` | Partial failure (some succeeded, some failed) |
|
|
144
|
+
|
|
118
145
|
## License
|
|
119
146
|
|
|
120
147
|
[Apache-2.0](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A tiny, agent-native CLI for generating images, video and text with dead-simple commands, stdin support and predictable artifact outputs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
package/src/cli.test.ts
CHANGED
|
@@ -22,7 +22,7 @@ describe("cli integration", () => {
|
|
|
22
22
|
test("--help exits 0 and lists subcommands", async () => {
|
|
23
23
|
const { exitCode, stdout } = await run("--help");
|
|
24
24
|
expect(exitCode).toBe(0);
|
|
25
|
-
for (const sub of ["text", "image", "video", "models"
|
|
25
|
+
for (const sub of ["text", "image", "video", "models"]) {
|
|
26
26
|
expect(stdout).toContain(sub);
|
|
27
27
|
}
|
|
28
28
|
});
|
|
@@ -33,31 +33,6 @@ describe("cli integration", () => {
|
|
|
33
33
|
expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+/);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
test("completions zsh exits 0 with valid output", async () => {
|
|
37
|
-
const { exitCode, stdout } = await run("completions", "zsh");
|
|
38
|
-
expect(exitCode).toBe(0);
|
|
39
|
-
expect(stdout).toContain("#compdef ai");
|
|
40
|
-
expect(stdout).toContain("--no-preview");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("completions bash exits 0 with valid output", async () => {
|
|
44
|
-
const { exitCode, stdout } = await run("completions", "bash");
|
|
45
|
-
expect(exitCode).toBe(0);
|
|
46
|
-
expect(stdout).toContain("complete -F");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("completions fish exits 0 with valid output", async () => {
|
|
50
|
-
const { exitCode, stdout } = await run("completions", "fish");
|
|
51
|
-
expect(exitCode).toBe(0);
|
|
52
|
-
expect(stdout).toContain("complete -c ai");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test("completions with invalid shell exits 1", async () => {
|
|
56
|
-
const { exitCode, stderr } = await run("completions", "powershell");
|
|
57
|
-
expect(exitCode).toBe(1);
|
|
58
|
-
expect(stderr).toContain("Unknown shell");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
36
|
test("text with no prompt and no stdin exits 1", async () => {
|
|
62
37
|
const { exitCode, stderr } = await run("text");
|
|
63
38
|
expect(exitCode).toBe(1);
|
package/src/commands/image.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { generateImage, gateway } from "ai";
|
|
1
|
+
import { generateImage, generateText, gateway } from "ai";
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
|
|
4
4
|
import { buildJobs, runJobs } from "../lib/jobs.js";
|
|
5
|
-
import { resolveModels } from "../lib/models.js";
|
|
5
|
+
import { fetchGatewayModels, resolveModels } from "../lib/models.js";
|
|
6
6
|
import { parsePositiveInt, parseSize, parseAspectRatio } from "../lib/parse.js";
|
|
7
7
|
import { readStdin } from "../lib/stdin.js";
|
|
8
8
|
|
|
@@ -57,15 +57,17 @@ export function registerImageCommand(program: Command) {
|
|
|
57
57
|
);
|
|
58
58
|
process.exit(1);
|
|
59
59
|
}
|
|
60
|
-
let imagePrompt: string | { images: Uint8Array[]; text?: string }
|
|
61
|
-
prompt!;
|
|
60
|
+
let imagePrompt: string | { images: Uint8Array[]; text?: string };
|
|
62
61
|
if (stdin) {
|
|
63
62
|
imagePrompt = prompt
|
|
64
63
|
? { images: [new Uint8Array(stdin)], text: prompt }
|
|
65
64
|
: { images: [new Uint8Array(stdin)] };
|
|
65
|
+
} else {
|
|
66
|
+
imagePrompt = prompt!;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
const
|
|
69
|
+
const gatewayModels = await fetchGatewayModels();
|
|
70
|
+
const models = resolveModels("image", opts.model, gatewayModels.image);
|
|
69
71
|
const countPerModel = opts.count
|
|
70
72
|
? parsePositiveInt(opts.count, "count")
|
|
71
73
|
: 1;
|
|
@@ -90,6 +92,57 @@ export function registerImageCommand(program: Command) {
|
|
|
90
92
|
jobs,
|
|
91
93
|
async (modelId) => {
|
|
92
94
|
const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
|
|
95
|
+
|
|
96
|
+
if (gatewayModels.languageImageModelIds.has(modelId)) {
|
|
97
|
+
const messageContent: Array<
|
|
98
|
+
| { type: "text"; text: string }
|
|
99
|
+
| { type: "image"; image: Uint8Array }
|
|
100
|
+
> = [];
|
|
101
|
+
if (typeof imagePrompt === "string") {
|
|
102
|
+
messageContent.push({ type: "text", text: imagePrompt });
|
|
103
|
+
} else {
|
|
104
|
+
for (const img of imagePrompt.images) {
|
|
105
|
+
messageContent.push({ type: "image", image: img });
|
|
106
|
+
}
|
|
107
|
+
if (imagePrompt.text) {
|
|
108
|
+
messageContent.push({
|
|
109
|
+
type: "text",
|
|
110
|
+
text: imagePrompt.text,
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
messageContent.push({
|
|
114
|
+
type: "text",
|
|
115
|
+
text: "Generate an image",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const creator = gatewayModels.all.find(
|
|
120
|
+
(m) => m.id === modelId
|
|
121
|
+
)?.creator;
|
|
122
|
+
const result = await generateText({
|
|
123
|
+
headers: {
|
|
124
|
+
"http-referer": "https://github.com/vercel-labs/ai-cli",
|
|
125
|
+
"x-title": "ai-cli",
|
|
126
|
+
},
|
|
127
|
+
model: gateway(modelId),
|
|
128
|
+
messages: [{ role: "user", content: messageContent }],
|
|
129
|
+
abortSignal: abort,
|
|
130
|
+
providerOptions:
|
|
131
|
+
creator === "google"
|
|
132
|
+
? { google: { responseModalities: ["IMAGE", "TEXT"] } }
|
|
133
|
+
: undefined,
|
|
134
|
+
});
|
|
135
|
+
const imageFile = result.files?.find((f) =>
|
|
136
|
+
f.mediaType.startsWith("image/")
|
|
137
|
+
);
|
|
138
|
+
if (!imageFile) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Model ${modelId} did not return an image in the response`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return Buffer.from(imageFile.uint8Array);
|
|
144
|
+
}
|
|
145
|
+
|
|
93
146
|
const result = await generateImage({
|
|
94
147
|
headers: {
|
|
95
148
|
"http-referer": "https://github.com/vercel-labs/ai-cli",
|
package/src/commands/models.ts
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
fetchGatewayModels,
|
|
5
|
+
type Modality,
|
|
6
|
+
type ModelEntry,
|
|
7
|
+
} from "../lib/models.js";
|
|
4
8
|
|
|
5
|
-
function
|
|
9
|
+
function groupByCreator(models: ModelEntry[]): Map<string, ModelEntry[]> {
|
|
6
10
|
const groups = new Map<string, ModelEntry[]>();
|
|
7
11
|
for (const m of models) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
if (!groups.has(provider)) groups.set(provider, []);
|
|
11
|
-
groups.get(provider)!.push(m);
|
|
12
|
+
if (!groups.has(m.creator)) groups.set(m.creator, []);
|
|
13
|
+
groups.get(m.creator)!.push(m);
|
|
12
14
|
}
|
|
13
15
|
return new Map(
|
|
14
16
|
[...groups.entries()].sort((a, b) => a[0].localeCompare(b[0]))
|
|
@@ -25,61 +27,42 @@ export function registerModelsCommand(program: Command) {
|
|
|
25
27
|
.command("models")
|
|
26
28
|
.description("List available models from AI Gateway")
|
|
27
29
|
.option("--type <type>", "Filter by type: text, image, video")
|
|
28
|
-
.option("--
|
|
30
|
+
.option("--creator <name>", "Filter by creator (e.g. openai, google)")
|
|
29
31
|
.option("--json", "Output as JSON (includes descriptions)")
|
|
30
32
|
.action(
|
|
31
|
-
async (opts: { type?: string;
|
|
33
|
+
async (opts: { type?: string; creator?: string; json?: boolean }) => {
|
|
32
34
|
const validTypes = ["text", "image", "video"];
|
|
33
|
-
const filterType = opts.type?.toLowerCase();
|
|
35
|
+
const filterType = opts.type?.toLowerCase() as Modality | undefined;
|
|
34
36
|
if (filterType && !validTypes.includes(filterType)) {
|
|
35
37
|
process.stderr.write(
|
|
36
38
|
`Error: --type must be one of: ${validTypes.join(", ")} (got "${opts.type}")\n`
|
|
37
39
|
);
|
|
38
40
|
process.exit(1);
|
|
39
41
|
}
|
|
40
|
-
const
|
|
42
|
+
const filterCreator = opts.creator?.toLowerCase();
|
|
41
43
|
|
|
42
44
|
const gatewayModels = await fetchGatewayModels();
|
|
43
45
|
|
|
44
|
-
const filterGrouped = (grouped: Map<string, ModelEntry[]>) => {
|
|
45
|
-
if (!filterProvider) return grouped;
|
|
46
|
-
const filtered = new Map<string, ModelEntry[]>();
|
|
47
|
-
for (const [provider, models] of grouped) {
|
|
48
|
-
if (provider.toLowerCase() === filterProvider) {
|
|
49
|
-
filtered.set(provider, models);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return filtered;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
46
|
if (opts.json) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
...(m.description ? { description: m.description } : {}),
|
|
61
|
-
});
|
|
62
|
-
if (!filterType || filterType === "text") {
|
|
63
|
-
output.text = Object.fromEntries(
|
|
64
|
-
[...filterGrouped(groupByProvider(gatewayModels.text))].map(
|
|
65
|
-
([provider, models]) => [provider, models.map(jsonMapper)]
|
|
66
|
-
)
|
|
47
|
+
let entries = gatewayModels.all;
|
|
48
|
+
if (filterType) {
|
|
49
|
+
entries = entries.filter((m) =>
|
|
50
|
+
m.capabilities.includes(filterType)
|
|
67
51
|
);
|
|
68
52
|
}
|
|
69
|
-
if (
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
([provider, models]) => [provider, models.map(jsonMapper)]
|
|
73
|
-
)
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
if (!filterType || filterType === "video") {
|
|
77
|
-
output.video = Object.fromEntries(
|
|
78
|
-
[...filterGrouped(groupByProvider(gatewayModels.video))].map(
|
|
79
|
-
([provider, models]) => [provider, models.map(jsonMapper)]
|
|
80
|
-
)
|
|
53
|
+
if (filterCreator) {
|
|
54
|
+
entries = entries.filter(
|
|
55
|
+
(m) => m.creator.toLowerCase() === filterCreator
|
|
81
56
|
);
|
|
82
57
|
}
|
|
58
|
+
const output = entries.map((m) => ({
|
|
59
|
+
id: m.id,
|
|
60
|
+
...(m.name ? { name: m.name } : {}),
|
|
61
|
+
...(m.description ? { description: m.description } : {}),
|
|
62
|
+
creator: m.creator,
|
|
63
|
+
capabilities: m.capabilities,
|
|
64
|
+
...(m.pricing ? { pricing: m.pricing } : {}),
|
|
65
|
+
}));
|
|
83
66
|
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
84
67
|
return;
|
|
85
68
|
}
|
|
@@ -94,13 +77,19 @@ export function registerModelsCommand(program: Command) {
|
|
|
94
77
|
|
|
95
78
|
let totalCount = 0;
|
|
96
79
|
for (const section of sections) {
|
|
97
|
-
|
|
80
|
+
let entries = section.entries;
|
|
81
|
+
if (filterCreator) {
|
|
82
|
+
entries = entries.filter(
|
|
83
|
+
(m) => m.creator.toLowerCase() === filterCreator
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
const grouped = groupByCreator(entries);
|
|
98
87
|
const count = [...grouped.values()].reduce((s, m) => s + m.length, 0);
|
|
99
88
|
if (count === 0) continue;
|
|
100
89
|
totalCount += count;
|
|
101
90
|
process.stdout.write(`\n${section.title} models (${count}):\n`);
|
|
102
|
-
for (const [
|
|
103
|
-
process.stdout.write(`\n ${
|
|
91
|
+
for (const [creator, models] of grouped) {
|
|
92
|
+
process.stdout.write(`\n ${creator}\n`);
|
|
104
93
|
for (const m of models) {
|
|
105
94
|
process.stdout.write(` ${modelName(m.id)}\n`);
|
|
106
95
|
}
|
package/src/commands/text.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { generateText, gateway } from "ai";
|
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
|
|
4
4
|
import { buildJobs, runJobs } from "../lib/jobs.js";
|
|
5
|
-
import { resolveModels } from "../lib/models.js";
|
|
5
|
+
import { fetchGatewayModels, resolveModels } from "../lib/models.js";
|
|
6
6
|
import type { OutputFormat } from "../lib/output.js";
|
|
7
7
|
import { parsePositiveInt, parseTemperature } from "../lib/parse.js";
|
|
8
8
|
import { readStdin, stdinAsText } from "../lib/stdin.js";
|
|
@@ -69,7 +69,8 @@ export function registerTextCommand(program: Command) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
const format = resolveFormat(opts.format);
|
|
72
|
-
const
|
|
72
|
+
const gatewayModels = await fetchGatewayModels();
|
|
73
|
+
const models = resolveModels("text", opts.model, gatewayModels.text);
|
|
73
74
|
const countPerModel = opts.count
|
|
74
75
|
? parsePositiveInt(opts.count, "count")
|
|
75
76
|
: 1;
|
package/src/commands/video.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { experimental_generateVideo as generateVideo, gateway } from "ai";
|
|
|
2
2
|
import type { Command } from "commander";
|
|
3
3
|
|
|
4
4
|
import { buildJobs, runJobs } from "../lib/jobs.js";
|
|
5
|
-
import { resolveModels } from "../lib/models.js";
|
|
5
|
+
import { fetchGatewayModels, resolveModels } from "../lib/models.js";
|
|
6
6
|
import {
|
|
7
7
|
parsePositiveInt,
|
|
8
8
|
parseAspectRatio,
|
|
@@ -65,7 +65,8 @@ export function registerVideoCommand(program: Command) {
|
|
|
65
65
|
: { image: new Uint8Array(stdin) };
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const
|
|
68
|
+
const gatewayModels = await fetchGatewayModels();
|
|
69
|
+
const models = resolveModels("video", opts.model, gatewayModels.video);
|
|
69
70
|
const countPerModel = opts.count
|
|
70
71
|
? parsePositiveInt(opts.count, "count")
|
|
71
72
|
: 1;
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
|
|
4
4
|
import pkg from "../package.json";
|
|
5
|
-
import { registerCompletionsCommand } from "./commands/completions.js";
|
|
6
5
|
import { registerImageCommand } from "./commands/image.js";
|
|
7
6
|
import { registerModelsCommand } from "./commands/models.js";
|
|
8
7
|
import { registerTextCommand } from "./commands/text.js";
|
|
@@ -21,7 +20,6 @@ registerTextCommand(program);
|
|
|
21
20
|
registerImageCommand(program);
|
|
22
21
|
registerVideoCommand(program);
|
|
23
22
|
registerModelsCommand(program);
|
|
24
|
-
registerCompletionsCommand(program);
|
|
25
23
|
|
|
26
24
|
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
27
25
|
const msg = err instanceof Error ? err.message : String(err);
|