videodraft 0.1.0 → 0.1.2

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
@@ -11,10 +11,14 @@ npx videodraft create "30s launch video for our espresso machine" --ar 9:16
11
11
  ## Install
12
12
 
13
13
  ```bash
14
- npm install -g videodraft # or: npx videodraft <command>
14
+ npm install -g videodraft # global: adds the `videodraft` command to your PATH
15
+ videodraft --version # → confirms it's installed
16
+
17
+ # …or run without installing — npx fetches and runs the latest each time:
18
+ npx videodraft <command>
15
19
  ```
16
20
 
17
- Requires Node ≥ 20.
21
+ Requires Node ≥ 20.18.1. (Publishing to npm doesn't put `videodraft` on your PATH — `npm i -g` or `npx` does. A Homebrew tap and a `curl | sh` installer are on the way.)
18
22
 
19
23
  ## Authenticate
20
24
 
@@ -81,6 +85,7 @@ Install the VideoDraft skill so your agent knows the workflow:
81
85
  npx videodraft skills install # zero-install: npx fetches the CLI and installs the skill
82
86
  videodraft skills install # if the CLI is on PATH — auto-detects your installed agents
83
87
  videodraft skills install --agent claude,codex # target specific agents (repeatable/comma; --all for every agent)
88
+ videodraft skills install --project # into ./.claude/skills for just this repo (else global)
84
89
  npx skills add videodraft-ai/cli # via the vercel-labs skills tool (discovery / 69-agent multiselect)
85
90
  ```
86
91
 
package/dist/client.js CHANGED
@@ -99,10 +99,16 @@ function writeConfig(config, env = process.env) {
99
99
  }
100
100
  }
101
101
  function updateConfig(mutate, env = process.env) {
102
- const config = readConfig(env);
103
- mutate(config);
104
- writeConfig(config, env);
105
- return config;
102
+ return withLockSync(
103
+ "config",
104
+ () => {
105
+ const config = readConfig(env);
106
+ mutate(config);
107
+ writeConfig(config, env);
108
+ return config;
109
+ },
110
+ env
111
+ );
106
112
  }
107
113
  function getProfile(name, env = process.env) {
108
114
  const config = readConfig(env);
@@ -110,15 +116,64 @@ function getProfile(name, env = process.env) {
110
116
  return { name: profileName, profile: config.profiles[profileName], config };
111
117
  }
112
118
  var LOCK_STALE_MS = 3e4;
119
+ function lockOwnerToken() {
120
+ return `${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
121
+ }
122
+ function releaseOwnedLock(lockPath, owner) {
123
+ try {
124
+ const current = JSON.parse(fs.readFileSync(lockPath, "utf8"));
125
+ if (current && current.owner === owner) {
126
+ fs.rmSync(lockPath, { force: true });
127
+ }
128
+ } catch {
129
+ }
130
+ }
131
+ function withLockSync(lockName, fn, env = process.env) {
132
+ const dir = configDir(env);
133
+ fs.mkdirSync(dir, { recursive: true, mode: 448 });
134
+ const lockPath = path.join(dir, `${lockName}.lock`);
135
+ const owner = lockOwnerToken();
136
+ const deadline = Date.now() + 1e4;
137
+ let held = false;
138
+ for (; ; ) {
139
+ try {
140
+ const fd = fs.openSync(lockPath, "wx");
141
+ fs.writeSync(fd, JSON.stringify({ owner, pid: process.pid, at: Date.now() }));
142
+ fs.closeSync(fd);
143
+ held = true;
144
+ break;
145
+ } catch {
146
+ try {
147
+ const stat = fs.statSync(lockPath);
148
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
149
+ fs.rmSync(lockPath, { force: true });
150
+ continue;
151
+ }
152
+ } catch {
153
+ continue;
154
+ }
155
+ if (Date.now() > deadline) break;
156
+ const until = Date.now() + 5;
157
+ while (Date.now() < until) {
158
+ }
159
+ }
160
+ }
161
+ try {
162
+ return fn();
163
+ } finally {
164
+ if (held) releaseOwnedLock(lockPath, owner);
165
+ }
166
+ }
113
167
  async function withLock(lockName, fn, env = process.env) {
114
168
  const dir = configDir(env);
115
169
  fs.mkdirSync(dir, { recursive: true, mode: 448 });
116
170
  const lockPath = path.join(dir, `${lockName}.lock`);
171
+ const owner = lockOwnerToken();
117
172
  const deadline = Date.now() + 15e3;
118
173
  for (; ; ) {
119
174
  try {
120
175
  const fd = fs.openSync(lockPath, "wx");
121
- fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
176
+ fs.writeSync(fd, JSON.stringify({ owner, pid: process.pid, at: Date.now() }));
122
177
  fs.closeSync(fd);
123
178
  break;
124
179
  } catch {
@@ -140,7 +195,7 @@ async function withLock(lockName, fn, env = process.env) {
140
195
  try {
141
196
  return await fn();
142
197
  } finally {
143
- fs.rmSync(lockPath, { force: true });
198
+ releaseOwnedLock(lockPath, owner);
144
199
  }
145
200
  }
146
201
 
@@ -494,10 +549,13 @@ function isExpiring(profile) {
494
549
  if (!profile.expires_at) return false;
495
550
  return new Date(profile.expires_at).getTime() - Date.now() < EXPIRY_SKEW_MS;
496
551
  }
497
- function saveRotatedTokens(profileName, tokens, env) {
552
+ function saveRotatedTokens(profileName, expected, tokens, env) {
498
553
  updateConfig((config) => {
499
554
  const profile = config.profiles[profileName];
500
555
  if (!profile) return;
556
+ if (profile.auth_kind !== "oauth" || profile.refresh_token !== expected.refreshToken || profile.client_id !== expected.clientId) {
557
+ return;
558
+ }
501
559
  profile.access_token = tokens.access_token;
502
560
  profile.refresh_token = tokens.refresh_token;
503
561
  profile.expires_at = new Date(Date.now() + tokens.expires_in * 1e3).toISOString();
@@ -512,14 +570,16 @@ async function refreshUnderLock(profileName, env) {
512
570
  if (profile.auth_kind !== "oauth") return profile.access_token;
513
571
  if (!isExpiring(profile)) return profile.access_token;
514
572
  if (!profile.refresh_token || !profile.client_id) return null;
573
+ const expected = { refreshToken: profile.refresh_token, clientId: profile.client_id };
515
574
  const tokens = await refreshAccessToken({
516
575
  baseUrl: profile.base_url || DEFAULT_BASE_URL,
517
- clientId: profile.client_id,
518
- refreshToken: profile.refresh_token
576
+ clientId: expected.clientId,
577
+ refreshToken: expected.refreshToken
519
578
  });
520
579
  if (!tokens) return null;
521
- saveRotatedTokens(profileName, tokens, env);
522
- return tokens.access_token;
580
+ saveRotatedTokens(profileName, expected, tokens, env);
581
+ const { profile: latest } = getProfile(profileName, env);
582
+ return latest?.access_token ?? tokens.access_token;
523
583
  },
524
584
  env
525
585
  );
@@ -764,6 +824,7 @@ async function downloadOutputs(urls, template, vars, fetchImpl = fetch) {
764
824
  // src/core/upload.ts
765
825
  import fs3 from "fs";
766
826
  import path3 from "path";
827
+ import { Readable as Readable2 } from "stream";
767
828
  var MIME_BY_EXT = {
768
829
  png: "image/png",
769
830
  jpg: "image/jpeg",
@@ -808,11 +869,13 @@ async function uploadFile(client, localPath, options = {}) {
808
869
  if (!uploadUrl || !filePath) {
809
870
  throw new CliError("create_media_upload did not return upload_url/file_path.");
810
871
  }
811
- const body = fs3.readFileSync(resolved);
872
+ const { size } = fs3.statSync(resolved);
812
873
  const putRes = await fetchImpl(uploadUrl, {
813
874
  method: "PUT",
814
- headers: { "content-type": contentType },
815
- body,
875
+ headers: { "content-type": contentType, "content-length": String(size) },
876
+ body: Readable2.toWeb(fs3.createReadStream(resolved)),
877
+ // Node/undici requires duplex:"half" when the body is a stream.
878
+ duplex: "half",
816
879
  signal: AbortSignal.timeout(6e5)
817
880
  });
818
881
  if (!putRes.ok) {
package/dist/index.js CHANGED
@@ -233,10 +233,16 @@ function writeConfig(config, env = process.env) {
233
233
  }
234
234
  }
235
235
  function updateConfig(mutate, env = process.env) {
236
- const config = readConfig(env);
237
- mutate(config);
238
- writeConfig(config, env);
239
- return config;
236
+ return withLockSync(
237
+ "config",
238
+ () => {
239
+ const config = readConfig(env);
240
+ mutate(config);
241
+ writeConfig(config, env);
242
+ return config;
243
+ },
244
+ env
245
+ );
240
246
  }
241
247
  function getProfile(name, env = process.env) {
242
248
  const config = readConfig(env);
@@ -247,21 +253,73 @@ function anonymousId(env = process.env) {
247
253
  const config = readConfig(env);
248
254
  if (config.anonymous_id) return config.anonymous_id;
249
255
  const id = crypto.randomUUID();
250
- updateConfig((c) => {
251
- c.anonymous_id = id;
252
- }, env);
256
+ try {
257
+ updateConfig((c) => {
258
+ c.anonymous_id = id;
259
+ }, env);
260
+ } catch {
261
+ }
253
262
  return id;
254
263
  }
255
264
  var LOCK_STALE_MS = 3e4;
265
+ function lockOwnerToken() {
266
+ return `${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
267
+ }
268
+ function releaseOwnedLock(lockPath, owner) {
269
+ try {
270
+ const current = JSON.parse(fs2.readFileSync(lockPath, "utf8"));
271
+ if (current && current.owner === owner) {
272
+ fs2.rmSync(lockPath, { force: true });
273
+ }
274
+ } catch {
275
+ }
276
+ }
277
+ function withLockSync(lockName, fn, env = process.env) {
278
+ const dir = configDir(env);
279
+ fs2.mkdirSync(dir, { recursive: true, mode: 448 });
280
+ const lockPath = path.join(dir, `${lockName}.lock`);
281
+ const owner = lockOwnerToken();
282
+ const deadline = Date.now() + 1e4;
283
+ let held = false;
284
+ for (; ; ) {
285
+ try {
286
+ const fd = fs2.openSync(lockPath, "wx");
287
+ fs2.writeSync(fd, JSON.stringify({ owner, pid: process.pid, at: Date.now() }));
288
+ fs2.closeSync(fd);
289
+ held = true;
290
+ break;
291
+ } catch {
292
+ try {
293
+ const stat = fs2.statSync(lockPath);
294
+ if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
295
+ fs2.rmSync(lockPath, { force: true });
296
+ continue;
297
+ }
298
+ } catch {
299
+ continue;
300
+ }
301
+ if (Date.now() > deadline) break;
302
+ const until = Date.now() + 5;
303
+ while (Date.now() < until) {
304
+ }
305
+ }
306
+ }
307
+ try {
308
+ return fn();
309
+ } finally {
310
+ if (held) releaseOwnedLock(lockPath, owner);
311
+ }
312
+ }
256
313
  async function withLock(lockName, fn, env = process.env) {
257
314
  const dir = configDir(env);
258
315
  fs2.mkdirSync(dir, { recursive: true, mode: 448 });
259
316
  const lockPath = path.join(dir, `${lockName}.lock`);
317
+ const owner = lockOwnerToken();
260
318
  const deadline = Date.now() + 15e3;
261
319
  for (; ; ) {
262
320
  try {
263
321
  const fd = fs2.openSync(lockPath, "wx");
264
- fs2.writeSync(fd, JSON.stringify({ pid: process.pid, at: Date.now() }));
322
+ fs2.writeSync(fd, JSON.stringify({ owner, pid: process.pid, at: Date.now() }));
265
323
  fs2.closeSync(fd);
266
324
  break;
267
325
  } catch {
@@ -283,7 +341,7 @@ async function withLock(lockName, fn, env = process.env) {
283
341
  try {
284
342
  return await fn();
285
343
  } finally {
286
- fs2.rmSync(lockPath, { force: true });
344
+ releaseOwnedLock(lockPath, owner);
287
345
  }
288
346
  }
289
347
 
@@ -310,26 +368,29 @@ function maybePrintFirstRunNotice(env = process.env) {
310
368
  );
311
369
  }
312
370
  function capture(event, properties = {}) {
313
- if (!telemetryEnabled()) return;
314
- const body = JSON.stringify({
315
- api_key: POSTHOG_KEY,
316
- event,
317
- distinct_id: anonymousId(),
318
- properties: {
319
- cli_version: VERSION,
320
- os: process.platform,
321
- arch: process.arch,
322
- node_major: Number(process.versions.node.split(".")[0]),
323
- is_ci: Boolean(process.env.CI),
324
- ...properties
325
- }
326
- });
327
- pending = fetch(`${POSTHOG_HOST}/capture/`, {
328
- method: "POST",
329
- headers: { "content-type": "application/json" },
330
- body,
331
- signal: AbortSignal.timeout(1500)
332
- }).catch(() => void 0);
371
+ try {
372
+ if (!telemetryEnabled()) return;
373
+ const body = JSON.stringify({
374
+ api_key: POSTHOG_KEY,
375
+ event,
376
+ distinct_id: anonymousId(),
377
+ properties: {
378
+ cli_version: VERSION,
379
+ os: process.platform,
380
+ arch: process.arch,
381
+ node_major: Number(process.versions.node.split(".")[0]),
382
+ is_ci: Boolean(process.env.CI),
383
+ ...properties
384
+ }
385
+ });
386
+ pending = fetch(`${POSTHOG_HOST}/capture/`, {
387
+ method: "POST",
388
+ headers: { "content-type": "application/json" },
389
+ body,
390
+ signal: AbortSignal.timeout(1500)
391
+ }).catch(() => void 0);
392
+ } catch {
393
+ }
333
394
  }
334
395
  async function shutdown() {
335
396
  if (!pending) return;
@@ -729,10 +790,13 @@ function isExpiring(profile) {
729
790
  if (!profile.expires_at) return false;
730
791
  return new Date(profile.expires_at).getTime() - Date.now() < EXPIRY_SKEW_MS;
731
792
  }
732
- function saveRotatedTokens(profileName, tokens, env) {
793
+ function saveRotatedTokens(profileName, expected, tokens, env) {
733
794
  updateConfig((config) => {
734
795
  const profile = config.profiles[profileName];
735
796
  if (!profile) return;
797
+ if (profile.auth_kind !== "oauth" || profile.refresh_token !== expected.refreshToken || profile.client_id !== expected.clientId) {
798
+ return;
799
+ }
736
800
  profile.access_token = tokens.access_token;
737
801
  profile.refresh_token = tokens.refresh_token;
738
802
  profile.expires_at = new Date(Date.now() + tokens.expires_in * 1e3).toISOString();
@@ -747,14 +811,16 @@ async function refreshUnderLock(profileName, env) {
747
811
  if (profile.auth_kind !== "oauth") return profile.access_token;
748
812
  if (!isExpiring(profile)) return profile.access_token;
749
813
  if (!profile.refresh_token || !profile.client_id) return null;
814
+ const expected = { refreshToken: profile.refresh_token, clientId: profile.client_id };
750
815
  const tokens = await refreshAccessToken({
751
816
  baseUrl: profile.base_url || DEFAULT_BASE_URL,
752
- clientId: profile.client_id,
753
- refreshToken: profile.refresh_token
817
+ clientId: expected.clientId,
818
+ refreshToken: expected.refreshToken
754
819
  });
755
820
  if (!tokens) return null;
756
- saveRotatedTokens(profileName, tokens, env);
757
- return tokens.access_token;
821
+ saveRotatedTokens(profileName, expected, tokens, env);
822
+ const { profile: latest } = getProfile(profileName, env);
823
+ return latest?.access_token ?? tokens.access_token;
758
824
  },
759
825
  env
760
826
  );
@@ -862,6 +928,8 @@ function openBrowser(url) {
862
928
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
863
929
  try {
864
930
  const child = spawn(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" });
931
+ child.on("error", () => {
932
+ });
865
933
  child.unref();
866
934
  } catch {
867
935
  }
@@ -950,9 +1018,8 @@ ${fmt.bold(ctxOut, "Open this URL to log in:")}
950
1018
  });
951
1019
  program.command("logout").description("Revoke the current OAuth grant (best-effort) and clear stored credentials").action(async function() {
952
1020
  const globals = this.optsWithGlobals();
953
- const profileName = globals.profile ?? "default";
954
1021
  const ctxOut = buildContext(this).out;
955
- const { profile } = getProfile(profileName);
1022
+ const { name: profileName, profile } = getProfile(globals.profile);
956
1023
  if (!profile) {
957
1024
  emit(ctxOut, { ok: true, message: "No stored credentials." }, (o) => note(o, "No stored credentials."));
958
1025
  return;
@@ -1172,6 +1239,8 @@ function registerProjectCommands(program) {
1172
1239
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1173
1240
  try {
1174
1241
  const child = spawn2(cmd, [url], { stdio: "ignore", detached: true, shell: process.platform === "win32" });
1242
+ child.on("error", () => {
1243
+ });
1175
1244
  child.unref();
1176
1245
  } catch {
1177
1246
  }
@@ -1344,6 +1413,43 @@ async function pollExport(client, ref, options = {}) {
1344
1413
  }
1345
1414
  }
1346
1415
 
1416
+ // src/core/media.ts
1417
+ var IMAGE_EXTS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp", "gif", "bmp", "heic", "heif", "avif", "svg"]);
1418
+ var VIDEO_EXTS = /* @__PURE__ */ new Set(["mp4", "webm", "mov", "m4v"]);
1419
+ var AUDIO_EXTS = /* @__PURE__ */ new Set(["mp3", "wav", "m4a", "ogg", "flac", "aac"]);
1420
+ function kindOf(url, typeHint) {
1421
+ const t = (typeHint ?? "").toLowerCase();
1422
+ if (t.includes("image")) return "image";
1423
+ if (t.includes("video")) return "video";
1424
+ if (t.includes("audio") || t.includes("music") || t.includes("sound") || t.includes("voice") || t.includes("speech") || t.includes("tts")) {
1425
+ return "audio";
1426
+ }
1427
+ const path5 = url.split(/[?#]/)[0]?.toLowerCase() ?? "";
1428
+ if (path5.includes("/img/")) return "image";
1429
+ if (path5.includes("/vid/")) return "video";
1430
+ if (path5.includes("/aud/")) return "audio";
1431
+ const ext = path5.match(/\.([a-z0-9]+)$/)?.[1];
1432
+ if (ext) {
1433
+ if (IMAGE_EXTS.has(ext)) return "image";
1434
+ if (VIDEO_EXTS.has(ext)) return "video";
1435
+ if (AUDIO_EXTS.has(ext)) return "audio";
1436
+ }
1437
+ return null;
1438
+ }
1439
+ function buildMediaDescriptors(urls, typeHint) {
1440
+ if (!Array.isArray(urls)) return [];
1441
+ const seen = /* @__PURE__ */ new Set();
1442
+ const out = [];
1443
+ for (const url of urls) {
1444
+ if (typeof url !== "string" || !/^https?:\/\//i.test(url) || seen.has(url)) continue;
1445
+ const kind = kindOf(url, typeHint);
1446
+ if (!kind) continue;
1447
+ seen.add(url);
1448
+ out.push({ kind, url });
1449
+ }
1450
+ return out;
1451
+ }
1452
+
1347
1453
  // src/core/download.ts
1348
1454
  import fs3 from "fs";
1349
1455
  import path2 from "path";
@@ -1401,6 +1507,7 @@ async function downloadOutputs(urls, template, vars, fetchImpl = fetch) {
1401
1507
  // src/core/upload.ts
1402
1508
  import fs4 from "fs";
1403
1509
  import path3 from "path";
1510
+ import { Readable as Readable2 } from "stream";
1404
1511
  var MIME_BY_EXT = {
1405
1512
  png: "image/png",
1406
1513
  jpg: "image/jpeg",
@@ -1445,11 +1552,13 @@ async function uploadFile(client, localPath, options = {}) {
1445
1552
  if (!uploadUrl || !filePath) {
1446
1553
  throw new CliError("create_media_upload did not return upload_url/file_path.");
1447
1554
  }
1448
- const body = fs4.readFileSync(resolved);
1555
+ const { size } = fs4.statSync(resolved);
1449
1556
  const putRes = await fetchImpl(uploadUrl, {
1450
1557
  method: "PUT",
1451
- headers: { "content-type": contentType },
1452
- body,
1558
+ headers: { "content-type": contentType, "content-length": String(size) },
1559
+ body: Readable2.toWeb(fs4.createReadStream(resolved)),
1560
+ // Node/undici requires duplex:"half" when the body is a stream.
1561
+ duplex: "half",
1453
1562
  signal: AbortSignal.timeout(6e5)
1454
1563
  });
1455
1564
  if (!putRes.ok) {
@@ -1543,9 +1652,10 @@ async function handleAsyncJob(ctx, submitted, options) {
1543
1652
  if (options.download && result.outputUrls.length > 0) {
1544
1653
  downloaded = await downloadOutputs(result.outputUrls, options.download, { job_id: jobId });
1545
1654
  }
1655
+ const media = buildMediaDescriptors(result.outputUrls, result.payload?.type);
1546
1656
  emit(
1547
1657
  ctx.out,
1548
- { job_id: jobId, status: result.status, outputs: result.outputUrls, downloaded_files: downloaded },
1658
+ { job_id: jobId, status: result.status, outputs: result.outputUrls, downloaded_files: downloaded, output_media: media },
1549
1659
  (o) => {
1550
1660
  note(o, fmt.green(o, `Completed \u2014 job ${jobId}`));
1551
1661
  for (const url of result.outputUrls) process.stdout.write(`${url}
@@ -1687,7 +1797,8 @@ function registerGenerateCommands(program) {
1687
1797
  if (opts.download && urls.length > 0) {
1688
1798
  downloaded = await downloadOutputs(urls, opts.download, { name: "voiceover" });
1689
1799
  }
1690
- emit(ctx.out, { ...result, downloaded_files: downloaded }, (o) => {
1800
+ const media = buildMediaDescriptors(urls, "audio");
1801
+ emit(ctx.out, { ...result, downloaded_files: downloaded, output_media: media }, (o) => {
1691
1802
  for (const url of urls) process.stdout.write(`${url}
1692
1803
  `);
1693
1804
  for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
@@ -1716,7 +1827,8 @@ function registerGenerateCommands(program) {
1716
1827
  if (opts.download && urls.length > 0) {
1717
1828
  downloaded = await downloadOutputs(urls, opts.download, { name: "music" });
1718
1829
  }
1719
- emit(ctx.out, { ...result, downloaded_files: downloaded }, (o) => {
1830
+ const media = buildMediaDescriptors(urls, "music");
1831
+ emit(ctx.out, { ...result, downloaded_files: downloaded, output_media: media }, (o) => {
1720
1832
  for (const url of urls) process.stdout.write(`${url}
1721
1833
  `);
1722
1834
  for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
@@ -1737,7 +1849,8 @@ function registerGenerateCommands(program) {
1737
1849
  if (opts.download && urls.length > 0) {
1738
1850
  downloaded = await downloadOutputs(urls, opts.download, { name: "upscaled" });
1739
1851
  }
1740
- emit(ctx.out, { ...result, downloaded_files: downloaded }, (o) => {
1852
+ const media = buildMediaDescriptors(urls, "image");
1853
+ emit(ctx.out, { ...result, downloaded_files: downloaded, output_media: media }, (o) => {
1741
1854
  for (const u of urls) process.stdout.write(`${u}
1742
1855
  `);
1743
1856
  for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
@@ -1876,7 +1989,18 @@ function registerPipelineCommands(program) {
1876
1989
  const failed = results.filter((r) => r.status === "failed").length;
1877
1990
  emit(
1878
1991
  ctx.out,
1879
- { job_ids: jobIds, results: results.map((r) => ({ status: r.status, outputs: r.outputUrls })) },
1992
+ {
1993
+ job_ids: jobIds,
1994
+ results: jobIds.map((jobId) => {
1995
+ const result = resultMap.get(jobId);
1996
+ return {
1997
+ job_id: jobId,
1998
+ status: result.status,
1999
+ outputs: result.outputUrls,
2000
+ output_media: buildMediaDescriptors(result.outputUrls, result.payload?.type)
2001
+ };
2002
+ })
2003
+ },
1880
2004
  (o) => {
1881
2005
  note(
1882
2006
  o,
@@ -2012,12 +2136,17 @@ function registerPipelineCommands(program) {
2012
2136
  name: "export"
2013
2137
  });
2014
2138
  }
2015
- emit(ctx.out, { export_id: exportId, video_url: result.videoUrl, downloaded_files: downloaded }, (o) => {
2016
- note(o, fmt.green(o, "Export finished."));
2017
- process.stdout.write(`${result.videoUrl}
2139
+ const media = buildMediaDescriptors([result.videoUrl], "video");
2140
+ emit(
2141
+ ctx.out,
2142
+ { export_id: exportId, video_url: result.videoUrl, downloaded_files: downloaded, output_media: media },
2143
+ (o) => {
2144
+ note(o, fmt.green(o, "Export finished."));
2145
+ process.stdout.write(`${result.videoUrl}
2018
2146
  `);
2019
- for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
2020
- });
2147
+ for (const f of downloaded ?? []) note(o, fmt.dim(o, `saved ${f.path}`));
2148
+ }
2149
+ );
2021
2150
  } catch (err) {
2022
2151
  spin.stop();
2023
2152
  throw err;
@@ -2034,7 +2163,8 @@ function registerPipelineCommands(program) {
2034
2163
  wait_seconds: opts.wait ? Number(opts.wait) : void 0
2035
2164
  })
2036
2165
  );
2037
- emit(ctx.out, result);
2166
+ const media = buildMediaDescriptors(extractOutputUrls(result), "video");
2167
+ emit(ctx.out, { ...result, output_media: media });
2038
2168
  });
2039
2169
  program.command("video-prompts <project_id>").description("Generate advisory per-shot motion/video prompts for a project").option("--ar <ratio>", "aspect ratio").option("--instructions <text>", "authoring instructions").option("--has-voiceover", "tell the generator the project has a voiceover track").option("--video-audio", "tell the generator that generated videos may include native audio").option("--has-bgm", "tell the generator the project has background music").action(async function(projectId) {
2040
2170
  const ctx = buildContext(this);
@@ -2063,7 +2193,8 @@ function registerJobCommands(program) {
2063
2193
  "check_generation_status",
2064
2194
  compact({ job_id: jobId, project_id: opts.project, scene_id: opts.sceneId })
2065
2195
  );
2066
- emit(ctx.out, result, (o) => {
2196
+ const media = buildMediaDescriptors(extractOutputUrls(result), result?.type);
2197
+ emit(ctx.out, { ...result, output_media: media }, (o) => {
2067
2198
  note(o, `${jobId}: ${result?.status ?? "unknown"}`);
2068
2199
  for (const url of extractOutputUrls(result)) process.stdout.write(`${url}
2069
2200
  `);
@@ -2105,6 +2236,7 @@ function registerJobCommands(program) {
2105
2236
  status: result.status,
2106
2237
  outputs: result.outputUrls,
2107
2238
  downloaded_files: downloaded,
2239
+ output_media: buildMediaDescriptors(result.outputUrls, result.payload?.type),
2108
2240
  ...result.status === "failed" ? { error: result.payload?.error } : {}
2109
2241
  });
2110
2242
  }
@@ -2331,7 +2463,8 @@ function registerAvatarCommands(program) {
2331
2463
  target_language: opts.language
2332
2464
  })
2333
2465
  );
2334
- emit(ctx.out, result, (o) => {
2466
+ const media = buildMediaDescriptors(extractOutputUrls(result), "audio");
2467
+ emit(ctx.out, { ...result, output_media: media }, (o) => {
2335
2468
  note(o, fmt.green(o, `Avatar video ${result?.avatar_video_id ?? "created"}.`));
2336
2469
  note(o, fmt.dim(o, `Render (paid): videodraft avatar render ${result?.avatar_video_id}`));
2337
2470
  });
@@ -2363,7 +2496,8 @@ function registerAvatarCommands(program) {
2363
2496
  spin.update(`Rendering avatar video \u2014 ${exportStatus}`);
2364
2497
  if (exportStatus === "completed") {
2365
2498
  spin.stop();
2366
- emit(ctx.out, status, (o) => {
2499
+ const media = buildMediaDescriptors(extractOutputUrls(status), "video");
2500
+ emit(ctx.out, { ...status, output_media: media }, (o) => {
2367
2501
  note(o, fmt.green(o, "Avatar render completed."));
2368
2502
  if (status?.video_url) process.stdout.write(`${status.video_url}
2369
2503
  `);
@@ -2391,7 +2525,8 @@ function registerAvatarCommands(program) {
2391
2525
  avatar.command("get <avatar_video_id>").description("Fetch one avatar video (status + video_url when rendered)").action(async function(avatarVideoId) {
2392
2526
  const ctx = buildContext(this);
2393
2527
  const result = await ctx.client.callTool("get_avatar_video", { avatar_video_id: avatarVideoId });
2394
- emit(ctx.out, result);
2528
+ const media = buildMediaDescriptors(extractOutputUrls(result), "video");
2529
+ emit(ctx.out, { ...result, output_media: media });
2395
2530
  });
2396
2531
  avatar.command("list").description("List your avatar videos").action(async function() {
2397
2532
  const ctx = buildContext(this);
@@ -2628,6 +2763,8 @@ _videodraft
2628
2763
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2629
2764
  try {
2630
2765
  const child = spawn3(cmd, [DOCS_URL], { stdio: "ignore", detached: true, shell: process.platform === "win32" });
2766
+ child.on("error", () => {
2767
+ });
2631
2768
  child.unref();
2632
2769
  } catch {
2633
2770
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "videodraft",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Official VideoDraft CLI — create AI videos, images, voiceovers and music from your terminal. Agent-friendly: --json everywhere, stable exit codes, async job polling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,8 +41,9 @@
41
41
  "README.md"
42
42
  ],
43
43
  "engines": {
44
- "node": ">=20"
44
+ "node": ">=20.18.1"
45
45
  },
46
+ "packageManager": "pnpm@10.28.1",
46
47
  "scripts": {
47
48
  "build": "tsup",
48
49
  "build:watch": "tsup --watch",