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 +7 -2
- package/dist/client.js +77 -14
- package/dist/index.js +191 -54
- package/package.json +3 -2
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 #
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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:
|
|
518
|
-
refreshToken:
|
|
576
|
+
clientId: expected.clientId,
|
|
577
|
+
refreshToken: expected.refreshToken
|
|
519
578
|
});
|
|
520
579
|
if (!tokens) return null;
|
|
521
|
-
saveRotatedTokens(profileName, tokens, env);
|
|
522
|
-
|
|
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
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
251
|
-
c
|
|
252
|
-
|
|
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
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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:
|
|
753
|
-
refreshToken:
|
|
817
|
+
clientId: expected.clientId,
|
|
818
|
+
refreshToken: expected.refreshToken
|
|
754
819
|
});
|
|
755
820
|
if (!tokens) return null;
|
|
756
|
-
saveRotatedTokens(profileName, tokens, env);
|
|
757
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|