ai-cli 0.2.0 → 0.3.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 +23 -2
- package/dist/index.js +30184 -0
- package/{src/lib/openh264.wasm → dist/openh264-d6yed0d8.wasm} +0 -0
- package/package.json +9 -5
- package/src/cli.test.ts +0 -70
- package/src/commands/image.ts +0 -189
- package/src/commands/models.ts +0 -106
- package/src/commands/text.ts +0 -118
- package/src/commands/video.ts +0 -114
- package/src/index.ts +0 -28
- 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 -307
- package/src/lib/models.ts +0 -173
- 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.3.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",
|
|
@@ -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,70 +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"]) {
|
|
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("text with no prompt and no stdin exits 1", async () => {
|
|
37
|
-
const { exitCode, stderr } = await run("text");
|
|
38
|
-
expect(exitCode).toBe(1);
|
|
39
|
-
expect(stderr).toContain("prompt is required");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("text --help exits 0 and lists flags", async () => {
|
|
43
|
-
const { exitCode, stdout } = await run("text", "--help");
|
|
44
|
-
expect(exitCode).toBe(0);
|
|
45
|
-
expect(stdout).toContain("--model");
|
|
46
|
-
expect(stdout).toContain("--format");
|
|
47
|
-
expect(stdout).toContain("--temperature");
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("image --help exits 0 and lists flags", async () => {
|
|
51
|
-
const { exitCode, stdout } = await run("image", "--help");
|
|
52
|
-
expect(exitCode).toBe(0);
|
|
53
|
-
expect(stdout).toContain("--no-preview");
|
|
54
|
-
expect(stdout).toContain("--size");
|
|
55
|
-
expect(stdout).toContain("--aspect-ratio");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("video --help exits 0 and lists flags", async () => {
|
|
59
|
-
const { exitCode, stdout } = await run("video", "--help");
|
|
60
|
-
expect(exitCode).toBe(0);
|
|
61
|
-
expect(stdout).toContain("--duration");
|
|
62
|
-
expect(stdout).toContain("--aspect-ratio");
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test("models --type invalid exits 1", async () => {
|
|
66
|
-
const { exitCode, stderr } = await run("models", "--type", "audio");
|
|
67
|
-
expect(exitCode).toBe(1);
|
|
68
|
-
expect(stderr).toContain("must be one of");
|
|
69
|
-
});
|
|
70
|
-
});
|
package/src/commands/image.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import { generateImage, generateText, gateway } from "ai";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
|
|
4
|
-
import { buildJobs, runJobs } from "../lib/jobs.js";
|
|
5
|
-
import { fetchGatewayModels, 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
|
-
if (stdin) {
|
|
62
|
-
imagePrompt = prompt
|
|
63
|
-
? { images: [new Uint8Array(stdin)], text: prompt }
|
|
64
|
-
: { images: [new Uint8Array(stdin)] };
|
|
65
|
-
} else {
|
|
66
|
-
imagePrompt = prompt!;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const gatewayModels = await fetchGatewayModels();
|
|
70
|
-
const models = resolveModels("image", opts.model, gatewayModels.image);
|
|
71
|
-
const countPerModel = opts.count
|
|
72
|
-
? parsePositiveInt(opts.count, "count")
|
|
73
|
-
: 1;
|
|
74
|
-
const size = opts.size ? parseSize(opts.size) : undefined;
|
|
75
|
-
const aspectRatio = opts.aspectRatio
|
|
76
|
-
? parseAspectRatio(opts.aspectRatio)
|
|
77
|
-
: undefined;
|
|
78
|
-
const provOpts = buildProviderOptions(opts);
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
(opts.quality || opts.style) &&
|
|
82
|
-
models.every((m) => !m.startsWith("openai/"))
|
|
83
|
-
) {
|
|
84
|
-
process.stderr.write(
|
|
85
|
-
"Warning: --quality and --style only apply to OpenAI models\n"
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const jobs = buildJobs(models, countPerModel);
|
|
90
|
-
|
|
91
|
-
const { total, failed } = await runJobs(
|
|
92
|
-
jobs,
|
|
93
|
-
async (modelId) => {
|
|
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
|
-
|
|
146
|
-
const result = await generateImage({
|
|
147
|
-
headers: {
|
|
148
|
-
"http-referer": "https://github.com/vercel-labs/ai-cli",
|
|
149
|
-
"x-title": "ai-cli",
|
|
150
|
-
},
|
|
151
|
-
model: gateway.image(modelId),
|
|
152
|
-
prompt: imagePrompt,
|
|
153
|
-
abortSignal: abort,
|
|
154
|
-
n: 1,
|
|
155
|
-
size,
|
|
156
|
-
aspectRatio,
|
|
157
|
-
providerOptions:
|
|
158
|
-
Object.keys(provOpts).length > 0 ? provOpts : undefined,
|
|
159
|
-
});
|
|
160
|
-
return Buffer.from(result.image.uint8Array);
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
noun: "image",
|
|
164
|
-
format: "image",
|
|
165
|
-
outputPath: opts.output,
|
|
166
|
-
quiet: opts.quiet,
|
|
167
|
-
json: opts.json,
|
|
168
|
-
display: opts.preview,
|
|
169
|
-
concurrency: opts.concurrency
|
|
170
|
-
? parsePositiveInt(opts.concurrency, "concurrency")
|
|
171
|
-
: DEFAULT_CONCURRENCY,
|
|
172
|
-
}
|
|
173
|
-
);
|
|
174
|
-
if (failed === total) process.exit(1);
|
|
175
|
-
if (failed > 0) process.exit(2);
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function buildProviderOptions(
|
|
180
|
-
opts: ImageOptions
|
|
181
|
-
): Record<string, Record<string, string>> {
|
|
182
|
-
const providerOptions: Record<string, Record<string, string>> = {};
|
|
183
|
-
if (opts.quality || opts.style) {
|
|
184
|
-
providerOptions.openai = {};
|
|
185
|
-
if (opts.quality) providerOptions.openai.quality = opts.quality;
|
|
186
|
-
if (opts.style) providerOptions.openai.style = opts.style;
|
|
187
|
-
}
|
|
188
|
-
return providerOptions;
|
|
189
|
-
}
|
package/src/commands/models.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import type { Command } from "commander";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
fetchGatewayModels,
|
|
5
|
-
type Modality,
|
|
6
|
-
type ModelEntry,
|
|
7
|
-
} from "../lib/models.js";
|
|
8
|
-
|
|
9
|
-
function groupByCreator(models: ModelEntry[]): Map<string, ModelEntry[]> {
|
|
10
|
-
const groups = new Map<string, ModelEntry[]>();
|
|
11
|
-
for (const m of models) {
|
|
12
|
-
if (!groups.has(m.creator)) groups.set(m.creator, []);
|
|
13
|
-
groups.get(m.creator)!.push(m);
|
|
14
|
-
}
|
|
15
|
-
return new Map(
|
|
16
|
-
[...groups.entries()].sort((a, b) => a[0].localeCompare(b[0]))
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function modelName(id: string): string {
|
|
21
|
-
const slash = id.indexOf("/");
|
|
22
|
-
return slash !== -1 ? id.slice(slash + 1) : id;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function registerModelsCommand(program: Command) {
|
|
26
|
-
program
|
|
27
|
-
.command("models")
|
|
28
|
-
.description("List available models from AI Gateway")
|
|
29
|
-
.option("--type <type>", "Filter by type: text, image, video")
|
|
30
|
-
.option("--creator <name>", "Filter by creator (e.g. openai, google)")
|
|
31
|
-
.option("--json", "Output as JSON (includes descriptions)")
|
|
32
|
-
.action(
|
|
33
|
-
async (opts: { type?: string; creator?: string; json?: boolean }) => {
|
|
34
|
-
const validTypes = ["text", "image", "video"];
|
|
35
|
-
const filterType = opts.type?.toLowerCase() as Modality | undefined;
|
|
36
|
-
if (filterType && !validTypes.includes(filterType)) {
|
|
37
|
-
process.stderr.write(
|
|
38
|
-
`Error: --type must be one of: ${validTypes.join(", ")} (got "${opts.type}")\n`
|
|
39
|
-
);
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
const filterCreator = opts.creator?.toLowerCase();
|
|
43
|
-
|
|
44
|
-
const gatewayModels = await fetchGatewayModels();
|
|
45
|
-
|
|
46
|
-
if (opts.json) {
|
|
47
|
-
let entries = gatewayModels.all;
|
|
48
|
-
if (filterType) {
|
|
49
|
-
entries = entries.filter((m) =>
|
|
50
|
-
m.capabilities.includes(filterType)
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
if (filterCreator) {
|
|
54
|
-
entries = entries.filter(
|
|
55
|
-
(m) => m.creator.toLowerCase() === filterCreator
|
|
56
|
-
);
|
|
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
|
-
}));
|
|
66
|
-
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const sections: { title: string; entries: ModelEntry[] }[] = [];
|
|
71
|
-
if (!filterType || filterType === "text")
|
|
72
|
-
sections.push({ title: "Text", entries: gatewayModels.text });
|
|
73
|
-
if (!filterType || filterType === "image")
|
|
74
|
-
sections.push({ title: "Image", entries: gatewayModels.image });
|
|
75
|
-
if (!filterType || filterType === "video")
|
|
76
|
-
sections.push({ title: "Video", entries: gatewayModels.video });
|
|
77
|
-
|
|
78
|
-
let totalCount = 0;
|
|
79
|
-
for (const section of sections) {
|
|
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);
|
|
87
|
-
const count = [...grouped.values()].reduce((s, m) => s + m.length, 0);
|
|
88
|
-
if (count === 0) continue;
|
|
89
|
-
totalCount += count;
|
|
90
|
-
process.stdout.write(`\n${section.title} models (${count}):\n`);
|
|
91
|
-
for (const [creator, models] of grouped) {
|
|
92
|
-
process.stdout.write(`\n ${creator}\n`);
|
|
93
|
-
for (const m of models) {
|
|
94
|
-
process.stdout.write(` ${modelName(m.id)}\n`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (totalCount === 0) {
|
|
100
|
-
process.stderr.write("No models found matching filters\n");
|
|
101
|
-
} else {
|
|
102
|
-
process.stdout.write("\n");
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
);
|
|
106
|
-
}
|
package/src/commands/text.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
import { generateText, gateway } from "ai";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
|
|
4
|
-
import { buildJobs, runJobs } from "../lib/jobs.js";
|
|
5
|
-
import { fetchGatewayModels, resolveModels } from "../lib/models.js";
|
|
6
|
-
import type { OutputFormat } from "../lib/output.js";
|
|
7
|
-
import { parsePositiveInt, parseTemperature } from "../lib/parse.js";
|
|
8
|
-
import { readStdin, stdinAsText } from "../lib/stdin.js";
|
|
9
|
-
|
|
10
|
-
const DEFAULT_CONCURRENCY = 4;
|
|
11
|
-
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
12
|
-
|
|
13
|
-
interface TextOptions {
|
|
14
|
-
model?: string;
|
|
15
|
-
output?: string;
|
|
16
|
-
format?: string;
|
|
17
|
-
system?: string;
|
|
18
|
-
maxTokens?: string;
|
|
19
|
-
temperature?: string;
|
|
20
|
-
count?: string;
|
|
21
|
-
concurrency?: string;
|
|
22
|
-
quiet?: boolean;
|
|
23
|
-
json?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function resolveFormat(fmt?: string): OutputFormat {
|
|
27
|
-
if (!fmt || fmt === "md") return "md";
|
|
28
|
-
if (fmt === "txt") return "txt";
|
|
29
|
-
throw new Error(`--format must be one of: md, txt (got "${fmt}")`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function registerTextCommand(program: Command) {
|
|
33
|
-
program
|
|
34
|
-
.command("text")
|
|
35
|
-
.description("Generate text from a prompt")
|
|
36
|
-
.argument("[prompt]", "The prompt to generate text from")
|
|
37
|
-
.option(
|
|
38
|
-
"-m, --model <model>",
|
|
39
|
-
"Model ID (creator/model-name), comma-separated for multi-model"
|
|
40
|
-
)
|
|
41
|
-
.option("-o, --output <path>", "Output file path or directory")
|
|
42
|
-
.option("-f, --format <fmt>", "Output format: md, txt (default: md)")
|
|
43
|
-
.option("-n, --count <n>", "Number of generations (default: 1)")
|
|
44
|
-
.option(
|
|
45
|
-
"-p, --concurrency <n>",
|
|
46
|
-
`Max parallel generations (default: ${DEFAULT_CONCURRENCY})`
|
|
47
|
-
)
|
|
48
|
-
.option("-s, --system <prompt>", "System prompt")
|
|
49
|
-
.option("--max-tokens <n>", "Maximum tokens to generate")
|
|
50
|
-
.option("-t, --temperature <n>", "Temperature (0-2)")
|
|
51
|
-
.option("-q, --quiet", "Suppress progress output")
|
|
52
|
-
.option("--json", "Output metadata as JSON")
|
|
53
|
-
.action(async (rawPrompt: string | undefined, opts: TextOptions) => {
|
|
54
|
-
const prompt = rawPrompt?.trim() || undefined;
|
|
55
|
-
const stdin = await readStdin();
|
|
56
|
-
if (!prompt && !stdin) {
|
|
57
|
-
process.stderr.write(
|
|
58
|
-
"Error: prompt is required (provide as argument or pipe via stdin)\n"
|
|
59
|
-
);
|
|
60
|
-
process.exit(1);
|
|
61
|
-
}
|
|
62
|
-
let fullPrompt: string;
|
|
63
|
-
if (stdin && prompt) {
|
|
64
|
-
fullPrompt = `${stdinAsText(stdin)}\n\n---\n\n${prompt}`;
|
|
65
|
-
} else if (stdin) {
|
|
66
|
-
fullPrompt = stdinAsText(stdin);
|
|
67
|
-
} else {
|
|
68
|
-
fullPrompt = prompt!;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const format = resolveFormat(opts.format);
|
|
72
|
-
const gatewayModels = await fetchGatewayModels();
|
|
73
|
-
const models = resolveModels("text", opts.model, gatewayModels.text);
|
|
74
|
-
const countPerModel = opts.count
|
|
75
|
-
? parsePositiveInt(opts.count, "count")
|
|
76
|
-
: 1;
|
|
77
|
-
const maxTokens = opts.maxTokens
|
|
78
|
-
? parsePositiveInt(opts.maxTokens, "max-tokens")
|
|
79
|
-
: undefined;
|
|
80
|
-
const temperature = opts.temperature
|
|
81
|
-
? parseTemperature(opts.temperature)
|
|
82
|
-
: undefined;
|
|
83
|
-
|
|
84
|
-
const jobs = buildJobs(models, countPerModel);
|
|
85
|
-
|
|
86
|
-
const { total, failed } = await runJobs(
|
|
87
|
-
jobs,
|
|
88
|
-
async (modelId) => {
|
|
89
|
-
const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
|
|
90
|
-
const result = await generateText({
|
|
91
|
-
headers: {
|
|
92
|
-
"http-referer": "https://github.com/vercel-labs/ai-cli",
|
|
93
|
-
"x-title": "ai-cli",
|
|
94
|
-
},
|
|
95
|
-
model: gateway(modelId),
|
|
96
|
-
prompt: fullPrompt,
|
|
97
|
-
system: opts.system,
|
|
98
|
-
maxOutputTokens: maxTokens,
|
|
99
|
-
temperature,
|
|
100
|
-
abortSignal: abort,
|
|
101
|
-
});
|
|
102
|
-
return result.text;
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
noun: "text",
|
|
106
|
-
format,
|
|
107
|
-
outputPath: opts.output,
|
|
108
|
-
quiet: opts.quiet,
|
|
109
|
-
json: opts.json,
|
|
110
|
-
concurrency: opts.concurrency
|
|
111
|
-
? parsePositiveInt(opts.concurrency, "concurrency")
|
|
112
|
-
: DEFAULT_CONCURRENCY,
|
|
113
|
-
}
|
|
114
|
-
);
|
|
115
|
-
if (failed === total) process.exit(1);
|
|
116
|
-
if (failed > 0) process.exit(2);
|
|
117
|
-
});
|
|
118
|
-
}
|
package/src/commands/video.ts
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { experimental_generateVideo as generateVideo, gateway } from "ai";
|
|
2
|
-
import type { Command } from "commander";
|
|
3
|
-
|
|
4
|
-
import { buildJobs, runJobs } from "../lib/jobs.js";
|
|
5
|
-
import { fetchGatewayModels, resolveModels } from "../lib/models.js";
|
|
6
|
-
import {
|
|
7
|
-
parsePositiveInt,
|
|
8
|
-
parseAspectRatio,
|
|
9
|
-
parseNonNegativeFloat,
|
|
10
|
-
} from "../lib/parse.js";
|
|
11
|
-
import { readStdin } from "../lib/stdin.js";
|
|
12
|
-
|
|
13
|
-
const DEFAULT_CONCURRENCY = 2;
|
|
14
|
-
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
15
|
-
|
|
16
|
-
interface VideoOptions {
|
|
17
|
-
model?: string;
|
|
18
|
-
output?: string;
|
|
19
|
-
count?: string;
|
|
20
|
-
aspectRatio?: string;
|
|
21
|
-
duration?: string;
|
|
22
|
-
quiet?: boolean;
|
|
23
|
-
json?: boolean;
|
|
24
|
-
concurrency?: string;
|
|
25
|
-
preview?: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function registerVideoCommand(program: Command) {
|
|
29
|
-
program
|
|
30
|
-
.command("video")
|
|
31
|
-
.description("Generate a video from a prompt")
|
|
32
|
-
.argument("[prompt]", "The prompt to generate a video from")
|
|
33
|
-
.option(
|
|
34
|
-
"-m, --model <model>",
|
|
35
|
-
"Model ID (creator/model-name), comma-separated for multi-model"
|
|
36
|
-
)
|
|
37
|
-
.option("-o, --output <path>", "Output file path or directory")
|
|
38
|
-
.option("-n, --count <n>", "Number of videos per model (default: 1)")
|
|
39
|
-
.option("--aspect-ratio <W:H>", "Aspect ratio (e.g. 16:9)")
|
|
40
|
-
.option("--duration <seconds>", "Video duration in seconds")
|
|
41
|
-
.option("-q, --quiet", "Suppress progress output")
|
|
42
|
-
.option("--json", "Output metadata as JSON")
|
|
43
|
-
.option(
|
|
44
|
-
"--no-preview",
|
|
45
|
-
"Disable inline video frame 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: VideoOptions) => {
|
|
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
|
-
|
|
61
|
-
let videoPrompt: string | { image: Uint8Array; text?: string } = prompt!;
|
|
62
|
-
if (stdin) {
|
|
63
|
-
videoPrompt = prompt
|
|
64
|
-
? { image: new Uint8Array(stdin), text: prompt }
|
|
65
|
-
: { image: new Uint8Array(stdin) };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const gatewayModels = await fetchGatewayModels();
|
|
69
|
-
const models = resolveModels("video", opts.model, gatewayModels.video);
|
|
70
|
-
const countPerModel = opts.count
|
|
71
|
-
? parsePositiveInt(opts.count, "count")
|
|
72
|
-
: 1;
|
|
73
|
-
const aspectRatio = opts.aspectRatio
|
|
74
|
-
? parseAspectRatio(opts.aspectRatio)
|
|
75
|
-
: undefined;
|
|
76
|
-
const duration = opts.duration
|
|
77
|
-
? parseNonNegativeFloat(opts.duration, "duration")
|
|
78
|
-
: undefined;
|
|
79
|
-
|
|
80
|
-
const jobs = buildJobs(models, countPerModel);
|
|
81
|
-
|
|
82
|
-
const { total, failed } = await runJobs(
|
|
83
|
-
jobs,
|
|
84
|
-
async (modelId) => {
|
|
85
|
-
const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
|
|
86
|
-
const result = await generateVideo({
|
|
87
|
-
headers: {
|
|
88
|
-
"http-referer": "https://github.com/vercel-labs/ai-cli",
|
|
89
|
-
"x-title": "ai-cli",
|
|
90
|
-
},
|
|
91
|
-
model: gateway.video(modelId),
|
|
92
|
-
prompt: videoPrompt,
|
|
93
|
-
abortSignal: abort,
|
|
94
|
-
aspectRatio,
|
|
95
|
-
duration,
|
|
96
|
-
});
|
|
97
|
-
return Buffer.from(result.video.uint8Array);
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
noun: "video",
|
|
101
|
-
format: "video",
|
|
102
|
-
outputPath: opts.output,
|
|
103
|
-
quiet: opts.quiet,
|
|
104
|
-
json: opts.json,
|
|
105
|
-
display: opts.preview,
|
|
106
|
-
concurrency: opts.concurrency
|
|
107
|
-
? parsePositiveInt(opts.concurrency, "concurrency")
|
|
108
|
-
: DEFAULT_CONCURRENCY,
|
|
109
|
-
}
|
|
110
|
-
);
|
|
111
|
-
if (failed === total) process.exit(1);
|
|
112
|
-
if (failed > 0) process.exit(2);
|
|
113
|
-
});
|
|
114
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
|
|
4
|
-
import pkg from "../package.json";
|
|
5
|
-
import { registerImageCommand } from "./commands/image.js";
|
|
6
|
-
import { registerModelsCommand } from "./commands/models.js";
|
|
7
|
-
import { registerTextCommand } from "./commands/text.js";
|
|
8
|
-
import { registerVideoCommand } from "./commands/video.js";
|
|
9
|
-
|
|
10
|
-
const program = new Command();
|
|
11
|
-
|
|
12
|
-
program
|
|
13
|
-
.name("ai")
|
|
14
|
-
.description(
|
|
15
|
-
"A tiny, agent-native CLI for generating images, video and text with dead-simple commands, stdin support and predictable artifact outputs"
|
|
16
|
-
)
|
|
17
|
-
.version(pkg.version);
|
|
18
|
-
|
|
19
|
-
registerTextCommand(program);
|
|
20
|
-
registerImageCommand(program);
|
|
21
|
-
registerVideoCommand(program);
|
|
22
|
-
registerModelsCommand(program);
|
|
23
|
-
|
|
24
|
-
program.parseAsync(process.argv).catch((err: unknown) => {
|
|
25
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
26
|
-
process.stderr.write(`Error: ${msg}\n`);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
});
|