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.
@@ -1,117 +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 { 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 models = resolveModels("text", opts.model);
73
- const countPerModel = opts.count
74
- ? parsePositiveInt(opts.count, "count")
75
- : 1;
76
- const maxTokens = opts.maxTokens
77
- ? parsePositiveInt(opts.maxTokens, "max-tokens")
78
- : undefined;
79
- const temperature = opts.temperature
80
- ? parseTemperature(opts.temperature)
81
- : undefined;
82
-
83
- const jobs = buildJobs(models, countPerModel);
84
-
85
- const { total, failed } = await runJobs(
86
- jobs,
87
- async (modelId) => {
88
- const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
89
- const result = await generateText({
90
- headers: {
91
- "http-referer": "https://github.com/vercel-labs/ai-cli",
92
- "x-title": "ai-cli",
93
- },
94
- model: gateway(modelId),
95
- prompt: fullPrompt,
96
- system: opts.system,
97
- maxOutputTokens: maxTokens,
98
- temperature,
99
- abortSignal: abort,
100
- });
101
- return result.text;
102
- },
103
- {
104
- noun: "text",
105
- format,
106
- outputPath: opts.output,
107
- quiet: opts.quiet,
108
- json: opts.json,
109
- concurrency: opts.concurrency
110
- ? parsePositiveInt(opts.concurrency, "concurrency")
111
- : DEFAULT_CONCURRENCY,
112
- }
113
- );
114
- if (failed === total) process.exit(1);
115
- if (failed > 0) process.exit(2);
116
- });
117
- }
@@ -1,113 +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 { 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 models = resolveModels("video", opts.model);
69
- const countPerModel = opts.count
70
- ? parsePositiveInt(opts.count, "count")
71
- : 1;
72
- const aspectRatio = opts.aspectRatio
73
- ? parseAspectRatio(opts.aspectRatio)
74
- : undefined;
75
- const duration = opts.duration
76
- ? parseNonNegativeFloat(opts.duration, "duration")
77
- : undefined;
78
-
79
- const jobs = buildJobs(models, countPerModel);
80
-
81
- const { total, failed } = await runJobs(
82
- jobs,
83
- async (modelId) => {
84
- const abort = AbortSignal.timeout(DEFAULT_TIMEOUT_MS);
85
- const result = await generateVideo({
86
- headers: {
87
- "http-referer": "https://github.com/vercel-labs/ai-cli",
88
- "x-title": "ai-cli",
89
- },
90
- model: gateway.video(modelId),
91
- prompt: videoPrompt,
92
- abortSignal: abort,
93
- aspectRatio,
94
- duration,
95
- });
96
- return Buffer.from(result.video.uint8Array);
97
- },
98
- {
99
- noun: "video",
100
- format: "video",
101
- outputPath: opts.output,
102
- quiet: opts.quiet,
103
- json: opts.json,
104
- display: opts.preview,
105
- concurrency: opts.concurrency
106
- ? parsePositiveInt(opts.concurrency, "concurrency")
107
- : DEFAULT_CONCURRENCY,
108
- }
109
- );
110
- if (failed === total) process.exit(1);
111
- if (failed > 0) process.exit(2);
112
- });
113
- }
package/src/index.ts DELETED
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { Command } from "commander";
3
-
4
- import pkg from "../package.json";
5
- import { registerCompletionsCommand } from "./commands/completions.js";
6
- import { registerImageCommand } from "./commands/image.js";
7
- import { registerModelsCommand } from "./commands/models.js";
8
- import { registerTextCommand } from "./commands/text.js";
9
- import { registerVideoCommand } from "./commands/video.js";
10
-
11
- const program = new Command();
12
-
13
- program
14
- .name("ai")
15
- .description(
16
- "A tiny, agent-native CLI for generating images, video and text with dead-simple commands, stdin support and predictable artifact outputs"
17
- )
18
- .version(pkg.version);
19
-
20
- registerTextCommand(program);
21
- registerImageCommand(program);
22
- registerVideoCommand(program);
23
- registerModelsCommand(program);
24
- registerCompletionsCommand(program);
25
-
26
- program.parseAsync(process.argv).catch((err: unknown) => {
27
- const msg = err instanceof Error ? err.message : String(err);
28
- process.stderr.write(`Error: ${msg}\n`);
29
- process.exit(1);
30
- });
package/src/lib/color.ts DELETED
@@ -1,5 +0,0 @@
1
- export function isColorEnabled(): boolean {
2
- if (process.env.NO_COLOR !== undefined) return false;
3
- if (process.env.FORCE_COLOR !== undefined) return true;
4
- return !!process.stderr.isTTY;
5
- }
@@ -1,164 +0,0 @@
1
- export interface DecodedFrame {
2
- yuv: Uint8Array;
3
- width: number;
4
- height: number;
5
- }
6
-
7
- interface OpenH264Module {
8
- _decoder_init(): number;
9
- _decoder_feed(ptr: number, len: number): number;
10
- _decoder_flush(): number;
11
- _decoder_destroy(): void;
12
- _get_has_frame(): number;
13
- _get_width(): number;
14
- _get_height(): number;
15
- _get_y_stride(): number;
16
- _get_uv_stride(): number;
17
- _get_y_ptr(): number;
18
- _get_u_ptr(): number;
19
- _get_v_ptr(): number;
20
- _malloc(size: number): number;
21
- _free(ptr: number): void;
22
- HEAPU8: Uint8Array;
23
- }
24
-
25
- const START_CODE = new Uint8Array([0x00, 0x00, 0x00, 0x01]);
26
-
27
- function buildAnnexB(
28
- sps: Uint8Array,
29
- pps: Uint8Array,
30
- idr: Uint8Array
31
- ): Uint8Array {
32
- const len = START_CODE.length * 3 + sps.length + pps.length + idr.length;
33
- const buf = new Uint8Array(len);
34
- let off = 0;
35
- buf.set(START_CODE, off);
36
- off += START_CODE.length;
37
- buf.set(sps, off);
38
- off += sps.length;
39
- buf.set(START_CODE, off);
40
- off += START_CODE.length;
41
- buf.set(pps, off);
42
- off += pps.length;
43
- buf.set(START_CODE, off);
44
- off += START_CODE.length;
45
- buf.set(idr, off);
46
- return buf;
47
- }
48
-
49
- function extractPlanarYUV(
50
- mod: OpenH264Module,
51
- width: number,
52
- height: number
53
- ): Uint8Array {
54
- const yStride = mod._get_y_stride();
55
- const uvStride = mod._get_uv_stride();
56
- const yPtr = mod._get_y_ptr();
57
- const uPtr = mod._get_u_ptr();
58
- const vPtr = mod._get_v_ptr();
59
-
60
- const chromaW = width >> 1;
61
- const chromaH = height >> 1;
62
- const ySize = width * height;
63
- const cSize = chromaW * chromaH;
64
- const out = new Uint8Array(ySize + cSize * 2);
65
-
66
- const heap = mod.HEAPU8;
67
-
68
- for (let y = 0; y < height; y++) {
69
- out.set(
70
- heap.subarray(yPtr + y * yStride, yPtr + y * yStride + width),
71
- y * width
72
- );
73
- }
74
-
75
- const cbOff = ySize;
76
- for (let y = 0; y < chromaH; y++) {
77
- out.set(
78
- heap.subarray(uPtr + y * uvStride, uPtr + y * uvStride + chromaW),
79
- cbOff + y * chromaW
80
- );
81
- }
82
-
83
- const crOff = cbOff + cSize;
84
- for (let y = 0; y < chromaH; y++) {
85
- out.set(
86
- heap.subarray(vPtr + y * uvStride, vPtr + y * uvStride + chromaW),
87
- crOff + y * chromaW
88
- );
89
- }
90
-
91
- return out;
92
- }
93
-
94
- let modulePromise: Promise<OpenH264Module> | null = null;
95
-
96
- function getModule(): Promise<OpenH264Module> {
97
- if (!modulePromise) {
98
- modulePromise = (async () => {
99
- const { readFileSync } = await import("fs");
100
- const wasmPath: string = (await import("./openh264.wasm")).default;
101
- const wasmBinary = readFileSync(wasmPath);
102
- const factory = (await import("./openh264.mjs")).default;
103
- return factory({
104
- wasmBinary,
105
- print: () => {},
106
- printErr: () => {},
107
- }) as Promise<OpenH264Module>;
108
- })();
109
- }
110
- return modulePromise;
111
- }
112
-
113
- export async function decodeIDR(
114
- sps: Uint8Array,
115
- pps: Uint8Array,
116
- sliceData: Uint8Array
117
- ): Promise<DecodedFrame | null> {
118
- if (!sps.length || !pps.length || !sliceData.length) return null;
119
-
120
- const mod = await getModule();
121
- const annexB = buildAnnexB(sps, pps, sliceData);
122
-
123
- if (mod._decoder_init() !== 0) return null;
124
-
125
- let ptr = 0;
126
- try {
127
- ptr = mod._malloc(annexB.length);
128
- if (!ptr) {
129
- mod._decoder_destroy();
130
- return null;
131
- }
132
- mod.HEAPU8.set(annexB, ptr);
133
-
134
- mod._decoder_feed(ptr, annexB.length);
135
-
136
- if (!mod._get_has_frame()) {
137
- mod._decoder_feed(0, 0);
138
- }
139
- if (!mod._get_has_frame()) {
140
- mod._decoder_flush();
141
- }
142
- if (!mod._get_has_frame()) {
143
- mod._free(ptr);
144
- mod._decoder_destroy();
145
- return null;
146
- }
147
-
148
- const width = mod._get_width();
149
- const height = mod._get_height();
150
- const yuv = extractPlanarYUV(mod, width, height);
151
-
152
- mod._free(ptr);
153
- mod._decoder_destroy();
154
- return { yuv, width, height };
155
- } catch {
156
- if (ptr) mod._free(ptr);
157
- try {
158
- mod._decoder_destroy();
159
- } catch {
160
- /* best-effort */
161
- }
162
- return null;
163
- }
164
- }
@@ -1,48 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
-
3
- import { decodeIDR } from "./h264-wasm.js";
4
-
5
- describe("decodeIDR (openh264 WASM)", () => {
6
- test("returns null for empty inputs", async () => {
7
- expect(
8
- await decodeIDR(new Uint8Array(0), new Uint8Array(0), new Uint8Array(0))
9
- ).toBeNull();
10
- });
11
-
12
- test("returns null for truncated SPS", async () => {
13
- const sps = new Uint8Array([0x67, 0x42]);
14
- const pps = new Uint8Array([0x68, 0xce, 0x38, 0x80]);
15
- const slice = new Uint8Array([0x65, 0x88, 0x80, 0x40]);
16
- expect(await decodeIDR(sps, pps, slice)).toBeNull();
17
- });
18
-
19
- test("gracefully handles malformed data without crashing", async () => {
20
- const sps = new Uint8Array([0x67, 0x42, 0x00, 0x0a, 0xe9, 0x40, 0x40]);
21
- const pps = new Uint8Array([0x68, 0xce, 0x38, 0x80]);
22
- const slice = new Uint8Array(100);
23
- slice[0] = 0x65;
24
- for (let i = 1; i < 100; i++) slice[i] = (i * 37) & 0xff;
25
-
26
- const result = await decodeIDR(sps, pps, slice);
27
- expect(
28
- result === null || (result && result.yuv instanceof Uint8Array)
29
- ).toBeTruthy();
30
- });
31
-
32
- test("returns DecodedFrame with valid YUV for well-formed data", async () => {
33
- const sps = new Uint8Array([
34
- 0x67, 0x42, 0x00, 0x0a, 0xe9, 0x40, 0x40, 0x04, 0x00, 0x00, 0x00, 0x04,
35
- 0x00, 0x00, 0x00, 0xc8, 0x40,
36
- ]);
37
- const pps = new Uint8Array([0x68, 0xce, 0x38, 0x80]);
38
- const slice = new Uint8Array(200);
39
- slice[0] = 0x65;
40
- for (let i = 1; i < 200; i++) slice[i] = (i * 37) & 0xff;
41
-
42
- const result = await decodeIDR(sps, pps, slice);
43
- // May return null for synthetic data, but should not throw
44
- expect(
45
- result === null || (result && result.width > 0 && result.height > 0)
46
- ).toBeTruthy();
47
- });
48
- });
package/src/lib/jobs.ts DELETED
@@ -1,192 +0,0 @@
1
- import {
2
- supportsKittyGraphics,
3
- displayImage,
4
- displayVideoFrame,
5
- } from "./kitty.js";
6
- import type { OutputFormat } from "./output.js";
7
- import { writeOutput } from "./output.js";
8
- import { pMap } from "./p-map.js";
9
- import { Progress, MultiProgress, formatElapsed } from "./progress.js";
10
-
11
- export interface Job {
12
- modelId: string;
13
- label: string;
14
- index: number;
15
- }
16
-
17
- export interface RunJobsOptions {
18
- noun: string;
19
- format: OutputFormat;
20
- outputPath?: string;
21
- quiet?: boolean;
22
- json?: boolean;
23
- concurrency: number;
24
- display?: boolean;
25
- }
26
-
27
- export function buildJobs(models: string[], countPerModel: number): Job[] {
28
- let jobIndex = 0;
29
- return models.flatMap((modelId) =>
30
- Array.from({ length: countPerModel }, (_, i) => ({
31
- modelId,
32
- label: models.length > 1 ? `${modelId} #${i + 1}` : `#${i + 1}`,
33
- index: jobIndex++,
34
- }))
35
- );
36
- }
37
-
38
- export interface RunJobsResult {
39
- total: number;
40
- failed: number;
41
- }
42
-
43
- export async function runJobs(
44
- jobs: Job[],
45
- generate: (modelId: string) => Promise<Buffer | string>,
46
- opts: RunJobsOptions
47
- ): Promise<RunJobsResult> {
48
- const { noun, format, outputPath, quiet, json, concurrency, display } = opts;
49
-
50
- if (jobs.length === 1) {
51
- const { modelId } = jobs[0];
52
- const progress = new Progress(quiet);
53
- const start = Date.now();
54
- progress.start(`Generating ${noun} with ${modelId}`);
55
-
56
- try {
57
- const data = await generate(modelId);
58
- const elapsed = Date.now() - start;
59
- progress.stop(`Generated ${noun} with ${modelId}`);
60
-
61
- if (json) {
62
- const path = await writeOutput({
63
- data,
64
- format,
65
- outputPath,
66
- quiet: true,
67
- display,
68
- });
69
- const meta = {
70
- elapsed_ms: elapsed,
71
- count: 1,
72
- results: [
73
- {
74
- index: 1,
75
- model: modelId,
76
- elapsed_ms: elapsed,
77
- success: true,
78
- file: path,
79
- },
80
- ],
81
- };
82
- process.stdout.write(JSON.stringify(meta, null, 2) + "\n");
83
- } else {
84
- await writeOutput({ data, format, outputPath, quiet, display });
85
- }
86
- } catch (err) {
87
- progress.stop();
88
- throw err;
89
- }
90
- return { total: 1, failed: 0 };
91
- }
92
-
93
- const multi = new MultiProgress(quiet);
94
- const start = Date.now();
95
- const shouldDisplay =
96
- display !== false &&
97
- (format === "image" || format === "video") &&
98
- process.stdout.isTTY &&
99
- supportsKittyGraphics();
100
-
101
- const lineIdxs = jobs.map((j) =>
102
- multi.addLine(`Generating ${noun} ${j.label} with ${j.modelId}`)
103
- );
104
-
105
- const results: {
106
- index: number;
107
- model: string;
108
- success: boolean;
109
- elapsed_ms: number;
110
- file: string | null;
111
- }[] = [];
112
- const pendingDisplayBuffers: Buffer[] = [];
113
-
114
- await pMap(
115
- jobs,
116
- async (job, i) => {
117
- multi.startLine(lineIdxs[i]);
118
- const genStart = Date.now();
119
- try {
120
- const data = await generate(job.modelId);
121
- const genElapsed = Date.now() - genStart;
122
- const suffix = `${i + 1}`;
123
- const path = await writeOutput({
124
- data,
125
- format,
126
- outputPath,
127
- suffix,
128
- quiet: true,
129
- display: false,
130
- });
131
- if (shouldDisplay && Buffer.isBuffer(data))
132
- pendingDisplayBuffers.push(data);
133
- const savedMsg = path
134
- ? `Saved to ${path}`
135
- : `${noun[0].toUpperCase()}${noun.slice(1)} ${job.label} written to stdout`;
136
- multi.completeLine(
137
- lineIdxs[i],
138
- `${savedMsg} (${formatElapsed(genElapsed)})`
139
- );
140
- results.push({
141
- index: i,
142
- model: job.modelId,
143
- success: true,
144
- elapsed_ms: genElapsed,
145
- file: path,
146
- });
147
- } catch (err: unknown) {
148
- const genElapsed = Date.now() - genStart;
149
- const msg = err instanceof Error ? err.message : String(err);
150
- multi.completeLine(
151
- lineIdxs[i],
152
- `${noun[0].toUpperCase()}${noun.slice(1)} ${job.label} failed: ${msg} (${formatElapsed(genElapsed)})`
153
- );
154
- results.push({
155
- index: i,
156
- model: job.modelId,
157
- success: false,
158
- elapsed_ms: genElapsed,
159
- file: null,
160
- });
161
- }
162
- },
163
- concurrency
164
- );
165
-
166
- if (json) {
167
- const totalElapsed = Date.now() - start;
168
- const meta = {
169
- elapsed_ms: totalElapsed,
170
- count: results.filter((r) => r.success).length,
171
- results: results.map((r) => ({
172
- index: r.index + 1,
173
- model: r.model,
174
- elapsed_ms: r.elapsed_ms,
175
- success: r.success,
176
- file: r.file,
177
- })),
178
- };
179
- process.stdout.write(JSON.stringify(meta, null, 2) + "\n");
180
- }
181
-
182
- for (const buf of pendingDisplayBuffers) {
183
- if (format === "video") {
184
- await displayVideoFrame(buf);
185
- } else {
186
- displayImage(buf);
187
- }
188
- }
189
-
190
- const failCount = results.filter((r) => !r.success).length;
191
- return { total: results.length, failed: failCount };
192
- }