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 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
- --provider <name> Filter by provider (e.g. openai, google)
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. If the gateway is unreachable, all model types fall back to a built-in list.
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.1.1",
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", "completions"]) {
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);
@@ -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 models = resolveModels("image", opts.model);
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",
@@ -1,14 +1,16 @@
1
1
  import type { Command } from "commander";
2
2
 
3
- import { fetchGatewayModels, type ModelEntry } from "../lib/models.js";
3
+ import {
4
+ fetchGatewayModels,
5
+ type Modality,
6
+ type ModelEntry,
7
+ } from "../lib/models.js";
4
8
 
5
- function groupByProvider(models: ModelEntry[]): Map<string, ModelEntry[]> {
9
+ function groupByCreator(models: ModelEntry[]): Map<string, ModelEntry[]> {
6
10
  const groups = new Map<string, ModelEntry[]>();
7
11
  for (const m of models) {
8
- const slash = m.id.indexOf("/");
9
- const provider = slash !== -1 ? m.id.slice(0, slash) : "other";
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("--provider <name>", "Filter by provider (e.g. openai, google)")
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; provider?: string; json?: boolean }) => {
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 filterProvider = opts.provider?.toLowerCase();
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
- const output: Record<string, unknown> = {};
57
- const jsonMapper = (m: ModelEntry) => ({
58
- id: m.id,
59
- ...(m.name ? { name: m.name } : {}),
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 (!filterType || filterType === "image") {
70
- output.image = Object.fromEntries(
71
- [...filterGrouped(groupByProvider(gatewayModels.image))].map(
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
- const grouped = filterGrouped(groupByProvider(section.entries));
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 [provider, models] of grouped) {
103
- process.stdout.write(`\n ${provider}\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
  }
@@ -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 models = resolveModels("text", opts.model);
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;
@@ -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 models = resolveModels("video", opts.model);
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);