@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,101 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { fetchModels } from '../lib/api.js';
4
+ function priceLabel(p) {
5
+ if (p === undefined)
6
+ return '';
7
+ if (p === 0)
8
+ return chalk.green(' free ');
9
+ return chalk.gray(`$${p.toFixed(p < 0.1 ? 3 : 2)}`);
10
+ }
11
+ function filterModels(models, query, type) {
12
+ const q = query?.toLowerCase().trim();
13
+ return models.filter((m) => {
14
+ if (type && m.type !== type)
15
+ return false;
16
+ if (!q)
17
+ return true;
18
+ return (m.model_id.toLowerCase().includes(q) ||
19
+ (m.name?.toLowerCase().includes(q) ?? false) ||
20
+ (m.description?.toLowerCase().includes(q) ?? false));
21
+ });
22
+ }
23
+ function shortDescription(m, max = 100) {
24
+ if (!m.description)
25
+ return '';
26
+ const single = m.description.replace(/\s+/g, ' ').trim();
27
+ return single.length > max ? single.slice(0, max - 1) + '…' : single;
28
+ }
29
+ export function registerModels(program) {
30
+ program
31
+ .command('models')
32
+ .description('Browse every Wavespeed model live from api.wavespeed.ai (cached for 1h)')
33
+ .argument('[query]', 'Search query (matches id, name, description)')
34
+ .option('-t, --type <name>', 'Filter by type (text-to-image, image-to-video, …)')
35
+ .option('-c, --category <name>', 'Alias for --type')
36
+ .option('--popular', 'Show only the top-sorted models (sort_order <= 10000)')
37
+ .option('--refresh', 'Force a live fetch and refresh the cache')
38
+ .option('--no-cache', 'Skip the cache for this call (do not read or write)')
39
+ .option('--json', 'Emit the raw model list as JSON')
40
+ .action(async (query, opts) => {
41
+ const type = opts.type ?? opts.category;
42
+ const spinner = !opts.json
43
+ ? ora('Fetching models from api.wavespeed.ai…').start()
44
+ : null;
45
+ let payload;
46
+ try {
47
+ payload = await fetchModels({ refresh: opts.refresh, noCache: !opts.cache });
48
+ }
49
+ catch (err) {
50
+ spinner?.fail(err.message ?? String(err));
51
+ if (opts.json) {
52
+ process.stdout.write(JSON.stringify({ error: err.message ?? String(err) }, null, 2) + '\n');
53
+ }
54
+ process.exit(1);
55
+ }
56
+ spinner?.stop();
57
+ let results = filterModels(payload.models, query, type);
58
+ if (opts.popular) {
59
+ results = results
60
+ .filter((m) => (m.sort_order ?? 99999) <= 10000)
61
+ .sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
62
+ }
63
+ else {
64
+ results.sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
65
+ }
66
+ if (opts.json) {
67
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
68
+ return;
69
+ }
70
+ const total = payload.models.length;
71
+ const shown = results.length;
72
+ console.log();
73
+ console.log(chalk.bold(`${shown.toLocaleString()} model${shown === 1 ? '' : 's'}`) +
74
+ chalk.gray(` of ${total.toLocaleString()} live on Wavespeed`) +
75
+ chalk.gray(payload.source === 'cache' ? ' · cached' : ' · fresh'));
76
+ console.log();
77
+ const byType = new Map();
78
+ for (const m of results) {
79
+ const t = m.type || 'other';
80
+ if (!byType.has(t))
81
+ byType.set(t, []);
82
+ byType.get(t).push(m);
83
+ }
84
+ for (const [t, models] of byType) {
85
+ console.log(chalk.hex('#7c5cff').bold(t) + chalk.gray(` ${models.length}`));
86
+ for (const m of models) {
87
+ const id = chalk.cyan(m.model_id.padEnd(52));
88
+ const price = priceLabel(m.base_price);
89
+ console.log(` ${id} ${price}`);
90
+ const d = shortDescription(m);
91
+ if (d)
92
+ console.log(` ${chalk.gray(d)}`);
93
+ }
94
+ console.log();
95
+ }
96
+ console.log(chalk.gray('Run any model with: ') +
97
+ chalk.cyan('wavespeed run <id> --input prompt="…"'));
98
+ console.log(chalk.gray('Refresh the cache: ') + chalk.cyan('wavespeed models --refresh'));
99
+ console.log();
100
+ });
101
+ }
@@ -0,0 +1,143 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import path from 'node:path';
4
+ import fs from 'node:fs';
5
+ import { requireClient } from '../lib/client.js';
6
+ import { parseInputs } from '../lib/inputs.js';
7
+ import { downloadOutputs, printOutputs } from '../lib/output.js';
8
+ import { emitJson, emitJsonError, isJsonMode, log, setJsonMode } from '../lib/log.js';
9
+ import { downloadOptsFromFlag } from '../lib/download-flag.js';
10
+ import { resolveModelToken } from '../lib/config.js';
11
+ function extractUrls(output) {
12
+ if (!output)
13
+ return [];
14
+ if (Array.isArray(output.outputs)) {
15
+ return output.outputs.filter((u) => typeof u === 'string');
16
+ }
17
+ if (typeof output.url === 'string')
18
+ return [output.url];
19
+ return [];
20
+ }
21
+ function failMissingModel() {
22
+ const msg = 'No model specified. Pass a model ID as the first argument, ' +
23
+ 'set `defaultModel` in wavespeed.json, or `wavespeed config --default-model <id>`.';
24
+ if (isJsonMode())
25
+ emitJsonError(msg);
26
+ else {
27
+ console.error(chalk.red('Error: ') + msg);
28
+ console.error(chalk.gray('Browse models: ') + chalk.cyan('wavespeed models'));
29
+ }
30
+ process.exit(1);
31
+ }
32
+ function failUnknownAlias(token) {
33
+ const msg = `"${token}" is neither a model ID (no \`/\`) nor a known alias.`;
34
+ if (isJsonMode())
35
+ emitJsonError(msg);
36
+ else {
37
+ console.error(chalk.red('Error: ') + msg);
38
+ console.error(chalk.gray(' See aliases: ') + chalk.cyan('wavespeed aliases'));
39
+ console.error(chalk.gray(' Browse models: ') + chalk.cyan('wavespeed models'));
40
+ }
41
+ process.exit(1);
42
+ }
43
+ export function registerRun(program) {
44
+ program
45
+ .command('run')
46
+ .description('Run any Wavespeed model — generic invoke with --input key=value pairs')
47
+ .argument('[model]', 'Model ID, alias name, or omit to use defaultModel.')
48
+ .option('-i, --input <pair...>', 'Inputs as key=value (repeatable). Use dotted keys for nested.', [])
49
+ .option('-p, --prompt <text>', 'Shorthand for --input prompt=<text>')
50
+ .option('--input-file <path>', 'JSON file with full input object')
51
+ .option('--download [path]', 'Save outputs locally (optional path template, e.g. "./out/{index}.{ext}")')
52
+ .option('--sync', 'Use synchronous mode (single request, no polling)')
53
+ .option('--json', 'Emit a single JSON object on stdout (progress goes to stderr)')
54
+ .option('--output-dir <dir>', 'Directory for --download when no path is given')
55
+ .action(async (modelArg, opts) => {
56
+ setJsonMode(!!opts.json);
57
+ // Resolve positional → underlying model + any default input from an alias.
58
+ const resolved = resolveModelToken(modelArg);
59
+ if (!resolved) {
60
+ if (modelArg)
61
+ failUnknownAlias(modelArg);
62
+ failMissingModel();
63
+ }
64
+ const model = resolved.model;
65
+ const aliasDefaults = resolved.defaultInput ?? {};
66
+ const aliasName = resolved.alias;
67
+ const client = requireClient();
68
+ // Merge order: alias defaults < --input-file < --input k=v < -p prompt
69
+ let input = { ...aliasDefaults };
70
+ if (opts.inputFile) {
71
+ Object.assign(input, JSON.parse(fs.readFileSync(path.resolve(opts.inputFile), 'utf8')));
72
+ }
73
+ Object.assign(input, parseInputs(opts.input ?? []));
74
+ if (opts.prompt)
75
+ input.prompt = opts.prompt;
76
+ log('');
77
+ log(chalk.bold('▶ ') +
78
+ chalk.cyan(model) +
79
+ (aliasName ? chalk.gray(` (alias: ${aliasName})`) : ''));
80
+ if (input.prompt)
81
+ log(chalk.gray(' prompt: ') + chalk.white(String(input.prompt)));
82
+ const spinner = ora({ text: 'Submitting…', color: 'magenta' });
83
+ if (!isJsonMode())
84
+ spinner.start();
85
+ const startedAt = Date.now();
86
+ let result;
87
+ try {
88
+ const iv = setInterval(() => {
89
+ const s = Math.floor((Date.now() - startedAt) / 1000);
90
+ spinner.text = `Generating… ${s}s`;
91
+ }, 200);
92
+ try {
93
+ result = await client.run(model, input, { enableSyncMode: !!opts.sync });
94
+ }
95
+ finally {
96
+ clearInterval(iv);
97
+ }
98
+ spinner.succeed(`Done in ${Math.floor((Date.now() - startedAt) / 1000)}s.`);
99
+ }
100
+ catch (err) {
101
+ spinner.fail(err.message ?? String(err));
102
+ if (isJsonMode())
103
+ emitJsonError(err.message ?? String(err));
104
+ process.exit(1);
105
+ }
106
+ const urls = extractUrls(result);
107
+ let saved = [];
108
+ const dlOpts = downloadOptsFromFlag(opts.download, {
109
+ model,
110
+ prompt: typeof input.prompt === 'string' ? input.prompt : undefined,
111
+ outputDir: opts.outputDir,
112
+ });
113
+ if (urls.length > 0 && dlOpts) {
114
+ const dlSpinner = ora('Downloading outputs…');
115
+ if (!isJsonMode())
116
+ dlSpinner.start();
117
+ try {
118
+ saved = await downloadOutputs(urls, dlOpts);
119
+ dlSpinner.succeed(`Saved ${saved.length} file${saved.length > 1 ? 's' : ''}.`);
120
+ }
121
+ catch (err) {
122
+ dlSpinner.fail(err.message ?? String(err));
123
+ }
124
+ }
125
+ if (isJsonMode()) {
126
+ emitJson({
127
+ model,
128
+ prompt: typeof input.prompt === 'string' ? input.prompt : undefined,
129
+ outputs: urls,
130
+ saved,
131
+ elapsed_ms: Date.now() - startedAt,
132
+ raw: result,
133
+ });
134
+ return;
135
+ }
136
+ if (urls.length === 0) {
137
+ log(chalk.yellow('No output URLs in response. Full payload:'));
138
+ log(JSON.stringify(result, null, 2));
139
+ return;
140
+ }
141
+ printOutputs(saved, urls);
142
+ });
143
+ }
@@ -0,0 +1,155 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import { fetchModels } from '../lib/api.js';
4
+ function getRequestSchema(model) {
5
+ return model.api_schema?.api_schemas?.[0]?.request_schema;
6
+ }
7
+ function orderedProps(schema) {
8
+ const all = Object.keys(schema.properties ?? {});
9
+ const order = schema['x-order-properties'];
10
+ if (!order)
11
+ return all;
12
+ const seen = new Set();
13
+ const out = [];
14
+ for (const k of order)
15
+ if (k in (schema.properties ?? {}) && !seen.has(k)) {
16
+ out.push(k);
17
+ seen.add(k);
18
+ }
19
+ for (const k of all)
20
+ if (!seen.has(k))
21
+ out.push(k);
22
+ return out;
23
+ }
24
+ function typeLabel(prop) {
25
+ if (Array.isArray(prop.type))
26
+ return prop.type.join('|');
27
+ if (prop.type === 'array' && prop.items?.type)
28
+ return `${prop.items.type}[]`;
29
+ return prop.type ?? 'any';
30
+ }
31
+ function wrap(text, indent, width = 88) {
32
+ const words = text.replace(/\s+/g, ' ').trim().split(' ');
33
+ const lines = [];
34
+ let cur = '';
35
+ for (const w of words) {
36
+ if ((cur + ' ' + w).length > width - indent.length) {
37
+ if (cur)
38
+ lines.push(cur);
39
+ cur = w;
40
+ }
41
+ else {
42
+ cur = cur ? cur + ' ' + w : w;
43
+ }
44
+ }
45
+ if (cur)
46
+ lines.push(cur);
47
+ return lines.map((l) => indent + chalk.gray(l)).join('\n');
48
+ }
49
+ function exampleValue(name, prop) {
50
+ if (prop.default !== undefined)
51
+ return JSON.stringify(prop.default);
52
+ if (prop.enum && prop.enum.length)
53
+ return JSON.stringify(prop.enum[0]);
54
+ const t = Array.isArray(prop.type) ? prop.type[0] : prop.type;
55
+ if (t === 'integer' || t === 'number')
56
+ return String(prop.minimum ?? 1);
57
+ if (t === 'boolean')
58
+ return 'true';
59
+ if (t === 'array')
60
+ return '"<url>"';
61
+ if (name.toLowerCase().includes('prompt'))
62
+ return '"…"';
63
+ if (name.toLowerCase().includes('image') || name.toLowerCase().includes('url'))
64
+ return '"<url>"';
65
+ return '"…"';
66
+ }
67
+ export function registerSchema(program) {
68
+ program
69
+ .command('schema')
70
+ .description('Show the input schema for a model — what params it accepts and which are required')
71
+ .argument('<model>', 'Model ID (e.g. google/nano-banana-2/edit). Run `wavespeed models` to find them.')
72
+ .option('--json', 'Emit the raw JSON schema on stdout')
73
+ .option('--refresh', 'Force a live fetch (bypass the 1h cache)')
74
+ .action(async (modelId, opts) => {
75
+ const spinner = !opts.json ? ora('Loading catalog…').start() : null;
76
+ let models;
77
+ try {
78
+ ({ models } = await fetchModels({ refresh: opts.refresh }));
79
+ }
80
+ catch (err) {
81
+ spinner?.fail(err.message ?? String(err));
82
+ if (opts.json) {
83
+ process.stdout.write(JSON.stringify({ error: err.message ?? String(err) }, null, 2) + '\n');
84
+ }
85
+ process.exit(1);
86
+ }
87
+ spinner?.stop();
88
+ const model = models.find((m) => m.model_id === modelId);
89
+ if (!model) {
90
+ const msg = `Unknown model "${modelId}". Run \`wavespeed models "${modelId.split('/').pop()}"\` to search.`;
91
+ if (opts.json)
92
+ process.stdout.write(JSON.stringify({ error: msg }, null, 2) + '\n');
93
+ else
94
+ console.error(chalk.red(msg));
95
+ process.exit(1);
96
+ }
97
+ const schema = getRequestSchema(model);
98
+ if (!schema) {
99
+ const msg = `Model "${modelId}" has no published input schema.`;
100
+ if (opts.json)
101
+ process.stdout.write(JSON.stringify({ error: msg }, null, 2) + '\n');
102
+ else
103
+ console.error(chalk.yellow(msg));
104
+ process.exit(1);
105
+ }
106
+ if (opts.json) {
107
+ process.stdout.write(JSON.stringify(schema, null, 2) + '\n');
108
+ return;
109
+ }
110
+ const requiredSet = new Set(schema.required ?? []);
111
+ const props = schema.properties ?? {};
112
+ const order = orderedProps(schema);
113
+ console.log();
114
+ console.log(chalk.bold('▶ ') + chalk.cyan(model.model_id));
115
+ console.log(chalk.gray(' type: ') +
116
+ chalk.white(model.type ?? 'unknown') +
117
+ (model.base_price !== undefined ? chalk.gray(' base price: ') + chalk.green(`$${model.base_price}`) : ''));
118
+ if (model.description) {
119
+ console.log(wrap(model.description, ' '));
120
+ }
121
+ console.log();
122
+ console.log(chalk.bold(`Inputs ${chalk.gray(`(${order.length} field${order.length === 1 ? '' : 's'}, ${requiredSet.size} required)`)}`));
123
+ console.log();
124
+ const nameWidth = Math.max(...order.map((n) => n.length), 8);
125
+ for (const name of order) {
126
+ const p = props[name];
127
+ const flag = requiredSet.has(name) ? chalk.red('*') : ' ';
128
+ const t = chalk.magenta(typeLabel(p).padEnd(10));
129
+ console.log(` ${flag} ${chalk.cyan(name.padEnd(nameWidth))} ${t}`);
130
+ if (p.description)
131
+ console.log(wrap(p.description, ' '.padEnd(nameWidth + 6)));
132
+ const tail = [];
133
+ if (p.default !== undefined)
134
+ tail.push(`default: ${JSON.stringify(p.default)}`);
135
+ if (p.enum)
136
+ tail.push(`enum: ${p.enum.map((v) => String(v)).join(' | ')}`);
137
+ if (p.minimum !== undefined || p.maximum !== undefined)
138
+ tail.push(`range: ${p.minimum ?? '−∞'}..${p.maximum ?? '∞'}`);
139
+ if (p.minItems !== undefined || p.maxItems !== undefined)
140
+ tail.push(`items: ${p.minItems ?? 0}..${p.maxItems ?? '∞'}`);
141
+ if (tail.length)
142
+ console.log(' '.repeat(nameWidth + 6) + chalk.gray(tail.join(' · ')));
143
+ }
144
+ // Build a copy-paste run example using required fields (and prompt if present).
145
+ const seedFields = [...requiredSet];
146
+ if (!requiredSet.has('prompt') && 'prompt' in props)
147
+ seedFields.push('prompt');
148
+ const seedParts = seedFields.map((n) => `-i ${n}=${exampleValue(n, props[n])}`);
149
+ console.log();
150
+ console.log(chalk.bold('Try it:'));
151
+ console.log(' ' +
152
+ chalk.cyan(`wavespeed run ${model.model_id}` + (seedParts.length ? ' ' + seedParts.join(' ') : '')));
153
+ console.log();
154
+ });
155
+ }
@@ -0,0 +1,40 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ function skillSourcePath() {
6
+ const here = path.dirname(fileURLToPath(import.meta.url));
7
+ // dist/commands/skill.js -> ../../skills/SKILL.md
8
+ return path.resolve(here, '..', '..', 'skills', 'SKILL.md');
9
+ }
10
+ export function writeSkill(cwd = process.cwd()) {
11
+ const dest = path.join(cwd, '.claude', 'skills', 'wavespeed', 'SKILL.md');
12
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
13
+ fs.copyFileSync(skillSourcePath(), dest);
14
+ return dest;
15
+ }
16
+ export function registerSkill(program) {
17
+ const skill = program
18
+ .command('skill')
19
+ .description('Install a Wavespeed skill for Claude Code, Cursor, Codex, and other coding agents');
20
+ skill
21
+ .command('install', { isDefault: true })
22
+ .description('Drop a SKILL.md into .claude/skills/wavespeed/ so Claude Code, Cursor, and Codex know how to call the CLI')
23
+ .option('--print', 'Print the skill body to stdout instead of writing a file')
24
+ .action((opts) => {
25
+ if (opts.print) {
26
+ process.stdout.write(fs.readFileSync(skillSourcePath(), 'utf8'));
27
+ return;
28
+ }
29
+ const dest = writeSkill();
30
+ console.log();
31
+ console.log(chalk.green('Installed skill: ') + chalk.cyan(path.relative(process.cwd(), dest)));
32
+ console.log();
33
+ console.log(chalk.bold('You\'re set.'));
34
+ console.log(chalk.gray(' Claude Code, Cursor, and other agents auto-discover .claude/skills/*.'));
35
+ console.log(chalk.gray(' Agents will call the CLI directly — no MCP server, no extra config.'));
36
+ console.log();
37
+ console.log(chalk.gray(' Quick check: ask the agent to run ') + chalk.cyan('wavespeed models') + chalk.gray('.'));
38
+ console.log();
39
+ });
40
+ }
@@ -0,0 +1,51 @@
1
+ import chalk from 'chalk';
2
+ import ora from 'ora';
3
+ import fs from 'node:fs';
4
+ import { requireClient } from '../lib/client.js';
5
+ export function registerUpload(program) {
6
+ program
7
+ .command('upload')
8
+ .description('Upload one or more local files to Wavespeed and print their CDN URLs')
9
+ .argument('<file...>', 'Path(s) to file(s)')
10
+ .option('--json', 'Emit a JSON object on stdout')
11
+ .action(async (files, opts) => {
12
+ for (const f of files) {
13
+ if (!fs.existsSync(f)) {
14
+ const msg = `File not found: ${f}`;
15
+ if (opts.json)
16
+ process.stdout.write(JSON.stringify({ error: msg }, null, 2) + '\n');
17
+ else
18
+ console.error(chalk.red(msg));
19
+ process.exit(1);
20
+ }
21
+ }
22
+ const client = requireClient();
23
+ const urls = [];
24
+ const errors = [];
25
+ for (const file of files) {
26
+ const spinner = !opts.json ? ora(`Uploading ${file}…`).start() : null;
27
+ try {
28
+ const url = await client.upload(file);
29
+ urls.push(url);
30
+ spinner?.succeed(`${file} → ${url}`);
31
+ }
32
+ catch (err) {
33
+ const message = err.message ?? String(err);
34
+ errors.push({ file, error: message });
35
+ spinner?.fail(`${file}: ${message}`);
36
+ }
37
+ }
38
+ if (opts.json) {
39
+ process.stdout.write(JSON.stringify(files.length === 1 && urls.length === 1 && errors.length === 0
40
+ ? { url: urls[0] }
41
+ : { urls, errors }, null, 2) + '\n');
42
+ }
43
+ else if (!opts.json && files.length === 1 && urls.length === 1) {
44
+ console.log();
45
+ console.log(chalk.cyan(urls[0]));
46
+ console.log();
47
+ }
48
+ if (errors.length > 0)
49
+ process.exit(1);
50
+ });
51
+ }
@@ -0,0 +1,67 @@
1
+ // Thin direct-API helpers for endpoints not exposed by the `wavespeed` SDK.
2
+ // The SDK only covers run() + upload(). For the catalog and balance we hit the
3
+ // REST API directly.
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import os from 'node:os';
7
+ import { getApiKey, getBaseUrl } from './config.js';
8
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
9
+ function cachePath() {
10
+ return path.join(os.homedir(), '.cache', 'wavespeed', 'models.json');
11
+ }
12
+ function readCache(baseUrl) {
13
+ try {
14
+ const raw = fs.readFileSync(cachePath(), 'utf8');
15
+ const parsed = JSON.parse(raw);
16
+ if (parsed.base_url !== baseUrl)
17
+ return null;
18
+ if (Date.now() - parsed.fetched_at > CACHE_TTL_MS)
19
+ return null;
20
+ return parsed.models;
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ function writeCache(baseUrl, models) {
27
+ try {
28
+ fs.mkdirSync(path.dirname(cachePath()), { recursive: true });
29
+ const body = { fetched_at: Date.now(), base_url: baseUrl, models };
30
+ fs.writeFileSync(cachePath(), JSON.stringify(body));
31
+ }
32
+ catch {
33
+ /* best-effort cache; ignore failures */
34
+ }
35
+ }
36
+ export async function fetchModels(opts = {}) {
37
+ const baseUrl = getBaseUrl();
38
+ if (!opts.noCache && !opts.refresh) {
39
+ const cached = readCache(baseUrl);
40
+ if (cached)
41
+ return { models: cached, source: 'cache' };
42
+ }
43
+ const apiKey = getApiKey();
44
+ if (!apiKey)
45
+ throw new Error('No API key configured. Run `wavespeed login`.');
46
+ const res = await fetch(`${baseUrl}/api/v3/models`, {
47
+ headers: { Authorization: `Bearer ${apiKey}` },
48
+ });
49
+ if (!res.ok) {
50
+ throw new Error(`GET /api/v3/models failed: ${res.status} ${res.statusText}`);
51
+ }
52
+ const json = (await res.json());
53
+ if (json.code !== 200) {
54
+ throw new Error(json.message || `API returned code ${json.code}`);
55
+ }
56
+ if (!opts.noCache)
57
+ writeCache(baseUrl, json.data);
58
+ return { models: json.data, source: 'live' };
59
+ }
60
+ export function clearModelsCache() {
61
+ try {
62
+ fs.unlinkSync(cachePath());
63
+ }
64
+ catch {
65
+ /* nothing to clear */
66
+ }
67
+ }
@@ -0,0 +1,13 @@
1
+ import chalk from 'chalk';
2
+ const LOGO = `
3
+ ██╗ ██╗ █████╗ ██╗ ██╗███████╗███████╗██████╗ ███████╗███████╗██████╗
4
+ ██║ ██║██╔══██╗██║ ██║██╔════╝██╔════╝██╔══██╗██╔════╝██╔════╝██╔══██╗
5
+ ██║ █╗ ██║███████║██║ ██║█████╗ ███████╗██████╔╝█████╗ █████╗ ██║ ██║
6
+ ██║███╗██║██╔══██║╚██╗ ██╔╝██╔══╝ ╚════██║██╔═══╝ ██╔══╝ ██╔══╝ ██║ ██║
7
+ ╚███╔███╔╝██║ ██║ ╚████╔╝ ███████╗███████║██║ ███████╗███████╗██████╔╝
8
+ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚══════╝╚═╝ ╚══════╝╚══════╝╚═════╝
9
+ `;
10
+ export function printBanner() {
11
+ process.stdout.write(chalk.hex('#7c5cff')(LOGO));
12
+ process.stdout.write(chalk.gray(' Every WaveSpeed model — image, video, audio, 3D — from your terminal.\n\n'));
13
+ }
@@ -0,0 +1,19 @@
1
+ import { Client } from 'wavespeed';
2
+ import chalk from 'chalk';
3
+ import { getApiKey, getBaseUrl } from './config.js';
4
+ export function requireClient() {
5
+ const apiKey = getApiKey();
6
+ if (!apiKey) {
7
+ console.error(chalk.red('\nNo API key configured.'));
8
+ console.error(chalk.gray('Run ') + chalk.cyan('wavespeed login') + chalk.gray(' to get started.'));
9
+ console.error(chalk.gray('Or set ') + chalk.cyan('WAVESPEED_API_KEY') + chalk.gray(' in your environment.\n'));
10
+ process.exit(1);
11
+ }
12
+ return new Client(apiKey, { baseUrl: getBaseUrl() });
13
+ }
14
+ export function maybeClient() {
15
+ const apiKey = getApiKey();
16
+ if (!apiKey)
17
+ return null;
18
+ return new Client(apiKey, { baseUrl: getBaseUrl() });
19
+ }
@@ -0,0 +1,86 @@
1
+ import Conf from 'conf';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ export const DEFAULTS = {
5
+ baseUrl: 'https://api.wavespeed.ai',
6
+ outputDir: 'wavespeed-output',
7
+ };
8
+ export const userConfig = new Conf({
9
+ projectName: 'wavespeed',
10
+ defaults: {},
11
+ });
12
+ export const PROJECT_FILE = 'wavespeed.json';
13
+ export function loadProjectConfig(cwd = process.cwd()) {
14
+ let dir = cwd;
15
+ while (true) {
16
+ const candidate = path.join(dir, PROJECT_FILE);
17
+ if (fs.existsSync(candidate)) {
18
+ try {
19
+ return JSON.parse(fs.readFileSync(candidate, 'utf8'));
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ const parent = path.dirname(dir);
26
+ if (parent === dir)
27
+ return null;
28
+ dir = parent;
29
+ }
30
+ }
31
+ export function getApiKey() {
32
+ return process.env.WAVESPEED_API_KEY || userConfig.get('apiKey');
33
+ }
34
+ export function getBaseUrl() {
35
+ return userConfig.get('baseUrl') || DEFAULTS.baseUrl;
36
+ }
37
+ export function resolveOutputDir() {
38
+ const project = loadProjectConfig();
39
+ return project?.outputDir || userConfig.get('outputDir') || DEFAULTS.outputDir;
40
+ }
41
+ export function resolveDefaultModel() {
42
+ const project = loadProjectConfig();
43
+ return project?.defaultModel || userConfig.get('defaultModel');
44
+ }
45
+ /**
46
+ * All defined aliases, project taking precedence over user-level.
47
+ */
48
+ export function loadAliases() {
49
+ const userAliases = userConfig.get('aliases') ?? {};
50
+ const projectAliases = loadProjectConfig()?.aliases ?? {};
51
+ return { ...userAliases, ...projectAliases };
52
+ }
53
+ export function findAlias(name) {
54
+ return loadAliases()[name];
55
+ }
56
+ /**
57
+ * Resolve a positional that may be a model_id, an alias, or undefined.
58
+ * Returns the underlying model_id and any default input the alias supplied.
59
+ *
60
+ * Rules:
61
+ * - tokens containing `/` are always treated as model_ids (never alias-resolved)
62
+ * - otherwise check aliases
63
+ * - if neither: try defaultModel, which itself may be an alias name
64
+ */
65
+ export function resolveModelToken(token) {
66
+ const tryResolve = (value, source) => {
67
+ if (value.includes('/'))
68
+ return { model: value, source };
69
+ const alias = findAlias(value);
70
+ if (alias)
71
+ return { model: alias.model, defaultInput: alias.input, source: 'alias', alias: value };
72
+ return null;
73
+ };
74
+ if (token) {
75
+ const direct = tryResolve(token, 'positional');
76
+ if (direct)
77
+ return direct;
78
+ // Unknown bare name — not a model_id, not an alias. Surface this to the caller
79
+ // so it can show a helpful error instead of silently picking the default.
80
+ return null;
81
+ }
82
+ const def = resolveDefaultModel();
83
+ if (!def)
84
+ return null;
85
+ return tryResolve(def, 'default');
86
+ }