videodraft 0.2.1 → 0.3.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.
package/dist/client.d.ts CHANGED
@@ -38,6 +38,13 @@ declare class VideoDraftClient {
38
38
  get endpoint(): string;
39
39
  rpc<T = any>(method: string, params?: unknown): Promise<T>;
40
40
  private post;
41
+ /**
42
+ * Make an authenticated REST request to a non-MCP API route (e.g.
43
+ * /api/elevenlabs-key, which manages the user's BYOK ElevenLabs key and has no
44
+ * MCP tool by design). Uses the same bearer token + 401 refresh as rpc().
45
+ * Returns the parsed JSON body; throws on non-2xx with the server's error.
46
+ */
47
+ restRequest<T = any>(method: string, path: string, body?: unknown): Promise<T>;
41
48
  /**
42
49
  * Batch several JSON-RPC requests into ONE HTTP round trip (the server
43
50
  * implements JSON-RPC 2.0 batching). With N concurrent jobs this turns N
package/dist/client.js CHANGED
@@ -264,6 +264,43 @@ var VideoDraftClient = class {
264
264
  signal: AbortSignal.timeout(this.requestTimeoutMs)
265
265
  });
266
266
  }
267
+ /**
268
+ * Make an authenticated REST request to a non-MCP API route (e.g.
269
+ * /api/elevenlabs-key, which manages the user's BYOK ElevenLabs key and has no
270
+ * MCP tool by design). Uses the same bearer token + 401 refresh as rpc().
271
+ * Returns the parsed JSON body; throws on non-2xx with the server's error.
272
+ */
273
+ async restRequest(method, path4, body) {
274
+ const doFetch = (token2) => this.fetchImpl(`${this.baseUrl}${path4}`, {
275
+ method,
276
+ headers: {
277
+ "content-type": "application/json",
278
+ authorization: `Bearer ${token2}`,
279
+ "user-agent": this.userAgent
280
+ },
281
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
282
+ signal: AbortSignal.timeout(this.requestTimeoutMs)
283
+ });
284
+ const token = await this.tokenProvider.getAccessToken();
285
+ let response = await doFetch(token);
286
+ if (response.status === 401 && this.tokenProvider.onUnauthorized) {
287
+ const fresh = await this.tokenProvider.onUnauthorized();
288
+ if (fresh) response = await doFetch(fresh);
289
+ }
290
+ if (response.status === 401) {
291
+ throw new AuthError("Token is invalid, expired, or revoked.");
292
+ }
293
+ let parsed = null;
294
+ try {
295
+ parsed = await response.json();
296
+ } catch {
297
+ }
298
+ if (!response.ok) {
299
+ const detail = parsed?.error?.message ?? parsed?.error ?? `HTTP ${response.status}`;
300
+ throw new RpcError(response.status, detail);
301
+ }
302
+ return parsed;
303
+ }
267
304
  /**
268
305
  * Batch several JSON-RPC requests into ONE HTTP round trip (the server
269
306
  * implements JSON-RPC 2.0 batching). With N concurrent jobs this turns N
package/dist/index.js CHANGED
@@ -24,8 +24,8 @@ function readVersionFromDisk() {
24
24
  }
25
25
  }
26
26
  function resolveVersion() {
27
- if ("0.2.1") {
28
- return "0.2.1";
27
+ if ("0.3.1") {
28
+ return "0.3.1";
29
29
  }
30
30
  return readVersionFromDisk();
31
31
  }
@@ -691,6 +691,43 @@ var VideoDraftClient = class {
691
691
  signal: AbortSignal.timeout(this.requestTimeoutMs)
692
692
  });
693
693
  }
694
+ /**
695
+ * Make an authenticated REST request to a non-MCP API route (e.g.
696
+ * /api/elevenlabs-key, which manages the user's BYOK ElevenLabs key and has no
697
+ * MCP tool by design). Uses the same bearer token + 401 refresh as rpc().
698
+ * Returns the parsed JSON body; throws on non-2xx with the server's error.
699
+ */
700
+ async restRequest(method, path5, body) {
701
+ const doFetch = (token2) => this.fetchImpl(`${this.baseUrl}${path5}`, {
702
+ method,
703
+ headers: {
704
+ "content-type": "application/json",
705
+ authorization: `Bearer ${token2}`,
706
+ "user-agent": this.userAgent
707
+ },
708
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
709
+ signal: AbortSignal.timeout(this.requestTimeoutMs)
710
+ });
711
+ const token = await this.tokenProvider.getAccessToken();
712
+ let response = await doFetch(token);
713
+ if (response.status === 401 && this.tokenProvider.onUnauthorized) {
714
+ const fresh = await this.tokenProvider.onUnauthorized();
715
+ if (fresh) response = await doFetch(fresh);
716
+ }
717
+ if (response.status === 401) {
718
+ throw new AuthError("Token is invalid, expired, or revoked.");
719
+ }
720
+ let parsed = null;
721
+ try {
722
+ parsed = await response.json();
723
+ } catch {
724
+ }
725
+ if (!response.ok) {
726
+ const detail = parsed?.error?.message ?? parsed?.error ?? `HTTP ${response.status}`;
727
+ throw new RpcError(response.status, detail);
728
+ }
729
+ return parsed;
730
+ }
694
731
  /**
695
732
  * Batch several JSON-RPC requests into ONE HTTP round trip (the server
696
733
  * implements JSON-RPC 2.0 batching). With N concurrent jobs this turns N
@@ -1057,6 +1094,127 @@ ${fmt.bold(ctxOut, "Open this URL to log in:")}
1057
1094
  ]);
1058
1095
  });
1059
1096
  });
1097
+ const el = program.command("elevenlabs").description("Manage your ElevenLabs API key (BYOK) \u2014 use your cloned voices");
1098
+ el.command("status").description("Show whether your ElevenLabs key is connected and active").action(async function() {
1099
+ const ctx = buildContext(this);
1100
+ const st = await ctx.client.restRequest("GET", "/api/elevenlabs-key");
1101
+ emit(ctx.out, st, (o) => {
1102
+ kv(o, [
1103
+ ["Connected", st?.hasKey ? `yes (\u2022\u2022\u2022\u2022${st.hint ?? ""})` : "no"],
1104
+ ["Active", st?.enabled ? "yes" : "no"],
1105
+ ["Server", ctx.baseUrl]
1106
+ ]);
1107
+ if (!st?.hasKey) {
1108
+ note(
1109
+ o,
1110
+ fmt.dim(o, "Connect one: videodraft elevenlabs set --key <xi-...>")
1111
+ );
1112
+ }
1113
+ });
1114
+ });
1115
+ el.command("set").description("Connect (or replace) your ElevenLabs API key and activate it").option("--key <key>", "your ElevenLabs API key (xi-...)").option("--no-enable", "store the key without turning it on").action(async function() {
1116
+ const opts = this.opts();
1117
+ const ctx = buildContext(this);
1118
+ let key = opts.key?.trim();
1119
+ if (!key && !process.stdin.isTTY) key = (await readStdin()).trim();
1120
+ if (!key) {
1121
+ throw new CliError(
1122
+ "Provide your key with --key <xi-...> (or pipe it via stdin).",
1123
+ EXIT.USAGE
1124
+ );
1125
+ }
1126
+ const spin = spinner(ctx.out, "Validating key with ElevenLabs\u2026");
1127
+ try {
1128
+ const res = await ctx.client.restRequest(
1129
+ "POST",
1130
+ "/api/elevenlabs-key",
1131
+ { key, enabled: opts.enable !== false }
1132
+ );
1133
+ spin.stop();
1134
+ capture("cli_elevenlabs_set");
1135
+ emit(
1136
+ ctx.out,
1137
+ res,
1138
+ (o) => note(
1139
+ o,
1140
+ fmt.green(
1141
+ o,
1142
+ `ElevenLabs key saved (\u2022\u2022\u2022\u2022${res.hint ?? ""})${res.enabled ? " and active \u2014 your cloned voices are ready." : " \u2014 run `videodraft elevenlabs enable` to turn it on."}`
1143
+ )
1144
+ )
1145
+ );
1146
+ } catch (e) {
1147
+ spin.stop();
1148
+ throw e;
1149
+ }
1150
+ });
1151
+ el.command("enable").description("Turn your ElevenLabs key on").action(async function() {
1152
+ const ctx = buildContext(this);
1153
+ const res = await ctx.client.restRequest(
1154
+ "PATCH",
1155
+ "/api/elevenlabs-key",
1156
+ { enabled: true }
1157
+ );
1158
+ emit(
1159
+ ctx.out,
1160
+ res,
1161
+ (o) => note(o, fmt.green(o, "ElevenLabs key is now active."))
1162
+ );
1163
+ });
1164
+ el.command("disable").description("Turn your ElevenLabs key off (use VideoDraft credits again)").action(async function() {
1165
+ const ctx = buildContext(this);
1166
+ const res = await ctx.client.restRequest(
1167
+ "PATCH",
1168
+ "/api/elevenlabs-key",
1169
+ { enabled: false }
1170
+ );
1171
+ emit(
1172
+ ctx.out,
1173
+ res,
1174
+ (o) => note(
1175
+ o,
1176
+ "ElevenLabs key disabled. ElevenLabs generations use the VideoDraft path."
1177
+ )
1178
+ );
1179
+ });
1180
+ el.command("remove").description("Delete your stored ElevenLabs API key").action(async function() {
1181
+ const ctx = buildContext(this);
1182
+ const res = await ctx.client.restRequest(
1183
+ "DELETE",
1184
+ "/api/elevenlabs-key"
1185
+ );
1186
+ capture("cli_elevenlabs_remove");
1187
+ emit(
1188
+ ctx.out,
1189
+ res ?? { ok: true },
1190
+ (o) => note(o, "ElevenLabs key removed.")
1191
+ );
1192
+ });
1193
+ el.command("voices").description("List your ElevenLabs cloned/professional voices").action(async function() {
1194
+ const ctx = buildContext(this);
1195
+ const res = await ctx.client.callTool("list_cloned_voices");
1196
+ emit(ctx.out, res, (o) => {
1197
+ if (!res?.byok_active) {
1198
+ note(
1199
+ o,
1200
+ fmt.dim(
1201
+ o,
1202
+ res?.hint ?? "Connect your ElevenLabs key first: videodraft elevenlabs set --key <xi-...>"
1203
+ )
1204
+ );
1205
+ return;
1206
+ }
1207
+ const voices = res?.voices ?? [];
1208
+ if (!voices.length) {
1209
+ note(o, "No cloned voices in your ElevenLabs account yet.");
1210
+ return;
1211
+ }
1212
+ kv(
1213
+ o,
1214
+ voices.map((v) => [v.name, v.id])
1215
+ );
1216
+ });
1217
+ });
1060
1218
  }
1061
1219
 
1062
1220
  // src/commands/account.ts
@@ -1749,7 +1907,7 @@ function registerGenerateCommands(program) {
1749
1907
  generate.command("video [prompt...]").description("Generate a video clip (async; per-second pricing \u2014 see --estimate)").option("--model <id>", "video model id (default google-veo3.1 fast)").option("--ar <ratio>", 'aspect ratio, e.g. "16:9", "9:16"').option("--duration <seconds>", "clip duration in seconds").option("--resolution <res>", 'e.g. "480p", "720p", "1080p", "4k"').option(
1750
1908
  "--quality <tier>",
1751
1909
  'e.g. "mini", "fast", "standard", "quality", "pro"'
1752
- ).option("--audio", "generate native model audio").option("--no-audio", "disable native model audio").option("--start-image <url|file>", "start frame (image-to-video)").option("--end-image <url|file>", "end frame (supported models only)").option("--ref <url|file>", "reference image (repeatable)", collect, []).option("--ref-video <url|file>", "reference video (repeatable; Seedance 2, Wan 2.7; local files uploaded)", collect, []).option("--ref-audio <url|file>", "reference audio (repeatable; Seedance 2; local files uploaded)", collect, []).option("--segment <prompt:seconds>", "multi-prompt segment (repeatable; Kling 3.0 / 3.0 Turbo / O3)", collect, []).option("--negative <text>", "negative prompt (Kling/Wan/Luma)").option("--seed <n>", "seed").option("--project <id>", "attach to a project").option("--session <id>", "AI Studio session id").option("--scene <n>", "0-based scene index").option("--shot <n>", "0-based shot index").option("--download <path>", "download outputs (template: {job_id} {index} {ext})").option("--no-wait", "submit and return the job id immediately").option("--estimate", "print the cost estimate and exit (spends nothing)").action(async function(promptWords = []) {
1910
+ ).option("--audio", "generate native model audio").option("--no-audio", "disable native model audio").option("--start-image <url|file>", "start frame (image-to-video)").option("--end-image <url|file>", "end frame (supported models only)").option("--ref <url|file>", "reference image (repeatable)", collect, []).option("--ref-video <url|file>", "reference video (repeatable; Gemini Omni Flash, Seedance 2, Kling O3, Wan 2.7; local files uploaded)", collect, []).option("--ref-audio <url|file>", "reference audio (repeatable; Seedance 2; local files uploaded)", collect, []).option("--segment <prompt:seconds>", "multi-prompt segment (repeatable; Kling 3.0 / 3.0 Turbo / O3)", collect, []).option("--negative <text>", "negative prompt (Kling/Wan/Luma)").option("--seed <n>", "seed").option("--project <id>", "attach to a project").option("--session <id>", "AI Studio session id").option("--scene <n>", "0-based scene index").option("--shot <n>", "0-based shot index").option("--download <path>", "download outputs (template: {job_id} {index} {ext})").option("--no-wait", "submit and return the job id immediately").option("--estimate", "print the cost estimate and exit (spends nothing)").action(async function(promptWords = []) {
1753
1911
  const ctx = buildContext(this);
1754
1912
  const opts = this.opts();
1755
1913
  const prompt = promptWords.join(" ").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "videodraft",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "Official VideoDraft CLI — create AI videos, images and audio from your terminal. Agent-friendly: --json everywhere, stable exit codes, async job polling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,8 +12,8 @@ videodraft models styles --json # visual style presets
12
12
 
13
13
  ## Defaults (safe starting points)
14
14
 
15
- - **Image**: `nano-banana-2` (the platform default, 1K). Use `--num 1..4` for variations of one prompt in a single call — never loop for variations.
16
- - **Video**: `google-veo3.1` at fast quality (6s / 720p) — the platform default.
15
+ - **Image**: `nano-banana-2` (the platform default, 1K, up to 14 reference images). Use `--num 1..4` for variations of one prompt in a single call — never loop for variations. `nano-banana-2-lite` is the fastest/cheapest Google direct image model (1K only, up to 14 reference images).
16
+ - **Video**: `google-veo3.1` at fast quality (6s / 720p) — the platform default. `gemini-omni-flash` is Google's any-to-any multimodal video model (text/image/video → video, auto or 3-10s, 720p, audio always on).
17
17
  - **Voiceover**: ElevenLabs Brittney (default voice).
18
18
  - **Music**: `lyria-3-clip-preview` (30s, cheap); `lyria-3-pro-preview` for 180s/quality; `elevenlabs-music` for music that can include vocals/lyrics.
19
19
  - **ElevenLabs audio**: `generate sound-effect`, `generate dialogue`, `generate voice-changer`, and `generate dub` are synchronous audio/media calls. Voice changer and dubbing require the source media duration in seconds for billing and currently accept source media up to 300s.
@@ -24,7 +24,7 @@ videodraft models styles --json # visual style presets
24
24
  - Most video models support only 16:9 / 9:16 / 1:1. A 3:4 request hard-fails on most.
25
25
  - `--seed` reproduces a specific output on models that support it (e.g. Flux, Ideogram V4); everything else ignores it. You do not need a seed for variation — `--num` already varies.
26
26
  - `--rendering-speed` applies to Ideogram (V3: `Default`/`Turbo`/`Quality`; V4: `Turbo`/`Balanced`/`Quality`) and affects image cost — pass it to `videodraft costs ... --rendering-speed <tier>` for an accurate estimate. Always trust `videodraft models image --json` over this list; new models and tiers appear there the moment the platform ships them, with no CLI update.
27
- - Reference inputs: `--ref <img>` (images), `--ref-video <v>` (Seedance 2, Wan 2.7), `--ref-audio <a>` (Seedance 2). The CLI uploads local files for all of these, so you can pass a path or a URL. `--segment "<prompt>:<seconds>"` (repeatable) drives multi-prompt models (Kling 3.0 / 3.0 Turbo / O3); total 3-15s. `generate image --video-ref` is the nano-banana-2 video reference.
27
+ - Reference inputs: `--ref <img>` (images), `--ref-video <v>` (Gemini Omni Flash, Seedance 2, Kling O3, Wan 2.7), `--ref-audio <a>` (Seedance 2). The CLI uploads local files for all of these, so you can pass a path or a URL. `--segment "<prompt>:<seconds>"` (repeatable) drives multi-prompt models (Kling 3.0 / 3.0 Turbo / O3); total 3-15s. `generate image --video-ref` is the nano-banana-2 video reference.
28
28
  - The top-level prompt is OPTIONAL for `generate video` with multi-prompt models and for Kling 3.0 Turbo (`--model kling-v3-turbo`) image-to-video — a `--segment`-only or `--start-image`-only call is valid. Every other model still needs a prompt; the server enforces per-model rules.
29
29
  - AI Production: `videodraft produce <project> --mode full_video` generates one Seedance 2 video per scene; poll with `videodraft generations`, then `videodraft finalize <project>` swaps them into the timeline before `export`.
30
30