@flatkey-ai/cli 0.1.4 → 0.1.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Flatkey AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,50 @@
1
- # @flatkey-ai/cli
1
+ # Flatkey CLI
2
2
 
3
- Unified Flatkey CLI for media workers. Generate images, videos, and audio through Flatkey credits.
3
+ [![npm version](https://img.shields.io/npm/v/@flatkey-ai/cli.svg)](https://www.npmjs.com/package/@flatkey-ai/cli)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@flatkey-ai/cli.svg)](https://www.npmjs.com/package/@flatkey-ai/cli)
5
+ [![Publish npm](https://github.com/flatkey-ai/flatkey-cli/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/flatkey-ai/flatkey-cli/actions/workflows/npm-publish.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+
8
+ Creative generation from the terminal for media teams and AI agents. Generate images, videos, audio, and text through one Flatkey credit balance.
9
+
10
+ Languages: [English](README.md) | [简体中文](docs/readme/README.zh-CN.md) | [日本語](docs/readme/README.ja-JP.md)
11
+
12
+ Website: [flatkey.ai](https://flatkey.ai/?utm_source=github&utm_medium=readme&utm_campaign=flatkey_cli)
13
+
14
+ ```bash
15
+ npm install -g @flatkey-ai/cli
16
+ export FLATKEY_API_KEY=<your-flatkey-api-key>
17
+
18
+ flatkey image generate --prompt "editorial cover, neon city at dawn" -o cover.png
19
+ flatkey video generate --prompt "cinematic product launch clip" --model seedance2 -o launch.mp4
20
+ flatkey audio generate --prompt "你好,这是 Flatkey 的配音测试" -o speech.mp3
21
+ flatkey models --json
22
+ flatkey credits --json
23
+ ```
24
+
25
+ ## Why Flatkey CLI
26
+
27
+ - One CLI for image, video, audio, and text generation.
28
+ - One `FLATKEY_API_KEY`, one credit balance.
29
+ - Agent-friendly JSON mode with clean stdout.
30
+ - Local output support with `--output` / `-o`.
31
+ - Cross-platform npm package for macOS, Linux, and Windows.
32
+ - Cool terminal progress animation for human runs; disabled in `--json` mode.
33
+
34
+ ## Product Comparison
35
+
36
+ Flatkey is built for media teams that want programmatic generation without paying a premium for a closed creative suite or waiting in long queues. Higgsfield is strong for creator-facing workflows, and Dreamina/Jimeng has good ByteDance video models, but both can become expensive or slow once the workflow moves from manual creation to repeated AI-agent testing. In our testing, Dreamina/Jimeng video generation can wait up to 5 hours during peak time, which makes one-by-one prompt iteration impractical.
37
+
38
+ | Need | Flatkey | Higgsfield | Dreamina / Jimeng |
39
+ | --- | --- | --- | --- |
40
+ | Cost for repeat testing | Flatkey token pricing is at least ~20% lower than comparable retail routes, and some modalities such as image can be ~40% lower. | Credit packages can be expensive once premium video/image models are used heavily. | Subscription credits can be cheaper in some local workflows, but API-style automation usually has plan and region constraints. |
41
+ | Programmatic access | First-class CLI and API-router workflow. Designed for agents, scripts, and batch testing. | Strong product UI for creators; CLI-style automation is not the primary product promise. | CLI/API access is gated behind higher-tier setup in many workflows, not the default creator entry path. |
42
+ | Queue / iteration speed | Built for fast CLI retries, `--dry-run`, local outputs, and unified model routing. | Good for polished creator workflows, but heavy credit usage makes broad testing costly. | Peak-time queues can block video iteration; we have observed waits up to 5 hours for a single video generation test. |
43
+ | Modalities | Image, video, TTS, SFX, music, text, credits, status, and model discovery behind one Flatkey key. | Strong image/video creator stack and media tooling. | Strong ByteDance image/video models, especially Seedance/Dreamina-style workflows. |
44
+ | Agent ergonomics | `--json`, `--output/-o`, `--dry-run`, `models`, `audio voices`, and predictable stdout/stderr. | More oriented around human creative workflows. | More oriented around app/product workflow than simple agent protocol. |
45
+ | Best fit | Media teams building repeatable generation pipelines and AI-agent workflows. | Creators who want a polished visual production suite. | Teams already deep in CapCut/Dreamina/Jimeng ecosystem and willing to accept plan/queue tradeoffs. |
46
+
47
+ In short: use Flatkey when the job is repeated media generation, cost-sensitive testing, and AI-agent automation. Use creator suites when you want their native editing UI more than a programmable router.
4
48
 
5
49
  ## Install
6
50
 
@@ -8,46 +52,204 @@ Unified Flatkey CLI for media workers. Generate images, videos, and audio throug
8
52
  npm install -g @flatkey-ai/cli
9
53
  ```
10
54
 
11
- Or run without installing:
55
+ Run without installing:
12
56
 
13
57
  ```bash
14
58
  npx @flatkey-ai/cli help --ai
15
59
  ```
16
60
 
17
- ## Auth
61
+ Check local version:
18
62
 
19
63
  ```bash
20
- flatkey onboard --api-key <key>
64
+ flatkey --version
65
+ flatkey -v
21
66
  ```
22
67
 
23
- Or use:
68
+ ## Auth
69
+
70
+ Use an environment variable:
24
71
 
25
72
  ```bash
26
73
  export FLATKEY_API_KEY=<key>
27
74
  ```
28
75
 
76
+ Or save it locally:
77
+
78
+ ```bash
79
+ flatkey onboard --api-key <key>
80
+ ```
81
+
82
+ Create a key at [console.flatkey.ai/keys](https://console.flatkey.ai/keys), then pass it with `--api-key`.
83
+
29
84
  ## Commands
30
85
 
86
+ ### Generate Image
87
+
88
+ ```bash
89
+ flatkey image generate \
90
+ --prompt "magazine cover, reflective typography, studio lighting" \
91
+ --model gpt-image-2 \
92
+ -o cover.png
93
+ ```
94
+
95
+ ### Generate Video
96
+
97
+ ```bash
98
+ flatkey video generate \
99
+ --prompt "8 second cinematic product reveal, glossy black background" \
100
+ --model seedance2 \
101
+ --ratio 16:9 \
102
+ --resolution 720p \
103
+ -o launch.mp4
104
+ ```
105
+
106
+ Video ratios: `16:9`, `9:16`, `4:3`, `3:4`, `21:9`, `1:1`.
107
+ Video resolutions: `480p`, `720p`, `1080p`.
108
+
109
+ ### Generate Audio
110
+
111
+ Text to speech:
112
+
113
+ ```bash
114
+ flatkey audio generate \
115
+ --prompt "你好,这是 Flatkey 网关的语音测试。" \
116
+ --voice-id EXAVITQu4vr4xnSDxMaL \
117
+ --model eleven_multilingual_v2 \
118
+ --stability 0.5 \
119
+ --similarity-boost 0.75 \
120
+ --style 0 \
121
+ -o speech.mp3
122
+ ```
123
+
124
+ List voices:
125
+
126
+ ```bash
127
+ flatkey audio voices --json
128
+ ```
129
+
130
+ Sound effects:
131
+
132
+ ```bash
133
+ flatkey audio sfx \
134
+ --prompt "glass shattering on the floor" \
135
+ --duration 3 \
136
+ -o sfx.mp3
137
+ ```
138
+
139
+ Music:
140
+
141
+ ```bash
142
+ flatkey audio music \
143
+ --prompt "calm ambient piano, sad mood" \
144
+ --music-length-ms 10000 \
145
+ -o music.mp3
146
+ ```
147
+
148
+ ### Generate Text
149
+
150
+ ```bash
151
+ flatkey text generate \
152
+ --prompt "write 5 sharp headlines for a creator tool launch" \
153
+ --model gpt-5.5 \
154
+ -o headlines.txt
155
+ ```
156
+
157
+ ### Credits, Status, Models
158
+
31
159
  ```bash
32
- flatkey image generate --prompt "magazine cover" --json
33
- flatkey image generate --prompt "magazine cover" --output cover.png --json
34
- flatkey video generate --prompt "product launch clip" --json
35
- flatkey audio generate --prompt "30 second voiceover" --json
36
- flatkey text generate --prompt "write a headline" --model gpt-5.5 --json
37
160
  flatkey credits --json
38
161
  flatkey status --json
39
162
  flatkey models --json
163
+ flatkey models --type image --json
164
+ flatkey help --ai
165
+ ```
166
+
167
+ ## Models
168
+
169
+ Live model list:
170
+
171
+ ```bash
172
+ flatkey models --json
173
+ ```
174
+
175
+ Useful current defaults:
176
+
177
+ | Type | Models |
178
+ | --- | --- |
179
+ | Image | `gpt-image-2`, `nano-banana-pro-preview`, Gemini image models |
180
+ | Video | `seedance2`, `veo-3`, `veo-3-fast` |
181
+ | Audio | `eleven_multilingual_v2`, ElevenLabs voices, sound effects, music |
182
+ | Text | `gpt-5.5`, Claude, Gemini, GLM, Grok models |
183
+
184
+ `models` reads from the Flatkey console model registry. Generation calls use the Flatkey router.
185
+
186
+ ## Agent Protocol
187
+
188
+ Use this when wiring Flatkey into Codex, Claude Code, Cursor, or other agents:
189
+
190
+ ```bash
40
191
  flatkey help --ai
41
192
  ```
42
193
 
43
- Default router: `https://router.flatkey.ai`.
194
+ Agent rules:
195
+
196
+ - Prefer `FLATKEY_API_KEY`.
197
+ - Always pass `--json` for machine-readable output.
198
+ - Use `--output` / `-o` when the generated file path matters.
199
+ - Call `flatkey models --json` before choosing a model.
200
+ - Call `flatkey audio voices --json` before choosing a TTS `voice_id`.
201
+ - Use `--dry-run` to inspect request shape without spending credits.
202
+
203
+ Example dry run:
204
+
205
+ ```bash
206
+ flatkey video generate \
207
+ --prompt "fashion campaign hero shot" \
208
+ --model seedance2 \
209
+ --dry-run \
210
+ --json
211
+ ```
212
+
213
+ ## Routing
214
+
215
+ - Generation router: `https://router.flatkey.ai`
216
+ - Model registry: `https://console.flatkey.ai/v1/available_models`
217
+ - Voice registry: `https://router.flatkey.ai/v1/voices`
218
+
219
+ Override router only when developing or testing:
220
+
221
+ ```bash
222
+ flatkey image generate --prompt "test" --base-url http://127.0.0.1:3000 --json
223
+ ```
44
224
 
45
225
  ## Release Channels
46
226
 
47
- Primary release is npm package `@flatkey-ai/cli`.
227
+ Primary release:
228
+
229
+ - npm package: [`@flatkey-ai/cli`](https://www.npmjs.com/package/@flatkey-ai/cli)
48
230
 
49
231
  Planned wrappers:
50
232
 
51
233
  - Homebrew formula.
52
234
  - Debian `.deb` package for apt-based install.
53
235
  - Windows MSI package.
236
+
237
+ ## Development
238
+
239
+ ```bash
240
+ git clone https://github.com/flatkey-ai/flatkey-cli.git
241
+ cd flatkey-cli
242
+ npm install
243
+ npm test
244
+ node bin/flatkey.js --version
245
+ ```
246
+
247
+ ## Star History
248
+
249
+ [![Star History Chart](https://api.star-history.com/svg?repos=flatkey-ai/flatkey-cli&type=Date)](https://www.star-history.com/#flatkey-ai/flatkey-cli&Date)
250
+
251
+ ## Links
252
+
253
+ - Website: [flatkey.ai](https://flatkey.ai/?utm_source=github&utm_medium=readme&utm_campaign=flatkey_cli_links)
254
+ - npm: [`@flatkey-ai/cli`](https://www.npmjs.com/package/@flatkey-ai/cli)
255
+ - Issues: [github.com/flatkey-ai/flatkey-cli/issues](https://github.com/flatkey-ai/flatkey-cli/issues)
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@flatkey-ai/cli",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Flatkey media generation CLI for image, video, audio, credits, status, and models.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "flatkey": "bin/flatkey.js"
8
8
  },
9
+ "files": [
10
+ "bin",
11
+ "src"
12
+ ],
9
13
  "scripts": {
10
14
  "test": "node --test"
11
15
  },
package/src/api.js CHANGED
@@ -43,28 +43,75 @@ export function generateVideo(options) {
43
43
  }
44
44
 
45
45
  export function planVideoRequest(options) {
46
+ const ratio = validateOptionalValue(
47
+ optionValue(options, "ratio", "aspect"),
48
+ ["16:9", "9:16", "4:3", "3:4", "21:9", "1:1"],
49
+ "ratio",
50
+ );
51
+ const resolution = validateOptionalValue(
52
+ options.resolution,
53
+ ["480p", "720p", "1080p"],
54
+ "resolution",
55
+ );
46
56
  return planJsonPost(options, "/v1/video/generations", cleanObject({
47
57
  model: options.model ?? "veo-3",
48
58
  prompt: options.prompt,
49
59
  duration: parseOptionalInteger(options.duration),
50
- aspect: options.aspect,
60
+ aspect: ratio,
61
+ ratio,
62
+ resolution,
63
+ quality: resolution,
51
64
  fps: parseOptionalInteger(options.fps),
52
65
  }));
53
66
  }
54
67
 
55
68
  export function generateAudio(options) {
56
- return requestJsonFromPlan(options, planAudioRequest(options));
69
+ return requestBinaryArtifactFromPlan(options, planAudioRequest(options));
57
70
  }
58
71
 
59
72
  export function planAudioRequest(options) {
60
- return planJsonPost(options, "/v1/audio/generations", cleanObject({
61
- model: options.model ?? "tts-1",
73
+ const voiceId = optionValue(options, "voiceId", "voice_id") ?? "EXAVITQu4vr4xnSDxMaL";
74
+ return planJsonPost(options, `/v1/text-to-speech/${encodeURIComponent(voiceId)}`, cleanObject({
75
+ text: options.text ?? options.prompt,
76
+ model_id: options.model ?? options.model_id ?? "eleven_multilingual_v2",
77
+ voice_settings: cleanObject({
78
+ stability: parseOptionalFloat(options.stability),
79
+ similarity_boost: parseOptionalFloat(optionValue(options, "similarityBoost", "similarity_boost")),
80
+ style: parseOptionalFloat(options.style),
81
+ }),
82
+ }));
83
+ }
84
+
85
+ export function generateAudioSfx(options) {
86
+ return requestBinaryArtifactFromPlan(options, planAudioSfxRequest(options));
87
+ }
88
+
89
+ export function planAudioSfxRequest(options) {
90
+ return planJsonPost(options, "/v1/sound-generation", cleanObject({
91
+ text: options.text ?? options.prompt,
92
+ duration_seconds: parseOptionalFloat(optionValue(options, "durationSeconds", "duration_seconds", "duration")),
93
+ }));
94
+ }
95
+
96
+ export function generateAudioMusic(options) {
97
+ return requestBinaryArtifactFromPlan(options, planAudioMusicRequest(options));
98
+ }
99
+
100
+ export function planAudioMusicRequest(options) {
101
+ return planJsonPost(options, "/v1/music", cleanObject({
62
102
  prompt: options.prompt,
63
- voice: options.voice,
64
- format: options.format,
103
+ music_length_ms: parseOptionalInteger(optionValue(options, "musicLengthMs", "music_length_ms")),
65
104
  }));
66
105
  }
67
106
 
107
+ export function getVoices(options) {
108
+ return requestJson(options, "/v1/voices");
109
+ }
110
+
111
+ export function planVoicesRequest(options) {
112
+ return planRequest(options, "/v1/voices");
113
+ }
114
+
68
115
  export function generateText(options) {
69
116
  return requestJsonFromPlan(options, planTextRequest(options));
70
117
  }
@@ -128,6 +175,24 @@ async function requestJsonFromPlan(options, plan) {
128
175
  return body;
129
176
  }
130
177
 
178
+ async function requestBinaryArtifactFromPlan(options, plan) {
179
+ const fetchImpl = options.fetch ?? fetch;
180
+ const response = await fetchImpl(plan.url, {
181
+ method: plan.method,
182
+ headers: plan.headers,
183
+ body: plan.body === undefined ? undefined : JSON.stringify(plan.body),
184
+ });
185
+ if (!response.ok) {
186
+ const body = await readJson(response);
187
+ throw new FlatkeyError(extractErrorMessage(body, response.status), {
188
+ status: response.status,
189
+ });
190
+ }
191
+ return {
192
+ data: [{ data: Buffer.from(await response.arrayBuffer()).toString("base64") }],
193
+ };
194
+ }
195
+
131
196
  async function requestJson(options, path, init = {}) {
132
197
  return requestJsonFromPlan(options, planRequest(options, path, {
133
198
  method: init.method ?? "GET",
@@ -159,6 +224,24 @@ function parseOptionalInteger(value) {
159
224
  return Number.parseInt(value, 10);
160
225
  }
161
226
 
227
+ function parseOptionalFloat(value) {
228
+ if (value === undefined) return undefined;
229
+ return Number.parseFloat(value);
230
+ }
231
+
232
+ function optionValue(options, ...keys) {
233
+ for (const key of keys) {
234
+ if (options[key] !== undefined) return options[key];
235
+ }
236
+ return undefined;
237
+ }
238
+
239
+ function validateOptionalValue(value, allowed, name) {
240
+ if (value === undefined) return undefined;
241
+ if (allowed.includes(value)) return value;
242
+ throw new Error(`Invalid ${name}: ${value}. Allowed values: ${allowed.join(", ")}`);
243
+ }
244
+
162
245
  function cleanObject(value) {
163
246
  return Object.fromEntries(
164
247
  Object.entries(value).filter(([, entry]) => entry !== undefined),
package/src/cli.js CHANGED
@@ -18,16 +18,44 @@ export function parseArgv(argv) {
18
18
  if (!group) {
19
19
  return { group: "help", action: undefined, options: {} };
20
20
  }
21
+ if (group === "--help" || group === "-h") {
22
+ return { group: "help", action: undefined, options: {} };
23
+ }
21
24
  if (group === "--version" || group === "-v") {
22
25
  return { group: "version", action: undefined, options: {} };
23
26
  }
24
27
  if (!COMMANDS.has(group)) {
25
28
  throw new Error(`Unknown command: ${group}`);
26
29
  }
30
+ if (group === "help" && maybeAction && !maybeAction.startsWith("--")) {
31
+ return {
32
+ group: "help",
33
+ action: undefined,
34
+ options: { command: maybeAction, ...parseOptions(rest) },
35
+ };
36
+ }
27
37
 
28
38
  const hasAction = GROUP_ACTIONS.has(group);
39
+ if (hasAction && isHelpToken(maybeAction)) {
40
+ return { group, action: undefined, options: { help: true } };
41
+ }
42
+ if (!hasAction && isHelpToken(maybeAction)) {
43
+ return { group, action: undefined, options: { help: true } };
44
+ }
29
45
  const action = hasAction ? maybeAction : undefined;
30
46
  const optionTokens = hasAction ? rest : argv.slice(1);
47
+ const hasHelpOption = optionTokens.some((token) => token === "--help" || token === "-h")
48
+ || (optionTokens.length === 1 && optionTokens[0] === "help");
49
+ if (hasHelpOption) {
50
+ return {
51
+ group,
52
+ action,
53
+ options: {
54
+ ...parseOptions(optionTokens.filter((token) => token !== "help" && token !== "--help" && token !== "-h")),
55
+ help: true,
56
+ },
57
+ };
58
+ }
31
59
 
32
60
  return {
33
61
  group,
@@ -36,6 +64,10 @@ export function parseArgv(argv) {
36
64
  };
37
65
  }
38
66
 
67
+ function isHelpToken(token) {
68
+ return token === "help" || token === "--help" || token === "-h";
69
+ }
70
+
39
71
  function parseOptions(tokens) {
40
72
  const options = {};
41
73
  for (let index = 0; index < tokens.length; index += 1) {
@@ -66,6 +98,11 @@ function parseOptions(tokens) {
66
98
 
67
99
  export async function main(argv) {
68
100
  const command = parseArgv(argv);
101
+ if (command.options.help) {
102
+ const result = await runCommand(command);
103
+ process.stdout.write(`${formatHuman(result)}\n`);
104
+ return;
105
+ }
69
106
  if (command.group === "onboard") {
70
107
  const { writeConfig } = await import("./config.js");
71
108
  const configPath = await writeConfig({ apiKey: command.options.api_key });
@@ -86,9 +123,14 @@ export async function runCommand(command, deps = {}) {
86
123
  const stderr = deps.stderr ?? process.stderr;
87
124
 
88
125
  if (command.group === "help") {
89
- const { getAiHelp, getHumanHelp } = await import("./help.js");
126
+ const { getAiHelp, getCommandHelp, getHumanHelp } = await import("./help.js");
127
+ if (command.options.command) return getCommandHelp(command.options.command);
90
128
  return command.options.ai ? getAiHelp() : getHumanHelp();
91
129
  }
130
+ if (command.options.help) {
131
+ const { getCommandHelp } = await import("./help.js");
132
+ return getCommandHelp(command.group, command.action);
133
+ }
92
134
  if (command.group === "version") {
93
135
  const version = await readPackageVersion();
94
136
  return command.options.json ? { version } : version;
@@ -103,7 +145,13 @@ export async function runCommand(command, deps = {}) {
103
145
  }
104
146
 
105
147
  if (["image", "video", "audio", "text"].includes(command.group)) {
106
- if (command.action !== "generate") {
148
+ if (command.group === "audio" && command.action === "voices") {
149
+ return handleVoices(command, deps);
150
+ }
151
+ const validAction = command.group === "audio"
152
+ ? ["generate", "sfx", "music"].includes(command.action)
153
+ : command.action === "generate";
154
+ if (!validAction) {
107
155
  throw new Error(`Unknown action for ${command.group}: ${command.action}`);
108
156
  }
109
157
  return handleGenerate(command, { ...deps, stdout, stderr });
@@ -116,10 +164,14 @@ async function handleGenerate(command, deps) {
116
164
  const { resolveApiKey } = await import("./config.js");
117
165
  const {
118
166
  generateAudio,
167
+ generateAudioMusic,
168
+ generateAudioSfx,
119
169
  generateImage,
120
170
  generateText,
121
171
  generateVideo,
172
+ planAudioMusicRequest,
122
173
  planAudioRequest,
174
+ planAudioSfxRequest,
123
175
  planImageRequest,
124
176
  planTextRequest,
125
177
  planVideoRequest,
@@ -144,7 +196,11 @@ async function handleGenerate(command, deps) {
144
196
  : command.group === "video"
145
197
  ? planVideoRequest(options)
146
198
  : command.group === "audio"
147
- ? planAudioRequest(options)
199
+ ? command.action === "sfx"
200
+ ? planAudioSfxRequest(options)
201
+ : command.action === "music"
202
+ ? planAudioMusicRequest(options)
203
+ : planAudioRequest(options)
148
204
  : planTextRequest(options);
149
205
  return { dryRun: true, kind: command.group, request: redactRequest(request) };
150
206
  }
@@ -159,7 +215,11 @@ async function handleGenerate(command, deps) {
159
215
  : command.group === "video"
160
216
  ? await generateVideo(options)
161
217
  : command.group === "audio"
162
- ? await generateAudio(options)
218
+ ? command.action === "sfx"
219
+ ? await generateAudioSfx(options)
220
+ : command.action === "music"
221
+ ? await generateAudioMusic(options)
222
+ : await generateAudio(options)
163
223
  : await generateText(options);
164
224
  if (command.group === "text") {
165
225
  const text = extractText(response);
@@ -179,6 +239,20 @@ async function handleGenerate(command, deps) {
179
239
  }
180
240
  }
181
241
 
242
+ async function handleVoices(command, deps) {
243
+ const { resolveApiKey } = await import("./config.js");
244
+ const { getVoices } = await import("./api.js");
245
+ const apiKey = await resolveApiKey({
246
+ apiKey: command.options.api_key,
247
+ env: deps.env ?? process.env,
248
+ });
249
+ return getVoices({
250
+ apiKey,
251
+ baseUrl: command.options.base_url,
252
+ fetch: deps.fetch,
253
+ });
254
+ }
255
+
182
256
  function extractText(response) {
183
257
  return response?.choices?.[0]?.message?.content
184
258
  ?? response?.output_text
package/src/config.js CHANGED
@@ -22,13 +22,13 @@ export async function resolveApiKey({
22
22
  if (saved?.apiKey) return saved.apiKey;
23
23
 
24
24
  throw new Error(
25
- "Missing Flatkey API key. Run `flatkey onboard --api-key <key>` or set FLATKEY_API_KEY.",
25
+ "Missing Flatkey API key. Create one at https://console.flatkey.ai/keys, then run `flatkey onboard --api-key <key>` or set FLATKEY_API_KEY.",
26
26
  );
27
27
  }
28
28
 
29
29
  export async function writeConfig({ apiKey, configDir = getDefaultConfigDir() }) {
30
- if (!apiKey) {
31
- throw new Error("Missing --api-key value.");
30
+ if (typeof apiKey !== "string" || apiKey.trim() === "") {
31
+ throw new Error("Missing --api-key value. Create a key at https://console.flatkey.ai/keys, then run `flatkey onboard --api-key <key>`.");
32
32
  }
33
33
 
34
34
  await mkdir(configDir, { recursive: true });