blitzreels 0.1.8 → 0.1.10

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.
Files changed (54) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +55 -3
  3. package/dist/cli-errors.d.ts.map +1 -1
  4. package/dist/cli-runtime.d.ts +1 -1
  5. package/dist/cli-runtime.d.ts.map +1 -1
  6. package/dist/cli-runtime.js +14 -1
  7. package/dist/cli-runtime.js.map +1 -1
  8. package/dist/command-metadata.d.ts.map +1 -1
  9. package/dist/command-registry.d.ts.map +1 -1
  10. package/dist/command-registry.js +2 -0
  11. package/dist/command-registry.js.map +1 -1
  12. package/dist/commands/auth-runtime.js +1 -1
  13. package/dist/commands/auth-runtime.js.map +1 -1
  14. package/dist/commands/caption-dry-run.d.ts.map +1 -1
  15. package/dist/commands/caption-dry-run.js +16 -0
  16. package/dist/commands/caption-dry-run.js.map +1 -1
  17. package/dist/commands/captions-specs.d.ts.map +1 -1
  18. package/dist/commands/captions-specs.js +8 -0
  19. package/dist/commands/captions-specs.js.map +1 -1
  20. package/dist/commands/captions.js +16 -0
  21. package/dist/commands/captions.js.map +1 -1
  22. package/dist/commands/clips.d.ts +3 -0
  23. package/dist/commands/clips.d.ts.map +1 -1
  24. package/dist/commands/clips.js +743 -9
  25. package/dist/commands/clips.js.map +1 -1
  26. package/dist/commands/editing-workflows.d.ts.map +1 -1
  27. package/dist/commands/editing-workflows.js +24 -6
  28. package/dist/commands/editing-workflows.js.map +1 -1
  29. package/dist/commands/media-assets.d.ts +11 -7
  30. package/dist/commands/media-assets.d.ts.map +1 -1
  31. package/dist/commands/media-assets.js +269 -4
  32. package/dist/commands/media-assets.js.map +1 -1
  33. package/dist/commands/media-runtime.d.ts +8 -5
  34. package/dist/commands/media-runtime.d.ts.map +1 -1
  35. package/dist/commands/media-runtime.js +209 -18
  36. package/dist/commands/media-runtime.js.map +1 -1
  37. package/dist/commands/projects.js +3 -0
  38. package/dist/commands/projects.js.map +1 -1
  39. package/dist/commands/recording-sets.d.ts +44 -0
  40. package/dist/commands/recording-sets.d.ts.map +1 -0
  41. package/dist/commands/recording-sets.js +482 -0
  42. package/dist/commands/recording-sets.js.map +1 -0
  43. package/dist/commands/system.js +1 -1
  44. package/dist/commands/system.js.map +1 -1
  45. package/dist/commands/text-overlay-spec.js +62 -2
  46. package/dist/commands/text-overlay-spec.js.map +1 -1
  47. package/dist/commands/timeline-runtime.d.ts.map +1 -1
  48. package/dist/index.js +14 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/preview-first-edit.d.ts.map +1 -1
  51. package/dist/update-notifier.d.ts.map +1 -1
  52. package/dist/update-notifier.js +7 -3
  53. package/dist/update-notifier.js.map +1 -1
  54. package/package.json +2 -2
@@ -1,7 +1,71 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
+ import { CliOperationError, CliUsageError } from "../cli-errors.js";
3
4
  import { commandSpec } from "../command-metadata.js";
4
5
  export const CLIPS_COMMAND_SPECS = [
6
+ commandSpec({
7
+ command: ["clip-share-pages", "get"],
8
+ usage: "blitzreels clip-share-pages get --asset-id ID [--json]",
9
+ flags: ["asset-id"],
10
+ description: "Inspect the public clip share page and its hidden clip ids.",
11
+ }),
12
+ commandSpec({
13
+ command: ["clip-share-pages", "publish"],
14
+ usage: "blitzreels clip-share-pages publish --asset-id ID --idempotency-key KEY [--allow-downloads true|false] [--dry-run] [--json]",
15
+ flags: [
16
+ "asset-id",
17
+ { name: "allow-downloads", type: "boolean" },
18
+ { name: "idempotency-key", required: true },
19
+ ],
20
+ description: "Publish the clip share page for a source asset.",
21
+ mutates: true,
22
+ dryRunSupported: true,
23
+ }),
24
+ commandSpec({
25
+ command: ["clip-share-pages", "unpublish"],
26
+ usage: "blitzreels clip-share-pages unpublish --asset-id ID --idempotency-key KEY [--dry-run] [--json]",
27
+ flags: ["asset-id", { name: "idempotency-key", required: true }],
28
+ description: "Disable the public clip share page for a source asset.",
29
+ mutates: true,
30
+ dryRunSupported: true,
31
+ }),
32
+ commandSpec({
33
+ command: ["clip-share-pages", "set-clip-visibility"],
34
+ usage: "blitzreels clip-share-pages set-clip-visibility --asset-id ID --clip-id ID --visibility visible|hidden --idempotency-key KEY [--dry-run] [--json]",
35
+ flags: [
36
+ "asset-id",
37
+ "clip-id",
38
+ { name: "visibility", enum: ["visible", "hidden"], required: true },
39
+ { name: "idempotency-key", required: true },
40
+ ],
41
+ description: "Show or hide one clip on its source asset share page.",
42
+ mutates: true,
43
+ dryRunSupported: true,
44
+ }),
45
+ commandSpec({
46
+ command: ["clip-batches", "get"],
47
+ usage: "blitzreels clip-batches get --batch-id ID [--json]",
48
+ flags: ["batch-id"],
49
+ description: "Inspect clip batch progress, Studio Sound status, and terminal errors.",
50
+ }),
51
+ commandSpec({
52
+ command: ["clip-batches", "wait"],
53
+ usage: "blitzreels clip-batches wait --batch-id ID [--interval-ms 3000] [--timeout-ms 1200000] [--json]",
54
+ flags: ["batch-id", "interval-ms", "timeout-ms"],
55
+ description: "Wait for a clip batch to complete or fail, including asynchronous Studio Sound.",
56
+ }),
57
+ commandSpec({
58
+ command: ["clip-batches", "recover"],
59
+ usage: "blitzreels clip-batches recover --batch-id ID [--target-count 10] --idempotency-key KEY [--dry-run] [--json]",
60
+ flags: [
61
+ "batch-id",
62
+ { name: "target-count", type: "number" },
63
+ { name: "idempotency-key", required: true },
64
+ ],
65
+ description: "Preserve accepted clips and refill a terminal batch in its existing project folder.",
66
+ mutates: true,
67
+ dryRunSupported: true,
68
+ }),
5
69
  commandSpec({
6
70
  command: ["clip-batches", "delete"],
7
71
  usage: "blitzreels clip-batches delete --batch-id ID --retention retain_projects_and_exports|delete_projects_and_exports --idempotency-key KEY --confirm-delete [--dry-run] [--json]",
@@ -42,14 +106,46 @@ export const CLIPS_COMMAND_SPECS = [
42
106
  "failed",
43
107
  ],
44
108
  },
45
- "limit",
109
+ {
110
+ name: "limit",
111
+ type: "number",
112
+ description: "Maximum 50 clips per page.",
113
+ },
46
114
  "offset",
47
115
  ],
48
116
  description: "List managed clip runs with bounded output.",
49
117
  }),
118
+ commandSpec({
119
+ command: ["clips", "attach-export"],
120
+ usage: "blitzreels clips attach-export --asset-id ID --project-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY [--dry-run] [--json]",
121
+ flags: [
122
+ "asset-id",
123
+ "project-id",
124
+ "export-id",
125
+ { name: "confirm-visual-qa", required: true },
126
+ { name: "idempotency-key", required: true },
127
+ ],
128
+ description: "Attach one visually accepted completed project export to managed clips.",
129
+ mutates: true,
130
+ destructive: true,
131
+ dryRunSupported: true,
132
+ }),
133
+ commandSpec({
134
+ command: ["clips", "promote-export"],
135
+ usage: "blitzreels clips promote-export --clip-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY [--dry-run] [--json]",
136
+ flags: [
137
+ "clip-id",
138
+ "export-id",
139
+ { name: "confirm-visual-qa", required: true },
140
+ { name: "idempotency-key", required: true },
141
+ ],
142
+ description: "Promote one visually accepted completed export as the managed clip delivery.",
143
+ mutates: true,
144
+ dryRunSupported: true,
145
+ }),
50
146
  commandSpec({
51
147
  command: ["clips", "create"],
52
- usage: "blitzreels clips create [--asset-id ID|--youtube-url URL] [--output ./clips] [--count 5] [--idempotency-key KEY]",
148
+ usage: "blitzreels clips create [--asset-id ID|--youtube-url URL] [--output ./clips] [--count 5] [--studio-sound] [--layout-mode people_first] [--content-type-hint podcast] [--idempotency-key KEY]",
53
149
  flags: [
54
150
  { name: "asset-id", enum: [] },
55
151
  "youtube-url",
@@ -62,7 +158,7 @@ export const CLIPS_COMMAND_SPECS = [
62
158
  name: "count",
63
159
  type: "number",
64
160
  default: 5,
65
- description: "Maximum rendered clips to download from the batch.",
161
+ description: "Maximum clips created by the batch.",
66
162
  },
67
163
  {
68
164
  name: "preset",
@@ -79,6 +175,28 @@ export const CLIPS_COMMAND_SPECS = [
79
175
  hidden: false,
80
176
  },
81
177
  { name: "caption-theme-id" },
178
+ {
179
+ name: "studio-sound",
180
+ type: "boolean",
181
+ description: "Clean the source audio once before rendering clips.",
182
+ },
183
+ {
184
+ name: "layout-mode",
185
+ enum: [
186
+ "auto",
187
+ "people_first",
188
+ "screen_first",
189
+ "preserve_full_source",
190
+ "prefer_split",
191
+ "prefer_focus",
192
+ "prefer_tutorial",
193
+ "prefer_demo",
194
+ ],
195
+ },
196
+ {
197
+ name: "content-type-hint",
198
+ enum: ["auto", "podcast", "tutorial", "demo", "generic"],
199
+ },
82
200
  {
83
201
  name: "idempotency-key",
84
202
  description: "Stable retry key; reuse only with identical inputs.",
@@ -90,6 +208,7 @@ export const CLIPS_COMMAND_SPECS = [
90
208
  examples: [
91
209
  "blitzreels clips create --asset-id ID --output ./clips",
92
210
  "blitzreels clips create --asset-id ID --logo-asset-id LOGO_ID --output ./clips --count 5",
211
+ "blitzreels clips create --asset-id ID --layout-mode people_first --content-type-hint podcast",
93
212
  "blitzreels clips create --youtube-url https://youtube.com/watch?v=...",
94
213
  ],
95
214
  }),
@@ -107,7 +226,7 @@ export const CLIPS_COMMAND_SPECS = [
107
226
  }),
108
227
  commandSpec({
109
228
  command: ["clips", "reselect"],
110
- usage: "blitzreels clips reselect --clip-id ID [--suggestion-id ID | --start-seconds 52 --end-seconds 64]",
229
+ usage: "blitzreels clips reselect --clip-id ID [--suggestion-id ID | --start-seconds 52 --end-seconds 64] [--layout-mode people_first] [--content-type-hint podcast] [--caption-style-id cinematic-film] [--caption-word-reveal-animation fade] [--caption-max-lines 2] [--caption-words-per-line 8] [--caption-page-combine-ms 3000]",
111
230
  flags: [
112
231
  "clip-id",
113
232
  "suggestion-id",
@@ -120,8 +239,54 @@ export const CLIPS_COMMAND_SPECS = [
120
239
  },
121
240
  { name: "min-duration", hidden: true },
122
241
  { name: "max-duration", hidden: true },
242
+ {
243
+ name: "layout-mode",
244
+ enum: [
245
+ "auto",
246
+ "people_first",
247
+ "screen_first",
248
+ "preserve_full_source",
249
+ "prefer_split",
250
+ "prefer_focus",
251
+ "prefer_tutorial",
252
+ "prefer_demo",
253
+ ],
254
+ },
255
+ {
256
+ name: "content-type-hint",
257
+ enum: ["auto", "podcast", "tutorial", "demo", "generic"],
258
+ },
259
+ {
260
+ name: "tutorial-stack-ratio",
261
+ enum: ["screen_70_face_30", "screen_50_face_50"],
262
+ hidden: true,
263
+ },
264
+ "captions-enabled",
265
+ "caption-style-id",
266
+ {
267
+ name: "caption-word-reveal-animation",
268
+ enum: [
269
+ "none",
270
+ "fade",
271
+ "scale",
272
+ "slide-up",
273
+ "slide-left",
274
+ "slide-mix",
275
+ "pop",
276
+ "drop",
277
+ "slam",
278
+ "typewriter",
279
+ "glitch",
280
+ "mask-reveal",
281
+ "bounce-up",
282
+ "split-reveal",
283
+ ],
284
+ },
285
+ "caption-max-lines",
286
+ "caption-words-per-line",
287
+ "caption-page-combine-ms",
123
288
  ],
124
- description: "Choose a different suggestion or time range for a clip.",
289
+ description: "Regenerate a clip from a suggestion or time range with optional layout and caption settings.",
125
290
  mutates: true,
126
291
  }),
127
292
  commandSpec({
@@ -163,10 +328,111 @@ export const CLIPS_COMMAND_SPECS = [
163
328
  mutates: true,
164
329
  }),
165
330
  ];
331
+ function parseClipShareVisibility({ value, }) {
332
+ if (value === "visible" || value === "hidden") {
333
+ return value;
334
+ }
335
+ throw new CliUsageError({
336
+ code: "invalid_flag_value",
337
+ message: "--visibility must be visible or hidden.",
338
+ });
339
+ }
340
+ function parseClipListLimit({ value, }) {
341
+ if (value === undefined)
342
+ return undefined;
343
+ if (Number.isInteger(value) && value >= 1 && value <= 50)
344
+ return value;
345
+ throw new CliUsageError({
346
+ code: "invalid_flag_value",
347
+ message: "--limit must be an integer between 1 and 50.",
348
+ });
349
+ }
350
+ function printClipBatchResult({ context, result, helpers, }) {
351
+ helpers.printResult({
352
+ json: context.json,
353
+ value: result,
354
+ human: [
355
+ `Batch ${result.batch.batch_id}: ${result.batch.status}`,
356
+ `Studio Sound: ${result.batch.studio_sound.status}`,
357
+ `Accepted: ${result.batch.rendered_count}/${result.batch.estimated_clip_count}`,
358
+ ...(result.batch.last_error_message === null
359
+ ? []
360
+ : [`Error: ${result.batch.last_error_message}`]),
361
+ ],
362
+ });
363
+ }
364
+ async function waitForClipBatch({ context, helpers, }) {
365
+ const batchId = helpers.requireStringFlag({
366
+ flags: context.args.flags,
367
+ name: "batch-id",
368
+ });
369
+ const intervalMs = helpers.getNumberFlag({
370
+ flags: context.args.flags,
371
+ name: "interval-ms",
372
+ }) ?? 3000;
373
+ const timeoutMs = helpers.getNumberFlag({
374
+ flags: context.args.flags,
375
+ name: "timeout-ms",
376
+ }) ?? 20 * 60 * 1000;
377
+ const startedAt = Date.now();
378
+ while (true) {
379
+ const result = await context.client.clipBatches.get({ batchId });
380
+ if (result.batch.studio_sound.status === "failed") {
381
+ throw new CliOperationError({
382
+ code: "studio_sound_failed",
383
+ exitCode: 5,
384
+ message: `Clip batch ${batchId} failed Studio Sound: ${result.batch.studio_sound.error_message ?? result.batch.last_error_message ?? "treatment was not applied"}`,
385
+ retryable: false,
386
+ suggestions: [
387
+ `blitzreels clip-batches recover --batch-id ${batchId} --idempotency-key NEW_KEY --json`,
388
+ ],
389
+ });
390
+ }
391
+ if (result.batch.status === "failed") {
392
+ throw new CliOperationError({
393
+ code: "clip_batch_failed",
394
+ exitCode: 5,
395
+ message: `Clip batch ${batchId} failed: ${result.batch.last_error_message ?? result.batch.studio_sound.error_message ?? "clip processing failed"}`,
396
+ retryable: false,
397
+ suggestions: [
398
+ `blitzreels clip-batches recover --batch-id ${batchId} --idempotency-key NEW_KEY --json`,
399
+ ],
400
+ });
401
+ }
402
+ if (result.batch.status === "completed_with_errors") {
403
+ throw new CliOperationError({
404
+ code: "clip_batch_incomplete",
405
+ exitCode: 5,
406
+ message: `Clip batch ${batchId} completed with ${result.batch.failed_count + result.batch.review_count} clips needing recovery.`,
407
+ retryable: false,
408
+ suggestions: [
409
+ `blitzreels clip-batches recover --batch-id ${batchId} --idempotency-key NEW_KEY --json`,
410
+ ],
411
+ });
412
+ }
413
+ if (result.batch.status === "completed") {
414
+ return result;
415
+ }
416
+ if (Date.now() - startedAt >= timeoutMs) {
417
+ throw new CliOperationError({
418
+ code: "clip_batch_wait_timeout",
419
+ exitCode: 7,
420
+ message: `Timed out waiting for clip batch ${batchId}.`,
421
+ retryable: true,
422
+ suggestions: [
423
+ `blitzreels clip-batches get --batch-id ${batchId} --json`,
424
+ ],
425
+ });
426
+ }
427
+ await helpers.sleep({ ms: intervalMs });
428
+ }
429
+ }
166
430
  export function createClipsCommandOperation({ helpers, }) {
167
431
  return {
168
432
  name: "clips",
169
- matches: ({ command }) => command[0] === "clips" || command[0] === "clip-batches",
433
+ matches: ({ command }) => command[0] === "clips" ||
434
+ command[0] === "clip-batches" ||
435
+ command[0] === "clip-share-pages",
170
436
  handle: ({ context }) => handleClipsCommand({ context, helpers }),
171
437
  buildDryRun: ({ context, spec }) => buildClipsDryRun({ context, spec, helpers }),
172
438
  };
@@ -174,6 +440,118 @@ export function createClipsCommandOperation({ helpers, }) {
174
440
  async function handleClipsCommand({ context, helpers, }) {
175
441
  const action = context.args.command[1];
176
442
  const flags = context.args.flags;
443
+ if (context.args.command[0] === "clip-share-pages") {
444
+ const assetId = helpers.requireStringFlag({ flags, name: "asset-id" });
445
+ if (action === "get") {
446
+ const result = await context.client.clipSharePages.get({ assetId });
447
+ helpers.printResult({
448
+ json: context.json,
449
+ value: result,
450
+ human: [
451
+ result.enabled
452
+ ? `Published: ${result.share_url}`
453
+ : "Share page is not published.",
454
+ `Hidden clips: ${result.hidden_clip_ids.length}`,
455
+ ],
456
+ });
457
+ return true;
458
+ }
459
+ if (action === "publish" || action === "unpublish") {
460
+ const result = await context.client.clipSharePages.setEnabled({
461
+ assetId,
462
+ enabled: action === "publish",
463
+ allowDownloads: helpers.getOptionalBooleanFlag({
464
+ flags,
465
+ name: "allow-downloads",
466
+ }),
467
+ idempotencyKey: helpers.requireStringFlag({
468
+ flags,
469
+ name: "idempotency-key",
470
+ }),
471
+ });
472
+ helpers.printResult({
473
+ json: context.json,
474
+ value: result,
475
+ human: [
476
+ result.enabled
477
+ ? `Published: ${result.share_url}`
478
+ : "Share page unpublished.",
479
+ ],
480
+ });
481
+ return true;
482
+ }
483
+ if (action === "set-clip-visibility") {
484
+ const visibility = parseClipShareVisibility({
485
+ value: helpers.requireStringFlag({
486
+ flags,
487
+ name: "visibility",
488
+ }),
489
+ });
490
+ const result = await context.client.clipSharePages.setClipVisibility({
491
+ assetId,
492
+ clipId: helpers.requireStringFlag({ flags, name: "clip-id" }),
493
+ visibility,
494
+ idempotencyKey: helpers.requireStringFlag({
495
+ flags,
496
+ name: "idempotency-key",
497
+ }),
498
+ });
499
+ helpers.printResult({
500
+ json: context.json,
501
+ value: result,
502
+ human: [`Clip ${result.clip_id}: ${result.visibility}`],
503
+ });
504
+ return true;
505
+ }
506
+ }
507
+ if (context.args.command[0] === "clip-batches" && action === "get") {
508
+ const result = await context.client.clipBatches.get({
509
+ batchId: helpers.requireStringFlag({ flags, name: "batch-id" }),
510
+ });
511
+ printClipBatchResult({ context, result, helpers });
512
+ return true;
513
+ }
514
+ if (context.args.command[0] === "clip-batches" && action === "wait") {
515
+ const result = await waitForClipBatch({ context, helpers });
516
+ printClipBatchResult({ context, result, helpers });
517
+ return true;
518
+ }
519
+ if (context.args.command[0] === "clip-batches" && action === "recover") {
520
+ const result = await context.client.clipBatches.recover({
521
+ batchId: helpers.requireStringFlag({ flags, name: "batch-id" }),
522
+ targetClipCount: helpers.getNumberFlag({ flags, name: "target-count" }) ?? null,
523
+ idempotencyKey: helpers.requireStringFlag({
524
+ flags,
525
+ name: "idempotency-key",
526
+ }),
527
+ });
528
+ if (result.status === "failed") {
529
+ throw new CliOperationError({
530
+ code: "clip_batch_recovery_failed",
531
+ exitCode: 5,
532
+ message: `Recovery batch ${result.recovery_batch_id ?? result.source_batch_id} failed: ${result.queue_reason ?? "clip recovery failed"}`,
533
+ retryable: false,
534
+ suggestions: [
535
+ 'blitzreels agent-context --command "clip-batches recover" --json',
536
+ ],
537
+ });
538
+ }
539
+ const statusLine = result.status === "satisfied"
540
+ ? `Batch ${result.source_batch_id} already has ${result.retained_clip_count}/${result.target_clip_count} accepted clips.`
541
+ : result.status === "queued"
542
+ ? `Recovery batch ${result.recovery_batch_id} queued for ${result.refill_clip_count} missing clips.`
543
+ : `Recovery batch ${result.recovery_batch_id} created; waiting: ${result.queue_reason ?? "not ready to queue"}.`;
544
+ helpers.printResult({
545
+ json: context.json,
546
+ value: result,
547
+ human: [
548
+ statusLine,
549
+ `Folder: ${result.project_folder_id}`,
550
+ "Duration policy: quality first; suggestion boundaries preserved.",
551
+ ],
552
+ });
553
+ return true;
554
+ }
177
555
  if (context.args.command[0] === "clip-batches" && action === "delete") {
178
556
  if (helpers.getOptionalBooleanFlag({ flags, name: "confirm-delete" }) !== true) {
179
557
  throw new Error("Pass --confirm-delete to delete a clip batch.");
@@ -216,6 +594,66 @@ async function handleClipsCommand({ context, helpers, }) {
216
594
  });
217
595
  return true;
218
596
  }
597
+ if (action === "attach-export") {
598
+ if (helpers.getOptionalBooleanFlag({
599
+ flags,
600
+ name: "confirm-visual-qa",
601
+ }) !== true) {
602
+ throw new CliUsageError({
603
+ code: "missing_visual_qa_confirmation",
604
+ message: "Pass --confirm-visual-qa only after reviewing the completed MP4.",
605
+ usage: "blitzreels clips attach-export --asset-id ID --project-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY",
606
+ });
607
+ }
608
+ const result = await context.client.clips.attachExport({
609
+ assetId: helpers.requireStringFlag({ flags, name: "asset-id" }),
610
+ projectId: helpers.requireStringFlag({ flags, name: "project-id" }),
611
+ exportId: helpers.requireStringFlag({ flags, name: "export-id" }),
612
+ confirmVisualQa: true,
613
+ idempotencyKey: helpers.requireStringFlag({
614
+ flags,
615
+ name: "idempotency-key",
616
+ }),
617
+ });
618
+ helpers.printResult({
619
+ json: context.json,
620
+ value: result,
621
+ human: [
622
+ `Attached export ${result.export_id} to managed clip ${result.clip_id}.`,
623
+ ],
624
+ });
625
+ return true;
626
+ }
627
+ if (action === "promote-export") {
628
+ if (helpers.getOptionalBooleanFlag({
629
+ flags,
630
+ name: "confirm-visual-qa",
631
+ }) !== true) {
632
+ throw new CliUsageError({
633
+ code: "missing_visual_qa_confirmation",
634
+ message: "Pass --confirm-visual-qa only after reviewing the completed MP4.",
635
+ usage: "blitzreels clips promote-export --clip-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY",
636
+ });
637
+ }
638
+ const result = await context.client.clips.promoteExport({
639
+ clipId: helpers.requireStringFlag({ flags, name: "clip-id" }),
640
+ exportId: helpers.requireStringFlag({ flags, name: "export-id" }),
641
+ confirmVisualQa: true,
642
+ idempotencyKey: helpers.requireStringFlag({
643
+ flags,
644
+ name: "idempotency-key",
645
+ }),
646
+ });
647
+ helpers.printResult({
648
+ json: context.json,
649
+ value: result,
650
+ human: [
651
+ `Promoted export ${result.export_id} for managed clip ${result.clip_id}.`,
652
+ `Previous export retained: ${result.previous_export_id}.`,
653
+ ],
654
+ });
655
+ return true;
656
+ }
219
657
  if (action === "list") {
220
658
  const result = await context.client.clips.list({
221
659
  assetId: helpers.getStringFlag({ flags, name: "asset-id" }),
@@ -223,7 +661,9 @@ async function handleClipsCommand({ context, helpers, }) {
223
661
  status: parseClipStatus({
224
662
  value: helpers.getStringFlag({ flags, name: "status" }),
225
663
  }),
226
- limit: helpers.getNumberFlag({ flags, name: "limit" }),
664
+ limit: parseClipListLimit({
665
+ value: helpers.getNumberFlag({ flags, name: "limit" }),
666
+ }),
227
667
  offset: helpers.getNumberFlag({ flags, name: "offset" }),
228
668
  });
229
669
  helpers.printResult({
@@ -259,6 +699,8 @@ async function handleClipsCommand({ context, helpers, }) {
259
699
  flags,
260
700
  name: "selection-mode",
261
701
  });
702
+ const layout = buildReselectLayout({ flags, helpers });
703
+ const captions = buildReselectCaptions({ flags, helpers });
262
704
  const result = await context.client.clips.reselect({
263
705
  clipId,
264
706
  ...(selectionMode === undefined
@@ -267,10 +709,14 @@ async function handleClipsCommand({ context, helpers, }) {
267
709
  startSeconds: selection.startSeconds,
268
710
  endSeconds: selection.endSeconds,
269
711
  target,
712
+ layout,
713
+ captions,
270
714
  }
271
715
  : {
272
716
  selection,
273
717
  target,
718
+ layout,
719
+ captions,
274
720
  }),
275
721
  });
276
722
  helpers.printClipResult({ context, result });
@@ -342,6 +788,128 @@ function formatClipListLine({ clip }) {
342
788
  function buildClipsDryRun({ context, spec, helpers, }) {
343
789
  const action = context.args.command[1];
344
790
  const flags = context.args.flags;
791
+ if (context.args.command[0] === "clips" && action === "attach-export") {
792
+ if (helpers.getOptionalBooleanFlag({
793
+ flags,
794
+ name: "confirm-visual-qa",
795
+ }) !== true) {
796
+ throw new CliUsageError({
797
+ code: "missing_visual_qa_confirmation",
798
+ message: "Pass --confirm-visual-qa only after reviewing the completed MP4.",
799
+ usage: "blitzreels clips attach-export --asset-id ID --project-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY",
800
+ });
801
+ }
802
+ return helpers.dryRunRequest({
803
+ spec,
804
+ method: "POST",
805
+ path: "/clips/attach-export",
806
+ headers: {
807
+ "Idempotency-Key": helpers.requireStringFlag({
808
+ flags,
809
+ name: "idempotency-key",
810
+ }),
811
+ },
812
+ body: {
813
+ asset_id: helpers.requireStringFlag({ flags, name: "asset-id" }),
814
+ project_id: helpers.requireStringFlag({ flags, name: "project-id" }),
815
+ export_id: helpers.requireStringFlag({ flags, name: "export-id" }),
816
+ confirm_visual_qa: true,
817
+ },
818
+ });
819
+ }
820
+ if (context.args.command[0] === "clips" && action === "promote-export") {
821
+ if (helpers.getOptionalBooleanFlag({
822
+ flags,
823
+ name: "confirm-visual-qa",
824
+ }) !== true) {
825
+ throw new CliUsageError({
826
+ code: "missing_visual_qa_confirmation",
827
+ message: "Pass --confirm-visual-qa only after reviewing the completed MP4.",
828
+ usage: "blitzreels clips promote-export --clip-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY",
829
+ });
830
+ }
831
+ const clipId = helpers.requireStringFlag({ flags, name: "clip-id" });
832
+ return helpers.dryRunRequest({
833
+ spec,
834
+ method: "POST",
835
+ path: `/clips/${encodeURIComponent(clipId)}/promote-export`,
836
+ headers: {
837
+ "Idempotency-Key": helpers.requireStringFlag({
838
+ flags,
839
+ name: "idempotency-key",
840
+ }),
841
+ },
842
+ body: {
843
+ export_id: helpers.requireStringFlag({ flags, name: "export-id" }),
844
+ confirm_visual_qa: true,
845
+ },
846
+ });
847
+ }
848
+ if (context.args.command[0] === "clip-share-pages") {
849
+ const assetId = helpers.requireStringFlag({ flags, name: "asset-id" });
850
+ if (action === "publish" || action === "unpublish") {
851
+ return helpers.dryRunRequest({
852
+ spec,
853
+ method: "POST",
854
+ path: `/clip-share-pages/${encodeURIComponent(assetId)}`,
855
+ headers: {
856
+ "Idempotency-Key": helpers.requireStringFlag({
857
+ flags,
858
+ name: "idempotency-key",
859
+ }),
860
+ },
861
+ body: {
862
+ enabled: action === "publish",
863
+ allow_downloads: helpers.getOptionalBooleanFlag({
864
+ flags,
865
+ name: "allow-downloads",
866
+ }),
867
+ },
868
+ });
869
+ }
870
+ if (action === "set-clip-visibility") {
871
+ const clipId = helpers.requireStringFlag({ flags, name: "clip-id" });
872
+ return helpers.dryRunRequest({
873
+ spec,
874
+ method: "POST",
875
+ path: `/clip-share-pages/${encodeURIComponent(assetId)}/clips/${encodeURIComponent(clipId)}/visibility`,
876
+ headers: {
877
+ "Idempotency-Key": helpers.requireStringFlag({
878
+ flags,
879
+ name: "idempotency-key",
880
+ }),
881
+ },
882
+ body: {
883
+ visibility: parseClipShareVisibility({
884
+ value: helpers.requireStringFlag({
885
+ flags,
886
+ name: "visibility",
887
+ }),
888
+ }),
889
+ },
890
+ });
891
+ }
892
+ }
893
+ if (context.args.command[0] === "clip-batches" && action === "recover") {
894
+ return helpers.dryRunRequest({
895
+ spec,
896
+ method: "POST",
897
+ path: `/clip-batches/${encodeURIComponent(helpers.requireStringFlag({ flags, name: "batch-id" }))}/recover`,
898
+ headers: {
899
+ "Idempotency-Key": helpers.requireStringFlag({
900
+ flags,
901
+ name: "idempotency-key",
902
+ }),
903
+ },
904
+ body: {
905
+ target_clip_count: helpers.getNumberFlag({ flags, name: "target-count" }) ?? null,
906
+ },
907
+ notes: [
908
+ "Accepted exports remain linked; only the missing accepted count is refilled.",
909
+ "Suggestion boundaries are preserved even when a coherent clip exceeds the target duration.",
910
+ ],
911
+ });
912
+ }
345
913
  if (context.args.command[0] === "clip-batches" && action === "delete") {
346
914
  return helpers.dryRunRequest({
347
915
  spec,
@@ -392,8 +960,10 @@ function buildClipsDryRun({ context, spec, helpers, }) {
392
960
  quality: null,
393
961
  },
394
962
  clip_preset_id: options.clipPresetId,
963
+ max_clip_count: options.count,
395
964
  caption_theme_id: options.captionThemeId,
396
- create_flow_options: {},
965
+ studio_sound: options.studioSound,
966
+ create_flow_options: buildClipWorkflowCreateFlowOptions({ options }),
397
967
  branding_options: buildClipWorkflowBrandingOptions({
398
968
  logoAssetId: options.logoAssetId,
399
969
  }),
@@ -426,6 +996,8 @@ function buildClipsDryRun({ context, spec, helpers, }) {
426
996
  flags,
427
997
  name: "selection-mode",
428
998
  });
999
+ const layout = buildReselectLayout({ flags, helpers });
1000
+ const captions = buildReselectCaptions({ flags, helpers });
429
1001
  return helpers.dryRunRequest({
430
1002
  spec,
431
1003
  method: "POST",
@@ -449,6 +1021,12 @@ function buildClipsDryRun({ context, spec, helpers, }) {
449
1021
  max_duration_seconds: target.maxDurationSeconds,
450
1022
  },
451
1023
  }),
1024
+ ...(layout === null
1025
+ ? {}
1026
+ : { layout: toApiClipLayout({ layout }) }),
1027
+ ...(captions === null
1028
+ ? {}
1029
+ : { captions: toApiClipCaptions({ captions }) }),
452
1030
  }
453
1031
  : {
454
1032
  selection: toApiClipSelection({ selection }),
@@ -458,6 +1036,8 @@ function buildClipsDryRun({ context, spec, helpers, }) {
458
1036
  min_duration_seconds: target.minDurationSeconds,
459
1037
  max_duration_seconds: target.maxDurationSeconds,
460
1038
  },
1039
+ layout: layout === null ? null : toApiClipLayout({ layout }),
1040
+ captions: captions === null ? null : toApiClipCaptions({ captions }),
461
1041
  },
462
1042
  });
463
1043
  }
@@ -501,6 +1081,17 @@ function parseClipBatchDeleteRetention({ value, }) {
501
1081
  }
502
1082
  throw new Error("--retention must be retain_projects_and_exports or delete_projects_and_exports.");
503
1083
  }
1084
+ function buildClipWorkflowCreateFlowOptions({ options, }) {
1085
+ return {
1086
+ ...(options.layoutMode === undefined
1087
+ ? {}
1088
+ : { reframeLayoutMode: options.layoutMode }),
1089
+ ...(options.contentTypeHint === undefined
1090
+ ? {}
1091
+ : { reframeContentTypeHint: options.contentTypeHint }),
1092
+ ...(options.studioSound ? { useStudioSound: true } : {}),
1093
+ };
1094
+ }
504
1095
  function parseClipWorkflowOptions({ flags, helpers, }) {
505
1096
  const count = helpers.getNumberFlag({ flags, name: "count" }) ?? 5;
506
1097
  if (!Number.isInteger(count) || count < 1 || count > 10) {
@@ -540,6 +1131,13 @@ function parseClipWorkflowOptions({ flags, helpers, }) {
540
1131
  captionThemeId: parseNullableWorkflowString({
541
1132
  value: helpers.getStringFlag({ flags, name: "caption-theme-id" }),
542
1133
  }),
1134
+ layoutMode: parseClipLayoutMode({
1135
+ value: helpers.getStringFlag({ flags, name: "layout-mode" }),
1136
+ }),
1137
+ contentTypeHint: parseClipContentTypeHint({
1138
+ value: helpers.getStringFlag({ flags, name: "content-type-hint" }),
1139
+ }),
1140
+ studioSound: helpers.getOptionalBooleanFlag({ flags, name: "studio-sound" }) ?? false,
543
1141
  idempotencyKey: helpers.getStringFlag({ flags, name: "idempotency-key" }) ?? null,
544
1142
  };
545
1143
  }
@@ -574,8 +1172,10 @@ async function createClipWorkflow({ context, helpers, }) {
574
1172
  quality: null,
575
1173
  },
576
1174
  clipPresetId: options.clipPresetId,
1175
+ maxClipCount: options.count,
577
1176
  captionThemeId: options.captionThemeId,
578
- createFlowOptions: {},
1177
+ studioSound: options.studioSound,
1178
+ createFlowOptions: buildClipWorkflowCreateFlowOptions({ options }),
579
1179
  brandingOptions: buildClipWorkflowBrandingOptions({
580
1180
  logoAssetId: options.logoAssetId,
581
1181
  }),
@@ -695,4 +1295,138 @@ function toApiClipSelection({ selection, }) {
695
1295
  end_seconds: selection.endSeconds,
696
1296
  };
697
1297
  }
1298
+ function parseClipLayoutMode({ value, }) {
1299
+ if (value === undefined ||
1300
+ value === "auto" ||
1301
+ value === "people_first" ||
1302
+ value === "screen_first" ||
1303
+ value === "preserve_full_source" ||
1304
+ value === "prefer_split" ||
1305
+ value === "prefer_focus" ||
1306
+ value === "prefer_tutorial" ||
1307
+ value === "prefer_demo") {
1308
+ return value;
1309
+ }
1310
+ throw new Error(`Unsupported clip layout mode: ${value}`);
1311
+ }
1312
+ function parseClipContentTypeHint({ value, }) {
1313
+ if (value === undefined ||
1314
+ value === "auto" ||
1315
+ value === "podcast" ||
1316
+ value === "tutorial" ||
1317
+ value === "demo" ||
1318
+ value === "generic") {
1319
+ return value;
1320
+ }
1321
+ throw new Error(`Unsupported clip content type hint: ${value}`);
1322
+ }
1323
+ function parseTutorialStackRatio({ value, }) {
1324
+ if (value === undefined ||
1325
+ value === "screen_70_face_30" ||
1326
+ value === "screen_50_face_50") {
1327
+ return value;
1328
+ }
1329
+ throw new Error(`Unsupported tutorial stack ratio: ${value}`);
1330
+ }
1331
+ function buildReselectLayout({ flags, helpers, }) {
1332
+ const mode = parseClipLayoutMode({
1333
+ value: helpers.getStringFlag({ flags, name: "layout-mode" }),
1334
+ });
1335
+ const contentTypeHint = parseClipContentTypeHint({
1336
+ value: helpers.getStringFlag({ flags, name: "content-type-hint" }),
1337
+ });
1338
+ const tutorialStackRatio = parseTutorialStackRatio({
1339
+ value: helpers.getStringFlag({ flags, name: "tutorial-stack-ratio" }),
1340
+ });
1341
+ if (mode === undefined &&
1342
+ contentTypeHint === undefined &&
1343
+ tutorialStackRatio === undefined) {
1344
+ return null;
1345
+ }
1346
+ return { mode, contentTypeHint, tutorialStackRatio };
1347
+ }
1348
+ function buildReselectCaptions({ flags, helpers, }) {
1349
+ const enabled = helpers.getOptionalBooleanFlag({
1350
+ flags,
1351
+ name: "captions-enabled",
1352
+ });
1353
+ const styleId = helpers.getStringFlag({
1354
+ flags,
1355
+ name: "caption-style-id",
1356
+ });
1357
+ const wordRevealAnimation = parseCaptionWordRevealAnimation({
1358
+ value: helpers.getStringFlag({
1359
+ flags,
1360
+ name: "caption-word-reveal-animation",
1361
+ }),
1362
+ });
1363
+ const maxLinesPerCaption = helpers.getNumberFlag({
1364
+ flags,
1365
+ name: "caption-max-lines",
1366
+ });
1367
+ const wordsPerLineLimit = helpers.getNumberFlag({
1368
+ flags,
1369
+ name: "caption-words-per-line",
1370
+ });
1371
+ const pageCombineMs = helpers.getNumberFlag({
1372
+ flags,
1373
+ name: "caption-page-combine-ms",
1374
+ });
1375
+ if (enabled === undefined &&
1376
+ styleId === undefined &&
1377
+ wordRevealAnimation === undefined &&
1378
+ maxLinesPerCaption === undefined &&
1379
+ wordsPerLineLimit === undefined &&
1380
+ pageCombineMs === undefined) {
1381
+ return null;
1382
+ }
1383
+ return {
1384
+ enabled,
1385
+ styleId,
1386
+ wordRevealAnimation,
1387
+ maxLinesPerCaption,
1388
+ wordsPerLineLimit,
1389
+ pageCombineMs,
1390
+ };
1391
+ }
1392
+ function parseCaptionWordRevealAnimation({ value, }) {
1393
+ const supported = [
1394
+ "none",
1395
+ "fade",
1396
+ "scale",
1397
+ "slide-up",
1398
+ "slide-left",
1399
+ "slide-mix",
1400
+ "pop",
1401
+ "drop",
1402
+ "slam",
1403
+ "typewriter",
1404
+ "glitch",
1405
+ "mask-reveal",
1406
+ "bounce-up",
1407
+ "split-reveal",
1408
+ ];
1409
+ if (value === undefined ||
1410
+ supported.some((candidate) => candidate === value)) {
1411
+ return value;
1412
+ }
1413
+ throw new Error(`Unsupported caption word reveal animation: ${value}`);
1414
+ }
1415
+ function toApiClipLayout({ layout }) {
1416
+ return {
1417
+ layout_mode: layout.mode,
1418
+ content_type_hint: layout.contentTypeHint,
1419
+ tutorial_stack_ratio: layout.tutorialStackRatio,
1420
+ };
1421
+ }
1422
+ function toApiClipCaptions({ captions }) {
1423
+ return {
1424
+ enabled: captions.enabled,
1425
+ style_id: captions.styleId,
1426
+ word_reveal_animation: captions.wordRevealAnimation,
1427
+ max_lines_per_caption: captions.maxLinesPerCaption,
1428
+ words_per_line_limit: captions.wordsPerLineLimit,
1429
+ page_combine_ms: captions.pageCombineMs,
1430
+ };
1431
+ }
698
1432
  //# sourceMappingURL=clips.js.map