@wavespeed/cli 0.1.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.
@@ -0,0 +1,24 @@
1
+ // Shared --download flag handling for image/video/run.
2
+ //
3
+ // Behavior (matches genmedia):
4
+ // (no flag) → no download, print URLs
5
+ // --download → save to outputDir with smart filename
6
+ // --download "./out.png" → save to that path (single output)
7
+ // --download "./out/{index}.{ext}" → templated path (multi output)
8
+ // Commander hands `--download` with no value as boolean `true`. With a value it
9
+ // gives us the string. With nothing, the property is `undefined`.
10
+ export function resolveDownloadOpt(value) {
11
+ if (value === undefined)
12
+ return { enabled: false };
13
+ if (value === true)
14
+ return { enabled: true };
15
+ if (typeof value === 'string' && value.length > 0)
16
+ return { enabled: true, template: value };
17
+ return { enabled: false };
18
+ }
19
+ export function downloadOptsFromFlag(flagValue, base) {
20
+ const r = resolveDownloadOpt(flagValue);
21
+ if (!r.enabled)
22
+ return null;
23
+ return { ...base, template: r.template };
24
+ }
@@ -0,0 +1,162 @@
1
+ // `wavespeed run <model> --help` introspects the model's request schema and
2
+ // prints the actual flags the model accepts (matches genmedia's behavior).
3
+ //
4
+ // Called from cli.ts BEFORE Commander parses, so we can intercept --help.
5
+ import chalk from 'chalk';
6
+ import { fetchModels } from './api.js';
7
+ import { findAlias } from './config.js';
8
+ function getRequestSchema(m) {
9
+ return m.api_schema?.api_schemas?.[0]?.request_schema;
10
+ }
11
+ function typeLabel(p) {
12
+ if (Array.isArray(p.type))
13
+ return p.type.join('|');
14
+ if (p.type === 'array' && p.items?.type)
15
+ return `${p.items.type}[]`;
16
+ return p.type ?? 'any';
17
+ }
18
+ function wrap(text, indent, width = 92) {
19
+ const words = text.replace(/\s+/g, ' ').trim().split(' ');
20
+ const lines = [];
21
+ let cur = '';
22
+ for (const w of words) {
23
+ if ((cur + ' ' + w).length > width - indent.length) {
24
+ if (cur)
25
+ lines.push(cur);
26
+ cur = w;
27
+ }
28
+ else {
29
+ cur = cur ? cur + ' ' + w : w;
30
+ }
31
+ }
32
+ if (cur)
33
+ lines.push(cur);
34
+ return lines.map((l) => indent + chalk.gray(l)).join('\n');
35
+ }
36
+ /**
37
+ * Detect `wavespeed run <model> --help` (or `-h`) in argv. Returns the model id
38
+ * if matched, else null. Order doesn't matter (`--help` can come before or after
39
+ * the model).
40
+ */
41
+ export function detectRunHelp(argv) {
42
+ const args = argv.slice(2);
43
+ if (args[0] !== 'run')
44
+ return null;
45
+ if (!args.some((a) => a === '--help' || a === '-h'))
46
+ return null;
47
+ // Find the first positional that looks like a model id (contains '/').
48
+ // We allow any non-flag token though — even a typo'd id should trigger this
49
+ // path so the error message guides the user.
50
+ for (const a of args.slice(1)) {
51
+ if (!a.startsWith('-'))
52
+ return a;
53
+ }
54
+ return null;
55
+ }
56
+ export async function printDynamicRunHelp(token) {
57
+ // Resolve aliases up-front so `wavespeed run <alias> -h` shows the underlying
58
+ // model's schema. Track the alias name + its default input for the example.
59
+ let modelId = token;
60
+ let aliasName;
61
+ let aliasDefaults;
62
+ if (!token.includes('/')) {
63
+ const alias = findAlias(token);
64
+ if (alias) {
65
+ modelId = alias.model;
66
+ aliasName = token;
67
+ aliasDefaults = alias.input;
68
+ }
69
+ }
70
+ let payload;
71
+ try {
72
+ payload = await fetchModels({});
73
+ }
74
+ catch (err) {
75
+ // Offline / not signed in — defer to static help.
76
+ process.stderr.write(chalk.gray(`(Could not fetch live schema: ${err.message ?? String(err)}. Falling back to static help.)\n`));
77
+ return;
78
+ }
79
+ const model = payload.models.find((m) => m.model_id === modelId);
80
+ if (!model) {
81
+ process.stdout.write(chalk.yellow(`Unknown model "${modelId}".`) +
82
+ chalk.gray(` Try `) +
83
+ chalk.cyan(`wavespeed models "${modelId.split('/').pop()}"`) +
84
+ chalk.gray(' to search.\n'));
85
+ process.exit(1);
86
+ }
87
+ const schema = getRequestSchema(model);
88
+ if (!schema?.properties) {
89
+ process.stdout.write(chalk.yellow(`Model "${modelId}" has no published schema.`) +
90
+ chalk.gray(' Run it generically with `-i k=v` pairs.\n'));
91
+ process.exit(0);
92
+ }
93
+ const required = new Set(schema.required ?? []);
94
+ const order = (schema['x-order-properties'] ?? Object.keys(schema.properties)).filter((k) => k in schema.properties);
95
+ console.log();
96
+ console.log(chalk.hex('#7c5cff').bold(` wavespeed run ${aliasName ?? model.model_id}`) +
97
+ (aliasName ? chalk.gray(` → ${model.model_id}`) : ''));
98
+ console.log(chalk.gray(' type: ') +
99
+ chalk.white(model.type ?? 'unknown') +
100
+ (model.base_price !== undefined
101
+ ? chalk.gray(' base price: ') + chalk.green(`$${model.base_price}`)
102
+ : ''));
103
+ if (model.description)
104
+ console.log(wrap(model.description, ' '));
105
+ if (aliasDefaults && Object.keys(aliasDefaults).length > 0) {
106
+ console.log();
107
+ console.log(chalk.gray(' alias defaults: ') + chalk.gray(JSON.stringify(aliasDefaults)));
108
+ }
109
+ console.log();
110
+ console.log(chalk.bold('Usage: ') +
111
+ chalk.cyan(`wavespeed run ${aliasName ?? model.model_id}`) +
112
+ chalk.gray(' [-i key=value ...] [--download [path]] [--json]'));
113
+ console.log();
114
+ console.log(chalk.bold('Inputs') +
115
+ chalk.gray(` (${order.length} field${order.length === 1 ? '' : 's'}, ${required.size} required)`));
116
+ console.log();
117
+ const nameWidth = Math.max(...order.map((n) => n.length), 8);
118
+ for (const name of order) {
119
+ const p = schema.properties[name];
120
+ const flag = required.has(name) ? chalk.red('*') : ' ';
121
+ const t = chalk.magenta(typeLabel(p).padEnd(10));
122
+ console.log(` ${flag} ${chalk.cyan(name.padEnd(nameWidth))} ${t}`);
123
+ if (p.description)
124
+ console.log(wrap(p.description, ' '.padEnd(nameWidth + 6)));
125
+ const tail = [];
126
+ if (p.default !== undefined)
127
+ tail.push(`default: ${JSON.stringify(p.default)}`);
128
+ if (p.enum)
129
+ tail.push(`enum: ${p.enum.map((v) => String(v)).join(' | ')}`);
130
+ if (p.minimum !== undefined || p.maximum !== undefined)
131
+ tail.push(`range: ${p.minimum ?? '−∞'}..${p.maximum ?? '∞'}`);
132
+ if (p.minItems !== undefined || p.maxItems !== undefined)
133
+ tail.push(`items: ${p.minItems ?? 0}..${p.maxItems ?? '∞'}`);
134
+ if (tail.length)
135
+ console.log(' '.repeat(nameWidth + 6) + chalk.gray(tail.join(' · ')));
136
+ }
137
+ // Build a copy-paste example: required fields + prompt, skipping anything
138
+ // already supplied by an alias's defaults.
139
+ const seedFields = [...required];
140
+ if (!required.has('prompt') && 'prompt' in schema.properties)
141
+ seedFields.push('prompt');
142
+ const example = seedFields
143
+ .filter((n) => !aliasDefaults || !(n in aliasDefaults))
144
+ .map((n) => {
145
+ const p = schema.properties[n];
146
+ if (p?.default !== undefined)
147
+ return `-i ${n}=${JSON.stringify(p.default)}`;
148
+ if (p?.enum?.length)
149
+ return `-i ${n}=${JSON.stringify(p.enum[0])}`;
150
+ if (n.includes('prompt'))
151
+ return `-i ${n}="…"`;
152
+ if (n.includes('image') || n.includes('url'))
153
+ return `-i ${n}="<url>"`;
154
+ return `-i ${n}="…"`;
155
+ })
156
+ .join(' ');
157
+ console.log();
158
+ console.log(chalk.bold('Example:'));
159
+ console.log(` ${chalk.cyan('wavespeed run ' + (aliasName ?? model.model_id) + (example ? ' ' + example : ''))}`);
160
+ console.log();
161
+ process.exit(0);
162
+ }
@@ -0,0 +1,52 @@
1
+ // Parse `--input key=value` pairs into a structured object suitable for the API.
2
+ // - bare strings stay strings
3
+ // - "123" / "1.5" parsed as number
4
+ // - "true" / "false" parsed as boolean
5
+ // - "[a,b,c]" parsed as array of strings
6
+ // - JSON values (starting with { or [) parsed as JSON
7
+ // - keys with dots like "size.width" become nested
8
+ function coerce(raw) {
9
+ if (raw === 'true')
10
+ return true;
11
+ if (raw === 'false')
12
+ return false;
13
+ if (raw === 'null')
14
+ return null;
15
+ if (/^-?\d+$/.test(raw))
16
+ return parseInt(raw, 10);
17
+ if (/^-?\d+\.\d+$/.test(raw))
18
+ return parseFloat(raw);
19
+ if (raw.startsWith('{') || raw.startsWith('[')) {
20
+ try {
21
+ return JSON.parse(raw);
22
+ }
23
+ catch {
24
+ /* fall through */
25
+ }
26
+ }
27
+ return raw;
28
+ }
29
+ export function parseInputs(pairs = []) {
30
+ const out = {};
31
+ for (const pair of pairs) {
32
+ const eq = pair.indexOf('=');
33
+ if (eq === -1) {
34
+ throw new Error(`Invalid --input value "${pair}". Use key=value.`);
35
+ }
36
+ const key = pair.slice(0, eq).trim();
37
+ const val = coerce(pair.slice(eq + 1));
38
+ setPath(out, key, val);
39
+ }
40
+ return out;
41
+ }
42
+ function setPath(obj, dotted, value) {
43
+ const parts = dotted.split('.');
44
+ let cur = obj;
45
+ for (let i = 0; i < parts.length - 1; i++) {
46
+ const k = parts[i];
47
+ if (typeof cur[k] !== 'object' || cur[k] === null)
48
+ cur[k] = {};
49
+ cur = cur[k];
50
+ }
51
+ cur[parts[parts.length - 1]] = value;
52
+ }
@@ -0,0 +1,27 @@
1
+ // Tiny logging helper so commands can emit progress to stderr while keeping
2
+ // stdout reserved for the final --json payload (and downstream pipes).
3
+ let jsonMode = false;
4
+ export function setJsonMode(on) {
5
+ jsonMode = on;
6
+ }
7
+ export function isJsonMode() {
8
+ return jsonMode;
9
+ }
10
+ export function log(line) {
11
+ if (jsonMode)
12
+ return;
13
+ process.stdout.write(line + '\n');
14
+ }
15
+ export function info(line) {
16
+ if (jsonMode) {
17
+ process.stderr.write(line + '\n');
18
+ return;
19
+ }
20
+ process.stdout.write(line + '\n');
21
+ }
22
+ export function emitJson(payload) {
23
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
24
+ }
25
+ export function emitJsonError(message, extra = {}) {
26
+ process.stdout.write(JSON.stringify({ error: message, ...extra }, null, 2) + '\n');
27
+ }
@@ -0,0 +1,77 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { pipeline } from 'node:stream/promises';
4
+ import { Readable } from 'node:stream';
5
+ import chalk from 'chalk';
6
+ import { resolveOutputDir } from './config.js';
7
+ const EXT_FROM_URL = /\.([a-z0-9]{2,5})(?:\?|#|$)/i;
8
+ function extFromUrl(url, fallback) {
9
+ const m = url.match(EXT_FROM_URL);
10
+ if (m)
11
+ return m[1].toLowerCase();
12
+ return fallback;
13
+ }
14
+ function slug(input) {
15
+ return input
16
+ .toLowerCase()
17
+ .replace(/[^a-z0-9]+/g, '-')
18
+ .replace(/^-+|-+$/g, '')
19
+ .slice(0, 48);
20
+ }
21
+ function stamp() {
22
+ return new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
23
+ }
24
+ function renderTemplate(tpl, vars) {
25
+ return tpl.replace(/\{(\w+)\}/g, (_, k) => vars[k] ?? `{${k}}`);
26
+ }
27
+ export async function downloadOutputs(urls, opts = {}) {
28
+ const baseDir = path.resolve(opts.outputDir || resolveOutputDir());
29
+ const ts = stamp();
30
+ const tag = opts.prompt ? slug(opts.prompt) : opts.model ? slug(opts.model) : 'output';
31
+ const saved = [];
32
+ for (let i = 0; i < urls.length; i++) {
33
+ const url = urls[i];
34
+ const ext = extFromUrl(url, opts.ext || 'bin');
35
+ let filepath;
36
+ if (opts.template) {
37
+ const rendered = renderTemplate(opts.template, {
38
+ index: String(i + 1),
39
+ ext,
40
+ stamp: ts,
41
+ slug: tag,
42
+ });
43
+ filepath = path.resolve(rendered);
44
+ // Single-output, no {index} → use the template path verbatim.
45
+ // Multi-output, no {index} → suffix to avoid clobbering.
46
+ if (urls.length > 1 && !opts.template.includes('{index}')) {
47
+ const parsed = path.parse(filepath);
48
+ filepath = path.join(parsed.dir, `${parsed.name}-${i + 1}${parsed.ext}`);
49
+ }
50
+ }
51
+ else {
52
+ const suffix = urls.length > 1 ? `-${i + 1}` : '';
53
+ filepath = path.join(baseDir, `${ts}-${tag}${suffix}.${ext}`);
54
+ }
55
+ fs.mkdirSync(path.dirname(filepath), { recursive: true });
56
+ const res = await fetch(url);
57
+ if (!res.ok || !res.body) {
58
+ throw new Error(`Download failed for ${url}: ${res.status} ${res.statusText}`);
59
+ }
60
+ await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(filepath));
61
+ saved.push(filepath);
62
+ }
63
+ return saved;
64
+ }
65
+ export function printOutputs(saved, remote) {
66
+ console.log();
67
+ if (saved.length) {
68
+ console.log(chalk.green('Saved:'));
69
+ for (const f of saved)
70
+ console.log(' ' + chalk.cyan(path.relative(process.cwd(), f)));
71
+ console.log();
72
+ }
73
+ console.log(chalk.gray(saved.length ? 'Source URLs:' : 'URLs:'));
74
+ for (const u of remote)
75
+ console.log(' ' + chalk.cyan(u));
76
+ console.log();
77
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@wavespeed/cli",
3
+ "version": "0.1.0",
4
+ "description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "main": "dist/index.js",
10
+ "bin": {
11
+ "wavespeed": "dist/cli.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "skills",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "dev": "tsx src/cli.ts",
22
+ "start": "node dist/cli.js",
23
+ "prepublishOnly": "npm run build",
24
+ "format": "prettier --write \"src/**/*.ts\"",
25
+ "lint": "tsc --noEmit",
26
+ "test": "tsx src/cli.ts --version"
27
+ },
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "keywords": [
32
+ "wavespeed",
33
+ "ai",
34
+ "cli",
35
+ "image-generation",
36
+ "video-generation",
37
+ "generative-ai",
38
+ "creative-tools"
39
+ ],
40
+ "author": "WaveSpeedAI <support@wavespeed.ai>",
41
+ "license": "MIT",
42
+ "homepage": "https://wavespeed.ai",
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "https://github.com/WaveSpeedAI/cli.git"
46
+ },
47
+ "bugs": {
48
+ "url": "https://github.com/WaveSpeedAI/cli/issues"
49
+ },
50
+ "dependencies": {
51
+ "@inquirer/prompts": "^7.0.0",
52
+ "chalk": "^5.3.0",
53
+ "clipboardy": "^4.0.0",
54
+ "commander": "^12.1.0",
55
+ "conf": "^13.0.0",
56
+ "open": "^10.1.0",
57
+ "ora": "^8.1.0",
58
+ "wavespeed": "^0.2.1"
59
+ },
60
+ "devDependencies": {
61
+ "@types/node": "^20.14.0",
62
+ "prettier": "^3.3.0",
63
+ "tsx": "^4.19.0",
64
+ "typescript": "^5.5.0"
65
+ }
66
+ }
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: wavespeed
3
+ description: Generate or edit AI media (image, video, audio, 3D) by calling the wavespeed CLI on the user's machine. Use whenever the user asks to create, edit, animate, upscale, or transform a visual asset, generate audio/TTS/music, or produce marketing creatives. Every model on the WaveSpeed platform is one `wavespeed run <id>` call.
4
+ ---
5
+
6
+ # WaveSpeed
7
+
8
+ You have access to the `wavespeed` CLI. Every generation flows through one verb. There are no `image` / `video` shortcuts; the model id is always explicit.
9
+
10
+ ## The three-step pattern
11
+
12
+ ```bash
13
+ # 1. FIND a model — search the live catalog
14
+ wavespeed models "nano banana"
15
+ wavespeed models --type image-to-video --popular
16
+
17
+ # 2. INSPECT its inputs — dynamic schema, per model
18
+ wavespeed run google/nano-banana-2/text-to-image -h
19
+
20
+ # 3. RUN it — always pass --json so you can read the result
21
+ wavespeed run google/nano-banana-2/text-to-image \
22
+ -p "a cyberpunk skyline at golden hour" \
23
+ -i aspect_ratio="16:9" -i resolution="2k" --json
24
+ ```
25
+
26
+ `run --json` returns `{ model, prompt, outputs: [url, ...], saved: [path, ...], elapsed_ms, raw }`. Use the URL when the user wants a link. Add `--download` if they need bytes on disk.
27
+
28
+ ## Recommended defaults
29
+
30
+ | Use case | Model |
31
+ |---|---|
32
+ | Text → image | `google/nano-banana-2/text-to-image` |
33
+ | Image edit (instruction-driven) | `google/nano-banana-2/edit` — requires `images: [url, ...]` |
34
+ | Text → video | `bytedance/seedance-2.0/text-to-video` |
35
+ | Image → video | `bytedance/seedance-2.0/image-to-video` — requires `image: url` |
36
+
37
+ These are good starting points. Browse alternatives with `wavespeed models <query>`.
38
+
39
+ ## Common recipes
40
+
41
+ ```bash
42
+ # Edit an existing image — upload first, then pass the URL
43
+ URL=$(wavespeed upload ./input.jpg --json | jq -r .url)
44
+ wavespeed run google/nano-banana-2/edit \
45
+ -p "replace the background with a sunlit kitchen" \
46
+ -i images="[\"$URL\"]" --json
47
+
48
+ # Image-to-video — same pattern
49
+ URL=$(wavespeed upload ./hero.jpg --json | jq -r .url)
50
+ wavespeed run bytedance/seedance-2.0/image-to-video \
51
+ -p "subtle parallax, gentle wind" \
52
+ -i image="$URL" -i duration=5 --json
53
+
54
+ # Save outputs locally with a template
55
+ wavespeed run ... -p "..." --download "./out/{index}.{ext}"
56
+ ```
57
+
58
+ ## Project config and aliases
59
+
60
+ If `wavespeed.json` exists (created by `wavespeed init`):
61
+
62
+ - **`defaultModel`** — lets `wavespeed run -p "…"` (no model arg) work.
63
+ - **Aliases** — named shortcuts that bundle model + default inputs. Run `wavespeed aliases` to see what's defined. `wavespeed run <alias> -h` shows the resolved schema. CLI `-i k=v` overrides alias defaults.
64
+
65
+ The CLI never modifies the user's prompt or inputs. What you typed is what hits the API.
66
+
67
+ ## Auth
68
+
69
+ `wavespeed status` shows whether the user is signed in. If not, ask them to run `wavespeed login` (opens https://wavespeed.ai/accesskey). **Never** ask the user to paste an API key into the chat — the CLI handles it.
70
+
71
+ ## Pitfalls
72
+
73
+ - Local file paths don't auto-upload — call `wavespeed upload` first to get a CDN URL.
74
+ - Don't invent model IDs. Always confirm via `wavespeed models` or `wavespeed schema <id>` before running.
75
+ - Use `--json` on every `run` so you can read `outputs[0]` programmatically.