@wavespeed/cli 0.4.2 → 0.4.4

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 CHANGED
@@ -1,6 +1,9 @@
1
1
  <div align="center">
2
2
  <a href="https://wavespeed.ai" target="_blank">
3
- <img src="https://raw.githubusercontent.com/WaveSpeedAI/waverless/main/docs/images/wavespeed-dark-logo.png" alt="WaveSpeedAI Logo" width="200"/>
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="assets/wavespeed-logo-dark.svg">
5
+ <img src="assets/wavespeed-logo-light.svg" alt="WaveSpeed" width="342" height="48"/>
6
+ </picture>
4
7
  </a>
5
8
 
6
9
  <h1>WaveSpeed CLI</h1>
@@ -74,14 +77,14 @@ wavespeed run ... -p "..." --json | jq '.outputs[0]'
74
77
 
75
78
  ```jsonc
76
79
  {
77
- "defaultModel": "google/nano-banana-2/text-to-image",
80
+ "defaultModel": "bytedance/seedream-v5.0-pro",
78
81
  "outputDir": "wavespeed-output",
79
82
  "aliases": {
80
- "hero": { "model": "google/nano-banana-2/text-to-image",
83
+ "hero": { "model": "bytedance/seedream-v5.0-pro",
81
84
  "input": { "aspect_ratio": "16:9", "resolution": "2k" } },
82
- "social": { "model": "google/nano-banana-2/text-to-image",
85
+ "social": { "model": "bytedance/seedream-v5.0-pro",
83
86
  "input": { "aspect_ratio": "1:1", "resolution": "1k" } },
84
- "vid": { "model": "bytedance/seedance-2.0/text-to-video",
87
+ "vid": { "model": "bytedance/seedance-2.5/text-to-video",
85
88
  "input": { "resolution": "720p", "duration": 5 } }
86
89
  }
87
90
  }
@@ -98,10 +101,12 @@ Resolution: positional with `/` is a model ID; otherwise looked up as an alias.
98
101
 
99
102
  | Use case | Model |
100
103
  |---|---|
101
- | Text → image | `google/nano-banana-2/text-to-image` |
102
- | Image edit | `google/nano-banana-2/edit` |
103
- | Text → video | `bytedance/seedance-2.0/text-to-video` |
104
- | Image → video | `bytedance/seedance-2.0/image-to-video` |
104
+ | Text → image | `bytedance/seedream-v5.0-pro` |
105
+ | Image edit | `bytedance/seedream-v5.0-pro/edit` |
106
+ | Text → video | `bytedance/seedance-2.5/text-to-video` |
107
+ | Image → video | `bytedance/seedance-2.5/image-to-video` |
108
+ | Video edit | `bytedance/seedance-2.5/video-edit` |
109
+ | Video extend | `bytedance/seedance-2.5/video-extend` |
105
110
 
106
111
  Browse alternatives with `wavespeed models <query>`.
107
112
 
@@ -115,6 +120,10 @@ wavespeed skill install # writes .claude/skills/wavespeed/SKILL.md
115
120
 
116
121
  The skill teaches the agent the three-step pattern: `models` to find, `run <id> -h` to discover params, `run <id> -p "…" --json` to execute. Every command supports `--json` for clean piping.
117
122
 
123
+ **OpenCode** loads the same file with no extra steps — `.claude/skills/` is on its skill search path.
124
+
125
+ **DeepSeek Harness (dsh)**: the same skill is packaged for dsh's `.dsh/skills` layout at [WaveSpeedAI/wavespeed-dsh-skill](https://github.com/WaveSpeedAI/wavespeed-dsh-skill).
126
+
118
127
  ## Commands
119
128
 
120
129
  ```
@@ -186,3 +195,8 @@ Stack: TypeScript, Commander, Inquirer, Chalk, Ora, the official `wavespeed` SDK
186
195
  ## License
187
196
 
188
197
  MIT — see [LICENSE](LICENSE).
198
+
199
+ ---
200
+
201
+ **[WaveSpeed AI](https://wavespeed.ai/)** — AI image & video generation platform.
202
+ Try it in the browser: **[Image generator](https://wavespeed.ai/image-generator)** · **[Video generator](https://wavespeed.ai/video-generator)**
package/dist/cli.js CHANGED
@@ -44,7 +44,7 @@ ${chalk.bold('Quick start:')}
44
44
  ${chalk.cyan('$ wavespeed models # browse the catalog')}
45
45
  ${chalk.cyan('$ wavespeed run wavespeed-ai/z-image/turbo -h # see the model\'s flags')}
46
46
  ${chalk.cyan('$ wavespeed run wavespeed-ai/z-image/turbo -p "a cyberpunk skyline" # ~5s')}
47
- ${chalk.cyan('$ wavespeed run bytedance/seedance-2.0/text-to-video -p "drone shot over snowy peaks"')}
47
+ ${chalk.cyan('$ wavespeed run bytedance/seedance-2.5/text-to-video -p "drone shot over snowy peaks"')}
48
48
 
49
49
  ${chalk.bold('Use from any coding agent (Claude Code, Cursor, Codex):')}
50
50
  ${chalk.cyan('$ wavespeed skill install')} ${chalk.gray('# drops a SKILL.md so the agent knows how to call wavespeed')}
@@ -23,7 +23,7 @@ export function registerAliases(program) {
23
23
  console.log(chalk.gray(JSON.stringify({
24
24
  aliases: {
25
25
  hero: {
26
- model: 'google/nano-banana-2/text-to-image',
26
+ model: 'bytedance/seedream-v5.0-pro',
27
27
  input: { aspect_ratio: '16:9', resolution: '2k' },
28
28
  },
29
29
  },
@@ -5,6 +5,7 @@ import ora from 'ora';
5
5
  import clipboard from 'clipboardy';
6
6
  import { userConfig, getBaseUrl, getApiKey, loadProjectConfig, PROJECT_FILE } from '../lib/config.js';
7
7
  import { LINKS } from '../lib/links.js';
8
+ import { clientAttributionHeaders } from '../lib/client-headers.js';
8
9
  const KEY_URL = 'https://wavespeed.ai/accesskey';
9
10
  function mask(key) {
10
11
  if (key.length < 12)
@@ -40,7 +41,7 @@ async function validateKey(apiKey, baseUrl) {
40
41
  let res = null;
41
42
  try {
42
43
  res = await fetch(`${baseUrl}/api/v3/balance`, {
43
- headers: { Authorization: `Bearer ${apiKey}` },
44
+ headers: { Authorization: `Bearer ${apiKey}`, ...clientAttributionHeaders() },
44
45
  });
45
46
  }
46
47
  catch (err) {
@@ -108,7 +109,7 @@ async function loginFlow(opts) {
108
109
  console.log(chalk.bold('Try it:'));
109
110
  console.log(' ' + chalk.cyan('wavespeed models # browse the catalog'));
110
111
  console.log(' ' + chalk.cyan('wavespeed run wavespeed-ai/z-image/turbo -p "a cyberpunk skyline at golden hour"'));
111
- console.log(' ' + chalk.cyan('wavespeed run bytedance/seedance-2.0/text-to-video -p "drone shot over snowy peaks"'));
112
+ console.log(' ' + chalk.cyan('wavespeed run bytedance/seedance-2.5/text-to-video -p "drone shot over snowy peaks"'));
112
113
  console.log();
113
114
  }
114
115
  export function registerAuth(program) {
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { input, confirm } from '@inquirer/prompts';
5
5
  import { PROJECT_FILE } from '../lib/config.js';
6
- const STARTER_MODEL = 'google/nano-banana-2/text-to-image';
6
+ const STARTER_MODEL = 'bytedance/seedream-v5.0-pro';
7
7
  export function registerInit(program) {
8
8
  program
9
9
  .command('init')
@@ -3,7 +3,7 @@ import ora from 'ora';
3
3
  import path from 'node:path';
4
4
  import fs from 'node:fs';
5
5
  import { requireClient } from '../lib/client.js';
6
- import { parseInputs } from '../lib/inputs.js';
6
+ import { parseInputs, withInputSyntaxHint } from '../lib/inputs.js';
7
7
  import { resolveLocalFiles } from '../lib/local-files.js';
8
8
  import { submitPrediction, waitForPrediction } from '../lib/api.js';
9
9
  import { uploadWithCache } from '../lib/upload-cache.js';
@@ -137,7 +137,7 @@ export function registerRun(program) {
137
137
  submitted = await submitPrediction(model, input, { sync: !!opts.sync });
138
138
  }
139
139
  catch (err) {
140
- const message = err.message ?? String(err);
140
+ const message = withInputSyntaxHint(err.message ?? String(err), opts.input);
141
141
  spinner.fail(message);
142
142
  if (isJsonMode())
143
143
  emitJsonError(message);
package/dist/lib/api.js CHANGED
@@ -5,6 +5,7 @@ import fs from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import os from 'node:os';
7
7
  import { getApiKey, getBaseUrl } from './config.js';
8
+ import { clientAttributionHeaders } from './client-headers.js';
8
9
  const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
9
10
  function cachePath() {
10
11
  return path.join(os.homedir(), '.cache', 'wavespeed', 'models.json');
@@ -44,7 +45,7 @@ export async function fetchModels(opts = {}) {
44
45
  if (!apiKey)
45
46
  throw new Error('No API key configured. Run `wavespeed login`.');
46
47
  const res = await fetch(`${baseUrl}/api/v3/models`, {
47
- headers: { Authorization: `Bearer ${apiKey}` },
48
+ headers: { Authorization: `Bearer ${apiKey}`, ...clientAttributionHeaders() },
48
49
  });
49
50
  if (!res.ok) {
50
51
  throw await httpError(res, 'GET', '/api/v3/models');
@@ -70,7 +71,7 @@ function authHeaders() {
70
71
  const apiKey = getApiKey();
71
72
  if (!apiKey)
72
73
  throw new Error('No API key configured. Run `wavespeed login`.');
73
- return { Authorization: `Bearer ${apiKey}` };
74
+ return { Authorization: `Bearer ${apiKey}`, ...clientAttributionHeaders() };
74
75
  }
75
76
  // Non-2xx responses still carry the platform's error envelope, and for
76
77
  // permission failures that body is the whole point: it names the role that
@@ -0,0 +1,40 @@
1
+ // Channel-attribution headers (X-Client-Name / X-Client-Version / X-Client-OS)
2
+ // for the CLI's direct REST calls, matching the convention wavespeed-desktop
3
+ // uses. Calls that go through the `wavespeed` SDK get the same headers from
4
+ // the SDK itself (via the clientName option in client.ts).
5
+ import fs from 'node:fs';
6
+ import path from 'node:path';
7
+ import os from 'node:os';
8
+ import { fileURLToPath } from 'node:url';
9
+ export const CLI_CLIENT_NAME = 'wavespeed-cli';
10
+ // Read version from package.json at runtime so a bump in package.json is
11
+ // reflected automatically (same approach as cli.ts).
12
+ function cliVersion() {
13
+ try {
14
+ const here = path.dirname(fileURLToPath(import.meta.url));
15
+ // dist/lib/client-headers.js → ../../package.json
16
+ const pkgPath = path.resolve(here, '..', '..', 'package.json');
17
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;
18
+ }
19
+ catch {
20
+ return '0.0.0';
21
+ }
22
+ }
23
+ // Lowercase OS name: darwin / linux / windows (win32 is reported as windows).
24
+ function operatingSystem() {
25
+ const platform = os.platform();
26
+ return platform === 'win32' ? 'windows' : platform;
27
+ }
28
+ // The WAVESPEED_CLIENT_NAME environment variable takes priority so wrapper
29
+ // channels (dsh skill, Claude plugin, Gemini extension) can brand themselves
30
+ // without code changes.
31
+ export function resolveClientName() {
32
+ return process.env.WAVESPEED_CLIENT_NAME || CLI_CLIENT_NAME;
33
+ }
34
+ export function clientAttributionHeaders() {
35
+ return {
36
+ 'X-Client-Name': resolveClientName(),
37
+ 'X-Client-Version': cliVersion(),
38
+ 'X-Client-OS': operatingSystem(),
39
+ };
40
+ }
@@ -1,6 +1,7 @@
1
1
  import { Client } from 'wavespeed';
2
2
  import chalk from 'chalk';
3
3
  import { getApiKey, getBaseUrl } from './config.js';
4
+ import { CLI_CLIENT_NAME } from './client-headers.js';
4
5
  export function requireClient() {
5
6
  const apiKey = getApiKey();
6
7
  if (!apiKey) {
@@ -9,11 +10,11 @@ export function requireClient() {
9
10
  console.error(chalk.gray('Or set ') + chalk.cyan('WAVESPEED_API_KEY') + chalk.gray(' in your environment.\n'));
10
11
  process.exit(1);
11
12
  }
12
- return new Client(apiKey, { baseUrl: getBaseUrl() });
13
+ return new Client(apiKey, { baseUrl: getBaseUrl(), clientName: CLI_CLIENT_NAME });
13
14
  }
14
15
  export function maybeClient() {
15
16
  const apiKey = getApiKey();
16
17
  if (!apiKey)
17
18
  return null;
18
- return new Client(apiKey, { baseUrl: getBaseUrl() });
19
+ return new Client(apiKey, { baseUrl: getBaseUrl(), clientName: CLI_CLIENT_NAME });
19
20
  }
@@ -2,8 +2,9 @@
2
2
  // - bare strings stay strings
3
3
  // - "123" / "1.5" parsed as number
4
4
  // - "true" / "false" parsed as boolean
5
- // - "[a,b,c]" parsed as array of strings
6
- // - JSON values (starting with { or [) parsed as JSON
5
+ // - values starting with { or [ are parsed as JSON, and stay strings if that
6
+ // fails — so an array has to be written as valid JSON: '["a","b"]'.
7
+ // A bare "[a,b,c]" is not JSON and reaches the API as a string.
7
8
  // - keys with dots like "size.width" become nested
8
9
  function coerce(raw) {
9
10
  if (raw === 'true')
@@ -50,3 +51,16 @@ function setPath(obj, dotted, value) {
50
51
  }
51
52
  cur[parts[parts.length - 1]] = value;
52
53
  }
54
+ /**
55
+ * The API rejects a scalar sent to an array field. The message says what is
56
+ * wrong but not how to write it, and `-i key=value` gives no hint that an
57
+ * array needs JSON — so point at the fix rather than mutating the input.
58
+ */
59
+ export function withInputSyntaxHint(message, pairs = []) {
60
+ const field = /field "([^"]+)" must be an array/.exec(message)?.[1];
61
+ if (!field)
62
+ return message;
63
+ const given = pairs.find((p) => p.slice(0, p.indexOf('=')).trim() === field);
64
+ const sample = given ? given.slice(given.indexOf('=') + 1) : 'value';
65
+ return `${message}\nWrite arrays as JSON: -i '${field}=["${sample}"]'`;
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wavespeed/cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "WaveSpeed CLI — one command to run any WaveSpeed AI model from your terminal.",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -56,7 +56,7 @@
56
56
  "conf": "^13.0.0",
57
57
  "open": "^10.1.0",
58
58
  "ora": "^8.1.0",
59
- "wavespeed": "^0.2.6"
59
+ "wavespeed": "^0.3.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^20.14.0",
package/skills/SKILL.md CHANGED
@@ -11,14 +11,14 @@ You have access to the `wavespeed` CLI. Every generation flows through one verb.
11
11
 
12
12
  ```bash
13
13
  # 1. FIND a model — search the live catalog
14
- wavespeed models "nano banana"
14
+ wavespeed models "seedream"
15
15
  wavespeed models --type image-to-video --popular
16
16
 
17
17
  # 2. INSPECT its inputs — dynamic schema, per model
18
- wavespeed run google/nano-banana-2/text-to-image -h
18
+ wavespeed run bytedance/seedream-v5.0-pro -h
19
19
 
20
20
  # 3. RUN it — always pass --json so you can read the result
21
- wavespeed run google/nano-banana-2/text-to-image \
21
+ wavespeed run bytedance/seedream-v5.0-pro \
22
22
  -p "a cyberpunk skyline at golden hour" \
23
23
  -i aspect_ratio="16:9" -i resolution="2k" --json
24
24
  ```
@@ -29,10 +29,12 @@ wavespeed run google/nano-banana-2/text-to-image \
29
29
 
30
30
  | Use case | Model |
31
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` |
32
+ | Text → image | `bytedance/seedream-v5.0-pro` |
33
+ | Image edit (instruction-driven) | `bytedance/seedream-v5.0-pro/edit` — requires `images: [url, ...]` |
34
+ | Text → video | `bytedance/seedance-2.5/text-to-video` |
35
+ | Image → video | `bytedance/seedance-2.5/image-to-video` — requires `image: url` |
36
+ | Video edit (instruction-driven) | `bytedance/seedance-2.5/video-edit` — requires `video: url` |
37
+ | Video extend | `bytedance/seedance-2.5/video-extend` — requires `video: url` |
36
38
 
37
39
  These are good starting points. Browse alternatives with `wavespeed models <query>`.
38
40
 
@@ -40,12 +42,12 @@ These are good starting points. Browse alternatives with `wavespeed models <quer
40
42
 
41
43
  ```bash
42
44
  # Edit an existing image — @path uploads the file and passes its URL (one step)
43
- wavespeed run google/nano-banana-2/edit \
45
+ wavespeed run bytedance/seedream-v5.0-pro/edit \
44
46
  -p "replace the background with a sunlit kitchen" \
45
47
  -i images='["@./input.jpg"]' --json
46
48
 
47
49
  # Image-to-video — same @ marker for single-URL fields
48
- wavespeed run bytedance/seedance-2.0/image-to-video \
50
+ wavespeed run bytedance/seedance-2.5/image-to-video \
49
51
  -p "subtle parallax, gentle wind" \
50
52
  -i image=@./hero.jpg -i duration=5 --json
51
53