blitzreels 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,448 +3,18 @@ import { spawn } from "node:child_process";
3
3
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
5
5
  import { basename, dirname, extname, join, resolve } from "node:path";
6
- import { BlitzReelsClient, BlitzReelsError, } from "@blitzreels/sdk";
7
- const CONFIG_PATH = join(homedir(), ".blitzreels", "config.json");
8
- const KEYCHAIN_SERVICE = "BlitzReels CLI";
9
- const KEYCHAIN_ACCOUNT = "api-key";
6
+ import { BlitzReelsError } from "@blitzreels/sdk";
7
+ import { CLI_VERSION, createCliClient, DEFAULT_BASE_URL, loadConfig, persistLoggedInApiKey, resolveRuntimeConfig, storeApiKey, } from "./cli-runtime.js";
8
+ import { commandLabel, commandsEqual, flagsForCommand, GLOBAL_FLAGS, suggestCommandSpecs, suggestValues, } from "./command-metadata.js";
9
+ import { COMMAND_SPECS } from "./command-registry.js";
10
+ import { handleExportsCommand } from "./commands/exports.js";
11
+ import { handleProjectsCommand } from "./commands/projects.js";
12
+ import { buildDryRunRequest, requireDeleteConfirmation, requirePreviewFirstConfirmation, } from "./preview-first-edit.js";
13
+ const UPDATE_CACHE_PATH = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "blitzreels", "update-check.json");
14
+ const UPDATE_CHECK_TTL_MS = 24 * 60 * 60 * 1000;
15
+ const UPDATE_CHECK_TIMEOUT_MS = 800;
10
16
  const DEFAULT_WAIT_INTERVAL_MS = 3000;
11
17
  const DEFAULT_WAIT_TIMEOUT_MS = 20 * 60 * 1000;
12
- const GLOBAL_FLAGS = ["api-key", "base-url", "json", "help"];
13
- const TEXT_STYLE_FLAGS = [
14
- "spec-json",
15
- "font-family",
16
- "font-size",
17
- "font-weight",
18
- "color",
19
- "background-color",
20
- "padding-x",
21
- "padding-y",
22
- "border-radius",
23
- "top",
24
- "left",
25
- "text-align",
26
- "text-shadow",
27
- "no-background",
28
- ];
29
- const CAPTION_WORD_SELECTOR_FLAGS = [
30
- "word-id",
31
- "word-ids",
32
- "timeline-item-id",
33
- "match-text",
34
- "match-pattern",
35
- ];
36
- const CAPTION_WORD_STYLE_FLAGS = [
37
- "style-json",
38
- "font-family",
39
- "font-weight",
40
- "font-style",
41
- "color",
42
- "active-color",
43
- "background-color",
44
- "active-animation",
45
- "text-transform",
46
- "text-shadow",
47
- "glow-color",
48
- "glow-blur-px",
49
- "scale",
50
- "offset-x",
51
- "offset-y",
52
- "rotation-deg",
53
- "letter-spacing",
54
- "opacity",
55
- "clear-existing",
56
- ];
57
- function commandSpec({ command, usage, flags = [], }) {
58
- return { command, usage, flags };
59
- }
60
- const COMMAND_SPECS = [
61
- commandSpec({
62
- command: ["auth", "login"],
63
- usage: "blitzreels auth login [--no-browser] [--timeout-ms 120000]",
64
- flags: ["no-browser", "timeout-ms"],
65
- }),
66
- commandSpec({
67
- command: ["auth", "set-key"],
68
- usage: "blitzreels auth set-key --api-key KEY [--base-url URL]",
69
- }),
70
- commandSpec({
71
- command: ["auth", "whoami"],
72
- usage: "blitzreels auth whoami [--json]",
73
- }),
74
- commandSpec({
75
- command: ["workspace", "settings", "get"],
76
- usage: "blitzreels workspace settings get [--json]",
77
- }),
78
- commandSpec({
79
- command: ["workspace", "settings", "update"],
80
- usage: "blitzreels workspace settings update [--name NAME] [--description TEXT|null] [--protected-words foo,bar] [--icon-storage-key KEY|null] [--aspect-ratio 9:16] [--orientation portrait] [--resolution 1080p] [--frame-rate 30] [--auto-manage-broll true] [--caption-theme-id ID|null]",
81
- flags: [
82
- "name",
83
- "description",
84
- "protected-words",
85
- "icon-storage-key",
86
- "aspect-ratio",
87
- "orientation",
88
- "resolution",
89
- "frame-rate",
90
- "auto-manage-broll",
91
- "caption-theme-id",
92
- ],
93
- }),
94
- commandSpec({
95
- command: ["projects", "list"],
96
- usage: "blitzreels projects list [--search QUERY] [--status active|archived|all] [--limit 50] [--offset 0] [--cursor CURSOR] [--json]",
97
- flags: ["search", "status", "limit", "offset", "cursor"],
98
- }),
99
- commandSpec({
100
- command: ["projects", "create"],
101
- usage: "blitzreels projects create --name NAME [--description TEXT] [--aspect-ratio 9:16] [--project-type video|carousel] [--carousel-settings-json JSON]",
102
- flags: [
103
- "name",
104
- "description",
105
- "aspect-ratio",
106
- "project-type",
107
- "carousel-settings-json",
108
- ],
109
- }),
110
- commandSpec({
111
- command: ["projects", "get"],
112
- usage: "blitzreels projects get --project-id ID [--json]",
113
- flags: ["project-id"],
114
- }),
115
- commandSpec({
116
- command: ["projects", "rename"],
117
- usage: "blitzreels projects rename --project-id ID --name NAME",
118
- flags: ["project-id", "name"],
119
- }),
120
- commandSpec({
121
- command: ["projects", "update"],
122
- usage: "blitzreels projects update --project-id ID [--name NAME] [--description TEXT|null]",
123
- flags: ["project-id", "name", "description"],
124
- }),
125
- commandSpec({
126
- command: ["projects", "delete"],
127
- usage: "blitzreels projects delete --project-id ID --confirm-delete",
128
- flags: ["project-id", "confirm-delete"],
129
- }),
130
- commandSpec({
131
- command: ["media", "list"],
132
- usage: "blitzreels media list [--folder-id ID] [--asset-type video|audio|image|document] [--search QUERY] [--limit 100] [--offset 0] [--json]",
133
- flags: ["folder-id", "asset-type", "search", "limit", "offset"],
134
- }),
135
- commandSpec({
136
- command: ["media", "get"],
137
- usage: "blitzreels media get --asset-id ID [--json]",
138
- flags: ["asset-id"],
139
- }),
140
- commandSpec({
141
- command: ["media", "delete"],
142
- usage: "blitzreels media delete --asset-id ID --confirm-delete",
143
- flags: ["asset-id", "confirm-delete"],
144
- }),
145
- commandSpec({
146
- command: ["media", "upload"],
147
- usage: "blitzreels media upload --file ./video.mp4 [--name NAME] [--content-type video/mp4] [--auto-process true] [--auto-transcribe true] [--auto-describe true] [--auto-suggest-shorts true]",
148
- flags: [
149
- "file",
150
- "name",
151
- "content-type",
152
- "auto-process",
153
- "auto-transcribe",
154
- "auto-describe",
155
- "auto-suggest-shorts",
156
- ],
157
- }),
158
- commandSpec({
159
- command: ["media", "import-youtube"],
160
- usage: "blitzreels media import-youtube --url URL [--folder-id ID|root] [--quality 1080p] [--fps 30] [--audio-language fr|null]",
161
- flags: ["url", "folder-id", "quality", "fps", "audio-language"],
162
- }),
163
- commandSpec({
164
- command: ["media", "rename"],
165
- usage: "blitzreels media rename --asset-id ID --name NAME",
166
- flags: ["asset-id", "name"],
167
- }),
168
- commandSpec({
169
- command: ["media", "update"],
170
- usage: "blitzreels media update --asset-id ID [--name NAME] [--description TEXT|null] [--folder-id ID|root] [--allow-broll true]",
171
- flags: ["asset-id", "name", "description", "folder-id", "allow-broll"],
172
- }),
173
- commandSpec({
174
- command: ["media", "move"],
175
- usage: "blitzreels media move --asset-id ID|--asset-ids ID1,ID2 --target-folder-id ID|root",
176
- flags: ["asset-id", "asset-ids", "target-folder-id"],
177
- }),
178
- commandSpec({
179
- command: ["media", "attach"],
180
- usage: "blitzreels media attach --project-id ID --asset-id ID [--at 0] [--duration 4] [--layer-index 2] [--allow-duplicate true] [--position-preset center] [--position-x 0] [--position-y 0] [--width-px 320] [--height-px 180] [--scale 1.2] [--opacity 0.9] [--animation-preset popIn]",
181
- flags: [
182
- "project-id",
183
- "asset-id",
184
- "at",
185
- "duration",
186
- "layer-index",
187
- "allow-duplicate",
188
- "position-preset",
189
- "position-x",
190
- "position-y",
191
- "width-px",
192
- "height-px",
193
- "scale",
194
- "opacity",
195
- "animation-preset",
196
- ],
197
- }),
198
- commandSpec({
199
- command: ["media", "folders", "list"],
200
- usage: "blitzreels media folders list [--parent-folder-id ID] [--json]",
201
- flags: ["parent-folder-id"],
202
- }),
203
- commandSpec({
204
- command: ["media", "folders", "create"],
205
- usage: "blitzreels media folders create --name NAME [--parent-folder-id ID] [--description TEXT|null] [--icon-type video]",
206
- flags: ["name", "parent-folder-id", "description", "icon-type"],
207
- }),
208
- commandSpec({
209
- command: ["media", "folders", "rename"],
210
- usage: "blitzreels media folders rename --folder-id ID --name NAME",
211
- flags: ["folder-id", "name"],
212
- }),
213
- commandSpec({
214
- command: ["media", "folders", "update"],
215
- usage: "blitzreels media folders update --folder-id ID [--name NAME] [--parent-folder-id ID|root] [--description TEXT|null] [--icon-type video]",
216
- flags: [
217
- "folder-id",
218
- "name",
219
- "parent-folder-id",
220
- "description",
221
- "icon-type",
222
- ],
223
- }),
224
- commandSpec({
225
- command: ["media", "folders", "delete"],
226
- usage: "blitzreels media folders delete --folder-id ID --confirm-delete [--cascade true]",
227
- flags: ["folder-id", "confirm-delete", "cascade"],
228
- }),
229
- commandSpec({
230
- command: ["project", "inspect"],
231
- usage: "blitzreels project inspect --project-id ID [--mode full] [--json]",
232
- flags: ["project-id", "mode"],
233
- }),
234
- commandSpec({
235
- command: ["project", "timeline"],
236
- usage: "blitzreels project timeline --project-id ID [--json]",
237
- flags: ["project-id"],
238
- }),
239
- commandSpec({
240
- command: ["project", "snapshot"],
241
- usage: "blitzreels project snapshot --project-id ID --at 00:03 [--target-width 608] [--image-format jpeg|png|webp] [--json]",
242
- flags: ["project-id", "at", "target-width", "image-format"],
243
- }),
244
- commandSpec({
245
- command: ["project", "snapshots"],
246
- usage: "blitzreels project snapshots --project-id ID --at 00:01,00:03 [--target-width 608] [--image-format jpeg|png|webp] [--json]",
247
- flags: ["project-id", "at", "target-width", "image-format"],
248
- }),
249
- commandSpec({
250
- command: ["timeline", "transform"],
251
- usage: "blitzreels timeline transform --project-id ID --timeline-item-id ID [--position-x 0] [--position-y 0] [--width-px 320|null] [--height-px 180|null] [--keep-aspect-ratio true]",
252
- flags: [
253
- "project-id",
254
- "timeline-item-id",
255
- "position-x",
256
- "position-y",
257
- "width-px",
258
- "height-px",
259
- "keep-aspect-ratio",
260
- ],
261
- }),
262
- commandSpec({
263
- command: ["timeline", "move"],
264
- usage: "blitzreels timeline move --project-id ID --timeline-item-id ID --at 00:03 [--layer-index 2]",
265
- flags: ["project-id", "timeline-item-id", "at", "layer-index"],
266
- }),
267
- commandSpec({
268
- command: ["timeline", "delete"],
269
- usage: "blitzreels timeline delete --project-id ID --timeline-item-id ID|--timeline-item-ids ID1,ID2 [--auto-pack true]",
270
- flags: ["project-id", "timeline-item-id", "timeline-item-ids", "auto-pack"],
271
- }),
272
- commandSpec({
273
- command: ["text", "add"],
274
- usage: "blitzreels text add --project-id ID --text \"Hook\" [--name NAME] [--at 0] [--duration 4] [--layer-index 2] [--spec-json JSON] [--font-family Inter] [--font-size 64] [--font-weight 700] [--color '#fff'] [--background-color '#000'] [--padding-x 32] [--padding-y 16] [--border-radius 24] [--top '18%'] [--left '50%'] [--text-align center] [--text-shadow '0 6px 18px #000'] [--no-background]",
275
- flags: [
276
- "project-id",
277
- "text",
278
- "name",
279
- "at",
280
- "duration",
281
- "layer-index",
282
- ...TEXT_STYLE_FLAGS,
283
- ],
284
- }),
285
- commandSpec({
286
- command: ["text", "list"],
287
- usage: "blitzreels text list --project-id ID [--json]",
288
- flags: ["project-id"],
289
- }),
290
- commandSpec({
291
- command: ["text", "update"],
292
- usage: "blitzreels text update --project-id ID --content-item-id ID [--text \"New hook\"] [--name NAME] [--spec-json JSON] [--font-family Pacifico] [--font-size 82] [--font-weight 700] [--color '#17FFA6'] [--background-color '#051914'] [--padding-x 32] [--padding-y 16] [--border-radius 24] [--top '18%'] [--left '50%'] [--text-align center] [--text-shadow '0 6px 18px #000'] [--no-background]",
293
- flags: [
294
- "project-id",
295
- "content-item-id",
296
- "text",
297
- "name",
298
- ...TEXT_STYLE_FLAGS,
299
- ],
300
- }),
301
- commandSpec({
302
- command: ["text", "remove"],
303
- usage: "blitzreels text remove --project-id ID --content-item-id ID",
304
- flags: ["project-id", "content-item-id"],
305
- }),
306
- commandSpec({
307
- command: ["audio", "add"],
308
- usage: "blitzreels audio add --project-id ID --asset-id ID [--at 0] [--duration 30] [--volume 0.6] [--fade-in 0.5] [--fade-out 0.5] [--loop true]",
309
- flags: [
310
- "project-id",
311
- "asset-id",
312
- "at",
313
- "duration",
314
- "volume",
315
- "fade-in",
316
- "fade-out",
317
- "loop",
318
- ],
319
- }),
320
- commandSpec({
321
- command: ["silence", "plan"],
322
- usage: "blitzreels silence plan --project-id ID --timeline-item-id ID [--padding-ms 120] [--silence-threshold-db -35] [--min-silence-duration 0.25] [--remove-blips-max-duration 0.12] [--use-caption-validation true] [--json]",
323
- flags: [
324
- "project-id",
325
- "timeline-item-id",
326
- "padding-ms",
327
- "silence-threshold-db",
328
- "min-silence-duration",
329
- "remove-blips-max-duration",
330
- "use-caption-validation",
331
- ],
332
- }),
333
- commandSpec({
334
- command: ["silence", "apply"],
335
- usage: "blitzreels silence apply --project-id ID --timeline-item-id ID --confirm-preview [--padding-ms 120] [--silence-threshold-db -35] [--min-silence-duration 0.25] [--remove-blips-max-duration 0.12] [--use-caption-validation true] [--delete-silent-clips true] [--manual-segment-overrides-json JSON]",
336
- flags: [
337
- "project-id",
338
- "timeline-item-id",
339
- "confirm-preview",
340
- "padding-ms",
341
- "silence-threshold-db",
342
- "min-silence-duration",
343
- "remove-blips-max-duration",
344
- "use-caption-validation",
345
- "delete-silent-clips",
346
- "manual-segment-overrides-json",
347
- ],
348
- }),
349
- commandSpec({
350
- command: ["mistakes", "plan"],
351
- usage: "blitzreels mistakes plan --project-id ID --timeline-item-id ID [--json]",
352
- flags: ["project-id", "timeline-item-id"],
353
- }),
354
- commandSpec({
355
- command: ["mistakes", "apply"],
356
- usage: "blitzreels mistakes apply --project-id ID --timeline-item-id ID --confirm-preview [--plan-json JSON] [--manual-verdicts-json JSON] [--delete-mistakes true]",
357
- flags: [
358
- "project-id",
359
- "timeline-item-id",
360
- "confirm-preview",
361
- "plan-json",
362
- "manual-verdicts-json",
363
- "delete-mistakes",
364
- ],
365
- }),
366
- commandSpec({
367
- command: ["captions", "words", "list"],
368
- usage: "blitzreels captions words list --project-id ID [--timeline-item-id ID] [--match-text TEXT] [--limit 100] [--offset 0]",
369
- flags: ["project-id", "timeline-item-id", "match-text", "limit", "offset"],
370
- }),
371
- commandSpec({
372
- command: ["captions", "words", "text"],
373
- usage: 'blitzreels captions words text --project-id ID --word-id ID --text "Fix"|--new-text "Fix"',
374
- flags: ["project-id", "word-id", "text", "new-text"],
375
- }),
376
- commandSpec({
377
- command: ["captions", "words", "batch-text"],
378
- usage: 'blitzreels captions words batch-text --project-id ID --updates-json \'[{"word_id":"...","new_text":"..."}]\'',
379
- flags: ["project-id", "updates-json"],
380
- }),
381
- commandSpec({
382
- command: ["captions", "words", "style"],
383
- usage: "blitzreels captions words style --project-id ID [--word-id ID|--word-ids IDs|--timeline-item-id ID|--match-text TEXT|--match-pattern numbers|caps|exclamations] [--style-json JSON|null] [--font-family Pacifico] [--font-weight 700] [--font-style italic] [--color '#17FFA6'] [--active-color '#fff'] [--background-color '#051914'] [--active-animation pop] [--text-transform uppercase] [--text-shadow '0 6px 18px #000'] [--glow-color '#17FFA6'] [--glow-blur-px 18] [--scale 1.45] [--offset-x 0] [--offset-y -8] [--rotation-deg -2] [--letter-spacing 1] [--opacity 1] [--clear-existing true]",
384
- flags: [
385
- "project-id",
386
- ...CAPTION_WORD_SELECTOR_FLAGS,
387
- ...CAPTION_WORD_STYLE_FLAGS,
388
- ],
389
- }),
390
- commandSpec({
391
- command: ["captions", "words", "emphasis"],
392
- usage: "blitzreels captions words emphasis --project-id ID [--word-id ID|--word-ids IDs|--timeline-item-id ID|--match-text TEXT|--match-pattern numbers|caps|exclamations] [--emphasis true]",
393
- flags: ["project-id", ...CAPTION_WORD_SELECTOR_FLAGS, "emphasis"],
394
- }),
395
- commandSpec({
396
- command: ["captions", "words", "delete"],
397
- usage: "blitzreels captions words delete --project-id ID --word-id ID|--word-ids ID1,ID2",
398
- flags: ["project-id", "word-id", "word-ids"],
399
- }),
400
- commandSpec({
401
- command: ["captions", "words", "merge"],
402
- usage: 'blitzreels captions words merge --project-id ID --word-ids ID1,ID2 [--text "Merged"]',
403
- flags: ["project-id", "word-ids", "text"],
404
- }),
405
- commandSpec({
406
- command: ["captions", "words", "split"],
407
- usage: 'blitzreels captions words split --project-id ID --word-id ID --words "foo,bar"',
408
- flags: ["project-id", "word-id", "words"],
409
- }),
410
- commandSpec({
411
- command: ["captions", "themes", "list"],
412
- usage: "blitzreels captions themes list [--json]",
413
- }),
414
- commandSpec({
415
- command: ["captions", "theme", "set"],
416
- usage: "blitzreels captions theme set --project-id ID --theme-id ID",
417
- flags: ["project-id", "theme-id"],
418
- }),
419
- commandSpec({
420
- command: ["captions", "theme", "set-default"],
421
- usage: "blitzreels captions theme set-default --theme-id ID",
422
- flags: ["theme-id"],
423
- }),
424
- commandSpec({
425
- command: ["exports", "start"],
426
- usage: "blitzreels exports start --project-id ID [--resolution 1080p] [--format mp4] [--include-captions true]",
427
- flags: ["project-id", "resolution", "format", "include-captions"],
428
- }),
429
- commandSpec({
430
- command: ["exports", "wait"],
431
- usage: "blitzreels exports wait --export-id ID [--interval-ms 3000] [--timeout-ms 1200000] [--json]",
432
- flags: ["export-id", "interval-ms", "timeout-ms"],
433
- }),
434
- commandSpec({
435
- command: ["exports", "download"],
436
- usage: "blitzreels exports download --export-id ID --output ./short.mp4",
437
- flags: ["export-id", "output"],
438
- }),
439
- commandSpec({
440
- command: ["api", "docs"],
441
- usage: "blitzreels api docs",
442
- }),
443
- commandSpec({
444
- command: ["api", "spec"],
445
- usage: "blitzreels api spec",
446
- }),
447
- ];
448
18
  class CliUsageError extends Error {
449
19
  code;
450
20
  suggestions;
@@ -464,33 +34,118 @@ async function main() {
464
34
  return;
465
35
  }
466
36
  if (getBooleanFlag({ flags: parsed.flags, name: "help" })) {
467
- printCommandHelpOrThrow({ command: parsed.command });
37
+ if (parsed.command.length === 0) {
38
+ printHelp();
39
+ return;
40
+ }
41
+ printCommandOrGroupHelpOrThrow({ command: parsed.command });
468
42
  return;
469
43
  }
44
+ if (!findCommandSpec({ command: parsed.command })) {
45
+ const groupSpecs = findGroupCommandSpecs({ command: parsed.command });
46
+ if (groupSpecs.length > 0) {
47
+ printGroupHelp({ command: parsed.command, specs: groupSpecs });
48
+ return;
49
+ }
50
+ }
470
51
  validateParsedArgs({ parsed });
471
- const config = await loadConfig();
472
- const apiKey = getStringFlag({ flags: parsed.flags, name: "api-key" }) ??
473
- process.env.BLITZREELS_API_KEY ??
474
- config.apiKey;
475
- const baseUrl = getStringFlag({ flags: parsed.flags, name: "base-url" }) ??
476
- process.env.BLITZREELS_BASE_URL ??
477
- config.baseUrl;
478
- const clientConfig = {
479
- apiKey,
480
- baseUrl,
481
- fetchFn: undefined,
482
- userAgent: "blitzreels-cli/0.0.0",
483
- };
52
+ validateSafeInputs({ parsed });
53
+ const storedConfig = await loadConfig();
54
+ const config = resolveRuntimeConfig({
55
+ flags: parsed.flags,
56
+ storedConfig,
57
+ });
484
58
  const context = {
485
59
  args: parsed,
486
60
  json: getBooleanFlag({ flags: parsed.flags, name: "json" }),
487
- client: new BlitzReelsClient(clientConfig),
488
- config: { apiKey, baseUrl, keyStorage: config.keyStorage },
61
+ dryRun: getBooleanFlag({ flags: parsed.flags, name: "dry-run" }),
62
+ fields: parseCsvFlag({ flags: parsed.flags, name: "fields" }),
63
+ client: createCliClient({ config }),
64
+ config,
489
65
  };
490
66
  await runCommand({ context });
67
+ await maybePrintUpdateNotice({
68
+ parsed,
69
+ baseUrl: config.baseUrl,
70
+ json: context.json,
71
+ });
491
72
  }
492
73
  async function runCommand({ context }) {
493
74
  const [group, action, subaction] = context.args.command;
75
+ if (group === "version") {
76
+ const update = await getUpdateStatus({
77
+ baseUrl: context.config.baseUrl,
78
+ cachePolicy: "refresh",
79
+ });
80
+ printResult({
81
+ json: context.json,
82
+ value: {
83
+ current: CLI_VERSION,
84
+ latest: update.manifest.latest,
85
+ update_available: update.updateAvailable,
86
+ upgrade: update.manifest.upgrade,
87
+ changelog_url: update.manifest.url,
88
+ },
89
+ human: buildVersionLines({ update }),
90
+ });
91
+ return;
92
+ }
93
+ if (group === "doctor") {
94
+ const update = await getUpdateStatus({
95
+ baseUrl: context.config.baseUrl,
96
+ cachePolicy: "refresh",
97
+ });
98
+ printResult({
99
+ json: context.json,
100
+ value: {
101
+ cli: {
102
+ current: CLI_VERSION,
103
+ latest: update.manifest.latest,
104
+ update_available: update.updateAvailable,
105
+ supported: !isVersionLessThan({
106
+ left: CLI_VERSION,
107
+ right: update.manifest.minSupported,
108
+ }),
109
+ },
110
+ config: {
111
+ api_key: context.config.apiKey ? "configured" : "missing",
112
+ base_url: context.config.baseUrl ?? "https://www.blitzreels.com/api/v1",
113
+ key_storage: context.config.keyStorage ?? "none",
114
+ },
115
+ upgrade: update.manifest.upgrade,
116
+ },
117
+ human: buildDoctorLines({ context, update }),
118
+ });
119
+ return;
120
+ }
121
+ if (group === "agent-context" || group === "schema") {
122
+ const result = buildAgentContext({
123
+ commandFilter: getStringFlag({
124
+ flags: context.args.flags,
125
+ name: "command",
126
+ }),
127
+ });
128
+ printResult({
129
+ json: context.json,
130
+ value: result,
131
+ human: buildAgentContextLines({ value: result }),
132
+ });
133
+ return;
134
+ }
135
+ if (group === "prime" || group === "llms") {
136
+ const result = buildAgentPrime({
137
+ commandFilter: getStringFlag({
138
+ flags: context.args.flags,
139
+ name: "command",
140
+ }),
141
+ });
142
+ printResult({
143
+ json: context.json,
144
+ value: result,
145
+ human: buildAgentPrimeLines({ value: result }),
146
+ });
147
+ return;
148
+ }
494
149
  if (group === "auth" && action === "set-key") {
495
150
  await setApiKey({ context });
496
151
  return;
@@ -501,14 +156,22 @@ async function runCommand({ context }) {
501
156
  }
502
157
  if (group === "auth" && action === "whoami") {
503
158
  const result = await context.client.auth.validate();
159
+ const displayEnvironment = resolveDisplayEnvironment({
160
+ baseUrl: context.config.baseUrl,
161
+ environment: result.environment,
162
+ });
163
+ const value = {
164
+ ...result,
165
+ display_environment: displayEnvironment,
166
+ };
504
167
  printResult({
505
168
  json: context.json,
506
- value: result,
169
+ value,
507
170
  human: [
508
171
  `Workspace: ${result.organization_id}`,
509
172
  `User: ${result.user_id}`,
510
173
  `Plan: ${result.plan}`,
511
- `Environment: ${result.environment}`,
174
+ `Environment: ${displayEnvironment}`,
512
175
  `Docs: ${result.docs_url}`,
513
176
  ],
514
177
  });
@@ -517,10 +180,18 @@ async function runCommand({ context }) {
517
180
  if (group === "api" && action === "spec") {
518
181
  const url = new URL(context.config.baseUrl ?? "https://www.blitzreels.com/api/v1");
519
182
  url.pathname = "/api/openapi.json";
183
+ const response = await fetch(url, {
184
+ headers: { Accept: "application/json" },
185
+ });
186
+ const spec = await readJsonResponse({
187
+ response,
188
+ url: url.toString(),
189
+ action: "fetch OpenAPI spec",
190
+ });
520
191
  printResult({
521
- json: context.json,
522
- value: { url: url.toString() },
523
- human: [url.toString()],
192
+ json: true,
193
+ value: spec,
194
+ human: [],
524
195
  });
525
196
  return;
526
197
  }
@@ -534,6 +205,14 @@ async function runCommand({ context }) {
534
205
  });
535
206
  return;
536
207
  }
208
+ if (context.dryRun) {
209
+ printResult({
210
+ json: true,
211
+ value: buildDryRunPreview({ context }),
212
+ human: [],
213
+ });
214
+ return;
215
+ }
537
216
  requireApiKey({ apiKey: context.config.apiKey });
538
217
  if (group === "workspace" && action === "settings" && subaction === "get") {
539
218
  const result = await context.client.workspace.getSettings();
@@ -575,120 +254,27 @@ async function runCommand({ context }) {
575
254
  });
576
255
  return;
577
256
  }
578
- if (group === "projects" && action === "list") {
579
- const result = await context.client.projects.list({
580
- limit: getNumberFlag({ flags: context.args.flags, name: "limit" }),
581
- cursor: getStringFlag({ flags: context.args.flags, name: "cursor" }),
582
- offset: getNumberFlag({ flags: context.args.flags, name: "offset" }),
583
- search: getStringFlag({ flags: context.args.flags, name: "search" }),
584
- status: parseProjectStatus({
585
- value: getStringFlag({ flags: context.args.flags, name: "status" }),
586
- }),
587
- });
588
- printResult({
589
- json: context.json,
590
- value: result,
591
- human: result.projects.map((project) => `${project.id} ${project.status} ${project.name} (${project.aspect_ratio ?? "-"})`),
592
- });
593
- return;
594
- }
595
- if (group === "projects" && action === "create") {
596
- const result = await context.client.projects.create({
597
- name: requireStringFlag({ flags: context.args.flags, name: "name" }),
598
- description: getStringFlag({
599
- flags: context.args.flags,
600
- name: "description",
601
- }),
602
- aspectRatio: parseAspectRatio({
603
- value: getStringFlag({
604
- flags: context.args.flags,
605
- name: "aspect-ratio",
606
- }),
607
- }),
608
- projectType: parseProjectType({
609
- value: getStringFlag({
610
- flags: context.args.flags,
611
- name: "project-type",
612
- }),
613
- }),
614
- carouselSettings: parseJsonObjectFlag({
615
- flags: context.args.flags,
616
- name: "carousel-settings-json",
617
- }),
618
- });
619
- printResult({
620
- json: context.json,
621
- value: result,
622
- human: [`Project created: ${result.id} ${result.name}`],
623
- });
624
- return;
625
- }
626
- if (group === "projects" && action === "get") {
627
- const result = await context.client.projects.get({
628
- projectId: requireStringFlag({
629
- flags: context.args.flags,
630
- name: "project-id",
631
- }),
632
- });
633
- printResult({
634
- json: context.json,
635
- value: result,
636
- human: [
637
- `${result.id} ${result.status} ${result.name}`,
638
- `Description: ${result.description ?? ""}`,
639
- ],
640
- });
641
- return;
642
- }
643
- if (group === "projects" && action === "rename") {
644
- const result = await context.client.projects.update({
645
- projectId: requireStringFlag({
646
- flags: context.args.flags,
647
- name: "project-id",
648
- }),
649
- name: requireStringFlag({ flags: context.args.flags, name: "name" }),
650
- description: undefined,
651
- });
652
- printResult({
653
- json: context.json,
654
- value: result,
655
- human: [`Project renamed: ${result.id} ${result.name}`],
656
- });
657
- return;
658
- }
659
- if (group === "projects" && action === "update") {
660
- const result = await context.client.projects.update({
661
- projectId: requireStringFlag({
662
- flags: context.args.flags,
663
- name: "project-id",
664
- }),
665
- name: getStringFlag({ flags: context.args.flags, name: "name" }),
666
- description: parseNullableStringFlag({
667
- flags: context.args.flags,
668
- name: "description",
669
- }),
670
- });
671
- printResult({
672
- json: context.json,
673
- value: result,
674
- human: [`Project updated: ${result.id} ${result.name}`],
675
- });
676
- return;
677
- }
678
- if (group === "projects" && action === "delete") {
679
- requireConfirmDelete({ flags: context.args.flags });
680
- const result = await context.client.projects.delete({
681
- projectId: requireStringFlag({
682
- flags: context.args.flags,
683
- name: "project-id",
684
- }),
685
- });
686
- printResult({
687
- json: context.json,
688
- value: result,
689
- human: ["Project deleted."],
257
+ if (group === "projects") {
258
+ const handled = await handleProjectsCommand({
259
+ context,
260
+ action,
261
+ helpers: {
262
+ getBooleanFlag,
263
+ getNumberFlag,
264
+ getStringFlag,
265
+ requireStringFlag,
266
+ parseNullableStringFlag,
267
+ parseProjectStatus,
268
+ parseAspectRatio,
269
+ parseProjectType,
270
+ parseJsonObjectFlag,
271
+ buildProjectDashboardUrl,
272
+ printResult,
273
+ },
690
274
  });
691
- return;
275
+ if (handled) {
276
+ return;
277
+ }
692
278
  }
693
279
  if (group === "media" && action === "list") {
694
280
  const result = await context.client.media.list({
@@ -719,13 +305,31 @@ async function runCommand({ context }) {
719
305
  value: result,
720
306
  human: [
721
307
  `${result.asset.id} ${result.asset.assetType} ${result.asset.name}`,
308
+ `Processing: ${result.asset.processingStatus ?? "unknown"}${result.asset.processingSubstage ? `/${result.asset.processingSubstage}` : ""}`,
309
+ ...(result.asset.processingError
310
+ ? [`Processing error: ${result.asset.processingError}`]
311
+ : []),
312
+ `Retryable: ${result.asset.retryable ? "yes" : "no"}`,
313
+ ...(result.asset.providerRequestIds.processJobRunId
314
+ ? [`Process job: ${result.asset.providerRequestIds.processJobRunId}`]
315
+ : []),
316
+ ...(result.asset.providerRequestIds.transcribeJobRunId
317
+ ? [
318
+ `Transcribe job: ${result.asset.providerRequestIds.transcribeJobRunId}`,
319
+ ]
320
+ : []),
722
321
  `URL: ${result.asset.fileUrl ?? ""}`,
723
322
  ],
724
323
  });
725
324
  return;
726
325
  }
727
326
  if (group === "media" && action === "delete") {
728
- requireConfirmDelete({ flags: context.args.flags });
327
+ requireDeleteConfirmation({
328
+ confirmed: getBooleanFlag({
329
+ flags: context.args.flags,
330
+ name: "confirm-delete",
331
+ }),
332
+ });
729
333
  const result = await context.client.media.delete({
730
334
  assetId: requireStringFlag({
731
335
  flags: context.args.flags,
@@ -833,7 +437,8 @@ async function runCommand({ context }) {
833
437
  });
834
438
  return;
835
439
  }
836
- if (group === "media" && action === "attach") {
440
+ if ((group === "media" && action === "attach") ||
441
+ (group === "broll" && action === "add")) {
837
442
  const result = await context.client.media.attachToProject({
838
443
  projectId: requireStringFlag({
839
444
  flags: context.args.flags,
@@ -891,24 +496,6 @@ async function runCommand({ context }) {
891
496
  }),
892
497
  }),
893
498
  });
894
- const requestedLayer = getNumberFlag({
895
- flags: context.args.flags,
896
- name: "layer-index",
897
- });
898
- if (requestedLayer !== undefined) {
899
- await Promise.all(result.inserted
900
- .map((item) => item.timeline_item_id)
901
- .filter((item) => typeof item === "string")
902
- .map((timelineItemId) => context.client.timeline.move({
903
- projectId: requireStringFlag({
904
- flags: context.args.flags,
905
- name: "project-id",
906
- }),
907
- timelineItemId,
908
- newStartSeconds: result.inserted.find((item) => item.timeline_item_id === timelineItemId)?.start_seconds ?? 0,
909
- newLayerIndex: requestedLayer,
910
- })));
911
- }
912
499
  printResult({
913
500
  json: context.json,
914
501
  value: result,
@@ -918,45 +505,268 @@ async function runCommand({ context }) {
918
505
  });
919
506
  return;
920
507
  }
921
- if (group === "media" && action === "folders" && subaction === "list") {
922
- const result = await context.client.media.folders.list({
923
- parentFolderId: getStringFlag({
508
+ if (group === "media" && action === "overlay" && subaction === "update") {
509
+ const result = await context.client.media.updateOverlay({
510
+ projectId: requireStringFlag({
924
511
  flags: context.args.flags,
925
- name: "parent-folder-id",
512
+ name: "project-id",
513
+ }),
514
+ timelineItemId: requireStringFlag({
515
+ flags: context.args.flags,
516
+ name: "timeline-item-id",
517
+ }),
518
+ positionPreset: parseMediaPositionPreset({
519
+ value: getStringFlag({
520
+ flags: context.args.flags,
521
+ name: "position-preset",
522
+ }),
523
+ }),
524
+ positionX: getNumberFlag({
525
+ flags: context.args.flags,
526
+ name: "position-x",
527
+ }),
528
+ positionY: getNumberFlag({
529
+ flags: context.args.flags,
530
+ name: "position-y",
531
+ }),
532
+ widthPx: getNumberFlag({
533
+ flags: context.args.flags,
534
+ name: "width-px",
535
+ }),
536
+ heightPx: getNumberFlag({
537
+ flags: context.args.flags,
538
+ name: "height-px",
539
+ }),
540
+ scale: getNumberFlag({ flags: context.args.flags, name: "scale" }),
541
+ animationPreset: parseMediaAnimationPreset({
542
+ value: getStringFlag({
543
+ flags: context.args.flags,
544
+ name: "animation-preset",
545
+ }),
546
+ }),
547
+ removeAnimations: getOptionalBooleanFlag({
548
+ flags: context.args.flags,
549
+ name: "remove-animations",
550
+ }),
551
+ blurredBackground: getOptionalBooleanFlag({
552
+ flags: context.args.flags,
553
+ name: "blurred-background",
926
554
  }),
927
555
  });
928
556
  printResult({
929
557
  json: context.json,
930
558
  value: result,
931
- human: result.folders.map((folder) => `${folder.id} ${folder.name} (${folder.assetCount ?? 0} assets)`),
559
+ human: [result.message],
932
560
  });
933
561
  return;
934
562
  }
935
- if (group === "media" && action === "folders" && subaction === "create") {
936
- const result = await context.client.media.folders.create({
937
- name: requireStringFlag({ flags: context.args.flags, name: "name" }),
938
- parentFolderId: parseFolderIdFlag({
939
- flags: context.args.flags,
940
- name: "parent-folder-id",
941
- }),
942
- description: parseNullableStringFlag({
943
- flags: context.args.flags,
944
- name: "description",
945
- }),
946
- iconType: getStringFlag({ flags: context.args.flags, name: "icon-type" }),
947
- });
563
+ if (group === "media" && action === "wait") {
564
+ const result = await waitForMediaAsset({ context });
948
565
  printResult({
949
566
  json: context.json,
950
567
  value: result,
951
- human: [`Folder created: ${result.folder.id} ${result.folder.name}`],
568
+ human: [
569
+ `Media ready: ${result.asset.id} ${result.asset.name} (${result.ready_for.join(", ")})`,
570
+ `Processing: ${result.asset.processingStatus ?? "unknown"}${result.asset.processingSubstage ? `/${result.asset.processingSubstage}` : ""}`,
571
+ ...(result.asset.processingError
572
+ ? [`Processing error: ${result.asset.processingError}`]
573
+ : []),
574
+ ],
952
575
  });
953
576
  return;
954
577
  }
955
- if (group === "media" && action === "folders" && subaction === "rename") {
956
- const result = await context.client.media.folders.update({
957
- folderId: requireStringFlag({
578
+ if (group === "media" && action === "shorts" && subaction === "list") {
579
+ const result = await context.client.media.shorts.list({
580
+ assetId: requireStringFlag({
958
581
  flags: context.args.flags,
959
- name: "folder-id",
582
+ name: "asset-id",
583
+ }),
584
+ });
585
+ printResult({
586
+ json: context.json,
587
+ value: result,
588
+ human: [
589
+ `Saved: ${result.saved.length}`,
590
+ `Drafts: ${result.drafts.length}`,
591
+ ...[...result.saved, ...result.drafts].map((suggestion) => JSON.stringify(suggestion)),
592
+ ],
593
+ });
594
+ return;
595
+ }
596
+ if (group === "media" && action === "shorts" && subaction === "generate") {
597
+ const result = await context.client.media.shorts.generate({
598
+ assetId: requireStringFlag({
599
+ flags: context.args.flags,
600
+ name: "asset-id",
601
+ }),
602
+ contentProfile: parseShortContentProfile({
603
+ value: getStringFlag({
604
+ flags: context.args.flags,
605
+ name: "content-profile",
606
+ }),
607
+ }),
608
+ autoTrimSilence: getOptionalBooleanFlag({
609
+ flags: context.args.flags,
610
+ name: "auto-trim-silence",
611
+ }),
612
+ });
613
+ printResult({
614
+ json: context.json,
615
+ value: result,
616
+ human: [`Short suggestion generation queued: ${result.run_id}`],
617
+ });
618
+ return;
619
+ }
620
+ if (group === "media" && action === "shorts" && subaction === "apply") {
621
+ const result = await context.client.media.shorts.apply({
622
+ assetId: requireStringFlag({
623
+ flags: context.args.flags,
624
+ name: "asset-id",
625
+ }),
626
+ suggestionId: requireStringFlag({
627
+ flags: context.args.flags,
628
+ name: "suggestion-id",
629
+ }),
630
+ projectId: requireStringFlag({
631
+ flags: context.args.flags,
632
+ name: "project-id",
633
+ }),
634
+ captionStyleId: parseNullableStringFlag({
635
+ flags: context.args.flags,
636
+ name: "caption-style-id",
637
+ }),
638
+ markSaved: getOptionalBooleanFlag({
639
+ flags: context.args.flags,
640
+ name: "mark-saved",
641
+ }),
642
+ allowUnverifiedExport: getOptionalBooleanFlag({
643
+ flags: context.args.flags,
644
+ name: "allow-unverified-export",
645
+ }),
646
+ automaticLayout: parseJsonObjectFlag({
647
+ flags: context.args.flags,
648
+ name: "automatic-layout-json",
649
+ }),
650
+ });
651
+ printResult({
652
+ json: context.json,
653
+ value: result,
654
+ human: [
655
+ `Short applied: ${result.timeline_item_id} in project ${result.project_id}`,
656
+ ],
657
+ });
658
+ return;
659
+ }
660
+ if (group === "clips" && action === "create") {
661
+ const result = await context.client.clips.create(toCreateClipOptions({ flags: context.args.flags }));
662
+ printClipResult({ context, result });
663
+ return;
664
+ }
665
+ if (group === "clips" && action === "get") {
666
+ const result = await context.client.clips.get({
667
+ clipId: requireStringFlag({
668
+ flags: context.args.flags,
669
+ name: "clip-id",
670
+ }),
671
+ });
672
+ printClipResult({ context, result });
673
+ return;
674
+ }
675
+ if (group === "clips" && action === "wait") {
676
+ const result = await waitForClip({ context });
677
+ printClipResult({ context, result });
678
+ return;
679
+ }
680
+ if (group === "clips" && action === "reselect") {
681
+ const result = await context.client.clips.reselect({
682
+ clipId: requireStringFlag({
683
+ flags: context.args.flags,
684
+ name: "clip-id",
685
+ }),
686
+ selection: buildClipSelection({ flags: context.args.flags }),
687
+ target: buildClipTarget({
688
+ flags: context.args.flags,
689
+ requireMaxDuration: false,
690
+ }),
691
+ });
692
+ printClipResult({ context, result });
693
+ return;
694
+ }
695
+ if (group === "clips" && action === "repair") {
696
+ const result = await context.client.clips.repair({
697
+ clipId: requireStringFlag({
698
+ flags: context.args.flags,
699
+ name: "clip-id",
700
+ }),
701
+ repairMode: parseClipRepairMode({
702
+ value: getStringFlag({
703
+ flags: context.args.flags,
704
+ name: "repair-mode",
705
+ }),
706
+ }),
707
+ });
708
+ printClipResult({ context, result });
709
+ return;
710
+ }
711
+ if (group === "clips" && action === "export") {
712
+ const result = await context.client.clips.export({
713
+ clipId: requireStringFlag({
714
+ flags: context.args.flags,
715
+ name: "clip-id",
716
+ }),
717
+ resolution: parseClipExportResolution({
718
+ value: getStringFlag({ flags: context.args.flags, name: "resolution" }),
719
+ }),
720
+ format: parseClipExportFormat({
721
+ value: getStringFlag({ flags: context.args.flags, name: "format" }),
722
+ }),
723
+ allowBlockingQaBypass: getOptionalBooleanFlag({
724
+ flags: context.args.flags,
725
+ name: "allow-blocking-qa-bypass",
726
+ }) ?? false,
727
+ });
728
+ printClipResult({ context, result });
729
+ return;
730
+ }
731
+ if (group === "media" && action === "folders" && subaction === "list") {
732
+ const result = await context.client.media.folders.list({
733
+ parentFolderId: getStringFlag({
734
+ flags: context.args.flags,
735
+ name: "parent-folder-id",
736
+ }),
737
+ });
738
+ printResult({
739
+ json: context.json,
740
+ value: result,
741
+ human: result.folders.map((folder) => `${folder.id} ${folder.name} (${folder.assetCount ?? 0} assets)`),
742
+ });
743
+ return;
744
+ }
745
+ if (group === "media" && action === "folders" && subaction === "create") {
746
+ const result = await context.client.media.folders.create({
747
+ name: requireStringFlag({ flags: context.args.flags, name: "name" }),
748
+ parentFolderId: parseFolderIdFlag({
749
+ flags: context.args.flags,
750
+ name: "parent-folder-id",
751
+ }),
752
+ description: parseNullableStringFlag({
753
+ flags: context.args.flags,
754
+ name: "description",
755
+ }),
756
+ iconType: getStringFlag({ flags: context.args.flags, name: "icon-type" }),
757
+ });
758
+ printResult({
759
+ json: context.json,
760
+ value: result,
761
+ human: [`Folder created: ${result.folder.id} ${result.folder.name}`],
762
+ });
763
+ return;
764
+ }
765
+ if (group === "media" && action === "folders" && subaction === "rename") {
766
+ const result = await context.client.media.folders.update({
767
+ folderId: requireStringFlag({
768
+ flags: context.args.flags,
769
+ name: "folder-id",
960
770
  }),
961
771
  name: requireStringFlag({ flags: context.args.flags, name: "name" }),
962
772
  parentFolderId: undefined,
@@ -995,7 +805,12 @@ async function runCommand({ context }) {
995
805
  return;
996
806
  }
997
807
  if (group === "media" && action === "folders" && subaction === "delete") {
998
- requireConfirmDelete({ flags: context.args.flags });
808
+ requireDeleteConfirmation({
809
+ confirmed: getBooleanFlag({
810
+ flags: context.args.flags,
811
+ name: "confirm-delete",
812
+ }),
813
+ });
999
814
  const result = await context.client.media.folders.delete({
1000
815
  folderId: requireStringFlag({
1001
816
  flags: context.args.flags,
@@ -1042,7 +857,10 @@ async function runCommand({ context }) {
1042
857
  printResult({
1043
858
  json: context.json,
1044
859
  value: result,
1045
- human: [JSON.stringify(result.data, null, 2)],
860
+ human: [
861
+ ...timelineLayerModelHelp(),
862
+ JSON.stringify(result.data, null, 2),
863
+ ],
1046
864
  });
1047
865
  return;
1048
866
  }
@@ -1203,6 +1021,10 @@ async function runCommand({ context }) {
1203
1021
  text: getStringFlag({ flags: context.args.flags, name: "text" }),
1204
1022
  name: getStringFlag({ flags: context.args.flags, name: "name" }),
1205
1023
  spec,
1024
+ durationSeconds: getNumberFlag({
1025
+ flags: context.args.flags,
1026
+ name: "duration",
1027
+ }),
1206
1028
  });
1207
1029
  printResult({
1208
1030
  json: context.json,
@@ -1293,45 +1115,60 @@ async function runCommand({ context }) {
1293
1115
  });
1294
1116
  return;
1295
1117
  }
1296
- if (group === "audio" && action === "add") {
1297
- const result = await context.client.audio.add({
1118
+ if (group === "effects" && action === "zoom" && subaction === "add") {
1119
+ const durationSeconds = getNumberFlag({
1120
+ flags: context.args.flags,
1121
+ name: "duration",
1122
+ });
1123
+ const result = await context.client.effects.zoom.add({
1298
1124
  projectId: requireStringFlag({
1299
1125
  flags: context.args.flags,
1300
1126
  name: "project-id",
1301
1127
  }),
1302
- assetId: requireStringFlag({
1128
+ timelineItemId: getStringFlag({
1303
1129
  flags: context.args.flags,
1304
- name: "asset-id",
1130
+ name: "timeline-item-id",
1305
1131
  }),
1306
- startSeconds: parseTimecode({
1307
- value: getStringFlag({ flags: context.args.flags, name: "at" }) ?? "0",
1132
+ contentItemId: getStringFlag({
1133
+ flags: context.args.flags,
1134
+ name: "content-item-id",
1308
1135
  }),
1309
- durationSeconds: getNumberFlag({
1136
+ effectId: getStringFlag({
1310
1137
  flags: context.args.flags,
1311
- name: "duration",
1138
+ name: "effect-id",
1312
1139
  }),
1313
- volume: getNumberFlag({ flags: context.args.flags, name: "volume" }),
1314
- fadeInSeconds: getNumberFlag({
1140
+ name: getStringFlag({ flags: context.args.flags, name: "name" }),
1141
+ startTimeSeconds: getTimelineStartFlag({ flags: context.args.flags }),
1142
+ durationSeconds,
1143
+ easing: parseEffectEasing({
1144
+ value: getStringFlag({ flags: context.args.flags, name: "easing" }),
1145
+ }),
1146
+ layerIndex: getNumberFlag({
1315
1147
  flags: context.args.flags,
1316
- name: "fade-in",
1148
+ name: "layer-index",
1317
1149
  }),
1318
- fadeOutSeconds: getNumberFlag({
1150
+ orderIndex: getNumberFlag({
1319
1151
  flags: context.args.flags,
1320
- name: "fade-out",
1152
+ name: "order-index",
1153
+ }),
1154
+ settings: buildZoomEffectSettings({
1155
+ flags: context.args.flags,
1156
+ durationSeconds,
1321
1157
  }),
1322
- loop: getOptionalBooleanFlag({ flags: context.args.flags, name: "loop" }),
1323
1158
  });
1324
1159
  printResult({
1325
1160
  json: context.json,
1326
1161
  value: result,
1327
- human: [
1328
- `Inserted ${result.summary.inserted}/${result.summary.total} audio item(s).`,
1329
- ],
1162
+ human: [`Zoom effect added: ${result.timeline_item_id}`],
1330
1163
  });
1331
1164
  return;
1332
1165
  }
1333
- if (group === "silence" && action === "plan") {
1334
- const result = await context.client.silence.plan({
1166
+ if (group === "effects" && action === "zoom" && subaction === "update") {
1167
+ const durationSeconds = getNumberFlag({
1168
+ flags: context.args.flags,
1169
+ name: "duration",
1170
+ });
1171
+ const result = await context.client.effects.zoom.update({
1335
1172
  projectId: requireStringFlag({
1336
1173
  flags: context.args.flags,
1337
1174
  name: "project-id",
@@ -1340,83 +1177,71 @@ async function runCommand({ context }) {
1340
1177
  flags: context.args.flags,
1341
1178
  name: "timeline-item-id",
1342
1179
  }),
1343
- paddingMs: getNumberFlag({
1344
- flags: context.args.flags,
1345
- name: "padding-ms",
1346
- }),
1347
- silenceThresholdDb: getNumberFlag({
1348
- flags: context.args.flags,
1349
- name: "silence-threshold-db",
1350
- }),
1351
- minSilenceDurationSeconds: getNumberFlag({
1352
- flags: context.args.flags,
1353
- name: "min-silence-duration",
1354
- }),
1355
- removeBlipsMaxDurationSeconds: getNumberFlag({
1356
- flags: context.args.flags,
1357
- name: "remove-blips-max-duration",
1180
+ durationSeconds,
1181
+ easing: parseEffectEasing({
1182
+ value: getStringFlag({ flags: context.args.flags, name: "easing" }),
1358
1183
  }),
1359
- useCaptionValidation: getOptionalBooleanFlag({
1184
+ settings: buildZoomEffectSettings({
1360
1185
  flags: context.args.flags,
1361
- name: "use-caption-validation",
1186
+ durationSeconds,
1362
1187
  }),
1363
1188
  });
1364
1189
  printResult({
1365
1190
  json: context.json,
1366
1191
  value: result,
1367
- human: [JSON.stringify(result, null, 2)],
1192
+ human: [`Zoom effect updated: ${result.timeline_item_id}`],
1368
1193
  });
1369
1194
  return;
1370
1195
  }
1371
- if (group === "silence" && action === "apply") {
1372
- const result = await context.client.silence.apply({
1196
+ if (group === "effects" && action === "mask" && subaction === "add") {
1197
+ const result = await context.client.effects.mask.add({
1373
1198
  projectId: requireStringFlag({
1374
1199
  flags: context.args.flags,
1375
1200
  name: "project-id",
1376
1201
  }),
1377
- timelineItemId: requireStringFlag({
1202
+ timelineItemId: getStringFlag({
1378
1203
  flags: context.args.flags,
1379
1204
  name: "timeline-item-id",
1380
1205
  }),
1381
- paddingMs: getNumberFlag({
1206
+ contentItemId: getStringFlag({
1382
1207
  flags: context.args.flags,
1383
- name: "padding-ms",
1208
+ name: "content-item-id",
1384
1209
  }),
1385
- silenceThresholdDb: getNumberFlag({
1210
+ effectId: getStringFlag({
1386
1211
  flags: context.args.flags,
1387
- name: "silence-threshold-db",
1212
+ name: "effect-id",
1388
1213
  }),
1389
- minSilenceDurationSeconds: getNumberFlag({
1214
+ name: getStringFlag({ flags: context.args.flags, name: "name" }),
1215
+ startTimeSeconds: getTimelineStartFlag({ flags: context.args.flags }),
1216
+ durationSeconds: getNumberFlag({
1390
1217
  flags: context.args.flags,
1391
- name: "min-silence-duration",
1218
+ name: "duration",
1392
1219
  }),
1393
- removeBlipsMaxDurationSeconds: getNumberFlag({
1394
- flags: context.args.flags,
1395
- name: "remove-blips-max-duration",
1220
+ easing: parseEffectEasing({
1221
+ value: getStringFlag({ flags: context.args.flags, name: "easing" }),
1396
1222
  }),
1397
- useCaptionValidation: getOptionalBooleanFlag({
1223
+ layerIndex: getNumberFlag({
1398
1224
  flags: context.args.flags,
1399
- name: "use-caption-validation",
1225
+ name: "layer-index",
1400
1226
  }),
1401
- deleteSilentClips: getOptionalBooleanFlag({
1227
+ orderIndex: getNumberFlag({
1402
1228
  flags: context.args.flags,
1403
- name: "delete-silent-clips",
1229
+ name: "order-index",
1404
1230
  }),
1405
- manualSegmentOverrides: parseStringRecordFlag({
1231
+ settings: parseJsonObjectFlag({
1406
1232
  flags: context.args.flags,
1407
- name: "manual-segment-overrides-json",
1233
+ name: "settings-json",
1408
1234
  }),
1409
- previewConfirmed: requireConfirmPreview({ flags: context.args.flags }),
1410
1235
  });
1411
1236
  printResult({
1412
1237
  json: context.json,
1413
1238
  value: result,
1414
- human: [JSON.stringify(result, null, 2)],
1239
+ human: [`Mask effect added: ${result.timeline_item_id}`],
1415
1240
  });
1416
1241
  return;
1417
1242
  }
1418
- if (group === "mistakes" && action === "plan") {
1419
- const result = await context.client.mistakes.plan({
1243
+ if (group === "effects" && action === "mask" && subaction === "update") {
1244
+ const result = await context.client.effects.mask.update({
1420
1245
  projectId: requireStringFlag({
1421
1246
  flags: context.args.flags,
1422
1247
  name: "project-id",
@@ -1425,450 +1250,2566 @@ async function runCommand({ context }) {
1425
1250
  flags: context.args.flags,
1426
1251
  name: "timeline-item-id",
1427
1252
  }),
1253
+ durationSeconds: getNumberFlag({
1254
+ flags: context.args.flags,
1255
+ name: "duration",
1256
+ }),
1257
+ easing: parseEffectEasing({
1258
+ value: getStringFlag({ flags: context.args.flags, name: "easing" }),
1259
+ }),
1260
+ settings: parseJsonObjectFlag({
1261
+ flags: context.args.flags,
1262
+ name: "settings-json",
1263
+ }),
1428
1264
  });
1429
1265
  printResult({
1430
1266
  json: context.json,
1431
1267
  value: result,
1432
- human: [JSON.stringify(result, null, 2)],
1268
+ human: [`Mask effect updated: ${result.timeline_item_id}`],
1433
1269
  });
1434
1270
  return;
1435
1271
  }
1436
- if (group === "mistakes" && action === "apply") {
1437
- const result = await context.client.mistakes.apply({
1272
+ if (group === "effects" && action === "colorgrade" && subaction === "add") {
1273
+ const result = await context.client.effects.colorgrade.add({
1438
1274
  projectId: requireStringFlag({
1439
1275
  flags: context.args.flags,
1440
1276
  name: "project-id",
1441
1277
  }),
1442
- timelineItemId: requireStringFlag({
1278
+ timelineItemId: getStringFlag({
1443
1279
  flags: context.args.flags,
1444
1280
  name: "timeline-item-id",
1445
1281
  }),
1446
- plan: parseJsonObjectFlag({
1282
+ contentItemId: getStringFlag({
1447
1283
  flags: context.args.flags,
1448
- name: "plan-json",
1284
+ name: "content-item-id",
1449
1285
  }),
1450
- manualVerdicts: parseStringRecordFlag({
1286
+ effectId: getStringFlag({
1451
1287
  flags: context.args.flags,
1452
- name: "manual-verdicts-json",
1288
+ name: "effect-id",
1453
1289
  }),
1454
- deleteMistakes: getOptionalBooleanFlag({
1290
+ name: getStringFlag({ flags: context.args.flags, name: "name" }),
1291
+ startTimeSeconds: getTimelineStartFlag({ flags: context.args.flags }),
1292
+ durationSeconds: getNumberFlag({
1455
1293
  flags: context.args.flags,
1456
- name: "delete-mistakes",
1294
+ name: "duration",
1295
+ }),
1296
+ easing: parseEffectEasing({
1297
+ value: getStringFlag({ flags: context.args.flags, name: "easing" }),
1298
+ }),
1299
+ layerIndex: getNumberFlag({
1300
+ flags: context.args.flags,
1301
+ name: "layer-index",
1302
+ }),
1303
+ orderIndex: getNumberFlag({
1304
+ flags: context.args.flags,
1305
+ name: "order-index",
1306
+ }),
1307
+ settings: parseJsonObjectFlag({
1308
+ flags: context.args.flags,
1309
+ name: "settings-json",
1457
1310
  }),
1458
- previewConfirmed: requireConfirmPreview({ flags: context.args.flags }),
1459
1311
  });
1460
1312
  printResult({
1461
1313
  json: context.json,
1462
1314
  value: result,
1463
- human: [JSON.stringify(result, null, 2)],
1315
+ human: [`Color grade effect added: ${result.timeline_item_id}`],
1464
1316
  });
1465
1317
  return;
1466
1318
  }
1467
- if (group === "captions" && action === "words" && subaction === "list") {
1468
- const result = await context.client.captions.listWords({
1319
+ if (group === "effects" &&
1320
+ action === "colorgrade" &&
1321
+ subaction === "update") {
1322
+ const result = await context.client.effects.colorgrade.update({
1469
1323
  projectId: requireStringFlag({
1470
1324
  flags: context.args.flags,
1471
1325
  name: "project-id",
1472
1326
  }),
1473
- timelineItemId: getStringFlag({
1327
+ timelineItemId: requireStringFlag({
1474
1328
  flags: context.args.flags,
1475
1329
  name: "timeline-item-id",
1476
1330
  }),
1477
- matchText: getStringFlag({
1331
+ durationSeconds: getNumberFlag({
1478
1332
  flags: context.args.flags,
1479
- name: "match-text",
1333
+ name: "duration",
1334
+ }),
1335
+ easing: parseEffectEasing({
1336
+ value: getStringFlag({ flags: context.args.flags, name: "easing" }),
1337
+ }),
1338
+ settings: parseJsonObjectFlag({
1339
+ flags: context.args.flags,
1340
+ name: "settings-json",
1480
1341
  }),
1481
- limit: getNumberFlag({ flags: context.args.flags, name: "limit" }),
1482
- offset: getNumberFlag({ flags: context.args.flags, name: "offset" }),
1483
1342
  });
1484
1343
  printResult({
1485
1344
  json: context.json,
1486
1345
  value: result,
1487
- human: result.words.map((word) => JSON.stringify(word)),
1346
+ human: [`Color grade effect updated: ${result.timeline_item_id}`],
1488
1347
  });
1489
1348
  return;
1490
1349
  }
1491
- if (group === "captions" && action === "words" && subaction === "text") {
1492
- const result = await context.client.captions.updateWordText({
1350
+ if (group === "audio" && action === "add") {
1351
+ const result = await context.client.audio.add({
1493
1352
  projectId: requireStringFlag({
1494
1353
  flags: context.args.flags,
1495
1354
  name: "project-id",
1496
1355
  }),
1497
- wordId: requireStringFlag({ flags: context.args.flags, name: "word-id" }),
1498
- newText: getStringFlag({ flags: context.args.flags, name: "new-text" }) ??
1499
- requireStringFlag({ flags: context.args.flags, name: "text" }),
1356
+ assetId: requireStringFlag({
1357
+ flags: context.args.flags,
1358
+ name: "asset-id",
1359
+ }),
1360
+ startSeconds: parseTimecode({
1361
+ value: getStringFlag({ flags: context.args.flags, name: "at" }) ?? "0",
1362
+ }),
1363
+ durationSeconds: getNumberFlag({
1364
+ flags: context.args.flags,
1365
+ name: "duration",
1366
+ }),
1367
+ volume: getNumberFlag({ flags: context.args.flags, name: "volume" }),
1368
+ fadeInSeconds: getNumberFlag({
1369
+ flags: context.args.flags,
1370
+ name: "fade-in",
1371
+ }),
1372
+ fadeOutSeconds: getNumberFlag({
1373
+ flags: context.args.flags,
1374
+ name: "fade-out",
1375
+ }),
1376
+ loop: getOptionalBooleanFlag({ flags: context.args.flags, name: "loop" }),
1500
1377
  });
1501
1378
  printResult({
1502
1379
  json: context.json,
1503
1380
  value: result,
1504
1381
  human: [
1505
- `Updated ${result.word_id}: ${result.old_text} -> ${result.new_text}`,
1382
+ `Inserted ${result.summary.inserted}/${result.summary.total} audio item(s).`,
1506
1383
  ],
1507
1384
  });
1508
1385
  return;
1509
1386
  }
1510
- if (group === "captions" &&
1511
- action === "words" &&
1512
- subaction === "batch-text") {
1513
- const result = await context.client.captions.batchUpdateWordText({
1387
+ if (group === "audio" && action === "update") {
1388
+ const result = await context.client.audio.update({
1514
1389
  projectId: requireStringFlag({
1515
1390
  flags: context.args.flags,
1516
1391
  name: "project-id",
1517
1392
  }),
1518
- updates: parseWordTextUpdatesFlag({
1393
+ timelineItemId: requireStringFlag({
1519
1394
  flags: context.args.flags,
1520
- name: "updates-json",
1395
+ name: "timeline-item-id",
1521
1396
  }),
1522
- });
1523
- printResult({
1524
- json: context.json,
1525
- value: result,
1526
- human: [result.message],
1527
- });
1528
- return;
1529
- }
1530
- if (group === "captions" && action === "words" && subaction === "style") {
1531
- const styleJson = parseNullableJsonObjectFlag({
1532
- flags: context.args.flags,
1533
- name: "style-json",
1534
- });
1535
- const style = styleJson === undefined
1536
- ? buildCaptionWordStyleFromFlags({ flags: context.args.flags })
1537
- : styleJson;
1538
- const result = await context.client.captions.setWordStyle({
1539
- ...buildCaptionWordSelector({ flags: context.args.flags }),
1540
- style,
1541
- clearExisting: getOptionalBooleanFlag({
1397
+ durationSeconds: getNumberFlag({
1542
1398
  flags: context.args.flags,
1543
- name: "clear-existing",
1399
+ name: "duration",
1400
+ }),
1401
+ volume: getNumberFlag({ flags: context.args.flags, name: "volume" }),
1402
+ fadeInSeconds: getNumberFlag({
1403
+ flags: context.args.flags,
1404
+ name: "fade-in",
1405
+ }),
1406
+ fadeOutSeconds: getNumberFlag({
1407
+ flags: context.args.flags,
1408
+ name: "fade-out",
1544
1409
  }),
1545
1410
  });
1546
1411
  printResult({
1547
1412
  json: context.json,
1548
1413
  value: result,
1549
- human: [`Updated ${result.updated_count} caption word(s).`],
1414
+ human: [`Audio updated: ${result.timeline_item_id}`],
1550
1415
  });
1551
1416
  return;
1552
1417
  }
1553
- if (group === "captions" && action === "words" && subaction === "emphasis") {
1554
- const result = await context.client.captions.setWordEmphasis({
1555
- ...buildCaptionWordSelector({ flags: context.args.flags }),
1556
- emphasis: getOptionalBooleanFlag({
1418
+ if (group === "audio" && action === "search") {
1419
+ const result = await context.client.audioLibrary.search({
1420
+ query: requireStringFlag({ flags: context.args.flags, name: "query" }),
1421
+ provider: parseAudioLibraryProviderFilter({
1422
+ value: getStringFlag({ flags: context.args.flags, name: "provider" }),
1423
+ }),
1424
+ license: parseAudioLibraryLicense({
1425
+ value: getStringFlag({ flags: context.args.flags, name: "license" }),
1426
+ }),
1427
+ audioType: parseAudioLibraryType({
1428
+ value: getStringFlag({ flags: context.args.flags, name: "audio-type" }),
1429
+ }),
1430
+ minDurationSeconds: getNumberFlag({
1557
1431
  flags: context.args.flags,
1558
- name: "emphasis",
1559
- }) ?? true,
1432
+ name: "min-duration",
1433
+ }),
1434
+ maxDurationSeconds: getNumberFlag({
1435
+ flags: context.args.flags,
1436
+ name: "max-duration",
1437
+ }),
1438
+ limit: getNumberFlag({ flags: context.args.flags, name: "limit" }),
1439
+ page: getNumberFlag({ flags: context.args.flags, name: "page" }),
1560
1440
  });
1561
1441
  printResult({
1562
1442
  json: context.json,
1563
1443
  value: result,
1564
- human: [`Updated ${result.updated_count} caption word(s).`],
1444
+ human: [
1445
+ ...result.warnings.map((warning) => `Warning: ${warning}`),
1446
+ ...result.items.map((item) => `${item.provider}:${item.external_id} ${item.title} ${item.duration_seconds ?? "-"}s ${item.license_code}`),
1447
+ ],
1565
1448
  });
1566
1449
  return;
1567
1450
  }
1568
- if (group === "captions" && action === "words" && subaction === "delete") {
1569
- const result = await context.client.captions.deleteWords({
1570
- projectId: requireStringFlag({
1451
+ if (group === "audio" && action === "import") {
1452
+ const result = await context.client.audioLibrary.importAsset({
1453
+ provider: parseAudioLibraryProvider({
1454
+ value: requireStringFlag({
1455
+ flags: context.args.flags,
1456
+ name: "provider",
1457
+ }),
1458
+ }),
1459
+ externalId: requireStringFlag({
1460
+ flags: context.args.flags,
1461
+ name: "external-id",
1462
+ }),
1463
+ folderId: parseFolderIdFlag({
1464
+ flags: context.args.flags,
1465
+ name: "folder-id",
1466
+ }),
1467
+ projectId: getStringFlag({
1571
1468
  flags: context.args.flags,
1572
1469
  name: "project-id",
1573
1470
  }),
1574
- wordIds: parseWordIds({ flags: context.args.flags }),
1471
+ startSeconds: parseTimecode({
1472
+ value: getStringFlag({ flags: context.args.flags, name: "at" }) ?? "0",
1473
+ }),
1474
+ durationSeconds: parseNullableNumberFlag({
1475
+ flags: context.args.flags,
1476
+ name: "duration",
1477
+ }),
1478
+ volume: getNumberFlag({ flags: context.args.flags, name: "volume" }),
1479
+ fadeInSeconds: getNumberFlag({
1480
+ flags: context.args.flags,
1481
+ name: "fade-in",
1482
+ }),
1483
+ fadeOutSeconds: getNumberFlag({
1484
+ flags: context.args.flags,
1485
+ name: "fade-out",
1486
+ }),
1487
+ loop: getOptionalBooleanFlag({ flags: context.args.flags, name: "loop" }),
1488
+ audioType: parseAudioLibraryType({
1489
+ value: getStringFlag({ flags: context.args.flags, name: "audio-type" }),
1490
+ }),
1575
1491
  });
1576
1492
  printResult({
1577
1493
  json: context.json,
1578
1494
  value: result,
1579
- human: [result.message],
1495
+ human: [
1496
+ `${result.imported ? "Imported" : "Reused"} ${result.asset.name} (${result.asset.id}).`,
1497
+ result.timeline_item_id
1498
+ ? `Inserted on timeline as ${result.timeline_item_id}.`
1499
+ : "No timeline insert requested.",
1500
+ ],
1580
1501
  });
1581
1502
  return;
1582
1503
  }
1583
- if (group === "captions" && action === "words" && subaction === "merge") {
1584
- const result = await context.client.captions.mergeWords({
1585
- projectId: requireStringFlag({
1586
- flags: context.args.flags,
1587
- name: "project-id",
1504
+ if (group === "stock" && action === "search") {
1505
+ const result = await context.client.stockMedia.search({
1506
+ query: requireStringFlag({ flags: context.args.flags, name: "query" }),
1507
+ assetType: parseStockAssetType({
1508
+ value: requireStringFlag({
1509
+ flags: context.args.flags,
1510
+ name: "asset-type",
1511
+ }),
1588
1512
  }),
1589
- wordIds: parseWordIds({ flags: context.args.flags }),
1590
- text: getStringFlag({ flags: context.args.flags, name: "text" }),
1513
+ provider: parseStockProvider({
1514
+ value: getStringFlag({ flags: context.args.flags, name: "provider" }),
1515
+ }),
1516
+ orientation: parseStockOrientation({
1517
+ value: getStringFlag({
1518
+ flags: context.args.flags,
1519
+ name: "orientation",
1520
+ }),
1521
+ }),
1522
+ limit: getNumberFlag({ flags: context.args.flags, name: "limit" }),
1523
+ page: getNumberFlag({ flags: context.args.flags, name: "page" }),
1591
1524
  });
1592
1525
  printResult({
1593
1526
  json: context.json,
1594
1527
  value: result,
1595
- human: [result.message],
1528
+ human: [
1529
+ ...result.warnings.map((warning) => `Warning: ${warning}`),
1530
+ ...result.items.map((item) => `${item.provider}:${item.provider_asset_id} ${item.asset_type} ${item.title} ${item.duration_seconds ?? "-"}s ${item.license_code}`),
1531
+ ],
1596
1532
  });
1597
1533
  return;
1598
1534
  }
1599
- if (group === "captions" && action === "words" && subaction === "split") {
1600
- const result = await context.client.captions.splitWord({
1601
- projectId: requireStringFlag({
1535
+ if (group === "stock" && action === "import") {
1536
+ const result = await context.client.stockMedia.importAsset({
1537
+ provider: parseRequiredStockProvider({
1538
+ value: requireStringFlag({
1539
+ flags: context.args.flags,
1540
+ name: "provider",
1541
+ }),
1542
+ }),
1543
+ providerAssetId: requireStringFlag({
1602
1544
  flags: context.args.flags,
1603
- name: "project-id",
1545
+ name: "provider-asset-id",
1546
+ }),
1547
+ assetType: parseStockAssetType({
1548
+ value: requireStringFlag({
1549
+ flags: context.args.flags,
1550
+ name: "asset-type",
1551
+ }),
1552
+ }),
1553
+ variantId: parseNullableStringFlag({
1554
+ flags: context.args.flags,
1555
+ name: "variant-id",
1556
+ }),
1557
+ folderId: parseFolderIdFlag({
1558
+ flags: context.args.flags,
1559
+ name: "folder-id",
1560
+ }),
1561
+ audioType: parseStockAudioType({
1562
+ value: getStringFlag({ flags: context.args.flags, name: "audio-type" }),
1604
1563
  }),
1605
- wordId: requireStringFlag({ flags: context.args.flags, name: "word-id" }),
1606
- words: parseCsvRequiredFlag({ flags: context.args.flags, name: "words" }),
1607
- });
1608
- printResult({
1609
- json: context.json,
1610
- value: result,
1611
- human: [result.message],
1612
1564
  });
1613
- return;
1614
- }
1615
- if (group === "captions" && action === "themes" && subaction === "list") {
1616
- const result = await context.client.captions.listThemes();
1617
1565
  printResult({
1618
1566
  json: context.json,
1619
1567
  value: result,
1620
- human: result.themes.map((theme) => JSON.stringify(theme)),
1568
+ human: [
1569
+ `${result.imported ? "Imported" : "Reused"} ${result.asset.name} (${result.asset.id}).`,
1570
+ ],
1621
1571
  });
1622
1572
  return;
1623
1573
  }
1624
- if (group === "captions" && action === "theme" && subaction === "set") {
1625
- const result = await context.client.captions.setProjectTheme({
1626
- projectId: requireStringFlag({
1627
- flags: context.args.flags,
1628
- name: "project-id",
1574
+ if (group === "library" && action === "search") {
1575
+ const result = await context.client.library.search({
1576
+ query: getStringFlag({ flags: context.args.flags, name: "query" }),
1577
+ assetType: parseNullableStockAssetType({
1578
+ value: getStringFlag({
1579
+ flags: context.args.flags,
1580
+ name: "asset-type",
1581
+ }),
1629
1582
  }),
1630
- themeId: requireStringFlag({
1631
- flags: context.args.flags,
1632
- name: "theme-id",
1583
+ audioType: parseGlobalBrollAudioType({
1584
+ value: getStringFlag({ flags: context.args.flags, name: "audio-type" }),
1633
1585
  }),
1586
+ limit: getNumberFlag({ flags: context.args.flags, name: "limit" }),
1587
+ offset: getNumberFlag({ flags: context.args.flags, name: "offset" }),
1634
1588
  });
1635
1589
  printResult({
1636
1590
  json: context.json,
1637
1591
  value: result,
1638
- human: ["Project caption theme updated."],
1592
+ human: result.items.map((item) => `${item.id} ${item.asset_type} ${item.title} ${item.duration_seconds ?? "-"}s ${item.license_code}`),
1639
1593
  });
1640
1594
  return;
1641
1595
  }
1642
- if (group === "captions" &&
1643
- action === "theme" &&
1644
- subaction === "set-default") {
1645
- const result = await context.client.captions.setDefaultTheme({
1646
- themeId: requireStringFlag({
1596
+ if (group === "library" && action === "import") {
1597
+ const result = await context.client.library.importAsset({
1598
+ libraryAssetId: requireStringFlag({
1647
1599
  flags: context.args.flags,
1648
- name: "theme-id",
1600
+ name: "library-asset-id",
1601
+ }),
1602
+ folderId: parseFolderIdFlag({
1603
+ flags: context.args.flags,
1604
+ name: "folder-id",
1649
1605
  }),
1650
1606
  });
1651
1607
  printResult({
1652
1608
  json: context.json,
1653
1609
  value: result,
1654
- human: ["Default caption theme updated."],
1610
+ human: [
1611
+ `${result.imported ? "Imported" : "Reused"} ${result.asset.name} (${result.asset.id}).`,
1612
+ ],
1655
1613
  });
1656
1614
  return;
1657
1615
  }
1658
- if (group === "exports" && action === "start") {
1659
- const result = await context.client.exports.start({
1616
+ if (group === "silence" && action === "plan") {
1617
+ const result = await context.client.silence.plan({
1660
1618
  projectId: requireStringFlag({
1661
1619
  flags: context.args.flags,
1662
1620
  name: "project-id",
1663
1621
  }),
1664
- resolution: getStringFlag({
1622
+ timelineItemId: requireStringFlag({
1623
+ flags: context.args.flags,
1624
+ name: "timeline-item-id",
1625
+ }),
1626
+ allMediaItems: undefined,
1627
+ paddingMs: getNumberFlag({
1665
1628
  flags: context.args.flags,
1666
- name: "resolution",
1629
+ name: "padding-ms",
1667
1630
  }),
1668
- format: getStringFlag({ flags: context.args.flags, name: "format" }),
1669
- includeCaptions: getOptionalBooleanFlag({
1631
+ silenceThresholdDb: getNumberFlag({
1670
1632
  flags: context.args.flags,
1671
- name: "include-captions",
1633
+ name: "silence-threshold-db",
1672
1634
  }),
1673
- });
1674
- printResult({
1675
- json: context.json,
1676
- value: result,
1677
- human: [`Export queued: ${result.export_id ?? result.job_id}`],
1678
- });
1679
- return;
1680
- }
1681
- if (group === "exports" && action === "wait") {
1682
- const result = await context.client.exports.wait({
1683
- exportId: requireStringFlag({
1635
+ minSilenceDurationSeconds: getNumberFlag({
1636
+ flags: context.args.flags,
1637
+ name: "min-silence-duration",
1638
+ }),
1639
+ removeBlipsMaxDurationSeconds: getNumberFlag({
1684
1640
  flags: context.args.flags,
1685
- name: "export-id",
1641
+ name: "remove-blips-max-duration",
1642
+ }),
1643
+ useCaptionValidation: getOptionalBooleanFlag({
1644
+ flags: context.args.flags,
1645
+ name: "use-caption-validation",
1686
1646
  }),
1687
- intervalMs: getNumberFlag({ flags: context.args.flags, name: "interval-ms" }) ??
1688
- DEFAULT_WAIT_INTERVAL_MS,
1689
- timeoutMs: getNumberFlag({ flags: context.args.flags, name: "timeout-ms" }) ??
1690
- DEFAULT_WAIT_TIMEOUT_MS,
1691
1647
  });
1692
1648
  printResult({
1693
1649
  json: context.json,
1694
1650
  value: result,
1695
- human: [
1696
- `Export ready: ${result.download_url ?? result.short_download_url ?? result.export_id}`,
1697
- ],
1651
+ human: [JSON.stringify(result, null, 2)],
1698
1652
  });
1699
1653
  return;
1700
1654
  }
1701
- if (group === "exports" && action === "download") {
1702
- const output = requireStringFlag({
1655
+ if (group === "silence" && action === "apply") {
1656
+ const allMediaItems = getOptionalBooleanFlag({
1703
1657
  flags: context.args.flags,
1704
- name: "output",
1705
- });
1706
- const bytes = await context.client.exports.download({
1707
- exportId: requireStringFlag({
1658
+ name: "all-media-items",
1659
+ }) ?? false;
1660
+ const result = await context.client.silence.apply({
1661
+ projectId: requireStringFlag({
1708
1662
  flags: context.args.flags,
1709
- name: "export-id",
1663
+ name: "project-id",
1710
1664
  }),
1711
- });
1712
- await writeFile(resolve(output), Buffer.from(bytes));
1713
- printResult({
1714
- json: context.json,
1715
- value: { output },
1716
- human: [`Downloaded export: ${output}`],
1717
- });
1718
- return;
1719
- }
1720
- throw buildUnknownCommandError({ command: context.args.command });
1721
- }
1722
- async function setApiKey({ context }) {
1723
- const apiKey = getStringFlag({ flags: context.args.flags, name: "api-key" }) ??
1724
- process.env.BLITZREELS_API_KEY;
1725
- if (!apiKey) {
1726
- throw new Error("Pass --api-key or set BLITZREELS_API_KEY.");
1727
- }
1728
- const savedToKeychain = await saveApiKey({ apiKey });
1729
- const config = {
1730
- apiKey: savedToKeychain ? "" : apiKey,
1731
- baseUrl: getStringFlag({ flags: context.args.flags, name: "base-url" }),
1732
- keyStorage: savedToKeychain ? "keychain" : "config",
1733
- };
1734
- await mkdir(dirname(CONFIG_PATH), { recursive: true });
1735
- await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), {
1736
- mode: 0o600,
1737
- });
1738
- printResult({
1739
- json: context.json,
1740
- value: { configPath: CONFIG_PATH },
1741
- human: [`API key saved to ${CONFIG_PATH}`],
1665
+ timelineItemId: allMediaItems
1666
+ ? undefined
1667
+ : requireStringFlag({
1668
+ flags: context.args.flags,
1669
+ name: "timeline-item-id",
1670
+ }),
1671
+ allMediaItems,
1672
+ paddingMs: getNumberFlag({
1673
+ flags: context.args.flags,
1674
+ name: "padding-ms",
1675
+ }),
1676
+ silenceThresholdDb: getNumberFlag({
1677
+ flags: context.args.flags,
1678
+ name: "silence-threshold-db",
1679
+ }),
1680
+ minSilenceDurationSeconds: getNumberFlag({
1681
+ flags: context.args.flags,
1682
+ name: "min-silence-duration",
1683
+ }),
1684
+ removeBlipsMaxDurationSeconds: getNumberFlag({
1685
+ flags: context.args.flags,
1686
+ name: "remove-blips-max-duration",
1687
+ }),
1688
+ useCaptionValidation: getOptionalBooleanFlag({
1689
+ flags: context.args.flags,
1690
+ name: "use-caption-validation",
1691
+ }),
1692
+ deleteSilentClips: getOptionalBooleanFlag({
1693
+ flags: context.args.flags,
1694
+ name: "delete-silent-clips",
1695
+ }),
1696
+ manualSegmentOverrides: parseStringRecordFlag({
1697
+ flags: context.args.flags,
1698
+ name: "manual-segment-overrides-json",
1699
+ }),
1700
+ previewConfirmed: requirePreviewFirstConfirmation({
1701
+ confirmed: getBooleanFlag({
1702
+ flags: context.args.flags,
1703
+ name: "confirm-preview",
1704
+ }),
1705
+ }),
1706
+ });
1707
+ printResult({
1708
+ json: context.json,
1709
+ value: result,
1710
+ human: [JSON.stringify(result, null, 2)],
1711
+ });
1712
+ return;
1713
+ }
1714
+ if (group === "mistakes" && action === "plan") {
1715
+ const result = await context.client.mistakes.plan({
1716
+ projectId: requireStringFlag({
1717
+ flags: context.args.flags,
1718
+ name: "project-id",
1719
+ }),
1720
+ timelineItemId: requireStringFlag({
1721
+ flags: context.args.flags,
1722
+ name: "timeline-item-id",
1723
+ }),
1724
+ });
1725
+ const value = getBooleanFlag({
1726
+ flags: context.args.flags,
1727
+ name: "summary",
1728
+ })
1729
+ ? summarizeMistakePlan({ plan: result })
1730
+ : result;
1731
+ printResult({
1732
+ json: context.json,
1733
+ value,
1734
+ human: [JSON.stringify(value, null, 2)],
1735
+ });
1736
+ return;
1737
+ }
1738
+ if (group === "mistakes" && action === "apply") {
1739
+ const result = await context.client.mistakes.apply({
1740
+ projectId: requireStringFlag({
1741
+ flags: context.args.flags,
1742
+ name: "project-id",
1743
+ }),
1744
+ timelineItemId: requireStringFlag({
1745
+ flags: context.args.flags,
1746
+ name: "timeline-item-id",
1747
+ }),
1748
+ plan: parseJsonObjectFlag({
1749
+ flags: context.args.flags,
1750
+ name: "plan-json",
1751
+ }),
1752
+ manualVerdicts: parseStringRecordFlag({
1753
+ flags: context.args.flags,
1754
+ name: "manual-verdicts-json",
1755
+ }),
1756
+ deleteMistakes: getOptionalBooleanFlag({
1757
+ flags: context.args.flags,
1758
+ name: "delete-mistakes",
1759
+ }),
1760
+ previewConfirmed: requirePreviewFirstConfirmation({
1761
+ confirmed: getBooleanFlag({
1762
+ flags: context.args.flags,
1763
+ name: "confirm-preview",
1764
+ }),
1765
+ }),
1766
+ });
1767
+ printResult({
1768
+ json: context.json,
1769
+ value: result,
1770
+ human: [JSON.stringify(result, null, 2)],
1771
+ });
1772
+ return;
1773
+ }
1774
+ if (group === "captions" &&
1775
+ ((action === "words" && subaction === "list") || action === "list-words")) {
1776
+ const result = await context.client.captions.listWords({
1777
+ projectId: requireStringFlag({
1778
+ flags: context.args.flags,
1779
+ name: "project-id",
1780
+ }),
1781
+ timelineItemId: getStringFlag({
1782
+ flags: context.args.flags,
1783
+ name: "timeline-item-id",
1784
+ }),
1785
+ matchText: getStringFlag({
1786
+ flags: context.args.flags,
1787
+ name: "match-text",
1788
+ }),
1789
+ limit: getNumberFlag({ flags: context.args.flags, name: "limit" }),
1790
+ offset: getNumberFlag({ flags: context.args.flags, name: "offset" }),
1791
+ });
1792
+ printResult({
1793
+ json: context.json,
1794
+ value: result,
1795
+ human: result.words.map((word) => JSON.stringify(word)),
1796
+ });
1797
+ return;
1798
+ }
1799
+ if (group === "captions" &&
1800
+ (action === "delete" || action === "hide" || action === "show")) {
1801
+ const selector = buildCaptionTrackSelector({ flags: context.args.flags });
1802
+ const result = action === "delete"
1803
+ ? await context.client.captions.delete(selector)
1804
+ : action === "hide"
1805
+ ? await context.client.captions.hide(selector)
1806
+ : await context.client.captions.show(selector);
1807
+ printResult({
1808
+ json: context.json,
1809
+ value: result,
1810
+ human: [result.message],
1811
+ });
1812
+ return;
1813
+ }
1814
+ if (group === "captions" && action === "words" && subaction === "text") {
1815
+ const result = await context.client.captions.updateWordText({
1816
+ projectId: requireStringFlag({
1817
+ flags: context.args.flags,
1818
+ name: "project-id",
1819
+ }),
1820
+ wordId: requireStringFlag({ flags: context.args.flags, name: "word-id" }),
1821
+ newText: getStringFlag({ flags: context.args.flags, name: "new-text" }) ??
1822
+ requireStringFlag({ flags: context.args.flags, name: "text" }),
1823
+ });
1824
+ printResult({
1825
+ json: context.json,
1826
+ value: result,
1827
+ human: [
1828
+ `Updated ${result.word_id}: ${result.old_text} -> ${result.new_text}`,
1829
+ ],
1830
+ });
1831
+ return;
1832
+ }
1833
+ if (group === "captions" &&
1834
+ action === "words" &&
1835
+ subaction === "batch-text") {
1836
+ const result = await context.client.captions.batchUpdateWordText({
1837
+ projectId: requireStringFlag({
1838
+ flags: context.args.flags,
1839
+ name: "project-id",
1840
+ }),
1841
+ updates: parseWordTextUpdatesFlag({
1842
+ flags: context.args.flags,
1843
+ name: "updates-json",
1844
+ }),
1845
+ });
1846
+ printResult({
1847
+ json: context.json,
1848
+ value: result,
1849
+ human: [result.message],
1850
+ });
1851
+ return;
1852
+ }
1853
+ if (group === "captions" &&
1854
+ ((action === "words" && subaction === "style") || action === "style-words")) {
1855
+ const styleJson = parseNullableJsonObjectFlag({
1856
+ flags: context.args.flags,
1857
+ name: "style-json",
1858
+ });
1859
+ const style = styleJson === undefined
1860
+ ? buildCaptionWordStyleFromFlags({ flags: context.args.flags })
1861
+ : styleJson;
1862
+ const result = await context.client.captions.setWordStyle({
1863
+ ...buildCaptionWordSelector({ flags: context.args.flags }),
1864
+ style,
1865
+ clearExisting: getOptionalBooleanFlag({
1866
+ flags: context.args.flags,
1867
+ name: "clear-existing",
1868
+ }),
1869
+ });
1870
+ printResult({
1871
+ json: context.json,
1872
+ value: result,
1873
+ human: [`Updated ${result.updated_count} caption word(s).`],
1874
+ });
1875
+ return;
1876
+ }
1877
+ if (group === "captions" &&
1878
+ ((action === "words" && subaction === "emphasis") || action === "emphasize")) {
1879
+ const result = await context.client.captions.setWordEmphasis({
1880
+ ...buildCaptionWordSelector({ flags: context.args.flags }),
1881
+ emphasis: getOptionalBooleanFlag({
1882
+ flags: context.args.flags,
1883
+ name: "emphasis",
1884
+ }) ?? true,
1885
+ });
1886
+ printResult({
1887
+ json: context.json,
1888
+ value: result,
1889
+ human: [`Updated ${result.updated_count} caption word(s).`],
1890
+ });
1891
+ return;
1892
+ }
1893
+ if (group === "captions" && action === "words" && subaction === "delete") {
1894
+ const result = await context.client.captions.deleteWords({
1895
+ projectId: requireStringFlag({
1896
+ flags: context.args.flags,
1897
+ name: "project-id",
1898
+ }),
1899
+ wordIds: parseWordIds({ flags: context.args.flags }),
1900
+ });
1901
+ printResult({
1902
+ json: context.json,
1903
+ value: result,
1904
+ human: [result.message],
1905
+ });
1906
+ return;
1907
+ }
1908
+ if (group === "captions" && action === "words" && subaction === "merge") {
1909
+ const result = await context.client.captions.mergeWords({
1910
+ projectId: requireStringFlag({
1911
+ flags: context.args.flags,
1912
+ name: "project-id",
1913
+ }),
1914
+ wordIds: parseWordIds({ flags: context.args.flags }),
1915
+ text: getStringFlag({ flags: context.args.flags, name: "text" }),
1916
+ });
1917
+ printResult({
1918
+ json: context.json,
1919
+ value: result,
1920
+ human: [result.message],
1921
+ });
1922
+ return;
1923
+ }
1924
+ if (group === "captions" && action === "words" && subaction === "split") {
1925
+ const result = await context.client.captions.splitWord({
1926
+ projectId: requireStringFlag({
1927
+ flags: context.args.flags,
1928
+ name: "project-id",
1929
+ }),
1930
+ wordId: requireStringFlag({ flags: context.args.flags, name: "word-id" }),
1931
+ words: parseCsvRequiredFlag({ flags: context.args.flags, name: "words" }),
1932
+ });
1933
+ printResult({
1934
+ json: context.json,
1935
+ value: result,
1936
+ human: [result.message],
1937
+ });
1938
+ return;
1939
+ }
1940
+ if (group === "captions" && action === "themes" && subaction === "list") {
1941
+ const result = await context.client.captions.listThemes();
1942
+ printResult({
1943
+ json: context.json,
1944
+ value: result,
1945
+ human: result.themes.length > 0
1946
+ ? result.themes.map((theme) => JSON.stringify(theme))
1947
+ : [
1948
+ "No custom caption themes yet.",
1949
+ "Create one in the dashboard, then use `blitzreels captions theme set --project-id ID --theme-id ID` or `blitzreels captions theme set-default --theme-id ID`.",
1950
+ ],
1951
+ });
1952
+ return;
1953
+ }
1954
+ if (group === "captions" && action === "theme" && subaction === "set") {
1955
+ const result = await context.client.captions.setProjectTheme({
1956
+ projectId: requireStringFlag({
1957
+ flags: context.args.flags,
1958
+ name: "project-id",
1959
+ }),
1960
+ themeId: requireStringFlag({
1961
+ flags: context.args.flags,
1962
+ name: "theme-id",
1963
+ }),
1964
+ });
1965
+ printResult({
1966
+ json: context.json,
1967
+ value: result,
1968
+ human: ["Project caption theme updated."],
1969
+ });
1970
+ return;
1971
+ }
1972
+ if (group === "captions" && action === "theme" && subaction === "update") {
1973
+ const result = await context.client.captions.updateTheme({
1974
+ themeId: requireStringFlag({
1975
+ flags: context.args.flags,
1976
+ name: "theme-id",
1977
+ }),
1978
+ name: getStringFlag({ flags: context.args.flags, name: "name" }),
1979
+ description: getStringFlag({
1980
+ flags: context.args.flags,
1981
+ name: "description",
1982
+ }),
1983
+ settings: buildCaptionThemeSettingsPatch({ flags: context.args.flags }),
1984
+ });
1985
+ printResult({
1986
+ json: context.json,
1987
+ value: result,
1988
+ human: ["Caption theme updated."],
1989
+ });
1990
+ return;
1991
+ }
1992
+ if (group === "captions" &&
1993
+ action === "theme" &&
1994
+ subaction === "set-default") {
1995
+ const result = await context.client.captions.setDefaultTheme({
1996
+ themeId: requireStringFlag({
1997
+ flags: context.args.flags,
1998
+ name: "theme-id",
1999
+ }),
2000
+ });
2001
+ printResult({
2002
+ json: context.json,
2003
+ value: result,
2004
+ human: ["Default caption theme updated."],
2005
+ });
2006
+ return;
2007
+ }
2008
+ if (group === "exports") {
2009
+ const handled = await handleExportsCommand({
2010
+ context,
2011
+ action,
2012
+ helpers: {
2013
+ getNumberFlag,
2014
+ getOptionalBooleanFlag,
2015
+ getStringFlag,
2016
+ requireStringFlag,
2017
+ printResult,
2018
+ },
2019
+ defaultWaitIntervalMs: DEFAULT_WAIT_INTERVAL_MS,
2020
+ defaultWaitTimeoutMs: DEFAULT_WAIT_TIMEOUT_MS,
2021
+ });
2022
+ if (handled) {
2023
+ return;
2024
+ }
2025
+ }
2026
+ throw buildUnknownCommandError({ command: context.args.command });
2027
+ }
2028
+ async function setApiKey({ context }) {
2029
+ const apiKey = getStringFlag({ flags: context.args.flags, name: "api-key" }) ??
2030
+ process.env.BLITZREELS_API_KEY;
2031
+ if (!apiKey) {
2032
+ throw new Error("Pass --api-key or set BLITZREELS_API_KEY.");
2033
+ }
2034
+ const stored = await storeApiKey({
2035
+ apiKey,
2036
+ baseUrl: getStringFlag({ flags: context.args.flags, name: "base-url" }),
2037
+ });
2038
+ printResult({
2039
+ json: context.json,
2040
+ value: { configPath: stored.configPath, keyStorage: stored.keyStorage },
2041
+ human: [`API key saved to ${stored.configPath}`],
2042
+ });
2043
+ }
2044
+ async function loginWithBrowser({ context, }) {
2045
+ const appUrl = resolveAppUrl({ baseUrl: context.config.baseUrl });
2046
+ const deviceUrl = `${appUrl}/api/cli/auth/device`;
2047
+ const startResponse = await fetch(deviceUrl, {
2048
+ method: "POST",
2049
+ headers: { Accept: "application/json" },
2050
+ });
2051
+ const login = parseDeviceLoginResponse({
2052
+ value: await readJsonResponse({
2053
+ response: startResponse,
2054
+ url: deviceUrl,
2055
+ action: "start CLI login",
2056
+ }),
2057
+ });
2058
+ const shouldOpenBrowser = !getBooleanFlag({
2059
+ flags: context.args.flags,
2060
+ name: "no-browser",
2061
+ });
2062
+ if (shouldOpenBrowser) {
2063
+ openBrowser({ url: login.verification_uri_complete });
2064
+ }
2065
+ const loginInstructions = [
2066
+ `First copy your one-time code: ${login.user_code}`,
2067
+ `Then open: ${login.verification_uri_complete}`,
2068
+ "Waiting for browser approval...",
2069
+ ];
2070
+ if (context.json) {
2071
+ console.error(loginInstructions.join("\n"));
2072
+ }
2073
+ else {
2074
+ printResult({
2075
+ json: context.json,
2076
+ value: {
2077
+ user_code: login.user_code,
2078
+ verification_uri: login.verification_uri_complete,
2079
+ expires_in: login.expires_in,
2080
+ },
2081
+ human: loginInstructions,
2082
+ });
2083
+ }
2084
+ const startedAt = Date.now();
2085
+ const timeoutMs = Math.min(login.expires_in * 1000, getNumberFlag({ flags: context.args.flags, name: "timeout-ms" }) ??
2086
+ login.expires_in * 1000);
2087
+ const intervalMs = Math.max(1000, login.interval * 1000);
2088
+ while (Date.now() - startedAt <= timeoutMs) {
2089
+ await sleep({ ms: intervalMs });
2090
+ const tokenUrl = `${appUrl}/api/cli/auth/token`;
2091
+ const pollResponse = await fetch(tokenUrl, {
2092
+ method: "POST",
2093
+ headers: {
2094
+ Accept: "application/json",
2095
+ "Content-Type": "application/json",
2096
+ },
2097
+ body: JSON.stringify({ device_code: login.device_code }),
2098
+ });
2099
+ const result = parseDeviceTokenResponse({
2100
+ value: await readJsonResponse({
2101
+ response: pollResponse,
2102
+ url: tokenUrl,
2103
+ action: "poll CLI login",
2104
+ }),
2105
+ });
2106
+ if (result.status === "approved" && result.api_key) {
2107
+ await persistLoggedInApiKey({
2108
+ apiKey: result.api_key,
2109
+ baseUrl: context.config.baseUrl,
2110
+ });
2111
+ printResult({
2112
+ json: context.json,
2113
+ value: { success: true, key_prefix: result.key_prefix ?? "" },
2114
+ human: [`Logged in. API key prefix: ${result.key_prefix ?? "saved"}`],
2115
+ });
2116
+ return;
2117
+ }
2118
+ if (result.status === "expired" ||
2119
+ result.status === "denied" ||
2120
+ result.status === "invalid" ||
2121
+ result.status === "consumed") {
2122
+ throw new Error(`Login failed: ${result.status}.`);
2123
+ }
2124
+ }
2125
+ throw new Error("Timed out waiting for browser approval.");
2126
+ }
2127
+ async function uploadMediaFile({ context }) {
2128
+ const filePath = resolve(requireStringFlag({ flags: context.args.flags, name: "file" }));
2129
+ const fileStats = await stat(filePath);
2130
+ if (!fileStats.isFile()) {
2131
+ throw new Error(`Not a file: ${filePath}`);
2132
+ }
2133
+ const fileName = getStringFlag({ flags: context.args.flags, name: "name" }) ??
2134
+ basename(filePath);
2135
+ const contentType = getStringFlag({ flags: context.args.flags, name: "content-type" }) ??
2136
+ inferContentType({ filePath });
2137
+ const autoJobs = buildUploadAutoJobNotice({ flags: context.args.flags });
2138
+ if (autoJobs.length > 0) {
2139
+ console.error(`BlitzReels upload will queue background jobs: ${autoJobs.join(", ")}. Use \`blitzreels media wait --asset-id ID --for all\` after upload when you need processed metadata.`);
2140
+ }
2141
+ const init = await context.client.media.initUpload({
2142
+ fileName,
2143
+ contentType,
2144
+ });
2145
+ const bytes = await readFile(filePath);
2146
+ const uploadResponse = await fetch(init.upload_url, {
2147
+ method: "PUT",
2148
+ headers: { "Content-Type": contentType },
2149
+ body: new Blob([new Uint8Array(bytes)], { type: contentType }),
2150
+ });
2151
+ if (!uploadResponse.ok) {
2152
+ throw new Error(`Upload failed with ${uploadResponse.status}: ${await uploadResponse.text()}`);
2153
+ }
2154
+ return context.client.media.finalizeUpload({
2155
+ storageKey: init.storage_key,
2156
+ fileName,
2157
+ contentType,
2158
+ fileSizeBytes: fileStats.size,
2159
+ autoProcessEnabled: getOptionalBooleanFlag({
2160
+ flags: context.args.flags,
2161
+ name: "auto-process",
2162
+ }),
2163
+ autoTranscribe: getOptionalBooleanFlag({
2164
+ flags: context.args.flags,
2165
+ name: "auto-transcribe",
2166
+ }),
2167
+ autoDescribeAfterTranscribe: getOptionalBooleanFlag({
2168
+ flags: context.args.flags,
2169
+ name: "auto-describe",
2170
+ }),
2171
+ autoSuggestShorts: getOptionalBooleanFlag({
2172
+ flags: context.args.flags,
2173
+ name: "auto-suggest-shorts",
2174
+ }),
2175
+ });
2176
+ }
2177
+ async function waitForMediaAsset({ context, }) {
2178
+ const assetId = requireStringFlag({
2179
+ flags: context.args.flags,
2180
+ name: "asset-id",
2181
+ });
2182
+ const projectId = getStringFlag({
2183
+ flags: context.args.flags,
2184
+ name: "project-id",
2185
+ });
2186
+ const waitFor = parseMediaWaitFor({
2187
+ value: getStringFlag({ flags: context.args.flags, name: "for" }),
2188
+ });
2189
+ const intervalMs = getNumberFlag({ flags: context.args.flags, name: "interval-ms" }) ??
2190
+ DEFAULT_WAIT_INTERVAL_MS;
2191
+ const timeoutMs = getNumberFlag({ flags: context.args.flags, name: "timeout-ms" }) ??
2192
+ 10 * 60 * 1000;
2193
+ const startedAt = Date.now();
2194
+ let lastAsset;
2195
+ while (true) {
2196
+ const result = await context.client.media.get({ assetId });
2197
+ lastAsset = result.asset;
2198
+ const clippingStatus = await context.client.media.clippingStatus({
2199
+ assetId,
2200
+ projectId,
2201
+ });
2202
+ const readyFor = getReadyMediaStates({
2203
+ asset: lastAsset,
2204
+ clippingStatus,
2205
+ });
2206
+ if (mediaWaitSatisfied({ readyFor, waitFor })) {
2207
+ return {
2208
+ asset: {
2209
+ id: lastAsset.id,
2210
+ name: lastAsset.name,
2211
+ processingStatus: lastAsset.processingStatus,
2212
+ processingSubstage: lastAsset.processingSubstage,
2213
+ processingError: lastAsset.processingError,
2214
+ processingErrors: lastAsset.processingErrors,
2215
+ retryable: lastAsset.retryable,
2216
+ providerRequestIds: lastAsset.providerRequestIds,
2217
+ durationSeconds: lastAsset.durationSeconds,
2218
+ aiDescription: lastAsset.aiDescription,
2219
+ },
2220
+ ready_for: readyFor,
2221
+ waited_ms: Date.now() - startedAt,
2222
+ };
2223
+ }
2224
+ if (Date.now() - startedAt >= timeoutMs) {
2225
+ throw new Error(`Timed out waiting for media asset ${assetId}. Last status: ${lastAsset.processingStatus ?? "unknown"}${lastAsset.processingSubstage ? `/${lastAsset.processingSubstage}` : ""}${lastAsset.processingError ? `: ${lastAsset.processingError}` : ""}.`);
2226
+ }
2227
+ await sleep({ ms: intervalMs });
2228
+ }
2229
+ }
2230
+ function getClipRecord({ result, }) {
2231
+ return isRecord(result.clip) ? result.clip : {};
2232
+ }
2233
+ function printClipResult({ context, result, }) {
2234
+ const clip = getClipRecord({ result });
2235
+ const clipId = typeof clip.clip_id === "string" ? clip.clip_id : "unknown";
2236
+ const status = typeof clip.status === "string" ? clip.status : "unknown";
2237
+ const nextAction = typeof clip.next_action === "string" ? clip.next_action : "unknown";
2238
+ const projectId = typeof clip.project_id === "string" ? clip.project_id : null;
2239
+ printResult({
2240
+ json: context.json,
2241
+ value: result,
2242
+ human: [
2243
+ `Clip ${clipId}: ${status}`,
2244
+ `Next action: ${nextAction}`,
2245
+ ...(projectId === null ? [] : [`Project: ${projectId}`]),
2246
+ ],
2247
+ });
2248
+ }
2249
+ function parseClipSourceType({ value, }) {
2250
+ if (value === "asset" || value === "youtube") {
2251
+ return value;
2252
+ }
2253
+ throw new Error("--source-type must be asset or youtube.");
2254
+ }
2255
+ function parseClipSelectionMode({ value, }) {
2256
+ if (value === undefined) {
2257
+ return "auto_best";
2258
+ }
2259
+ if (value === "auto_best" ||
2260
+ value === "suggestion" ||
2261
+ value === "time_range") {
2262
+ return value;
2263
+ }
2264
+ throw new Error("--selection-mode must be auto_best, suggestion, or time_range.");
2265
+ }
2266
+ function parseClipLayoutMode({ value, }) {
2267
+ if (value === undefined) {
2268
+ return "auto";
2269
+ }
2270
+ if (value === "auto" ||
2271
+ value === "prefer_split" ||
2272
+ value === "prefer_focus") {
2273
+ return value;
2274
+ }
2275
+ throw new Error("--layout-mode must be auto, prefer_split, or prefer_focus.");
2276
+ }
2277
+ function parseClipContentTypeHint({ value, }) {
2278
+ if (value === undefined) {
2279
+ return undefined;
2280
+ }
2281
+ if (value === "auto" ||
2282
+ value === "podcast" ||
2283
+ value === "tutorial" ||
2284
+ value === "generic") {
2285
+ return value;
2286
+ }
2287
+ throw new Error("--content-type-hint must be auto, podcast, tutorial, or generic.");
2288
+ }
2289
+ function parseClipQaMode({ value, }) {
2290
+ if (value === undefined) {
2291
+ return "required";
2292
+ }
2293
+ if (value === "required" || value === "permissive") {
2294
+ return value;
2295
+ }
2296
+ throw new Error("--qa-mode must be required or permissive.");
2297
+ }
2298
+ function parseClipRepairMode({ value, }) {
2299
+ if (value === undefined) {
2300
+ return "auto";
2301
+ }
2302
+ if (value === "auto" ||
2303
+ value === "prefer_split" ||
2304
+ value === "prefer_focus" ||
2305
+ value === "prefer_tutorial" ||
2306
+ value === "move_captions_off_faces") {
2307
+ return value;
2308
+ }
2309
+ throw new Error("--repair-mode must be auto, prefer_split, prefer_focus, prefer_tutorial, or move_captions_off_faces.");
2310
+ }
2311
+ function parseClipExportResolution({ value, }) {
2312
+ if (value === undefined) {
2313
+ return "1080p";
2314
+ }
2315
+ if (value === "720p" || value === "1080p") {
2316
+ return value;
2317
+ }
2318
+ throw new Error("--resolution must be 720p or 1080p.");
2319
+ }
2320
+ function parseClipExportFormat({ value, }) {
2321
+ if (value === undefined) {
2322
+ return "mp4";
2323
+ }
2324
+ if (value === "mp4" || value === "mov") {
2325
+ return value;
2326
+ }
2327
+ throw new Error("--format must be mp4 or mov.");
2328
+ }
2329
+ function buildClipSource({ flags }) {
2330
+ const sourceType = parseClipSourceType({
2331
+ value: requireStringFlag({ flags, name: "source-type" }),
2332
+ });
2333
+ if (sourceType === "asset") {
2334
+ return {
2335
+ sourceType,
2336
+ assetId: requireStringFlag({ flags, name: "asset-id" }),
2337
+ youtubeUrl: null,
2338
+ };
2339
+ }
2340
+ return {
2341
+ sourceType,
2342
+ assetId: null,
2343
+ youtubeUrl: requireStringFlag({ flags, name: "youtube-url" }),
2344
+ };
2345
+ }
2346
+ function requireNumberFlag({ flags, name, }) {
2347
+ const value = getNumberFlag({ flags, name });
2348
+ if (value === undefined) {
2349
+ throw new Error(`Missing required --${name}.`);
2350
+ }
2351
+ return value;
2352
+ }
2353
+ function buildClipSelection({ flags }) {
2354
+ const mode = parseClipSelectionMode({
2355
+ value: getStringFlag({ flags, name: "selection-mode" }),
2356
+ });
2357
+ return {
2358
+ mode,
2359
+ suggestionId: mode === "suggestion"
2360
+ ? requireStringFlag({ flags, name: "suggestion-id" })
2361
+ : (parseNullableStringFlag({ flags, name: "suggestion-id" }) ?? null),
2362
+ startSeconds: mode === "time_range"
2363
+ ? requireNumberFlag({ flags, name: "start-seconds" })
2364
+ : (parseNullableNumberFlag({ flags, name: "start-seconds" }) ?? null),
2365
+ endSeconds: mode === "time_range"
2366
+ ? requireNumberFlag({ flags, name: "end-seconds" })
2367
+ : (parseNullableNumberFlag({ flags, name: "end-seconds" }) ?? null),
2368
+ };
2369
+ }
2370
+ function buildClipTarget({ flags, requireMaxDuration, }) {
2371
+ const maxDurationSeconds = getNumberFlag({ flags, name: "max-duration" });
2372
+ const minDurationInput = parseNullableNumberFlag({
2373
+ flags,
2374
+ name: "min-duration",
2375
+ });
2376
+ if (maxDurationSeconds === undefined) {
2377
+ if (requireMaxDuration) {
2378
+ throw new Error("Missing required --max-duration.");
2379
+ }
2380
+ if (minDurationInput !== undefined && minDurationInput !== null) {
2381
+ throw new Error("--max-duration is required when --min-duration is set.");
2382
+ }
2383
+ return null;
2384
+ }
2385
+ const minDurationSeconds = minDurationInput ?? null;
2386
+ if (minDurationSeconds !== null && minDurationSeconds > maxDurationSeconds) {
2387
+ throw new Error("--min-duration must be less than or equal to --max-duration.");
2388
+ }
2389
+ return {
2390
+ minDurationSeconds,
2391
+ maxDurationSeconds,
2392
+ };
2393
+ }
2394
+ function toCreateClipOptions({ flags, }) {
2395
+ const target = buildClipTarget({ flags, requireMaxDuration: true });
2396
+ if (target === null) {
2397
+ throw new Error("Missing required --max-duration.");
2398
+ }
2399
+ return {
2400
+ projectName: parseNullableStringFlag({ flags, name: "name" }) ?? null,
2401
+ source: buildClipSource({ flags }),
2402
+ selection: buildClipSelection({ flags }),
2403
+ target: {
2404
+ aspectRatio: "9:16",
2405
+ ...target,
2406
+ },
2407
+ layout: {
2408
+ mode: parseClipLayoutMode({
2409
+ value: getStringFlag({ flags, name: "layout-mode" }),
2410
+ }),
2411
+ contentTypeHint: parseClipContentTypeHint({
2412
+ value: getStringFlag({ flags, name: "content-type-hint" }),
2413
+ }),
2414
+ },
2415
+ captions: {
2416
+ enabled: getOptionalBooleanFlag({ flags, name: "captions" }) ?? true,
2417
+ styleId: parseNullableStringFlag({ flags, name: "caption-style-id" }) ?? null,
2418
+ },
2419
+ qa: {
2420
+ mode: parseClipQaMode({
2421
+ value: getStringFlag({ flags, name: "qa-mode" }),
2422
+ }),
2423
+ },
2424
+ export: {
2425
+ autoExport: getOptionalBooleanFlag({ flags, name: "auto-export" }) ?? false,
2426
+ },
2427
+ };
2428
+ }
2429
+ async function waitForClip({ context, }) {
2430
+ const clipId = requireStringFlag({
2431
+ flags: context.args.flags,
2432
+ name: "clip-id",
2433
+ });
2434
+ const intervalMs = getNumberFlag({ flags: context.args.flags, name: "interval-ms" }) ??
2435
+ DEFAULT_WAIT_INTERVAL_MS;
2436
+ const timeoutMs = getNumberFlag({ flags: context.args.flags, name: "timeout-ms" }) ??
2437
+ 10 * 60 * 1000;
2438
+ const startedAt = Date.now();
2439
+ while (true) {
2440
+ const result = await context.client.clips.get({ clipId });
2441
+ const clip = getClipRecord({ result });
2442
+ const nextAction = typeof clip.next_action === "string" ? clip.next_action : null;
2443
+ const status = typeof clip.status === "string" ? clip.status : null;
2444
+ if (nextAction !== "poll" || status === "failed" || status === "exported") {
2445
+ return result;
2446
+ }
2447
+ if (Date.now() - startedAt >= timeoutMs) {
2448
+ throw new Error(`Timed out waiting for clip ${clipId}. Last status: ${status ?? "unknown"}.`);
2449
+ }
2450
+ await sleep({ ms: intervalMs });
2451
+ }
2452
+ }
2453
+ function buildUploadAutoJobNotice({ flags }) {
2454
+ const jobs = [];
2455
+ if (getOptionalBooleanFlag({ flags, name: "auto-process" }) !== false) {
2456
+ jobs.push("process");
2457
+ }
2458
+ if (getOptionalBooleanFlag({ flags, name: "auto-transcribe" }) !== false) {
2459
+ jobs.push("transcribe");
2460
+ }
2461
+ if (getOptionalBooleanFlag({ flags, name: "auto-describe" }) !== false) {
2462
+ jobs.push("describe");
2463
+ }
2464
+ if (getOptionalBooleanFlag({ flags, name: "auto-suggest-shorts" }) !== false) {
2465
+ jobs.push("suggest-shorts");
2466
+ }
2467
+ return jobs;
2468
+ }
2469
+ function parseMediaWaitFor({ value, }) {
2470
+ if (value === undefined) {
2471
+ return "processing";
2472
+ }
2473
+ if (value === "processing" ||
2474
+ value === "transcription" ||
2475
+ value === "description" ||
2476
+ value === "shorts" ||
2477
+ value === "all") {
2478
+ return value;
2479
+ }
2480
+ throw new Error("--for must be processing, transcription, description, shorts, or all.");
2481
+ }
2482
+ function getReadyMediaStates({ asset, clippingStatus, }) {
2483
+ const ready = new Set();
2484
+ if (asset.processingStatus === "completed" ||
2485
+ asset.durationSeconds !== null) {
2486
+ ready.add("processing");
2487
+ }
2488
+ const readiness = isRecord(clippingStatus.readiness)
2489
+ ? clippingStatus.readiness
2490
+ : {};
2491
+ if (readiness.transcript_ready === true) {
2492
+ ready.add("transcription");
2493
+ }
2494
+ if (asset.aiDescription && asset.aiDescription.trim().length > 0) {
2495
+ ready.add("description");
2496
+ }
2497
+ if (readiness.suggestions_ready === true) {
2498
+ ready.add("shorts");
2499
+ }
2500
+ return [...ready];
2501
+ }
2502
+ function mediaWaitSatisfied({ readyFor, waitFor, }) {
2503
+ if (waitFor === "all") {
2504
+ return (readyFor.includes("processing") &&
2505
+ readyFor.includes("transcription") &&
2506
+ readyFor.includes("description") &&
2507
+ readyFor.includes("shorts"));
2508
+ }
2509
+ return readyFor.includes(waitFor);
2510
+ }
2511
+ function parseShortContentProfile({ value, }) {
2512
+ if (value === undefined) {
2513
+ return undefined;
2514
+ }
2515
+ if (value === "auto" ||
2516
+ value === "podcast" ||
2517
+ value === "tutorial" ||
2518
+ value === "generic") {
2519
+ return value;
2520
+ }
2521
+ throw new Error("--content-profile must be auto, podcast, tutorial, or generic.");
2522
+ }
2523
+ function summarizeMistakePlan({ plan, }) {
2524
+ const record = isRecord(plan) ? plan : {};
2525
+ const segments = Array.isArray(record.segments)
2526
+ ? record.segments
2527
+ : Array.isArray(record.analyzedSegments)
2528
+ ? record.analyzedSegments
2529
+ : [];
2530
+ const verdictCounts = {};
2531
+ const topSegments = segments.slice(0, 20).map((segment) => {
2532
+ const item = isRecord(segment) ? segment : {};
2533
+ const verdict = typeof item.verdict === "string" ? item.verdict : "unknown";
2534
+ verdictCounts[verdict] = (verdictCounts[verdict] ?? 0) + 1;
2535
+ return {
2536
+ label: item.label,
2537
+ verdict,
2538
+ confidence: item.confidence,
2539
+ trim_start_seconds: item.trimStartSeconds,
2540
+ trim_end_seconds: item.trimEndSeconds,
2541
+ duration_seconds: item.durationSeconds,
2542
+ reason: item.reason,
2543
+ transcript_preview: item.transcriptPreview,
2544
+ };
2545
+ });
2546
+ return {
2547
+ timeline_item_id: record.timelineItemId,
2548
+ media_asset_id: record.mediaAssetId,
2549
+ summary: record.summary,
2550
+ verdict_counts: verdictCounts,
2551
+ segment_count: segments.length,
2552
+ top_segments: topSegments,
2553
+ truncated: segments.length > topSegments.length,
2554
+ };
2555
+ }
2556
+ function isRecord(value) {
2557
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2558
+ }
2559
+ function buildAgentContext({ commandFilter, surfaceName = "agent-context", }) {
2560
+ const commands = commandFilter
2561
+ ? COMMAND_SPECS.filter((spec) => commandsEqual(spec.command, splitCommandFilter({ value: commandFilter })))
2562
+ : COMMAND_SPECS;
2563
+ if (commandFilter && commands.length === 0) {
2564
+ throw new CliUsageError({
2565
+ code: "unknown_command",
2566
+ message: `Unknown command for ${surfaceName}: ${commandFilter}`,
2567
+ suggestions: suggestCommands({
2568
+ command: splitCommandFilter({ value: commandFilter }),
2569
+ }).map((spec) => commandLabel({ command: spec.command })),
2570
+ });
2571
+ }
2572
+ return {
2573
+ schema_version: "1",
2574
+ cli: {
2575
+ name: "blitzreels",
2576
+ version: CLI_VERSION,
2577
+ default_base_url: DEFAULT_BASE_URL,
2578
+ },
2579
+ commands: commands.map((spec) => ({
2580
+ command: commandLabel({ command: spec.command }),
2581
+ description: spec.description,
2582
+ usage: spec.usage,
2583
+ mutates: spec.mutates,
2584
+ destructive: spec.destructive,
2585
+ requires_auth: spec.requiresAuth,
2586
+ flags: flagsForCommand({ spec }),
2587
+ examples: spec.examples,
2588
+ errors: buildCommandErrors({ spec }),
2589
+ })),
2590
+ };
2591
+ }
2592
+ function buildAgentPrime({ commandFilter, }) {
2593
+ const context = buildAgentContext({
2594
+ commandFilter,
2595
+ surfaceName: "prime",
2596
+ });
2597
+ return {
2598
+ schema_version: "1",
2599
+ cli: context.cli,
2600
+ instructions: [
2601
+ "Use the BlitzReels CLI for BlitzReels video project inspection, editing, captions, media placement, snapshots, and exports.",
2602
+ "Prefer CLI/SDK/API calls over database access. Do not diagnose normal media pipeline issues by reading Postgres first.",
2603
+ "Use --json for any command whose output you will parse.",
2604
+ "Inspect before editing: project inspect, project timeline, media get, captions list/words list, or clips get.",
2605
+ "Use --dry-run on supported mutating commands before risky edits. Destructive commands still require explicit confirmation flags.",
2606
+ "After every write, verify with project timeline, project inspect, project snapshot, project snapshots, media get, or clips get/wait.",
2607
+ ...timelineLayerModelHelp(),
2608
+ "When a CLI command fails, read the structured error, suggestions, and usage. Do not retry random flag names.",
2609
+ ],
2610
+ workflow: [
2611
+ "1. Run blitzreels auth whoami --json if auth state is uncertain.",
2612
+ "2. Orient with blitzreels project inspect --project-id PROJECT_ID --mode full --json or blitzreels project timeline --project-id PROJECT_ID --json.",
2613
+ "3. Select exact project, media, timeline, caption, or clip IDs from read commands.",
2614
+ "4. Apply the narrowest edit command. Use --dry-run first when available.",
2615
+ "5. Verify the resulting timeline, media state, clip state, snapshot, or export.",
2616
+ ],
2617
+ command_schema_command: commandFilter === undefined
2618
+ ? "blitzreels agent-context --json"
2619
+ : `blitzreels agent-context --command ${JSON.stringify(commandFilter)} --json`,
2620
+ command_filter: commandFilter ?? null,
2621
+ commands: context.commands,
2622
+ };
2623
+ }
2624
+ function timelineLayerModelHelp() {
2625
+ return [
2626
+ "Timeline layer rule: lower layer_index renders above higher layer_index.",
2627
+ "Default visual order: captions 0, text/effects 1, image B-roll 2, main video 3, audio 4, background/fill 5.",
2628
+ "Use layer_index 2 for normal visible image/video B-roll above the main clip; use 1 only when it must sit above other overlays, and avoid 0 unless replacing captions.",
2629
+ ];
2630
+ }
2631
+ function splitCommandFilter({ value }) {
2632
+ return value
2633
+ .trim()
2634
+ .split(/\s+/)
2635
+ .filter((part) => part.length > 0);
2636
+ }
2637
+ function buildCommandErrors({ spec, }) {
2638
+ const errors = [
2639
+ { code: "unknown_command", description: "Command is not registered." },
2640
+ { code: "unknown_flag", description: "Flag is not accepted by command." },
2641
+ { code: "cli_error", description: "Input or local execution failed." },
2642
+ ];
2643
+ if (spec.requiresAuth) {
2644
+ errors.push({
2645
+ code: "api_error",
2646
+ description: "BlitzReels API returned a non-2xx response.",
2647
+ });
2648
+ }
2649
+ if (spec.mutates && !spec.dryRunSupported) {
2650
+ errors.push({
2651
+ code: "unsupported_dry_run",
2652
+ description: "--dry-run was used on a command that cannot preview.",
2653
+ });
2654
+ }
2655
+ return errors;
2656
+ }
2657
+ function buildAgentContextLines({ value }) {
2658
+ return value.commands.map((command) => `${command.command} ${command.mutates ? "write" : "read"}${command.destructive ? " destructive" : ""} - ${command.description}`);
2659
+ }
2660
+ function buildAgentPrimeLines({ value }) {
2661
+ const commandLines = value.commands.map((command) => `- ${command.command}: ${command.usage}${command.mutates ? " [writes]" : " [reads]"}${command.destructive ? " [destructive]" : ""}`);
2662
+ return [
2663
+ "<EXTREMELY_IMPORTANT>",
2664
+ "# BlitzReels CLI Usage Guide for AI Agents",
2665
+ "",
2666
+ ...value.instructions.map((line) => `- ${line}`),
2667
+ "",
2668
+ "## Recommended Workflow",
2669
+ "",
2670
+ ...value.workflow.map((line) => `- ${line}`),
2671
+ "",
2672
+ "## Machine-Readable Command Contract",
2673
+ "",
2674
+ `Run: \`${value.command_schema_command}\``,
2675
+ "",
2676
+ "## Relevant Commands",
2677
+ "",
2678
+ ...commandLines,
2679
+ "",
2680
+ "</EXTREMELY_IMPORTANT>",
2681
+ ];
2682
+ }
2683
+ function resolveDisplayEnvironment({ baseUrl, environment, }) {
2684
+ if (environment === "test" &&
2685
+ (baseUrl?.includes("localhost") || baseUrl?.includes("127.0.0.1"))) {
2686
+ return "local";
2687
+ }
2688
+ return environment;
2689
+ }
2690
+ function buildProjectDashboardUrl({ baseUrl, projectId, }) {
2691
+ const url = new URL(baseUrl ?? DEFAULT_BASE_URL);
2692
+ if (url.pathname.startsWith("/api/")) {
2693
+ url.pathname = `/dashboard/projects/${encodeURIComponent(projectId)}`;
2694
+ }
2695
+ else {
2696
+ url.pathname = `/dashboard/projects/${encodeURIComponent(projectId)}`;
2697
+ }
2698
+ url.search = "";
2699
+ url.hash = "";
2700
+ return url.toString();
2701
+ }
2702
+ function buildDryRunPreview({ context, }) {
2703
+ const spec = findCommandSpec({ command: context.args.command });
2704
+ if (!spec?.dryRunSupported) {
2705
+ throw new CliUsageError({
2706
+ code: "unsupported_dry_run",
2707
+ message: "--dry-run is not supported for this command.",
2708
+ usage: spec?.usage,
2709
+ });
2710
+ }
2711
+ const [group, action, subaction] = context.args.command;
2712
+ const flags = context.args.flags;
2713
+ const projectId = () => requireStringFlag({ flags, name: "project-id" });
2714
+ const assetId = () => requireStringFlag({ flags, name: "asset-id" });
2715
+ const folderId = () => requireStringFlag({ flags, name: "folder-id" });
2716
+ const timelineItemId = () => requireStringFlag({ flags, name: "timeline-item-id" });
2717
+ if (group === "workspace" &&
2718
+ action === "settings" &&
2719
+ subaction === "update") {
2720
+ return dryRunRequest({
2721
+ spec,
2722
+ method: "PATCH",
2723
+ path: "/workspace/settings",
2724
+ body: {
2725
+ name: getStringFlag({ flags, name: "name" }),
2726
+ description: parseNullableStringFlag({ flags, name: "description" }),
2727
+ protected_words: parseCsvFlag({ flags, name: "protected-words" }),
2728
+ icon_storage_key: parseNullableStringFlag({
2729
+ flags,
2730
+ name: "icon-storage-key",
2731
+ }),
2732
+ defaults: toApiWorkspaceDefaults({ flags }),
2733
+ },
2734
+ });
2735
+ }
2736
+ if (group === "projects" && action === "create") {
2737
+ return dryRunRequest({
2738
+ spec,
2739
+ method: "POST",
2740
+ path: "/projects",
2741
+ body: {
2742
+ name: requireStringFlag({ flags, name: "name" }),
2743
+ description: getStringFlag({ flags, name: "description" }),
2744
+ aspect_ratio: parseAspectRatio({
2745
+ value: getStringFlag({ flags, name: "aspect-ratio" }),
2746
+ }),
2747
+ project_type: parseProjectType({
2748
+ value: getStringFlag({ flags, name: "project-type" }),
2749
+ }),
2750
+ carousel_settings: parseJsonObjectFlag({
2751
+ flags,
2752
+ name: "carousel-settings-json",
2753
+ }),
2754
+ },
2755
+ });
2756
+ }
2757
+ if (group === "projects" && (action === "rename" || action === "update")) {
2758
+ return dryRunRequest({
2759
+ spec,
2760
+ method: "PATCH",
2761
+ path: `/projects/${encodeURIComponent(projectId())}`,
2762
+ body: {
2763
+ name: action === "rename"
2764
+ ? requireStringFlag({ flags, name: "name" })
2765
+ : getStringFlag({ flags, name: "name" }),
2766
+ description: action === "rename"
2767
+ ? undefined
2768
+ : parseNullableStringFlag({ flags, name: "description" }),
2769
+ },
2770
+ });
2771
+ }
2772
+ if (group === "projects" && action === "delete") {
2773
+ requireDeleteConfirmation({
2774
+ confirmed: getBooleanFlag({ flags, name: "confirm-delete" }),
2775
+ });
2776
+ return dryRunRequest({
2777
+ spec,
2778
+ method: "DELETE",
2779
+ path: `/projects/${encodeURIComponent(projectId())}`,
2780
+ body: undefined,
2781
+ });
2782
+ }
2783
+ if (group === "media" && action === "delete") {
2784
+ requireDeleteConfirmation({
2785
+ confirmed: getBooleanFlag({ flags, name: "confirm-delete" }),
2786
+ });
2787
+ return dryRunRequest({
2788
+ spec,
2789
+ method: "POST",
2790
+ path: `/workspace/media/assets/${encodeURIComponent(assetId())}/delete`,
2791
+ body: {},
2792
+ });
2793
+ }
2794
+ if (group === "media" && (action === "update" || action === "rename")) {
2795
+ return dryRunRequest({
2796
+ spec,
2797
+ method: "PATCH",
2798
+ path: `/workspace/media/assets/${encodeURIComponent(assetId())}`,
2799
+ body: {
2800
+ name: action === "rename"
2801
+ ? requireStringFlag({ flags, name: "name" })
2802
+ : getStringFlag({ flags, name: "name" }),
2803
+ description: action === "rename"
2804
+ ? undefined
2805
+ : parseNullableStringFlag({ flags, name: "description" }),
2806
+ folder_id: action === "rename"
2807
+ ? undefined
2808
+ : parseFolderIdFlag({ flags, name: "folder-id" }),
2809
+ allow_using_as_broll: action === "rename"
2810
+ ? undefined
2811
+ : getOptionalBooleanFlag({ flags, name: "allow-broll" }),
2812
+ },
2813
+ });
2814
+ }
2815
+ if (group === "media" && action === "move") {
2816
+ return dryRunRequest({
2817
+ spec,
2818
+ method: "POST",
2819
+ path: "/workspace/media/assets/move",
2820
+ body: {
2821
+ asset_ids: parseAssetIds({ flags }),
2822
+ target_folder_id: requireFolderIdFlag({
2823
+ flags,
2824
+ name: "target-folder-id",
2825
+ }),
2826
+ },
2827
+ });
2828
+ }
2829
+ if ((group === "media" && action === "attach") ||
2830
+ (group === "broll" && action === "add")) {
2831
+ return dryRunRequest({
2832
+ spec,
2833
+ method: "POST",
2834
+ path: `/projects/${encodeURIComponent(projectId())}/timeline/media`,
2835
+ body: {
2836
+ items: [
2837
+ {
2838
+ asset_id: assetId(),
2839
+ start_seconds: getStringFlag({ flags, name: "at" }) === undefined
2840
+ ? 0
2841
+ : parseTimecode({
2842
+ value: requireStringFlag({ flags, name: "at" }),
2843
+ }),
2844
+ duration_seconds: getNumberFlag({ flags, name: "duration" }) ?? null,
2845
+ position_preset: toApiMediaPositionPreset({
2846
+ value: parseMediaPositionPreset({
2847
+ value: getStringFlag({ flags, name: "position-preset" }),
2848
+ }),
2849
+ }),
2850
+ position_x: getNumberFlag({ flags, name: "position-x" }),
2851
+ position_y: getNumberFlag({ flags, name: "position-y" }),
2852
+ width_px: getNumberFlag({ flags, name: "width-px" }),
2853
+ height_px: getNumberFlag({ flags, name: "height-px" }),
2854
+ scale: getNumberFlag({ flags, name: "scale" }),
2855
+ opacity: getNumberFlag({ flags, name: "opacity" }),
2856
+ animation_preset: toApiMediaAnimationPreset({
2857
+ value: parseMediaAnimationPreset({
2858
+ value: getStringFlag({ flags, name: "animation-preset" }),
2859
+ }),
2860
+ }),
2861
+ layer_index: getNumberFlag({ flags, name: "layer-index" }),
2862
+ },
2863
+ ],
2864
+ allow_duplicate: getOptionalBooleanFlag({
2865
+ flags,
2866
+ name: "allow-duplicate",
2867
+ }),
2868
+ },
2869
+ });
2870
+ }
2871
+ if (group === "media" && action === "overlay" && subaction === "update") {
2872
+ return dryRunRequest({
2873
+ spec,
2874
+ method: "PATCH",
2875
+ path: `/projects/${encodeURIComponent(projectId())}/timeline/media/${encodeURIComponent(requireStringFlag({ flags, name: "timeline-item-id" }))}`,
2876
+ body: {
2877
+ position_preset: toApiMediaPositionPreset({
2878
+ value: parseMediaPositionPreset({
2879
+ value: getStringFlag({ flags, name: "position-preset" }),
2880
+ }),
2881
+ }),
2882
+ position_x: getNumberFlag({ flags, name: "position-x" }),
2883
+ position_y: getNumberFlag({ flags, name: "position-y" }),
2884
+ width_px: getNumberFlag({ flags, name: "width-px" }),
2885
+ height_px: getNumberFlag({ flags, name: "height-px" }),
2886
+ scale: getNumberFlag({ flags, name: "scale" }),
2887
+ animation_preset: toApiMediaAnimationPreset({
2888
+ value: parseMediaAnimationPreset({
2889
+ value: getStringFlag({ flags, name: "animation-preset" }),
2890
+ }),
2891
+ }),
2892
+ remove_animations: getOptionalBooleanFlag({
2893
+ flags,
2894
+ name: "remove-animations",
2895
+ }),
2896
+ blurred_background: getOptionalBooleanFlag({
2897
+ flags,
2898
+ name: "blurred-background",
2899
+ }),
2900
+ },
2901
+ });
2902
+ }
2903
+ if (group === "clips" && action === "create") {
2904
+ const options = toCreateClipOptions({ flags });
2905
+ return dryRunRequest({
2906
+ spec,
2907
+ method: "POST",
2908
+ path: "/clips",
2909
+ body: {
2910
+ project_name: options.projectName,
2911
+ source: {
2912
+ source_type: options.source.sourceType,
2913
+ asset_id: options.source.assetId,
2914
+ youtube_url: options.source.youtubeUrl,
2915
+ },
2916
+ selection: {
2917
+ selection_mode: options.selection.mode,
2918
+ suggestion_id: options.selection.suggestionId,
2919
+ start_seconds: options.selection.startSeconds,
2920
+ end_seconds: options.selection.endSeconds,
2921
+ },
2922
+ target: {
2923
+ aspect_ratio: options.target.aspectRatio,
2924
+ min_duration_seconds: options.target.minDurationSeconds,
2925
+ max_duration_seconds: options.target.maxDurationSeconds,
2926
+ },
2927
+ layout: {
2928
+ layout_mode: options.layout.mode,
2929
+ content_type_hint: options.layout.contentTypeHint,
2930
+ },
2931
+ captions: {
2932
+ enabled: options.captions.enabled,
2933
+ style_id: options.captions.styleId,
2934
+ },
2935
+ qa: {
2936
+ qa_mode: options.qa.mode,
2937
+ },
2938
+ export: {
2939
+ auto_export: options.export.autoExport,
2940
+ },
2941
+ },
2942
+ });
2943
+ }
2944
+ if (group === "clips" && action === "reselect") {
2945
+ const target = buildClipTarget({ flags, requireMaxDuration: false });
2946
+ const selection = buildClipSelection({ flags });
2947
+ return dryRunRequest({
2948
+ spec,
2949
+ method: "POST",
2950
+ path: `/clips/${encodeURIComponent(requireStringFlag({ flags, name: "clip-id" }))}/reselect`,
2951
+ body: {
2952
+ selection: {
2953
+ selection_mode: selection.mode,
2954
+ suggestion_id: selection.suggestionId,
2955
+ start_seconds: selection.startSeconds,
2956
+ end_seconds: selection.endSeconds,
2957
+ },
2958
+ target: target === null
2959
+ ? null
2960
+ : {
2961
+ min_duration_seconds: target.minDurationSeconds,
2962
+ max_duration_seconds: target.maxDurationSeconds,
2963
+ },
2964
+ },
2965
+ });
2966
+ }
2967
+ if (group === "clips" && action === "repair") {
2968
+ return dryRunRequest({
2969
+ spec,
2970
+ method: "POST",
2971
+ path: `/clips/${encodeURIComponent(requireStringFlag({ flags, name: "clip-id" }))}/repair`,
2972
+ body: {
2973
+ repair_mode: parseClipRepairMode({
2974
+ value: getStringFlag({ flags, name: "repair-mode" }),
2975
+ }),
2976
+ },
2977
+ });
2978
+ }
2979
+ if (group === "clips" && action === "export") {
2980
+ return dryRunRequest({
2981
+ spec,
2982
+ method: "POST",
2983
+ path: `/clips/${encodeURIComponent(requireStringFlag({ flags, name: "clip-id" }))}/export`,
2984
+ body: {
2985
+ resolution: parseClipExportResolution({
2986
+ value: getStringFlag({ flags, name: "resolution" }),
2987
+ }),
2988
+ format: parseClipExportFormat({
2989
+ value: getStringFlag({ flags, name: "format" }),
2990
+ }),
2991
+ allow_blocking_qa_bypass: getOptionalBooleanFlag({
2992
+ flags,
2993
+ name: "allow-blocking-qa-bypass",
2994
+ }) ?? false,
2995
+ },
2996
+ });
2997
+ }
2998
+ if (group === "media" && action === "folders" && subaction === "create") {
2999
+ return dryRunRequest({
3000
+ spec,
3001
+ method: "POST",
3002
+ path: "/workspace/media/folders",
3003
+ body: {
3004
+ name: requireStringFlag({ flags, name: "name" }),
3005
+ parent_folder_id: parseFolderIdFlag({
3006
+ flags,
3007
+ name: "parent-folder-id",
3008
+ }),
3009
+ description: parseNullableStringFlag({ flags, name: "description" }),
3010
+ icon_type: getStringFlag({ flags, name: "icon-type" }),
3011
+ },
3012
+ });
3013
+ }
3014
+ if (group === "media" &&
3015
+ action === "folders" &&
3016
+ (subaction === "rename" || subaction === "update")) {
3017
+ return dryRunRequest({
3018
+ spec,
3019
+ method: "PATCH",
3020
+ path: `/workspace/media/folders/${encodeURIComponent(folderId())}`,
3021
+ body: {
3022
+ name: subaction === "rename"
3023
+ ? requireStringFlag({ flags, name: "name" })
3024
+ : getStringFlag({ flags, name: "name" }),
3025
+ parent_folder_id: subaction === "rename"
3026
+ ? undefined
3027
+ : parseFolderIdFlag({ flags, name: "parent-folder-id" }),
3028
+ description: subaction === "rename"
3029
+ ? undefined
3030
+ : parseNullableStringFlag({ flags, name: "description" }),
3031
+ icon_type: subaction === "rename"
3032
+ ? undefined
3033
+ : getStringFlag({ flags, name: "icon-type" }),
3034
+ },
3035
+ });
3036
+ }
3037
+ if (group === "media" && action === "folders" && subaction === "delete") {
3038
+ requireDeleteConfirmation({
3039
+ confirmed: getBooleanFlag({ flags, name: "confirm-delete" }),
3040
+ });
3041
+ return dryRunRequest({
3042
+ spec,
3043
+ method: "DELETE",
3044
+ path: `/workspace/media/folders/${encodeURIComponent(folderId())}`,
3045
+ query: { cascade: getOptionalBooleanFlag({ flags, name: "cascade" }) },
3046
+ body: undefined,
3047
+ });
3048
+ }
3049
+ if (group === "text" && action === "add") {
3050
+ const text = requireStringFlag({ flags, name: "text" });
3051
+ const durationSeconds = getNumberFlag({ flags, name: "duration" }) ?? 4;
3052
+ return dryRunRequest({
3053
+ spec,
3054
+ method: "POST",
3055
+ path: `/projects/${encodeURIComponent(projectId())}/content-items`,
3056
+ body: {
3057
+ kind: "overlay",
3058
+ text,
3059
+ name: getStringFlag({ flags, name: "name" }),
3060
+ spec: parseJsonObjectFlag({ flags, name: "spec-json" }) ??
3061
+ (hasTextStyleFlags({ flags })
3062
+ ? buildTextOverlaySpec({
3063
+ text,
3064
+ name: getStringFlag({ flags, name: "name" }),
3065
+ durationSeconds,
3066
+ flags,
3067
+ })
3068
+ : undefined),
3069
+ start_seconds: parseTimecode({
3070
+ value: getStringFlag({ flags, name: "at" }) ?? "0",
3071
+ }),
3072
+ duration_seconds: durationSeconds,
3073
+ layer_index: getNumberFlag({ flags, name: "layer-index" }),
3074
+ },
3075
+ });
3076
+ }
3077
+ if (group === "text" && action === "update") {
3078
+ return dryRunRequest({
3079
+ spec,
3080
+ method: "PATCH",
3081
+ path: `/projects/${encodeURIComponent(projectId())}/content-items/${encodeURIComponent(requireStringFlag({ flags, name: "content-item-id" }))}`,
3082
+ body: {
3083
+ name: getStringFlag({ flags, name: "name" }),
3084
+ text: getStringFlag({ flags, name: "text" }),
3085
+ spec: parseJsonObjectFlag({ flags, name: "spec-json" }),
3086
+ duration_seconds: getNumberFlag({ flags, name: "duration" }),
3087
+ },
3088
+ notes: hasTextStyleFlags({ flags })
3089
+ ? [
3090
+ "Style flags require reading the existing content item before building the final spec during real execution.",
3091
+ ]
3092
+ : undefined,
3093
+ });
3094
+ }
3095
+ if (group === "text" && action === "remove") {
3096
+ return dryRunRequest({
3097
+ spec,
3098
+ method: "DELETE",
3099
+ path: `/projects/${encodeURIComponent(projectId())}/content-items/${encodeURIComponent(requireStringFlag({ flags, name: "content-item-id" }))}`,
3100
+ body: undefined,
3101
+ });
3102
+ }
3103
+ if (group === "timeline" && action === "transform") {
3104
+ return dryRunRequest({
3105
+ spec,
3106
+ method: "POST",
3107
+ path: `/projects/${encodeURIComponent(projectId())}/timeline/edits`,
3108
+ body: {
3109
+ action: "transform",
3110
+ timeline_item_id: timelineItemId(),
3111
+ position_x: getNumberFlag({ flags, name: "position-x" }),
3112
+ position_y: getNumberFlag({ flags, name: "position-y" }),
3113
+ width_px: parseNullableNumberFlag({ flags, name: "width-px" }),
3114
+ height_px: parseNullableNumberFlag({ flags, name: "height-px" }),
3115
+ keep_aspect_ratio: getOptionalBooleanFlag({
3116
+ flags,
3117
+ name: "keep-aspect-ratio",
3118
+ }),
3119
+ },
3120
+ });
3121
+ }
3122
+ if (group === "timeline" && action === "move") {
3123
+ return dryRunRequest({
3124
+ spec,
3125
+ method: "POST",
3126
+ path: `/projects/${encodeURIComponent(projectId())}/timeline/edits`,
3127
+ body: {
3128
+ action: "move",
3129
+ timeline_item_id: timelineItemId(),
3130
+ new_start_seconds: parseTimecode({
3131
+ value: requireStringFlag({ flags, name: "at" }),
3132
+ }),
3133
+ new_layer_index: getNumberFlag({ flags, name: "layer-index" }),
3134
+ },
3135
+ });
3136
+ }
3137
+ if (group === "timeline" && action === "delete") {
3138
+ return dryRunRequest({
3139
+ spec,
3140
+ method: "POST",
3141
+ path: `/projects/${encodeURIComponent(projectId())}/timeline/edits`,
3142
+ body: {
3143
+ action: "delete",
3144
+ timeline_item_ids: parseTimelineItemIds({ flags }),
3145
+ auto_pack: getOptionalBooleanFlag({ flags, name: "auto-pack" }),
3146
+ },
3147
+ });
3148
+ }
3149
+ if (group === "audio" && action === "add") {
3150
+ return dryRunRequest({
3151
+ spec,
3152
+ method: "POST",
3153
+ path: `/projects/${encodeURIComponent(projectId())}/timeline/audio`,
3154
+ body: {
3155
+ items: [
3156
+ {
3157
+ asset_id: assetId(),
3158
+ start_seconds: parseTimecode({
3159
+ value: getStringFlag({ flags, name: "at" }) ?? "0",
3160
+ }),
3161
+ duration_seconds: getNumberFlag({ flags, name: "duration" }),
3162
+ volume: getNumberFlag({ flags, name: "volume" }),
3163
+ fade_in_seconds: getNumberFlag({ flags, name: "fade-in" }),
3164
+ fade_out_seconds: getNumberFlag({ flags, name: "fade-out" }),
3165
+ loop: getOptionalBooleanFlag({ flags, name: "loop" }),
3166
+ },
3167
+ ],
3168
+ },
3169
+ });
3170
+ }
3171
+ if (group === "audio" && action === "import") {
3172
+ return dryRunRequest({
3173
+ spec,
3174
+ method: "POST",
3175
+ path: "/audio-library/import",
3176
+ body: {
3177
+ provider: parseAudioLibraryProvider({
3178
+ value: requireStringFlag({ flags, name: "provider" }),
3179
+ }),
3180
+ external_id: requireStringFlag({ flags, name: "external-id" }),
3181
+ folder_id: parseFolderIdFlag({ flags, name: "folder-id" }),
3182
+ project_id: getStringFlag({ flags, name: "project-id" }),
3183
+ start_seconds: parseTimecode({
3184
+ value: getStringFlag({ flags, name: "at" }) ?? "0",
3185
+ }),
3186
+ duration_seconds: parseNullableNumberFlag({ flags, name: "duration" }),
3187
+ volume: getNumberFlag({ flags, name: "volume" }),
3188
+ fade_in_seconds: getNumberFlag({ flags, name: "fade-in" }),
3189
+ fade_out_seconds: getNumberFlag({ flags, name: "fade-out" }),
3190
+ loop: getOptionalBooleanFlag({ flags, name: "loop" }),
3191
+ audio_type: parseAudioLibraryType({
3192
+ value: getStringFlag({ flags, name: "audio-type" }),
3193
+ }),
3194
+ },
3195
+ });
3196
+ }
3197
+ if (group === "audio" && action === "update") {
3198
+ return dryRunRequest({
3199
+ spec,
3200
+ method: "PATCH",
3201
+ path: `/projects/${encodeURIComponent(projectId())}/timeline/audio/${encodeURIComponent(requireStringFlag({ flags, name: "timeline-item-id" }))}`,
3202
+ body: {
3203
+ duration_seconds: getNumberFlag({ flags, name: "duration" }),
3204
+ volume: getNumberFlag({ flags, name: "volume" }),
3205
+ fade_in_seconds: getNumberFlag({ flags, name: "fade-in" }),
3206
+ fade_out_seconds: getNumberFlag({ flags, name: "fade-out" }),
3207
+ },
3208
+ });
3209
+ }
3210
+ if (group === "library" && action === "import") {
3211
+ const libraryAssetId = requireStringFlag({
3212
+ flags,
3213
+ name: "library-asset-id",
3214
+ });
3215
+ return dryRunRequest({
3216
+ spec,
3217
+ method: "POST",
3218
+ path: `/library/broll/${encodeURIComponent(libraryAssetId)}/import`,
3219
+ body: {
3220
+ folder_id: parseFolderIdFlag({ flags, name: "folder-id" }),
3221
+ },
3222
+ });
3223
+ }
3224
+ if (group === "captions" && action === "words") {
3225
+ return buildCaptionWordsDryRun({ spec, flags, subaction });
3226
+ }
3227
+ if (group === "captions" &&
3228
+ (action === "list-words" ||
3229
+ action === "style-words" ||
3230
+ action === "emphasize")) {
3231
+ const aliasSubaction = action === "list-words"
3232
+ ? "list"
3233
+ : action === "style-words"
3234
+ ? "style"
3235
+ : "emphasis";
3236
+ return buildCaptionWordsDryRun({ spec, flags, subaction: aliasSubaction });
3237
+ }
3238
+ if (group === "captions" &&
3239
+ (action === "delete" || action === "hide" || action === "show")) {
3240
+ return buildCaptionTrackDryRun({ spec, flags, action });
3241
+ }
3242
+ if (group === "captions" && action === "theme" && subaction === "set") {
3243
+ return dryRunRequest({
3244
+ spec,
3245
+ method: "POST",
3246
+ path: `/projects/${encodeURIComponent(projectId())}/caption-theme`,
3247
+ body: {
3248
+ theme_id: requireStringFlag({ flags, name: "theme-id" }),
3249
+ },
3250
+ });
3251
+ }
3252
+ if (group === "captions" && action === "theme" && subaction === "update") {
3253
+ return dryRunRequest({
3254
+ spec,
3255
+ method: "PATCH",
3256
+ path: `/caption-themes/${encodeURIComponent(requireStringFlag({ flags, name: "theme-id" }))}`,
3257
+ body: {
3258
+ name: getStringFlag({ flags, name: "name" }),
3259
+ description: getStringFlag({ flags, name: "description" }),
3260
+ settings: buildCaptionThemeSettingsPatch({ flags }),
3261
+ },
3262
+ });
3263
+ }
3264
+ if (group === "captions" &&
3265
+ action === "theme" &&
3266
+ subaction === "set-default") {
3267
+ return dryRunRequest({
3268
+ spec,
3269
+ method: "POST",
3270
+ path: `/caption-themes/${encodeURIComponent(requireStringFlag({ flags, name: "theme-id" }))}/set-default`,
3271
+ body: {},
3272
+ });
3273
+ }
3274
+ if (group === "exports" && action === "start") {
3275
+ return dryRunRequest({
3276
+ spec,
3277
+ method: "POST",
3278
+ path: `/projects/${encodeURIComponent(projectId())}/export`,
3279
+ body: {
3280
+ resolution: getStringFlag({ flags, name: "resolution" }),
3281
+ format: getStringFlag({ flags, name: "format" }),
3282
+ },
3283
+ });
3284
+ }
3285
+ throw new CliUsageError({
3286
+ code: "unsupported_dry_run",
3287
+ message: `--dry-run is not implemented for ${commandLabel({ command: spec.command })}.`,
3288
+ usage: spec.usage,
1742
3289
  });
1743
3290
  }
1744
- async function loginWithBrowser({ context, }) {
1745
- const appUrl = resolveAppUrl({ baseUrl: context.config.baseUrl });
1746
- const deviceUrl = `${appUrl}/api/cli/auth/device`;
1747
- const startResponse = await fetch(deviceUrl, {
1748
- method: "POST",
1749
- headers: { Accept: "application/json" },
1750
- });
1751
- const login = parseDeviceLoginResponse({
1752
- value: await readJsonResponse({
1753
- response: startResponse,
1754
- url: deviceUrl,
1755
- action: "start CLI login",
1756
- }),
1757
- });
1758
- const shouldOpenBrowser = !getBooleanFlag({
1759
- flags: context.args.flags,
1760
- name: "no-browser",
1761
- });
1762
- if (shouldOpenBrowser) {
1763
- openBrowser({ url: login.verification_uri_complete });
3291
+ function buildCaptionWordsDryRun({ spec, flags, subaction, }) {
3292
+ const projectId = requireStringFlag({ flags, name: "project-id" });
3293
+ const basePath = `/projects/${encodeURIComponent(projectId)}/captions/words`;
3294
+ if (subaction === "text") {
3295
+ return dryRunRequest({
3296
+ spec,
3297
+ method: "POST",
3298
+ path: `${basePath}/text`,
3299
+ body: {
3300
+ word_id: requireStringFlag({ flags, name: "word-id" }),
3301
+ new_text: getStringFlag({ flags, name: "new-text" }) ??
3302
+ requireStringFlag({ flags, name: "text" }),
3303
+ },
3304
+ });
1764
3305
  }
1765
- const loginInstructions = [
1766
- `First copy your one-time code: ${login.user_code}`,
1767
- `Then open: ${login.verification_uri_complete}`,
1768
- "Waiting for browser approval...",
1769
- ];
1770
- if (context.json) {
1771
- console.error(loginInstructions.join("\n"));
3306
+ if (subaction === "batch-text") {
3307
+ return dryRunRequest({
3308
+ spec,
3309
+ method: "POST",
3310
+ path: `${basePath}/batch-text`,
3311
+ body: {
3312
+ updates: parseWordTextUpdatesFlag({ flags, name: "updates-json" }),
3313
+ },
3314
+ });
1772
3315
  }
1773
- else {
1774
- printResult({
1775
- json: context.json,
1776
- value: {
1777
- user_code: login.user_code,
1778
- verification_uri: login.verification_uri_complete,
1779
- expires_in: login.expires_in,
3316
+ if (subaction === "style") {
3317
+ const styleJson = parseNullableJsonObjectFlag({
3318
+ flags,
3319
+ name: "style-json",
3320
+ });
3321
+ return dryRunRequest({
3322
+ spec,
3323
+ method: "POST",
3324
+ path: `${basePath}/style`,
3325
+ body: {
3326
+ ...buildCaptionWordSelectorBody({ flags }),
3327
+ style: styleJson === undefined
3328
+ ? buildCaptionWordStyleFromFlags({ flags })
3329
+ : styleJson,
3330
+ clear_existing: getOptionalBooleanFlag({
3331
+ flags,
3332
+ name: "clear-existing",
3333
+ }),
1780
3334
  },
1781
- human: loginInstructions,
1782
3335
  });
1783
3336
  }
1784
- const startedAt = Date.now();
1785
- const timeoutMs = Math.min(login.expires_in * 1000, getNumberFlag({ flags: context.args.flags, name: "timeout-ms" }) ??
1786
- login.expires_in * 1000);
1787
- const intervalMs = Math.max(1000, login.interval * 1000);
1788
- while (Date.now() - startedAt <= timeoutMs) {
1789
- await sleep({ ms: intervalMs });
1790
- const tokenUrl = `${appUrl}/api/cli/auth/token`;
1791
- const pollResponse = await fetch(tokenUrl, {
3337
+ if (subaction === "emphasis") {
3338
+ return dryRunRequest({
3339
+ spec,
1792
3340
  method: "POST",
1793
- headers: {
1794
- Accept: "application/json",
1795
- "Content-Type": "application/json",
3341
+ path: `${basePath}/emphasis`,
3342
+ body: {
3343
+ ...buildCaptionWordSelectorBody({ flags }),
3344
+ emphasis: getOptionalBooleanFlag({ flags, name: "emphasis" }) ?? true,
1796
3345
  },
1797
- body: JSON.stringify({ device_code: login.device_code }),
1798
3346
  });
1799
- const result = parseDeviceTokenResponse({
1800
- value: await readJsonResponse({
1801
- response: pollResponse,
1802
- url: tokenUrl,
1803
- action: "poll CLI login",
3347
+ }
3348
+ if (subaction === "delete") {
3349
+ return dryRunRequest({
3350
+ spec,
3351
+ method: "POST",
3352
+ path: `${basePath}/delete`,
3353
+ body: { word_ids: parseWordIds({ flags }) },
3354
+ });
3355
+ }
3356
+ if (subaction === "merge") {
3357
+ return dryRunRequest({
3358
+ spec,
3359
+ method: "POST",
3360
+ path: `${basePath}/merge`,
3361
+ body: {
3362
+ word_ids: parseWordIds({ flags }),
3363
+ text: getStringFlag({ flags, name: "text" }),
3364
+ },
3365
+ });
3366
+ }
3367
+ if (subaction === "split") {
3368
+ return dryRunRequest({
3369
+ spec,
3370
+ method: "POST",
3371
+ path: `${basePath}/split`,
3372
+ body: {
3373
+ word_id: requireStringFlag({ flags, name: "word-id" }),
3374
+ words: parseCsvRequiredFlag({ flags, name: "words" }),
3375
+ },
3376
+ });
3377
+ }
3378
+ throw new CliUsageError({
3379
+ code: "unsupported_dry_run",
3380
+ message: `--dry-run is not implemented for captions words ${subaction ?? ""}.`,
3381
+ usage: spec.usage,
3382
+ });
3383
+ }
3384
+ function buildCaptionTrackDryRun({ spec, flags, action, }) {
3385
+ const selector = buildCaptionTrackSelectorBody({ flags });
3386
+ return dryRunRequest({
3387
+ spec,
3388
+ method: "POST",
3389
+ path: `/projects/${encodeURIComponent(requireStringFlag({ flags, name: "project-id" }))}/captions/${action}`,
3390
+ body: selector,
3391
+ });
3392
+ }
3393
+ function buildCaptionWordSelectorBody({ flags, }) {
3394
+ return {
3395
+ word_ids: parseCsvFlag({ flags, name: "word-ids" }) ??
3396
+ parseCsvFlag({ flags, name: "word-id" }),
3397
+ match_text: getStringFlag({ flags, name: "match-text" }),
3398
+ match_pattern: parseCaptionMatchPattern({
3399
+ value: getStringFlag({ flags, name: "match-pattern" }),
3400
+ }),
3401
+ timeline_item_id: getStringFlag({ flags, name: "timeline-item-id" }),
3402
+ all: getOptionalBooleanFlag({ flags, name: "all" }),
3403
+ from_seconds: getStringFlag({ flags, name: "from" }) === undefined
3404
+ ? undefined
3405
+ : parseTimecode({
3406
+ value: requireStringFlag({ flags, name: "from" }),
3407
+ }),
3408
+ to_seconds: getStringFlag({ flags, name: "to" }) === undefined
3409
+ ? undefined
3410
+ : parseTimecode({
3411
+ value: requireStringFlag({ flags, name: "to" }),
1804
3412
  }),
3413
+ };
3414
+ }
3415
+ function buildCaptionTrackSelectorBody({ flags, }) {
3416
+ return {
3417
+ all: getOptionalBooleanFlag({ flags, name: "all" }),
3418
+ caption_id: getStringFlag({ flags, name: "caption-id" }),
3419
+ timeline_item_id: getStringFlag({ flags, name: "timeline-item-id" }),
3420
+ from_seconds: getStringFlag({ flags, name: "from" }) === undefined
3421
+ ? undefined
3422
+ : parseTimecode({
3423
+ value: requireStringFlag({ flags, name: "from" }),
3424
+ }),
3425
+ to_seconds: getStringFlag({ flags, name: "to" }) === undefined
3426
+ ? undefined
3427
+ : parseTimecode({
3428
+ value: requireStringFlag({ flags, name: "to" }),
3429
+ }),
3430
+ };
3431
+ }
3432
+ function buildCaptionTrackSelector({ flags }) {
3433
+ const body = buildCaptionTrackSelectorBody({ flags });
3434
+ return {
3435
+ projectId: requireStringFlag({ flags, name: "project-id" }),
3436
+ all: body.all,
3437
+ captionId: body.caption_id,
3438
+ timelineItemId: body.timeline_item_id,
3439
+ fromSeconds: body.from_seconds,
3440
+ toSeconds: body.to_seconds,
3441
+ };
3442
+ }
3443
+ function dryRunRequest({ spec, method, path, query, body, followUpRequests, notes, }) {
3444
+ return buildDryRunRequest({
3445
+ spec,
3446
+ method,
3447
+ path,
3448
+ query,
3449
+ body,
3450
+ followUpRequests,
3451
+ notes,
3452
+ });
3453
+ }
3454
+ function toApiWorkspaceDefaults({ flags, }) {
3455
+ const defaults = buildWorkspaceDefaults({ flags });
3456
+ if (defaults === undefined) {
3457
+ return undefined;
3458
+ }
3459
+ return {
3460
+ aspect_ratio: defaults.aspectRatio,
3461
+ orientation: defaults.orientation,
3462
+ resolution: defaults.resolution,
3463
+ frame_rate: defaults.frameRate,
3464
+ auto_manage_broll: defaults.autoManageBroll,
3465
+ caption_theme_id: defaults.captionThemeId,
3466
+ };
3467
+ }
3468
+ function applyFieldSelection({ value, fields, }) {
3469
+ if (Array.isArray(value)) {
3470
+ return value.map((item) => applyFieldSelection({ value: item, fields }));
3471
+ }
3472
+ if (!value || typeof value !== "object") {
3473
+ return value;
3474
+ }
3475
+ const record = value;
3476
+ const arrayEntries = Object.entries(record).filter(([, item]) => Array.isArray(item));
3477
+ if (arrayEntries.length === 1) {
3478
+ const [key, items] = arrayEntries[0];
3479
+ return {
3480
+ ...record,
3481
+ [key]: items.map((item) => applyFieldSelection({ value: item, fields })),
3482
+ };
3483
+ }
3484
+ return Object.fromEntries(fields
3485
+ .filter((field) => field in record)
3486
+ .map((field) => [field, record[field]]));
3487
+ }
3488
+ function validateSafeInputs({ parsed }) {
3489
+ for (const [name, value] of Object.entries(parsed.flags)) {
3490
+ if (typeof value !== "string") {
3491
+ continue;
3492
+ }
3493
+ rejectControlChars({ name, value });
3494
+ if (isIdLikeFlag({ name })) {
3495
+ rejectUnsafeId({ name, value });
3496
+ }
3497
+ if (name === "url") {
3498
+ rejectUnsafeUrl({ name, value });
3499
+ }
3500
+ if (name === "output") {
3501
+ rejectUnsafeOutputPath({ value });
3502
+ }
3503
+ }
3504
+ }
3505
+ function rejectControlChars({ name, value, }) {
3506
+ if (Array.from(value).some((character) => {
3507
+ const code = character.charCodeAt(0);
3508
+ return code < 32 || code === 127;
3509
+ })) {
3510
+ throw new CliUsageError({
3511
+ code: "unsafe_input",
3512
+ message: `--${name} contains control characters.`,
1805
3513
  });
1806
- if (result.status === "approved" && result.api_key) {
1807
- await persistLoggedInApiKey({
1808
- apiKey: result.api_key,
1809
- baseUrl: context.config.baseUrl,
1810
- });
1811
- printResult({
1812
- json: context.json,
1813
- value: { success: true, key_prefix: result.key_prefix ?? "" },
1814
- human: [`Logged in. API key prefix: ${result.key_prefix ?? "saved"}`],
1815
- });
3514
+ }
3515
+ }
3516
+ function isIdLikeFlag({ name }) {
3517
+ return name === "id" || name.endsWith("-id") || name.endsWith("-ids");
3518
+ }
3519
+ function rejectUnsafeId({ name, value, }) {
3520
+ if (/[?#%]/.test(value)) {
3521
+ throw new CliUsageError({
3522
+ code: "unsafe_input",
3523
+ message: `--${name} must be an ID, not a URL or encoded path.`,
3524
+ });
3525
+ }
3526
+ }
3527
+ function rejectUnsafeUrl({ name, value, }) {
3528
+ let url;
3529
+ try {
3530
+ url = new URL(value);
3531
+ }
3532
+ catch {
3533
+ throw new CliUsageError({
3534
+ code: "unsafe_input",
3535
+ message: `--${name} must be a valid URL.`,
3536
+ });
3537
+ }
3538
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
3539
+ throw new CliUsageError({
3540
+ code: "unsafe_input",
3541
+ message: `--${name} must use http or https.`,
3542
+ });
3543
+ }
3544
+ if (url.username || url.password) {
3545
+ throw new CliUsageError({
3546
+ code: "unsafe_input",
3547
+ message: `--${name} must not include embedded credentials.`,
3548
+ });
3549
+ }
3550
+ }
3551
+ function rejectUnsafeOutputPath({ value }) {
3552
+ const outputPath = resolve(value);
3553
+ const sensitiveDirs = [join(homedir(), ".ssh"), join(homedir(), ".gnupg")];
3554
+ if (sensitiveDirs.some((dir) => outputPath.startsWith(`${dir}/`) || outputPath === dir)) {
3555
+ throw new CliUsageError({
3556
+ code: "unsafe_input",
3557
+ message: "--output cannot write into sensitive home directories.",
3558
+ });
3559
+ }
3560
+ }
3561
+ async function maybePrintUpdateNotice({ parsed, baseUrl, json, }) {
3562
+ if (!shouldRunPassiveUpdateCheck({ parsed, json })) {
3563
+ return;
3564
+ }
3565
+ try {
3566
+ const update = await getUpdateStatus({ baseUrl, cachePolicy: "cache" });
3567
+ if (!update.updateAvailable) {
1816
3568
  return;
1817
3569
  }
1818
- if (result.status === "expired" ||
1819
- result.status === "denied" ||
1820
- result.status === "invalid" ||
1821
- result.status === "consumed") {
1822
- throw new Error(`Login failed: ${result.status}.`);
1823
- }
3570
+ console.error(buildUpdateNotice({ update }).join("\n"));
3571
+ }
3572
+ catch {
3573
+ // Update checks must never affect the requested CLI command.
3574
+ }
3575
+ }
3576
+ function shouldRunPassiveUpdateCheck({ parsed, json, }) {
3577
+ if (json) {
3578
+ return false;
3579
+ }
3580
+ if (process.env.BLITZREELS_NO_UPDATE_NOTIFIER === "1") {
3581
+ return false;
3582
+ }
3583
+ if (process.env.CI === "true") {
3584
+ return false;
3585
+ }
3586
+ if (!process.stderr.isTTY &&
3587
+ process.env.BLITZREELS_FORCE_UPDATE_NOTIFIER !== "1") {
3588
+ return false;
3589
+ }
3590
+ if (parsed.command.length === 0 ||
3591
+ parsed.command[0] === "version" ||
3592
+ parsed.command[0] === "doctor" ||
3593
+ parsed.command[0] === "agent-context" ||
3594
+ parsed.command[0] === "schema" ||
3595
+ parsed.command[0] === "prime" ||
3596
+ parsed.command[0] === "llms" ||
3597
+ getBooleanFlag({ flags: parsed.flags, name: "help" })) {
3598
+ return false;
3599
+ }
3600
+ return true;
3601
+ }
3602
+ async function getUpdateStatus({ baseUrl, cachePolicy, }) {
3603
+ const cached = await readUpdateCache();
3604
+ const now = Date.now();
3605
+ if (cachePolicy === "cache" &&
3606
+ cached &&
3607
+ now - cached.fetchedAt < UPDATE_CHECK_TTL_MS) {
3608
+ return buildUpdateStatus({ manifest: cached.manifest, source: "cache" });
3609
+ }
3610
+ const manifest = await fetchUpdateManifest({ baseUrl }).catch(() => undefined);
3611
+ if (manifest) {
3612
+ await writeUpdateCache({ manifest });
3613
+ return buildUpdateStatus({ manifest, source: "network" });
3614
+ }
3615
+ if (cached) {
3616
+ return buildUpdateStatus({ manifest: cached.manifest, source: "cache" });
3617
+ }
3618
+ return buildUpdateStatus({
3619
+ manifest: buildFallbackUpdateManifest(),
3620
+ source: "fallback",
3621
+ });
3622
+ }
3623
+ function buildUpdateStatus({ manifest, source, }) {
3624
+ return {
3625
+ current: CLI_VERSION,
3626
+ manifest,
3627
+ updateAvailable: isVersionLessThan({
3628
+ left: CLI_VERSION,
3629
+ right: manifest.latest,
3630
+ }),
3631
+ source,
3632
+ };
3633
+ }
3634
+ async function fetchUpdateManifest({ baseUrl, }) {
3635
+ const controller = new AbortController();
3636
+ const timeout = setTimeout(() => {
3637
+ controller.abort();
3638
+ }, UPDATE_CHECK_TIMEOUT_MS);
3639
+ try {
3640
+ const response = await fetch(resolveUpdateManifestUrl({ baseUrl }), {
3641
+ headers: {
3642
+ Accept: "application/json",
3643
+ "User-Agent": `blitzreels-cli/${CLI_VERSION}`,
3644
+ },
3645
+ signal: controller.signal,
3646
+ });
3647
+ const value = await readJsonResponse({
3648
+ response,
3649
+ url: resolveUpdateManifestUrl({ baseUrl }),
3650
+ action: "check CLI updates",
3651
+ });
3652
+ return parseUpdateManifest({ value });
3653
+ }
3654
+ finally {
3655
+ clearTimeout(timeout);
1824
3656
  }
1825
- throw new Error("Timed out waiting for browser approval.");
1826
3657
  }
1827
- async function uploadMediaFile({ context }) {
1828
- const filePath = resolve(requireStringFlag({ flags: context.args.flags, name: "file" }));
1829
- const fileStats = await stat(filePath);
1830
- if (!fileStats.isFile()) {
1831
- throw new Error(`Not a file: ${filePath}`);
3658
+ function resolveUpdateManifestUrl({ baseUrl, }) {
3659
+ if (process.env.BLITZREELS_UPDATE_MANIFEST_URL) {
3660
+ return process.env.BLITZREELS_UPDATE_MANIFEST_URL;
1832
3661
  }
1833
- const fileName = getStringFlag({ flags: context.args.flags, name: "name" }) ??
1834
- basename(filePath);
1835
- const contentType = getStringFlag({ flags: context.args.flags, name: "content-type" }) ??
1836
- inferContentType({ filePath });
1837
- const init = await context.client.media.initUpload({
1838
- fileName,
1839
- contentType,
1840
- });
1841
- const bytes = await readFile(filePath);
1842
- const uploadResponse = await fetch(init.upload_url, {
1843
- method: "PUT",
1844
- headers: { "Content-Type": contentType },
1845
- body: new Blob([new Uint8Array(bytes)], { type: contentType }),
1846
- });
1847
- if (!uploadResponse.ok) {
1848
- throw new Error(`Upload failed with ${uploadResponse.status}: ${await uploadResponse.text()}`);
3662
+ return `${resolveAppUrl({ baseUrl })}/api/cli/releases`;
3663
+ }
3664
+ async function readUpdateCache() {
3665
+ try {
3666
+ const raw = await readFile(UPDATE_CACHE_PATH, "utf8");
3667
+ const parsed = JSON.parse(raw);
3668
+ return parseUpdateCache({ value: parsed });
1849
3669
  }
1850
- return context.client.media.finalizeUpload({
1851
- storageKey: init.storage_key,
1852
- fileName,
1853
- contentType,
1854
- fileSizeBytes: fileStats.size,
1855
- autoProcessEnabled: getOptionalBooleanFlag({
1856
- flags: context.args.flags,
1857
- name: "auto-process",
1858
- }),
1859
- autoTranscribe: getOptionalBooleanFlag({
1860
- flags: context.args.flags,
1861
- name: "auto-transcribe",
1862
- }),
1863
- autoDescribeAfterTranscribe: getOptionalBooleanFlag({
1864
- flags: context.args.flags,
1865
- name: "auto-describe",
1866
- }),
1867
- autoSuggestShorts: getOptionalBooleanFlag({
1868
- flags: context.args.flags,
1869
- name: "auto-suggest-shorts",
1870
- }),
3670
+ catch {
3671
+ return undefined;
3672
+ }
3673
+ }
3674
+ async function writeUpdateCache({ manifest, }) {
3675
+ await mkdir(dirname(UPDATE_CACHE_PATH), { recursive: true });
3676
+ await writeFile(UPDATE_CACHE_PATH, JSON.stringify({ fetchedAt: Date.now(), manifest }, null, 2), { mode: 0o600 });
3677
+ }
3678
+ function parseUpdateCache({ value, }) {
3679
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3680
+ return undefined;
3681
+ }
3682
+ const record = value;
3683
+ if (typeof record.fetchedAt !== "number") {
3684
+ return undefined;
3685
+ }
3686
+ return {
3687
+ fetchedAt: record.fetchedAt,
3688
+ manifest: parseUpdateManifest({ value: record.manifest }),
3689
+ };
3690
+ }
3691
+ function parseUpdateManifest({ value }) {
3692
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3693
+ throw new Error("CLI release manifest returned an invalid response.");
3694
+ }
3695
+ const record = value;
3696
+ const upgrade = record.upgrade;
3697
+ if (!upgrade || typeof upgrade !== "object" || Array.isArray(upgrade)) {
3698
+ throw new Error("CLI release manifest is missing upgrade commands.");
3699
+ }
3700
+ const upgradeRecord = upgrade;
3701
+ const severity = record.severity === "warning" || record.severity === "critical"
3702
+ ? record.severity
3703
+ : "notice";
3704
+ if (typeof record.latest !== "string" ||
3705
+ typeof record.minRecommended !== "string" ||
3706
+ typeof record.minSupported !== "string" ||
3707
+ typeof record.releasedAt !== "string" ||
3708
+ !Array.isArray(record.changes) ||
3709
+ typeof upgradeRecord.npm !== "string" ||
3710
+ typeof upgradeRecord.pnpm !== "string" ||
3711
+ typeof upgradeRecord.npx !== "string" ||
3712
+ typeof record.url !== "string") {
3713
+ throw new Error("CLI release manifest returned an incomplete response.");
3714
+ }
3715
+ return {
3716
+ latest: record.latest,
3717
+ minRecommended: record.minRecommended,
3718
+ minSupported: record.minSupported,
3719
+ severity,
3720
+ releasedAt: record.releasedAt,
3721
+ changes: record.changes.filter((change) => typeof change === "string"),
3722
+ upgrade: {
3723
+ npm: upgradeRecord.npm,
3724
+ pnpm: upgradeRecord.pnpm,
3725
+ npx: upgradeRecord.npx,
3726
+ },
3727
+ url: record.url,
3728
+ };
3729
+ }
3730
+ function buildFallbackUpdateManifest() {
3731
+ return {
3732
+ latest: CLI_VERSION,
3733
+ minRecommended: CLI_VERSION,
3734
+ minSupported: CLI_VERSION,
3735
+ severity: "notice",
3736
+ releasedAt: "2026-05-08",
3737
+ changes: [],
3738
+ upgrade: {
3739
+ npm: "npm i -g blitzreels@latest",
3740
+ pnpm: "pnpm add -g blitzreels@latest",
3741
+ npx: "npx blitzreels@latest",
3742
+ },
3743
+ url: "https://www.blitzreels.com/changelog",
3744
+ };
3745
+ }
3746
+ function buildUpdateNotice({ update }) {
3747
+ return [
3748
+ "",
3749
+ `Update available: blitzreels ${update.current} -> ${update.manifest.latest}`,
3750
+ "",
3751
+ "New:",
3752
+ ...update.manifest.changes.slice(0, 3).map((change) => `- ${change}`),
3753
+ "",
3754
+ "Upgrade:",
3755
+ update.manifest.upgrade.npm,
3756
+ `Changelog: ${update.manifest.url}`,
3757
+ ];
3758
+ }
3759
+ function buildVersionLines({ update }) {
3760
+ const lines = [`blitzreels ${CLI_VERSION}`];
3761
+ if (update.updateAvailable) {
3762
+ lines.push(`Latest: ${update.manifest.latest}`);
3763
+ lines.push("Update available.");
3764
+ lines.push(`Upgrade: ${update.manifest.upgrade.npm}`);
3765
+ lines.push(`Changelog: ${update.manifest.url}`);
3766
+ }
3767
+ else {
3768
+ lines.push(`Latest: ${update.manifest.latest}`);
3769
+ lines.push("Up to date.");
3770
+ }
3771
+ return lines;
3772
+ }
3773
+ function buildDoctorLines({ context, update, }) {
3774
+ const supported = !isVersionLessThan({
3775
+ left: CLI_VERSION,
3776
+ right: update.manifest.minSupported,
1871
3777
  });
3778
+ const lines = [
3779
+ `CLI: ${CLI_VERSION}`,
3780
+ `Latest: ${update.manifest.latest}`,
3781
+ `Supported: ${supported ? "yes" : "no"}`,
3782
+ `API key: ${context.config.apiKey ? "configured" : "missing"}`,
3783
+ `Base URL: ${context.config.baseUrl ?? "https://www.blitzreels.com/api/v1"}`,
3784
+ `Key storage: ${context.config.keyStorage ?? "none"}`,
3785
+ ];
3786
+ if (update.updateAvailable) {
3787
+ lines.push(`Upgrade: ${update.manifest.upgrade.npm}`);
3788
+ lines.push(`Changelog: ${update.manifest.url}`);
3789
+ }
3790
+ return lines;
3791
+ }
3792
+ function isVersionLessThan({ left, right, }) {
3793
+ const leftParts = parseSemver({ value: left });
3794
+ const rightParts = parseSemver({ value: right });
3795
+ for (let index = 0; index < 3; index++) {
3796
+ const leftPart = leftParts[index] ?? 0;
3797
+ const rightPart = rightParts[index] ?? 0;
3798
+ if (leftPart < rightPart) {
3799
+ return true;
3800
+ }
3801
+ if (leftPart > rightPart) {
3802
+ return false;
3803
+ }
3804
+ }
3805
+ return false;
3806
+ }
3807
+ function parseSemver({ value }) {
3808
+ return value
3809
+ .replace(/^v/, "")
3810
+ .split(".", 3)
3811
+ .map((part) => Number.parseInt(part.replace(/\D.*$/, ""), 10))
3812
+ .map((part) => (Number.isFinite(part) ? part : 0));
1872
3813
  }
1873
3814
  async function readJsonResponse({ response, url, action, }) {
1874
3815
  const contentType = response.headers.get("content-type") ?? "";
@@ -1945,23 +3886,6 @@ function parseDeviceTokenResponse({ value, }) {
1945
3886
  key_prefix: typeof record.key_prefix === "string" ? record.key_prefix : undefined,
1946
3887
  };
1947
3888
  }
1948
- async function loadConfig() {
1949
- try {
1950
- const raw = await readFile(CONFIG_PATH, "utf8");
1951
- const value = JSON.parse(raw);
1952
- const keyStorage = value.keyStorage === "keychain" ? "keychain" : "config";
1953
- const keychainApiKey = keyStorage === "keychain" ? await readKeychainApiKey() : "";
1954
- return {
1955
- apiKey: keychainApiKey ||
1956
- (typeof value.apiKey === "string" ? value.apiKey : ""),
1957
- baseUrl: typeof value.baseUrl === "string" ? value.baseUrl : undefined,
1958
- keyStorage,
1959
- };
1960
- }
1961
- catch {
1962
- return { apiKey: "", baseUrl: undefined, keyStorage: undefined };
1963
- }
1964
- }
1965
3889
  function parseArgs({ argv }) {
1966
3890
  const command = [];
1967
3891
  const flags = {};
@@ -1995,39 +3919,81 @@ function validateParsedArgs({ parsed }) {
1995
3919
  if (!spec) {
1996
3920
  throw buildUnknownCommandError({ command: parsed.command });
1997
3921
  }
1998
- const allowedFlags = new Set([...GLOBAL_FLAGS, ...spec.flags]);
3922
+ const allowedFlags = new Set([
3923
+ ...GLOBAL_FLAGS,
3924
+ ...spec.flags.map((flag) => flag.name),
3925
+ ]);
1999
3926
  for (const flag of Object.keys(parsed.flags)) {
2000
3927
  if (!allowedFlags.has(flag)) {
2001
3928
  throw buildUnknownFlagError({ spec, flag });
2002
3929
  }
2003
3930
  }
3931
+ if (getBooleanFlag({ flags: parsed.flags, name: "dry-run" }) &&
3932
+ !spec.dryRunSupported) {
3933
+ throw new CliUsageError({
3934
+ code: "unsupported_dry_run",
3935
+ message: "--dry-run is not supported for this command.",
3936
+ usage: spec.usage,
3937
+ });
3938
+ }
2004
3939
  }
2005
3940
  function findCommandSpec({ command, }) {
2006
3941
  return COMMAND_SPECS.find((spec) => commandsEqual(spec.command, command));
2007
3942
  }
2008
- function commandsEqual(left, right) {
2009
- return (left.length === right.length &&
2010
- left.every((part, index) => part === right[index]));
2011
- }
2012
- function commandLabel({ command }) {
2013
- return command.join(" ");
2014
- }
2015
- function printCommandHelpOrThrow({ command }) {
3943
+ function printCommandOrGroupHelpOrThrow({ command, }) {
2016
3944
  const spec = findCommandSpec({ command });
2017
- if (!spec) {
2018
- throw buildUnknownCommandError({ command });
3945
+ if (spec) {
3946
+ printCommandHelp({ spec });
3947
+ return;
3948
+ }
3949
+ const groupSpecs = findGroupCommandSpecs({ command });
3950
+ if (groupSpecs.length > 0) {
3951
+ printGroupHelp({ command, specs: groupSpecs });
3952
+ return;
2019
3953
  }
2020
- printCommandHelp({ spec });
3954
+ throw buildUnknownCommandError({ command });
2021
3955
  }
2022
3956
  function printCommandHelp({ spec }) {
2023
- const flags = [...new Set([...spec.flags, ...GLOBAL_FLAGS])].sort();
3957
+ const flags = flagsForCommand({ spec });
3958
+ const agentHelp = isAgentHelpEnvironment()
3959
+ ? `\nAgent instructions:\n blitzreels prime --command ${JSON.stringify(commandLabel({ command: spec.command }))}\n blitzreels agent-context --command ${JSON.stringify(commandLabel({ command: spec.command }))} --json\n`
3960
+ : "";
2024
3961
  console.log(`Usage:
2025
3962
  ${spec.usage}
2026
3963
 
3964
+ Description:
3965
+ ${spec.description}
3966
+
2027
3967
  Flags:
2028
- ${flags.map((flag) => ` --${flag}`).join("\n")}
3968
+ ${flags
3969
+ .map((flag) => {
3970
+ const required = flag.required ? " required" : "";
3971
+ const values = flag.enum ? ` (${flag.enum.join("|")})` : "";
3972
+ const defaultValue = flag.default === undefined ? "" : ` default=${String(flag.default)}`;
3973
+ return ` --${flag.name} ${flag.type}${required}${values}${defaultValue} ${flag.description}`;
3974
+ })
3975
+ .join("\n")}
3976
+
3977
+ Examples:
3978
+ ${spec.examples.map((example) => ` ${example}`).join("\n")}
3979
+ ${agentHelp}
2029
3980
  `);
2030
3981
  }
3982
+ function isAgentHelpEnvironment() {
3983
+ const truthyEnv = [
3984
+ "BLITZREELS_AGENT_HELP",
3985
+ "CODEX_SHELL",
3986
+ "CLAUDECODE",
3987
+ "CLAUDE_CODE",
3988
+ "CURSOR_AGENT",
3989
+ "OPENCODE",
3990
+ "AIDER",
3991
+ ];
3992
+ return truthyEnv.some((name) => {
3993
+ const value = process.env[name];
3994
+ return value !== undefined && value !== "" && value !== "0";
3995
+ });
3996
+ }
2031
3997
  function buildUnknownCommandError({ command, }) {
2032
3998
  const suggestions = suggestCommands({ command }).map((spec) => spec.usage);
2033
3999
  return new CliUsageError({
@@ -2037,8 +4003,38 @@ function buildUnknownCommandError({ command, }) {
2037
4003
  usage: suggestions[0],
2038
4004
  });
2039
4005
  }
4006
+ function findGroupCommandSpecs({ command, }) {
4007
+ if (command.length === 0) {
4008
+ return [];
4009
+ }
4010
+ return COMMAND_SPECS.filter((spec) => commandsEqual(spec.command.slice(0, command.length), command));
4011
+ }
4012
+ function printGroupHelp({ command, specs, }) {
4013
+ const label = commandLabel({ command });
4014
+ const agentHelp = isAgentHelpEnvironment()
4015
+ ? `\nAgent instructions:\n blitzreels prime --command ${JSON.stringify(label)}\n blitzreels agent-context --command ${JSON.stringify(label)} --json\n`
4016
+ : "";
4017
+ console.log(`blitzreels ${label}
4018
+
4019
+ Available commands:
4020
+ ${specs.map((spec) => ` ${spec.usage}`).join("\n")}
4021
+
4022
+ Examples:
4023
+ ${specs
4024
+ .slice(0, 4)
4025
+ .flatMap((spec) => spec.examples.slice(0, 1))
4026
+ .map((example) => ` ${example}`)
4027
+ .join("\n")}
4028
+ ${agentHelp}
4029
+ `);
4030
+ }
2040
4031
  function buildUnknownFlagError({ spec, flag, }) {
2041
- const flags = [...new Set([...spec.flags, ...GLOBAL_FLAGS])];
4032
+ const flags = [
4033
+ ...new Set([
4034
+ ...spec.flags.map((candidate) => candidate.name),
4035
+ ...GLOBAL_FLAGS,
4036
+ ]),
4037
+ ];
2042
4038
  const suggestions = suggestValues({ value: flag, candidates: flags }).map((candidate) => `--${candidate}`);
2043
4039
  return new CliUsageError({
2044
4040
  code: "unknown_flag",
@@ -2048,55 +4044,7 @@ function buildUnknownFlagError({ spec, flag, }) {
2048
4044
  });
2049
4045
  }
2050
4046
  function suggestCommands({ command }) {
2051
- const joined = commandLabel({ command });
2052
- const prefixMatches = COMMAND_SPECS.filter((spec) => commandsEqual(spec.command.slice(0, command.length), command));
2053
- if (prefixMatches.length > 0) {
2054
- return prefixMatches.slice(0, 3);
2055
- }
2056
- return COMMAND_SPECS.map((spec) => ({
2057
- spec,
2058
- score: commandSimilarity({
2059
- left: joined,
2060
- right: commandLabel({ command: spec.command }),
2061
- }),
2062
- }))
2063
- .filter((item) => item.score >= 0.45)
2064
- .sort((left, right) => right.score - left.score)
2065
- .slice(0, 3)
2066
- .map((item) => item.spec);
2067
- }
2068
- function suggestValues({ value, candidates, }) {
2069
- return candidates
2070
- .map((candidate) => ({
2071
- candidate,
2072
- score: commandSimilarity({ left: value, right: candidate }),
2073
- }))
2074
- .filter((item) => item.score >= 0.45)
2075
- .sort((left, right) => right.score - left.score)
2076
- .slice(0, 3)
2077
- .map((item) => item.candidate);
2078
- }
2079
- function commandSimilarity({ left, right, }) {
2080
- const maxLength = Math.max(left.length, right.length);
2081
- if (maxLength === 0) {
2082
- return 1;
2083
- }
2084
- return 1 - levenshteinDistance({ left, right }) / maxLength;
2085
- }
2086
- function levenshteinDistance({ left, right, }) {
2087
- const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
2088
- const current = Array.from({ length: right.length + 1 }, () => 0);
2089
- for (let leftIndex = 1; leftIndex <= left.length; leftIndex++) {
2090
- current[0] = leftIndex;
2091
- for (let rightIndex = 1; rightIndex <= right.length; rightIndex++) {
2092
- const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
2093
- current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (previous[rightIndex] ?? 0) + 1, (previous[rightIndex - 1] ?? 0) + substitutionCost);
2094
- }
2095
- for (let index = 0; index < previous.length; index++) {
2096
- previous[index] = current[index] ?? 0;
2097
- }
2098
- }
2099
- return previous[right.length] ?? 0;
4047
+ return suggestCommandSpecs({ command, specs: COMMAND_SPECS });
2100
4048
  }
2101
4049
  function usageForCurrentCommand() {
2102
4050
  const parsed = parseArgs({ argv: process.argv.slice(2) });
@@ -2222,16 +4170,92 @@ function parseStringRecordFlag({ flags, name, }) {
2222
4170
  }
2223
4171
  return value;
2224
4172
  }
2225
- function requireConfirmPreview({ flags }) {
2226
- if (!getBooleanFlag({ flags, name: "confirm-preview" })) {
2227
- throw new Error("Destructive edits require --confirm-preview after reviewing a plan.");
4173
+ function getTimelineStartFlag({ flags }) {
4174
+ const value = getStringFlag({ flags, name: "at" });
4175
+ return value === undefined ? undefined : parseTimecode({ value });
4176
+ }
4177
+ function parseEffectEasing({ value, }) {
4178
+ if (value === undefined) {
4179
+ return undefined;
2228
4180
  }
2229
- return true;
4181
+ if (value === "linear" ||
4182
+ value === "easeIn" ||
4183
+ value === "easeOut" ||
4184
+ value === "easeInOut") {
4185
+ return value;
4186
+ }
4187
+ throw new Error("--easing must be linear, easeIn, easeOut, or easeInOut.");
4188
+ }
4189
+ function parseZoomStyle({ value, }) {
4190
+ if (value === undefined) {
4191
+ return undefined;
4192
+ }
4193
+ if (value === "transition" || value === "hard") {
4194
+ return value;
4195
+ }
4196
+ throw new Error("--style must be transition or hard.");
4197
+ }
4198
+ function parseZoomPhase({ value, }) {
4199
+ if (value === undefined) {
4200
+ return undefined;
4201
+ }
4202
+ if (value === "in" ||
4203
+ value === "out" ||
4204
+ value === "both" ||
4205
+ value === "none") {
4206
+ return value;
4207
+ }
4208
+ throw new Error("--phase must be in, out, both, or none.");
2230
4209
  }
2231
- function requireConfirmDelete({ flags }) {
2232
- if (!getBooleanFlag({ flags, name: "confirm-delete" })) {
2233
- throw new Error("Delete commands require --confirm-delete.");
4210
+ function maybePoint({ x, y, }) {
4211
+ if (x === undefined && y === undefined) {
4212
+ return undefined;
2234
4213
  }
4214
+ return { x: x ?? 0, y: y ?? 0 };
4215
+ }
4216
+ function buildZoomEffectSettings({ flags, durationSeconds, }) {
4217
+ const fromJson = parseJsonObjectFlag({ flags, name: "settings-json" });
4218
+ const scaleStart = getNumberFlag({ flags, name: "scale-start" });
4219
+ const scaleEnd = getNumberFlag({ flags, name: "scale-end" });
4220
+ const translateStart = maybePoint({
4221
+ x: getNumberFlag({ flags, name: "translate-start-x" }),
4222
+ y: getNumberFlag({ flags, name: "translate-start-y" }),
4223
+ });
4224
+ const translateEnd = maybePoint({
4225
+ x: getNumberFlag({ flags, name: "translate-end-x" }),
4226
+ y: getNumberFlag({ flags, name: "translate-end-y" }),
4227
+ });
4228
+ const anchor = maybePoint({
4229
+ x: getNumberFlag({ flags, name: "anchor-x" }),
4230
+ y: getNumberFlag({ flags, name: "anchor-y" }),
4231
+ });
4232
+ return {
4233
+ ...(fromJson ?? {}),
4234
+ scale: scaleStart === undefined && scaleEnd === undefined
4235
+ ? fromJson?.scale
4236
+ : {
4237
+ start: scaleStart ?? 1,
4238
+ end: scaleEnd ?? scaleStart ?? 1,
4239
+ },
4240
+ translate: translateStart === undefined && translateEnd === undefined
4241
+ ? fromJson?.translate
4242
+ : {
4243
+ start: translateStart ?? { x: 0, y: 0 },
4244
+ end: translateEnd ?? translateStart ?? { x: 0, y: 0 },
4245
+ },
4246
+ anchor: anchor ?? fromJson?.anchor,
4247
+ style: parseZoomStyle({ value: getStringFlag({ flags, name: "style" }) }) ??
4248
+ fromJson?.style,
4249
+ phase: parseZoomPhase({ value: getStringFlag({ flags, name: "phase" }) }) ??
4250
+ fromJson?.phase,
4251
+ inDurationSeconds: getNumberFlag({ flags, name: "in-duration" }) ??
4252
+ durationSeconds ??
4253
+ fromJson?.inDurationSeconds,
4254
+ outDurationSeconds: parseNullableNumberFlag({ flags, name: "out-duration" }) ??
4255
+ fromJson?.outDurationSeconds,
4256
+ outScale: parseNullableNumberFlag({ flags, name: "out-scale" }) ??
4257
+ fromJson?.outScale,
4258
+ };
2235
4259
  }
2236
4260
  function parseTimecode({ value }) {
2237
4261
  const trimmed = value.trim();
@@ -2267,75 +4291,11 @@ function openBrowser({ url }) {
2267
4291
  });
2268
4292
  child.unref();
2269
4293
  }
2270
- function canUseKeychain() {
2271
- return (process.platform === "darwin" &&
2272
- process.env.BLITZREELS_DISABLE_KEYCHAIN !== "1");
2273
- }
2274
- function runSecurity({ args, }) {
2275
- return new Promise((resolveCommand) => {
2276
- const child = spawn("security", args, {
2277
- stdio: ["ignore", "pipe", "ignore"],
2278
- });
2279
- const chunks = [];
2280
- child.stdout.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
2281
- child.on("error", () => resolveCommand({ stdout: "", ok: false }));
2282
- child.on("close", (code) => resolveCommand({
2283
- stdout: Buffer.concat(chunks).toString("utf8").trim(),
2284
- ok: code === 0,
2285
- }));
2286
- });
2287
- }
2288
4294
  function sleep({ ms }) {
2289
4295
  return new Promise((resolveSleep) => {
2290
4296
  setTimeout(resolveSleep, ms);
2291
4297
  });
2292
4298
  }
2293
- async function saveApiKey({ apiKey }) {
2294
- if (!canUseKeychain()) {
2295
- return false;
2296
- }
2297
- const result = await runSecurity({
2298
- args: [
2299
- "add-generic-password",
2300
- "-a",
2301
- KEYCHAIN_ACCOUNT,
2302
- "-s",
2303
- KEYCHAIN_SERVICE,
2304
- "-w",
2305
- apiKey,
2306
- "-U",
2307
- ],
2308
- });
2309
- return result.ok;
2310
- }
2311
- async function readKeychainApiKey() {
2312
- if (!canUseKeychain()) {
2313
- return "";
2314
- }
2315
- const result = await runSecurity({
2316
- args: [
2317
- "find-generic-password",
2318
- "-a",
2319
- KEYCHAIN_ACCOUNT,
2320
- "-s",
2321
- KEYCHAIN_SERVICE,
2322
- "-w",
2323
- ],
2324
- });
2325
- return result.ok ? result.stdout : "";
2326
- }
2327
- async function persistLoggedInApiKey({ apiKey, baseUrl, }) {
2328
- const savedToKeychain = await saveApiKey({ apiKey });
2329
- const config = {
2330
- apiKey: savedToKeychain ? "" : apiKey,
2331
- baseUrl,
2332
- keyStorage: savedToKeychain ? "keychain" : "config",
2333
- };
2334
- await mkdir(dirname(CONFIG_PATH), { recursive: true });
2335
- await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), {
2336
- mode: 0o600,
2337
- });
2338
- }
2339
4299
  function parseImageFormat({ value, }) {
2340
4300
  if (value === undefined) {
2341
4301
  return undefined;
@@ -2388,6 +4348,110 @@ function parseAssetType({ value, }) {
2388
4348
  }
2389
4349
  throw new Error("--asset-type must be video, audio, image, document, or all.");
2390
4350
  }
4351
+ function parseAudioLibraryProviderFilter({ value, }) {
4352
+ if (value === undefined) {
4353
+ return undefined;
4354
+ }
4355
+ if (value === "all" ||
4356
+ value === "openverse" ||
4357
+ value === "freesound" ||
4358
+ value === "wikimedia") {
4359
+ return value;
4360
+ }
4361
+ throw new Error("--provider must be all, openverse, freesound, or wikimedia.");
4362
+ }
4363
+ function parseAudioLibraryProvider({ value, }) {
4364
+ const parsed = parseAudioLibraryProviderFilter({ value });
4365
+ if (parsed === "openverse" ||
4366
+ parsed === "freesound" ||
4367
+ parsed === "wikimedia") {
4368
+ return parsed;
4369
+ }
4370
+ throw new Error("--provider must be openverse, freesound, or wikimedia.");
4371
+ }
4372
+ function parseAudioLibraryLicense({ value, }) {
4373
+ if (value === undefined) {
4374
+ return undefined;
4375
+ }
4376
+ if (value === "cc0" ||
4377
+ value === "public_domain" ||
4378
+ value === "attribution" ||
4379
+ value === "all") {
4380
+ return value;
4381
+ }
4382
+ throw new Error("--license must be cc0, public_domain, attribution, or all.");
4383
+ }
4384
+ function parseAudioLibraryType({ value, }) {
4385
+ if (value === undefined) {
4386
+ return undefined;
4387
+ }
4388
+ if (value === "music" ||
4389
+ value === "sound_effect" ||
4390
+ value === "ambient" ||
4391
+ value === "other") {
4392
+ return value;
4393
+ }
4394
+ throw new Error("--audio-type must be music, sound_effect, ambient, or other.");
4395
+ }
4396
+ function parseStockProvider({ value, }) {
4397
+ if (value === undefined) {
4398
+ return null;
4399
+ }
4400
+ if (value === "pexels" || value === "freesound") {
4401
+ return value;
4402
+ }
4403
+ throw new Error("--provider must be pexels or freesound.");
4404
+ }
4405
+ function parseRequiredStockProvider({ value, }) {
4406
+ const parsed = parseStockProvider({ value });
4407
+ if (parsed !== null) {
4408
+ return parsed;
4409
+ }
4410
+ throw new Error("--provider must be pexels or freesound.");
4411
+ }
4412
+ function parseStockAssetType({ value, }) {
4413
+ if (value === "image" || value === "video" || value === "audio") {
4414
+ return value;
4415
+ }
4416
+ throw new Error("--asset-type must be image, video, or audio.");
4417
+ }
4418
+ function parseNullableStockAssetType({ value, }) {
4419
+ return value === undefined ? null : parseStockAssetType({ value });
4420
+ }
4421
+ function parseStockOrientation({ value, }) {
4422
+ if (value === undefined) {
4423
+ return null;
4424
+ }
4425
+ if (value === "landscape" || value === "portrait" || value === "square") {
4426
+ return value;
4427
+ }
4428
+ throw new Error("--orientation must be landscape, portrait, or square.");
4429
+ }
4430
+ function parseStockAudioType({ value, }) {
4431
+ if (value === undefined) {
4432
+ return undefined;
4433
+ }
4434
+ if (value === "sound_effect" ||
4435
+ value === "ambient" ||
4436
+ value === "background_music" ||
4437
+ value === "other") {
4438
+ return value;
4439
+ }
4440
+ throw new Error("--audio-type must be sound_effect, ambient, background_music, or other.");
4441
+ }
4442
+ function parseGlobalBrollAudioType({ value, }) {
4443
+ if (value === undefined) {
4444
+ return null;
4445
+ }
4446
+ if (value === "music" ||
4447
+ value === "sound_effect" ||
4448
+ value === "ambient" ||
4449
+ value === "background_music" ||
4450
+ value === "other") {
4451
+ return value;
4452
+ }
4453
+ throw new Error("--audio-type must be music, sound_effect, ambient, background_music, or other.");
4454
+ }
2391
4455
  function parseYoutubeQuality({ value, }) {
2392
4456
  if (value === undefined) {
2393
4457
  return undefined;
@@ -2409,10 +4473,13 @@ function parseMediaPositionPreset({ value, }) {
2409
4473
  value === "top-right" ||
2410
4474
  value === "bottom-left" ||
2411
4475
  value === "bottom-right" ||
2412
- value === "full-screen") {
4476
+ value === "fullscreen") {
2413
4477
  return value;
2414
4478
  }
2415
- throw new Error("--position-preset must be center, top-left, top-right, bottom-left, bottom-right, or full-screen.");
4479
+ if (value === "full-screen") {
4480
+ return "fullscreen";
4481
+ }
4482
+ throw new Error("--position-preset must be center, top-left, top-right, bottom-left, bottom-right, fullscreen, or full-screen.");
2416
4483
  }
2417
4484
  function parseMediaAnimationPreset({ value, }) {
2418
4485
  if (value === undefined) {
@@ -2422,13 +4489,32 @@ function parseMediaAnimationPreset({ value, }) {
2422
4489
  value === "fadeIn" ||
2423
4490
  value === "fadeOut" ||
2424
4491
  value === "zoomIn" ||
4492
+ value === "zoomOut" ||
2425
4493
  value === "slideIn" ||
4494
+ value === "slideLeft" ||
4495
+ value === "slideRight" ||
4496
+ value === "slideUp" ||
4497
+ value === "slideDown" ||
2426
4498
  value === "popIn" ||
2427
4499
  value === "bounce" ||
2428
4500
  value === "spin") {
2429
4501
  return value;
2430
4502
  }
2431
- throw new Error("--animation-preset must be none, fadeIn, fadeOut, zoomIn, slideIn, popIn, bounce, or spin.");
4503
+ throw new Error("--animation-preset must be none, fadeIn, fadeOut, zoomIn, zoomOut, slideIn, slideLeft, slideRight, slideUp, slideDown, popIn, bounce, or spin.");
4504
+ }
4505
+ function toApiMediaPositionPreset({ value, }) {
4506
+ if (value === "top-left")
4507
+ return "pip-top-left";
4508
+ if (value === "top-right")
4509
+ return "pip-top-right";
4510
+ if (value === "bottom-left")
4511
+ return "pip-bottom-left";
4512
+ if (value === "bottom-right")
4513
+ return "pip-bottom-right";
4514
+ return value;
4515
+ }
4516
+ function toApiMediaAnimationPreset({ value, }) {
4517
+ return value === "slideIn" ? "slideUp" : value;
2432
4518
  }
2433
4519
  function parseCaptionMatchPattern({ value, }) {
2434
4520
  if (value === undefined) {
@@ -2782,6 +4868,17 @@ function buildCaptionWordSelector({ flags }) {
2782
4868
  value: getStringFlag({ flags, name: "match-pattern" }),
2783
4869
  }),
2784
4870
  timelineItemId: getStringFlag({ flags, name: "timeline-item-id" }),
4871
+ all: getOptionalBooleanFlag({ flags, name: "all" }),
4872
+ fromSeconds: getStringFlag({ flags, name: "from" }) === undefined
4873
+ ? undefined
4874
+ : parseTimecode({
4875
+ value: requireStringFlag({ flags, name: "from" }),
4876
+ }),
4877
+ toSeconds: getStringFlag({ flags, name: "to" }) === undefined
4878
+ ? undefined
4879
+ : parseTimecode({
4880
+ value: requireStringFlag({ flags, name: "to" }),
4881
+ }),
2785
4882
  };
2786
4883
  }
2787
4884
  function buildCaptionWordStyleFromFlags({ flags, }) {
@@ -2876,6 +4973,58 @@ function buildCaptionWordStyleFromFlags({ flags, }) {
2876
4973
  }
2877
4974
  return style;
2878
4975
  }
4976
+ function buildCaptionThemeSettingsPatch({ flags, }) {
4977
+ const settingsJson = parseJsonObjectFlag({ flags, name: "settings-json" });
4978
+ if (settingsJson !== undefined) {
4979
+ return settingsJson;
4980
+ }
4981
+ const settings = {};
4982
+ setIfDefined({
4983
+ target: settings,
4984
+ key: "textShadow",
4985
+ value: getStringFlag({ flags, name: "text-shadow" }),
4986
+ });
4987
+ setIfDefined({
4988
+ target: settings,
4989
+ key: "shadowMode",
4990
+ value: parseCaptionShadowMode({
4991
+ value: getStringFlag({ flags, name: "shadow-mode" }),
4992
+ }),
4993
+ });
4994
+ setIfDefined({
4995
+ target: settings,
4996
+ key: "textStrokeEnabled",
4997
+ value: getOptionalBooleanFlag({ flags, name: "text-stroke-enabled" }),
4998
+ });
4999
+ setIfDefined({
5000
+ target: settings,
5001
+ key: "textStrokeColor",
5002
+ value: getStringFlag({ flags, name: "text-stroke-color" }),
5003
+ });
5004
+ setIfDefined({
5005
+ target: settings,
5006
+ key: "textStrokeWidthPx",
5007
+ value: getNumberFlag({ flags, name: "text-stroke-width-px" }),
5008
+ });
5009
+ setIfDefined({
5010
+ target: settings,
5011
+ key: "textStrokeBlurPx",
5012
+ value: getNumberFlag({ flags, name: "text-stroke-blur-px" }),
5013
+ });
5014
+ return Object.keys(settings).length === 0 ? undefined : settings;
5015
+ }
5016
+ function parseCaptionShadowMode({ value, }) {
5017
+ if (value === undefined) {
5018
+ return undefined;
5019
+ }
5020
+ if (value === "null") {
5021
+ return null;
5022
+ }
5023
+ if (value === "text-shadow" || value === "drop-shadow") {
5024
+ return value;
5025
+ }
5026
+ throw new Error("--shadow-mode must be text-shadow, drop-shadow, or null.");
5027
+ }
2879
5028
  function setIfDefined({ target, key, value, }) {
2880
5029
  if (value !== undefined) {
2881
5030
  target[key] = value;
@@ -2896,14 +5045,28 @@ function requireApiKey({ apiKey }) {
2896
5045
  throw new Error("Missing API key. Run `blitzreels auth login`, `blitzreels auth set-key --api-key ...`, or set BLITZREELS_API_KEY.");
2897
5046
  }
2898
5047
  }
2899
- function printResult({ json, value, human, }) {
5048
+ function printResult({ json, value, human, fields, }) {
2900
5049
  if (json) {
2901
- console.log(JSON.stringify(value, null, 2));
5050
+ const selectedFields = fields ??
5051
+ parseCsvFlag({
5052
+ flags: parseArgs({ argv: process.argv.slice(2) }).flags,
5053
+ name: "fields",
5054
+ });
5055
+ console.log(JSON.stringify(selectedFields && selectedFields.length > 0
5056
+ ? applyFieldSelection({ value, fields: selectedFields })
5057
+ : value, null, 2));
2902
5058
  return;
2903
5059
  }
2904
5060
  console.log(human.join("\n"));
2905
5061
  }
2906
5062
  function printHelp() {
5063
+ const agentHelp = isAgentHelpEnvironment()
5064
+ ? `
5065
+ Agent instructions:
5066
+ blitzreels prime
5067
+ blitzreels agent-context --json
5068
+ `
5069
+ : "";
2907
5070
  console.log(`blitzreels
2908
5071
 
2909
5072
  Usage:
@@ -2913,6 +5076,7 @@ Global flags:
2913
5076
  --api-key KEY
2914
5077
  --base-url URL
2915
5078
  --json
5079
+ ${agentHelp}
2916
5080
  `);
2917
5081
  }
2918
5082
  main().catch((error) => {