ai-cli 0.1.1 → 0.2.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.
- package/README.md +30 -3
- package/dist/index.js +30027 -0
- package/{src/lib/openh264.wasm → dist/openh264-d6yed0d8.wasm} +0 -0
- package/package.json +9 -5
- package/src/cli.test.ts +0 -95
- package/src/commands/completions.ts +0 -296
- package/src/commands/image.ts +0 -136
- package/src/commands/models.ts +0 -117
- package/src/commands/text.ts +0 -117
- package/src/commands/video.ts +0 -113
- package/src/index.ts +0 -30
- package/src/lib/color.ts +0 -5
- package/src/lib/h264-wasm.ts +0 -164
- package/src/lib/h264.test.ts +0 -48
- package/src/lib/jobs.ts +0 -192
- package/src/lib/kitty.ts +0 -55
- package/src/lib/models.test.ts +0 -197
- package/src/lib/models.ts +0 -163
- package/src/lib/mp4.test.ts +0 -231
- package/src/lib/mp4.ts +0 -560
- package/src/lib/openh264.d.mts +0 -28
- package/src/lib/openh264.mjs +0 -423
- package/src/lib/openh264.wasm.d.ts +0 -2
- package/src/lib/output.ts +0 -97
- package/src/lib/p-map.test.ts +0 -63
- package/src/lib/p-map.ts +0 -30
- package/src/lib/parse.test.ts +0 -114
- package/src/lib/parse.ts +0 -44
- package/src/lib/png.test.ts +0 -104
- package/src/lib/png.ts +0 -90
- package/src/lib/progress.ts +0 -214
- package/src/lib/shimmer.test.ts +0 -39
- package/src/lib/shimmer.ts +0 -42
- package/src/lib/stdin.ts +0 -31
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/vercel-labs/ai-cli.git"
|
|
10
10
|
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=20"
|
|
13
|
+
},
|
|
11
14
|
"bin": {
|
|
12
|
-
"ai": "./
|
|
15
|
+
"ai": "./dist/index.js"
|
|
13
16
|
},
|
|
14
17
|
"files": [
|
|
15
|
-
"
|
|
18
|
+
"dist",
|
|
16
19
|
"README.md"
|
|
17
20
|
],
|
|
18
21
|
"keywords": [
|
|
@@ -27,13 +30,14 @@
|
|
|
27
30
|
],
|
|
28
31
|
"scripts": {
|
|
29
32
|
"dev": "bun run src/index.ts",
|
|
30
|
-
"
|
|
33
|
+
"clean": "bun -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
|
34
|
+
"build": "bun run clean && bun build src/index.ts --target=node --outdir=dist",
|
|
31
35
|
"typecheck": "tsc --noEmit",
|
|
32
36
|
"format": "oxfmt --write src/",
|
|
33
37
|
"format:check": "oxfmt --check src/",
|
|
34
38
|
"lint": "oxlint src/",
|
|
35
39
|
"test": "bun test",
|
|
36
|
-
"
|
|
40
|
+
"prepack": "bun run typecheck && bun run build"
|
|
37
41
|
},
|
|
38
42
|
"dependencies": {
|
|
39
43
|
"ai": "^6.0.173",
|
package/src/cli.test.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
const CLI = ["bun", "run", "src/index.ts"];
|
|
4
|
-
const ROOT = import.meta.dir + "/..";
|
|
5
|
-
|
|
6
|
-
async function run(...args: string[]) {
|
|
7
|
-
const proc = Bun.spawn([...CLI, ...args], {
|
|
8
|
-
cwd: ROOT,
|
|
9
|
-
stdout: "pipe",
|
|
10
|
-
stderr: "pipe",
|
|
11
|
-
stdin: "ignore",
|
|
12
|
-
});
|
|
13
|
-
const [stdout, stderr] = await Promise.all([
|
|
14
|
-
new Response(proc.stdout).text(),
|
|
15
|
-
new Response(proc.stderr).text(),
|
|
16
|
-
]);
|
|
17
|
-
const exitCode = await proc.exited;
|
|
18
|
-
return { exitCode, stdout, stderr };
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe("cli integration", () => {
|
|
22
|
-
test("--help exits 0 and lists subcommands", async () => {
|
|
23
|
-
const { exitCode, stdout } = await run("--help");
|
|
24
|
-
expect(exitCode).toBe(0);
|
|
25
|
-
for (const sub of ["text", "image", "video", "models", "completions"]) {
|
|
26
|
-
expect(stdout).toContain(sub);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("--version exits 0 and prints semver", async () => {
|
|
31
|
-
const { exitCode, stdout } = await run("--version");
|
|
32
|
-
expect(exitCode).toBe(0);
|
|
33
|
-
expect(stdout.trim()).toMatch(/^\d+\.\d+\.\d+/);
|
|
34
|
-
});
|
|
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
|
-
test("text with no prompt and no stdin exits 1", async () => {
|
|
62
|
-
const { exitCode, stderr } = await run("text");
|
|
63
|
-
expect(exitCode).toBe(1);
|
|
64
|
-
expect(stderr).toContain("prompt is required");
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("text --help exits 0 and lists flags", async () => {
|
|
68
|
-
const { exitCode, stdout } = await run("text", "--help");
|
|
69
|
-
expect(exitCode).toBe(0);
|
|
70
|
-
expect(stdout).toContain("--model");
|
|
71
|
-
expect(stdout).toContain("--format");
|
|
72
|
-
expect(stdout).toContain("--temperature");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("image --help exits 0 and lists flags", async () => {
|
|
76
|
-
const { exitCode, stdout } = await run("image", "--help");
|
|
77
|
-
expect(exitCode).toBe(0);
|
|
78
|
-
expect(stdout).toContain("--no-preview");
|
|
79
|
-
expect(stdout).toContain("--size");
|
|
80
|
-
expect(stdout).toContain("--aspect-ratio");
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
test("video --help exits 0 and lists flags", async () => {
|
|
84
|
-
const { exitCode, stdout } = await run("video", "--help");
|
|
85
|
-
expect(exitCode).toBe(0);
|
|
86
|
-
expect(stdout).toContain("--duration");
|
|
87
|
-
expect(stdout).toContain("--aspect-ratio");
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("models --type invalid exits 1", async () => {
|
|
91
|
-
const { exitCode, stderr } = await run("models", "--type", "audio");
|
|
92
|
-
expect(exitCode).toBe(1);
|
|
93
|
-
expect(stderr).toContain("must be one of");
|
|
94
|
-
});
|
|
95
|
-
});
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
FALLBACK_TEXT_MODELS,
|
|
5
|
-
FALLBACK_IMAGE_MODELS,
|
|
6
|
-
FALLBACK_VIDEO_MODELS,
|
|
7
|
-
} from "../lib/models.js";
|
|
8
|
-
|
|
9
|
-
export function registerCompletionsCommand(program: Command) {
|
|
10
|
-
program
|
|
11
|
-
.command("completions")
|
|
12
|
-
.description("Output shell completion script")
|
|
13
|
-
.argument("<shell>", "Shell type: zsh, bash, fish")
|
|
14
|
-
.action((shell: string) => {
|
|
15
|
-
switch (shell.toLowerCase()) {
|
|
16
|
-
case "zsh":
|
|
17
|
-
process.stdout.write(generateZsh());
|
|
18
|
-
break;
|
|
19
|
-
case "bash":
|
|
20
|
-
process.stdout.write(generateBash());
|
|
21
|
-
break;
|
|
22
|
-
case "fish":
|
|
23
|
-
process.stdout.write(generateFish());
|
|
24
|
-
break;
|
|
25
|
-
default:
|
|
26
|
-
process.stderr.write(
|
|
27
|
-
`Unknown shell: ${shell}. Supported: zsh, bash, fish\n`
|
|
28
|
-
);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const SUBCOMMANDS = ["text", "image", "video", "models", "completions", "help"];
|
|
35
|
-
const GLOBAL_FLAGS = [
|
|
36
|
-
"--model",
|
|
37
|
-
"--output",
|
|
38
|
-
"--count",
|
|
39
|
-
"--concurrency",
|
|
40
|
-
"--quiet",
|
|
41
|
-
"--json",
|
|
42
|
-
"--help",
|
|
43
|
-
"--version",
|
|
44
|
-
];
|
|
45
|
-
const TEXT_FLAGS = ["--format", "--system", "--max-tokens", "--temperature"];
|
|
46
|
-
const IMAGE_FLAGS = [
|
|
47
|
-
"--size",
|
|
48
|
-
"--aspect-ratio",
|
|
49
|
-
"--quality",
|
|
50
|
-
"--style",
|
|
51
|
-
"--no-preview",
|
|
52
|
-
];
|
|
53
|
-
const VIDEO_FLAGS = ["--aspect-ratio", "--duration", "--no-preview"];
|
|
54
|
-
const MODEL_FLAGS = ["--type", "--provider", "--json", "--help"];
|
|
55
|
-
|
|
56
|
-
const ALL_MODELS = [
|
|
57
|
-
...FALLBACK_TEXT_MODELS,
|
|
58
|
-
...FALLBACK_IMAGE_MODELS,
|
|
59
|
-
...FALLBACK_VIDEO_MODELS,
|
|
60
|
-
];
|
|
61
|
-
const MODEL_NAMES = ALL_MODELS.map((m) => m.slice(m.indexOf("/") + 1));
|
|
62
|
-
|
|
63
|
-
function generateZsh(): string {
|
|
64
|
-
return `#compdef ai
|
|
65
|
-
|
|
66
|
-
_ai() {
|
|
67
|
-
local -a subcommands
|
|
68
|
-
subcommands=(
|
|
69
|
-
'text:Generate text from a prompt'
|
|
70
|
-
'image:Generate an image from a prompt'
|
|
71
|
-
'video:Generate a video from a prompt'
|
|
72
|
-
'models:List available models'
|
|
73
|
-
'completions:Output shell completion script'
|
|
74
|
-
'help:Display help'
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
local -a models
|
|
78
|
-
models=(${ALL_MODELS.join(" ")})
|
|
79
|
-
|
|
80
|
-
local -a model_names
|
|
81
|
-
model_names=(${MODEL_NAMES.join(" ")})
|
|
82
|
-
|
|
83
|
-
_arguments -C \\
|
|
84
|
-
'1:command:->cmd' \\
|
|
85
|
-
'*::arg:->args'
|
|
86
|
-
|
|
87
|
-
case $state in
|
|
88
|
-
cmd)
|
|
89
|
-
_describe 'command' subcommands
|
|
90
|
-
;;
|
|
91
|
-
args)
|
|
92
|
-
case $words[1] in
|
|
93
|
-
text)
|
|
94
|
-
_arguments \\
|
|
95
|
-
'-m[Model ID]:model:($models $model_names)' \\
|
|
96
|
-
'--model[Model ID]:model:($models $model_names)' \\
|
|
97
|
-
'-o[Output path]:file:_files' \\
|
|
98
|
-
'--output[Output path]:file:_files' \\
|
|
99
|
-
'-f[Format]:format:(md txt)' \\
|
|
100
|
-
'--format[Format]:format:(md txt)' \\
|
|
101
|
-
'-n[Count]:count:' \\
|
|
102
|
-
'--count[Count]:count:' \\
|
|
103
|
-
'-p[Concurrency]:concurrency:' \\
|
|
104
|
-
'--concurrency[Concurrency]:concurrency:' \\
|
|
105
|
-
'-s[System prompt]:system:' \\
|
|
106
|
-
'--system[System prompt]:system:' \\
|
|
107
|
-
'--max-tokens[Max tokens]:tokens:' \\
|
|
108
|
-
'-t[Temperature]:temp:' \\
|
|
109
|
-
'--temperature[Temperature]:temp:' \\
|
|
110
|
-
'-q[Quiet]' \\
|
|
111
|
-
'--quiet[Quiet]' \\
|
|
112
|
-
'--json[JSON output]' \\
|
|
113
|
-
'*:prompt:'
|
|
114
|
-
;;
|
|
115
|
-
image)
|
|
116
|
-
_arguments \\
|
|
117
|
-
'-m[Model ID]:model:($models $model_names)' \\
|
|
118
|
-
'--model[Model ID]:model:($models $model_names)' \\
|
|
119
|
-
'-o[Output path]:file:_files' \\
|
|
120
|
-
'--output[Output path]:file:_files' \\
|
|
121
|
-
'-n[Count]:count:' \\
|
|
122
|
-
'--count[Count]:count:' \\
|
|
123
|
-
'-p[Concurrency]:concurrency:' \\
|
|
124
|
-
'--concurrency[Concurrency]:concurrency:' \\
|
|
125
|
-
'--size[Size]:size:' \\
|
|
126
|
-
'--aspect-ratio[Aspect ratio]:ratio:' \\
|
|
127
|
-
'--quality[Quality]:quality:(standard hd)' \\
|
|
128
|
-
'--style[Style]:style:(vivid natural)' \\
|
|
129
|
-
'--no-preview[Disable inline image preview]' \\
|
|
130
|
-
'-q[Quiet]' \\
|
|
131
|
-
'--quiet[Quiet]' \\
|
|
132
|
-
'--json[JSON output]' \\
|
|
133
|
-
'*:prompt:'
|
|
134
|
-
;;
|
|
135
|
-
video)
|
|
136
|
-
_arguments \\
|
|
137
|
-
'-m[Model ID]:model:($models $model_names)' \\
|
|
138
|
-
'--model[Model ID]:model:($models $model_names)' \\
|
|
139
|
-
'-o[Output path]:file:_files' \\
|
|
140
|
-
'--output[Output path]:file:_files' \\
|
|
141
|
-
'-n[Count]:count:' \\
|
|
142
|
-
'--count[Count]:count:' \\
|
|
143
|
-
'-p[Concurrency]:concurrency:' \\
|
|
144
|
-
'--concurrency[Concurrency]:concurrency:' \\
|
|
145
|
-
'--aspect-ratio[Aspect ratio]:ratio:' \\
|
|
146
|
-
'--duration[Duration]:seconds:' \\
|
|
147
|
-
'--no-preview[Disable inline video frame preview]' \\
|
|
148
|
-
'-q[Quiet]' \\
|
|
149
|
-
'--quiet[Quiet]' \\
|
|
150
|
-
'--json[JSON output]' \\
|
|
151
|
-
'*:prompt:'
|
|
152
|
-
;;
|
|
153
|
-
models)
|
|
154
|
-
_arguments \\
|
|
155
|
-
'--type[Filter by type]:type:(text image video)' \\
|
|
156
|
-
'--provider[Filter by provider]:provider:' \\
|
|
157
|
-
'--json[JSON output]'
|
|
158
|
-
;;
|
|
159
|
-
completions)
|
|
160
|
-
_arguments '1:shell:(zsh bash fish)'
|
|
161
|
-
;;
|
|
162
|
-
esac
|
|
163
|
-
;;
|
|
164
|
-
esac
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
_ai "$@"
|
|
168
|
-
`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function generateBash(): string {
|
|
172
|
-
return `_ai_completions() {
|
|
173
|
-
local cur prev subcmd
|
|
174
|
-
COMPREPLY=()
|
|
175
|
-
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
176
|
-
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
177
|
-
subcmd="\${COMP_WORDS[1]}"
|
|
178
|
-
|
|
179
|
-
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
180
|
-
COMPREPLY=($(compgen -W "${SUBCOMMANDS.join(" ")}" -- "$cur"))
|
|
181
|
-
return
|
|
182
|
-
fi
|
|
183
|
-
|
|
184
|
-
case "$prev" in
|
|
185
|
-
-m|--model)
|
|
186
|
-
COMPREPLY=($(compgen -W "${ALL_MODELS.join(" ")} ${MODEL_NAMES.join(" ")}" -- "$cur"))
|
|
187
|
-
return
|
|
188
|
-
;;
|
|
189
|
-
-o|--output)
|
|
190
|
-
COMPREPLY=($(compgen -f -- "$cur"))
|
|
191
|
-
return
|
|
192
|
-
;;
|
|
193
|
-
-f|--format)
|
|
194
|
-
COMPREPLY=($(compgen -W "md txt" -- "$cur"))
|
|
195
|
-
return
|
|
196
|
-
;;
|
|
197
|
-
--quality)
|
|
198
|
-
COMPREPLY=($(compgen -W "standard hd" -- "$cur"))
|
|
199
|
-
return
|
|
200
|
-
;;
|
|
201
|
-
--style)
|
|
202
|
-
COMPREPLY=($(compgen -W "vivid natural" -- "$cur"))
|
|
203
|
-
return
|
|
204
|
-
;;
|
|
205
|
-
--type)
|
|
206
|
-
COMPREPLY=($(compgen -W "text image video" -- "$cur"))
|
|
207
|
-
return
|
|
208
|
-
;;
|
|
209
|
-
esac
|
|
210
|
-
|
|
211
|
-
case "$subcmd" in
|
|
212
|
-
text)
|
|
213
|
-
COMPREPLY=($(compgen -W "${[...GLOBAL_FLAGS, ...TEXT_FLAGS].join(" ")}" -- "$cur"))
|
|
214
|
-
;;
|
|
215
|
-
image)
|
|
216
|
-
COMPREPLY=($(compgen -W "${[...GLOBAL_FLAGS, ...IMAGE_FLAGS].join(" ")}" -- "$cur"))
|
|
217
|
-
;;
|
|
218
|
-
video)
|
|
219
|
-
COMPREPLY=($(compgen -W "${[...GLOBAL_FLAGS, ...VIDEO_FLAGS].join(" ")}" -- "$cur"))
|
|
220
|
-
;;
|
|
221
|
-
models)
|
|
222
|
-
COMPREPLY=($(compgen -W "${MODEL_FLAGS.join(" ")}" -- "$cur"))
|
|
223
|
-
;;
|
|
224
|
-
completions)
|
|
225
|
-
COMPREPLY=($(compgen -W "zsh bash fish" -- "$cur"))
|
|
226
|
-
;;
|
|
227
|
-
esac
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
complete -F _ai_completions ai
|
|
231
|
-
`;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function generateFish(): string {
|
|
235
|
-
const lines: string[] = [];
|
|
236
|
-
lines.push("# ai completions for fish");
|
|
237
|
-
lines.push("");
|
|
238
|
-
|
|
239
|
-
for (const sub of SUBCOMMANDS) {
|
|
240
|
-
lines.push(`complete -c ai -n '__fish_use_subcommand' -a '${sub}'`);
|
|
241
|
-
}
|
|
242
|
-
lines.push("");
|
|
243
|
-
|
|
244
|
-
const SHORT_FLAG_MAP: Record<string, string> = {
|
|
245
|
-
"--model": "m",
|
|
246
|
-
"--output": "o",
|
|
247
|
-
"--count": "n",
|
|
248
|
-
"--concurrency": "p",
|
|
249
|
-
"--quiet": "q",
|
|
250
|
-
"--format": "f",
|
|
251
|
-
"--system": "s",
|
|
252
|
-
"--temperature": "t",
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
const addFlags = (sub: string, flags: string[]) => {
|
|
256
|
-
for (const flag of flags) {
|
|
257
|
-
const name = flag.replace(/^--/, "");
|
|
258
|
-
const short = SHORT_FLAG_MAP[flag];
|
|
259
|
-
const shortPart = short ? ` -s ${short}` : "";
|
|
260
|
-
lines.push(
|
|
261
|
-
`complete -c ai -n '__fish_seen_subcommand_from ${sub}'${shortPart} -l '${name}'`
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
addFlags("text", [...GLOBAL_FLAGS, ...TEXT_FLAGS]);
|
|
267
|
-
addFlags("image", [...GLOBAL_FLAGS, ...IMAGE_FLAGS]);
|
|
268
|
-
addFlags("video", [...GLOBAL_FLAGS, ...VIDEO_FLAGS]);
|
|
269
|
-
addFlags("models", MODEL_FLAGS);
|
|
270
|
-
|
|
271
|
-
lines.push("");
|
|
272
|
-
lines.push(
|
|
273
|
-
`complete -c ai -n '__fish_seen_subcommand_from completions' -a 'zsh bash fish'`
|
|
274
|
-
);
|
|
275
|
-
lines.push("");
|
|
276
|
-
|
|
277
|
-
const modelCompletions = ALL_MODELS.concat(MODEL_NAMES);
|
|
278
|
-
lines.push(
|
|
279
|
-
`complete -c ai -n '__fish_seen_subcommand_from text image video' -s m -l model -a '${modelCompletions.join(" ")}'`
|
|
280
|
-
);
|
|
281
|
-
lines.push(
|
|
282
|
-
`complete -c ai -n '__fish_seen_subcommand_from text' -s f -l format -a 'md txt'`
|
|
283
|
-
);
|
|
284
|
-
lines.push(
|
|
285
|
-
`complete -c ai -n '__fish_seen_subcommand_from image' -l quality -a 'standard hd'`
|
|
286
|
-
);
|
|
287
|
-
lines.push(
|
|
288
|
-
`complete -c ai -n '__fish_seen_subcommand_from image' -l style -a 'vivid natural'`
|
|
289
|
-
);
|
|
290
|
-
lines.push(
|
|
291
|
-
`complete -c ai -n '__fish_seen_subcommand_from models' -l type -a 'text image video'`
|
|
292
|
-
);
|
|
293
|
-
lines.push("");
|
|
294
|
-
|
|
295
|
-
return lines.join("\n");
|
|
296
|
-
}
|
package/src/commands/image.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { generateImage, gateway } from "ai";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
|
|
4
|
-
import { buildJobs, runJobs } from "../lib/jobs.js";
|
|
5
|
-
import { resolveModels } from "../lib/models.js";
|
|
6
|
-
import { parsePositiveInt, parseSize, parseAspectRatio } from "../lib/parse.js";
|
|
7
|
-
import { readStdin } from "../lib/stdin.js";
|
|
8
|
-
|
|
9
|
-
const DEFAULT_CONCURRENCY = 4;
|
|
10
|
-
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
11
|
-
|
|
12
|
-
interface ImageOptions {
|
|
13
|
-
model?: string;
|
|
14
|
-
output?: string;
|
|
15
|
-
count?: string;
|
|
16
|
-
size?: string;
|
|
17
|
-
aspectRatio?: string;
|
|
18
|
-
quality?: string;
|
|
19
|
-
style?: string;
|
|
20
|
-
quiet?: boolean;
|
|
21
|
-
json?: boolean;
|
|
22
|
-
concurrency?: string;
|
|
23
|
-
preview?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function registerImageCommand(program: Command) {
|
|
27
|
-
program
|
|
28
|
-
.command("image")
|
|
29
|
-
.description("Generate an image from a prompt")
|
|
30
|
-
.argument("[prompt]", "The prompt to generate an image from")
|
|
31
|
-
.option(
|
|
32
|
-
"-m, --model <model>",
|
|
33
|
-
"Model ID (creator/model-name), comma-separated for multi-model"
|
|
34
|
-
)
|
|
35
|
-
.option("-o, --output <path>", "Output file path or directory")
|
|
36
|
-
.option("-n, --count <n>", "Number of images per model (default: 1)")
|
|
37
|
-
.option("--size <WxH>", "Image size (e.g. 1024x1024)")
|
|
38
|
-
.option("--aspect-ratio <W:H>", "Aspect ratio (e.g. 16:9)")
|
|
39
|
-
.option("--quality <level>", "Quality (standard, hd)")
|
|
40
|
-
.option("--style <style>", "Style (e.g. vivid, natural)")
|
|
41
|
-
.option("-q, --quiet", "Suppress progress output")
|
|
42
|
-
.option("--json", "Output metadata as JSON")
|
|
43
|
-
.option(
|
|
44
|
-
"--no-preview",
|
|
45
|
-
"Disable inline image preview in supported terminals"
|
|
46
|
-
)
|
|
47
|
-
.option(
|
|
48
|
-
"-p, --concurrency <n>",
|
|
49
|
-
`Max parallel generations (default: ${DEFAULT_CONCURRENCY})`
|
|
50
|
-
)
|
|
51
|
-
.action(async (rawPrompt: string | undefined, opts: ImageOptions) => {
|
|
52
|
-
const prompt = rawPrompt?.trim() || undefined;
|
|
53
|
-
const stdin = await readStdin();
|
|
54
|
-
if (!prompt && !stdin) {
|
|
55
|
-
process.stderr.write(
|
|
56
|
-
"Error: prompt is required (provide as argument or pipe via stdin)\n"
|
|
57
|
-
);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
let imagePrompt: string | { images: Uint8Array[]; text?: string } =
|
|
61
|
-
prompt!;
|
|
62
|
-
if (stdin) {
|
|
63
|
-
imagePrompt = prompt
|
|
64
|
-
? { images: [new Uint8Array(stdin)], text: prompt }
|
|
65
|
-
: { images: [new Uint8Array(stdin)] };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const models = resolveModels("image", opts.model);
|
|
69
|
-
const countPerModel = opts.count
|
|
70
|
-
? parsePositiveInt(opts.count, "count")
|
|
71
|
-
: 1;
|
|
72
|
-
const size = opts.size ? parseSize(opts.size) : undefined;
|
|
73
|
-
const aspectRatio = opts.aspectRatio
|
|
74
|
-
? parseAspectRatio(opts.aspectRatio)
|
|
75
|
-
: undefined;
|
|
76
|
-
const provOpts = buildProviderOptions(opts);
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
(opts.quality || opts.style) &&
|
|
80
|
-
models.every((m) => !m.startsWith("openai/"))
|
|
81
|
-
) {
|
|
82
|
-
process.stderr.write(
|
|
83
|
-
"Warning: --quality and --style only apply to OpenAI models\n"
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const jobs = buildJobs(models, countPerModel);
|
|
88
|
-
|
|
89
|
-
const { total, failed } = await runJobs(
|
|
90
|
-
jobs,
|
|
91
|
-
async (modelId) => {
|
|
92
|
-
const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
|
|
93
|
-
const result = await generateImage({
|
|
94
|
-
headers: {
|
|
95
|
-
"http-referer": "https://github.com/vercel-labs/ai-cli",
|
|
96
|
-
"x-title": "ai-cli",
|
|
97
|
-
},
|
|
98
|
-
model: gateway.image(modelId),
|
|
99
|
-
prompt: imagePrompt,
|
|
100
|
-
abortSignal: abort,
|
|
101
|
-
n: 1,
|
|
102
|
-
size,
|
|
103
|
-
aspectRatio,
|
|
104
|
-
providerOptions:
|
|
105
|
-
Object.keys(provOpts).length > 0 ? provOpts : undefined,
|
|
106
|
-
});
|
|
107
|
-
return Buffer.from(result.image.uint8Array);
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
noun: "image",
|
|
111
|
-
format: "image",
|
|
112
|
-
outputPath: opts.output,
|
|
113
|
-
quiet: opts.quiet,
|
|
114
|
-
json: opts.json,
|
|
115
|
-
display: opts.preview,
|
|
116
|
-
concurrency: opts.concurrency
|
|
117
|
-
? parsePositiveInt(opts.concurrency, "concurrency")
|
|
118
|
-
: DEFAULT_CONCURRENCY,
|
|
119
|
-
}
|
|
120
|
-
);
|
|
121
|
-
if (failed === total) process.exit(1);
|
|
122
|
-
if (failed > 0) process.exit(2);
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function buildProviderOptions(
|
|
127
|
-
opts: ImageOptions
|
|
128
|
-
): Record<string, Record<string, string>> {
|
|
129
|
-
const providerOptions: Record<string, Record<string, string>> = {};
|
|
130
|
-
if (opts.quality || opts.style) {
|
|
131
|
-
providerOptions.openai = {};
|
|
132
|
-
if (opts.quality) providerOptions.openai.quality = opts.quality;
|
|
133
|
-
if (opts.style) providerOptions.openai.style = opts.style;
|
|
134
|
-
}
|
|
135
|
-
return providerOptions;
|
|
136
|
-
}
|
package/src/commands/models.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
|
|
3
|
-
import { fetchGatewayModels, type ModelEntry } from "../lib/models.js";
|
|
4
|
-
|
|
5
|
-
function groupByProvider(models: ModelEntry[]): Map<string, ModelEntry[]> {
|
|
6
|
-
const groups = new Map<string, ModelEntry[]>();
|
|
7
|
-
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
|
-
}
|
|
13
|
-
return new Map(
|
|
14
|
-
[...groups.entries()].sort((a, b) => a[0].localeCompare(b[0]))
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function modelName(id: string): string {
|
|
19
|
-
const slash = id.indexOf("/");
|
|
20
|
-
return slash !== -1 ? id.slice(slash + 1) : id;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function registerModelsCommand(program: Command) {
|
|
24
|
-
program
|
|
25
|
-
.command("models")
|
|
26
|
-
.description("List available models from AI Gateway")
|
|
27
|
-
.option("--type <type>", "Filter by type: text, image, video")
|
|
28
|
-
.option("--provider <name>", "Filter by provider (e.g. openai, google)")
|
|
29
|
-
.option("--json", "Output as JSON (includes descriptions)")
|
|
30
|
-
.action(
|
|
31
|
-
async (opts: { type?: string; provider?: string; json?: boolean }) => {
|
|
32
|
-
const validTypes = ["text", "image", "video"];
|
|
33
|
-
const filterType = opts.type?.toLowerCase();
|
|
34
|
-
if (filterType && !validTypes.includes(filterType)) {
|
|
35
|
-
process.stderr.write(
|
|
36
|
-
`Error: --type must be one of: ${validTypes.join(", ")} (got "${opts.type}")\n`
|
|
37
|
-
);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
const filterProvider = opts.provider?.toLowerCase();
|
|
41
|
-
|
|
42
|
-
const gatewayModels = await fetchGatewayModels();
|
|
43
|
-
|
|
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
|
-
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
|
-
)
|
|
67
|
-
);
|
|
68
|
-
}
|
|
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
|
-
)
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const sections: { title: string; entries: ModelEntry[] }[] = [];
|
|
88
|
-
if (!filterType || filterType === "text")
|
|
89
|
-
sections.push({ title: "Text", entries: gatewayModels.text });
|
|
90
|
-
if (!filterType || filterType === "image")
|
|
91
|
-
sections.push({ title: "Image", entries: gatewayModels.image });
|
|
92
|
-
if (!filterType || filterType === "video")
|
|
93
|
-
sections.push({ title: "Video", entries: gatewayModels.video });
|
|
94
|
-
|
|
95
|
-
let totalCount = 0;
|
|
96
|
-
for (const section of sections) {
|
|
97
|
-
const grouped = filterGrouped(groupByProvider(section.entries));
|
|
98
|
-
const count = [...grouped.values()].reduce((s, m) => s + m.length, 0);
|
|
99
|
-
if (count === 0) continue;
|
|
100
|
-
totalCount += count;
|
|
101
|
-
process.stdout.write(`\n${section.title} models (${count}):\n`);
|
|
102
|
-
for (const [provider, models] of grouped) {
|
|
103
|
-
process.stdout.write(`\n ${provider}\n`);
|
|
104
|
-
for (const m of models) {
|
|
105
|
-
process.stdout.write(` ${modelName(m.id)}\n`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (totalCount === 0) {
|
|
111
|
-
process.stderr.write("No models found matching filters\n");
|
|
112
|
-
} else {
|
|
113
|
-
process.stdout.write("\n");
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
);
|
|
117
|
-
}
|