blitzreels 0.1.7 → 0.1.9

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 (92) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +73 -5
  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 +1 -1
  7. package/dist/cli-runtime.js.map +1 -1
  8. package/dist/command-metadata.d.ts.map +1 -1
  9. package/dist/command-metadata.js +2 -0
  10. package/dist/command-metadata.js.map +1 -1
  11. package/dist/command-registry.d.ts.map +1 -1
  12. package/dist/command-registry.js +2 -0
  13. package/dist/command-registry.js.map +1 -1
  14. package/dist/commands/auth-runtime.js +1 -1
  15. package/dist/commands/auth-runtime.js.map +1 -1
  16. package/dist/commands/bulk-delete-runtime.d.ts +12 -0
  17. package/dist/commands/bulk-delete-runtime.d.ts.map +1 -0
  18. package/dist/commands/bulk-delete-runtime.js +21 -0
  19. package/dist/commands/bulk-delete-runtime.js.map +1 -0
  20. package/dist/commands/caption-dry-run.d.ts +1 -0
  21. package/dist/commands/caption-dry-run.d.ts.map +1 -1
  22. package/dist/commands/caption-dry-run.js +32 -0
  23. package/dist/commands/caption-dry-run.js.map +1 -1
  24. package/dist/commands/caption-overrides.d.ts +13 -1
  25. package/dist/commands/caption-overrides.d.ts.map +1 -1
  26. package/dist/commands/caption-overrides.js +35 -2
  27. package/dist/commands/caption-overrides.js.map +1 -1
  28. package/dist/commands/caption-style-commands.d.ts.map +1 -1
  29. package/dist/commands/caption-style-commands.js +13 -3
  30. package/dist/commands/caption-style-commands.js.map +1 -1
  31. package/dist/commands/captions-specs.d.ts.map +1 -1
  32. package/dist/commands/captions-specs.js +18 -2
  33. package/dist/commands/captions-specs.js.map +1 -1
  34. package/dist/commands/captions.js +32 -0
  35. package/dist/commands/captions.js.map +1 -1
  36. package/dist/commands/clips.d.ts +4 -0
  37. package/dist/commands/clips.d.ts.map +1 -1
  38. package/dist/commands/clips.js +775 -7
  39. package/dist/commands/clips.js.map +1 -1
  40. package/dist/commands/command-operation.d.ts +2 -2
  41. package/dist/commands/command-operation.d.ts.map +1 -1
  42. package/dist/commands/command-operation.js +1 -1
  43. package/dist/commands/command-operation.js.map +1 -1
  44. package/dist/commands/editing-workflows.d.ts.map +1 -1
  45. package/dist/commands/editing-workflows.js +24 -6
  46. package/dist/commands/editing-workflows.js.map +1 -1
  47. package/dist/commands/faceless.d.ts.map +1 -1
  48. package/dist/commands/faceless.js +1 -0
  49. package/dist/commands/faceless.js.map +1 -1
  50. package/dist/commands/jobs.d.ts.map +1 -1
  51. package/dist/commands/jobs.js +2 -0
  52. package/dist/commands/jobs.js.map +1 -1
  53. package/dist/commands/media-assets.d.ts +5 -7
  54. package/dist/commands/media-assets.d.ts.map +1 -1
  55. package/dist/commands/media-assets.js +481 -21
  56. package/dist/commands/media-assets.js.map +1 -1
  57. package/dist/commands/media-overlays.d.ts.map +1 -1
  58. package/dist/commands/media-overlays.js +20 -2
  59. package/dist/commands/media-overlays.js.map +1 -1
  60. package/dist/commands/media-runtime.d.ts +4 -7
  61. package/dist/commands/media-runtime.d.ts.map +1 -1
  62. package/dist/commands/media-runtime.js +39 -7
  63. package/dist/commands/media-runtime.js.map +1 -1
  64. package/dist/commands/project-read.d.ts +30 -0
  65. package/dist/commands/project-read.d.ts.map +1 -1
  66. package/dist/commands/project-read.js +183 -6
  67. package/dist/commands/project-read.js.map +1 -1
  68. package/dist/commands/projects.d.ts.map +1 -1
  69. package/dist/commands/projects.js +125 -2
  70. package/dist/commands/projects.js.map +1 -1
  71. package/dist/commands/recording-sets.d.ts +44 -0
  72. package/dist/commands/recording-sets.d.ts.map +1 -0
  73. package/dist/commands/recording-sets.js +385 -0
  74. package/dist/commands/recording-sets.js.map +1 -0
  75. package/dist/commands/system.js +1 -1
  76. package/dist/commands/system.js.map +1 -1
  77. package/dist/commands/text-overlay-spec.js +62 -2
  78. package/dist/commands/text-overlay-spec.js.map +1 -1
  79. package/dist/commands/timeline-runtime.d.ts +30 -0
  80. package/dist/commands/timeline-runtime.d.ts.map +1 -1
  81. package/dist/commands/timeline-runtime.js +181 -42
  82. package/dist/commands/timeline-runtime.js.map +1 -1
  83. package/dist/commands/timeline.d.ts.map +1 -1
  84. package/dist/commands/timeline.js +158 -3
  85. package/dist/commands/timeline.js.map +1 -1
  86. package/dist/index.js +17 -3
  87. package/dist/index.js.map +1 -1
  88. package/dist/preview-first-edit.d.ts.map +1 -1
  89. package/dist/update-notifier.d.ts.map +1 -1
  90. package/dist/update-notifier.js +7 -3
  91. package/dist/update-notifier.js.map +1 -1
  92. package/package.json +6 -3
@@ -1,7 +1,85 @@
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 [--dry-run] [--json]",
15
+ flags: ["asset-id", { name: "idempotency-key", required: true }],
16
+ description: "Publish the clip share page for a source asset.",
17
+ mutates: true,
18
+ dryRunSupported: true,
19
+ }),
20
+ commandSpec({
21
+ command: ["clip-share-pages", "unpublish"],
22
+ usage: "blitzreels clip-share-pages unpublish --asset-id ID --idempotency-key KEY [--dry-run] [--json]",
23
+ flags: ["asset-id", { name: "idempotency-key", required: true }],
24
+ description: "Disable the public clip share page for a source asset.",
25
+ mutates: true,
26
+ dryRunSupported: true,
27
+ }),
28
+ commandSpec({
29
+ command: ["clip-share-pages", "set-clip-visibility"],
30
+ usage: "blitzreels clip-share-pages set-clip-visibility --asset-id ID --clip-id ID --visibility visible|hidden --idempotency-key KEY [--dry-run] [--json]",
31
+ flags: [
32
+ "asset-id",
33
+ "clip-id",
34
+ { name: "visibility", enum: ["visible", "hidden"], required: true },
35
+ { name: "idempotency-key", required: true },
36
+ ],
37
+ description: "Show or hide one clip on its source asset share page.",
38
+ mutates: true,
39
+ dryRunSupported: true,
40
+ }),
41
+ commandSpec({
42
+ command: ["clip-batches", "get"],
43
+ usage: "blitzreels clip-batches get --batch-id ID [--json]",
44
+ flags: ["batch-id"],
45
+ description: "Inspect clip batch progress, Studio Sound status, and terminal errors.",
46
+ }),
47
+ commandSpec({
48
+ command: ["clip-batches", "wait"],
49
+ usage: "blitzreels clip-batches wait --batch-id ID [--interval-ms 3000] [--timeout-ms 1200000] [--json]",
50
+ flags: ["batch-id", "interval-ms", "timeout-ms"],
51
+ description: "Wait for a clip batch to complete or fail, including asynchronous Studio Sound.",
52
+ }),
53
+ commandSpec({
54
+ command: ["clip-batches", "recover"],
55
+ usage: "blitzreels clip-batches recover --batch-id ID [--target-count 10] --idempotency-key KEY [--dry-run] [--json]",
56
+ flags: [
57
+ "batch-id",
58
+ { name: "target-count", type: "number" },
59
+ { name: "idempotency-key", required: true },
60
+ ],
61
+ description: "Preserve accepted clips and refill a terminal batch in its existing project folder.",
62
+ mutates: true,
63
+ dryRunSupported: true,
64
+ }),
65
+ commandSpec({
66
+ command: ["clip-batches", "delete"],
67
+ 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]",
68
+ flags: [
69
+ "batch-id",
70
+ {
71
+ name: "retention",
72
+ enum: ["retain_projects_and_exports", "delete_projects_and_exports"],
73
+ default: "retain_projects_and_exports",
74
+ },
75
+ "idempotency-key",
76
+ "confirm-delete",
77
+ ],
78
+ description: "Delete a terminal clip batch with an explicit project/export retention policy.",
79
+ mutates: true,
80
+ destructive: true,
81
+ dryRunSupported: true,
82
+ }),
5
83
  commandSpec({
6
84
  command: ["clips", "list"],
7
85
  usage: "blitzreels clips list [--asset-id ID] [--project-id ID] [--status ready|exported|failed|needs_repair] [--limit 20] [--offset 0] [--json]",
@@ -29,9 +107,37 @@ export const CLIPS_COMMAND_SPECS = [
29
107
  ],
30
108
  description: "List managed clip runs with bounded output.",
31
109
  }),
110
+ commandSpec({
111
+ command: ["clips", "attach-export"],
112
+ usage: "blitzreels clips attach-export --asset-id ID --project-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY [--dry-run] [--json]",
113
+ flags: [
114
+ "asset-id",
115
+ "project-id",
116
+ "export-id",
117
+ { name: "confirm-visual-qa", required: true },
118
+ { name: "idempotency-key", required: true },
119
+ ],
120
+ description: "Attach one visually accepted completed project export to managed clips.",
121
+ mutates: true,
122
+ destructive: true,
123
+ dryRunSupported: true,
124
+ }),
125
+ commandSpec({
126
+ command: ["clips", "promote-export"],
127
+ usage: "blitzreels clips promote-export --clip-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY [--dry-run] [--json]",
128
+ flags: [
129
+ "clip-id",
130
+ "export-id",
131
+ { name: "confirm-visual-qa", required: true },
132
+ { name: "idempotency-key", required: true },
133
+ ],
134
+ description: "Promote one visually accepted completed export as the managed clip delivery.",
135
+ mutates: true,
136
+ dryRunSupported: true,
137
+ }),
32
138
  commandSpec({
33
139
  command: ["clips", "create"],
34
- usage: "blitzreels clips create [--asset-id ID|--youtube-url URL] [--output ./clips] [--count 5] [--idempotency-key KEY]",
140
+ usage: "blitzreels clips create [--asset-id ID|--youtube-url URL] [--output ./clips] [--count 5] [--layout-mode people_first] [--content-type-hint podcast] [--idempotency-key KEY]",
35
141
  flags: [
36
142
  { name: "asset-id", enum: [] },
37
143
  "youtube-url",
@@ -44,7 +150,7 @@ export const CLIPS_COMMAND_SPECS = [
44
150
  name: "count",
45
151
  type: "number",
46
152
  default: 5,
47
- description: "Maximum rendered clips to download from the batch.",
153
+ description: "Maximum clips created by the batch.",
48
154
  },
49
155
  {
50
156
  name: "preset",
@@ -61,6 +167,23 @@ export const CLIPS_COMMAND_SPECS = [
61
167
  hidden: false,
62
168
  },
63
169
  { name: "caption-theme-id" },
170
+ {
171
+ name: "layout-mode",
172
+ enum: [
173
+ "auto",
174
+ "people_first",
175
+ "screen_first",
176
+ "preserve_full_source",
177
+ "prefer_split",
178
+ "prefer_focus",
179
+ "prefer_tutorial",
180
+ "prefer_demo",
181
+ ],
182
+ },
183
+ {
184
+ name: "content-type-hint",
185
+ enum: ["auto", "podcast", "tutorial", "demo", "generic"],
186
+ },
64
187
  {
65
188
  name: "idempotency-key",
66
189
  description: "Stable retry key; reuse only with identical inputs.",
@@ -72,6 +195,7 @@ export const CLIPS_COMMAND_SPECS = [
72
195
  examples: [
73
196
  "blitzreels clips create --asset-id ID --output ./clips",
74
197
  "blitzreels clips create --asset-id ID --logo-asset-id LOGO_ID --output ./clips --count 5",
198
+ "blitzreels clips create --asset-id ID --layout-mode people_first --content-type-hint podcast",
75
199
  "blitzreels clips create --youtube-url https://youtube.com/watch?v=...",
76
200
  ],
77
201
  }),
@@ -89,7 +213,7 @@ export const CLIPS_COMMAND_SPECS = [
89
213
  }),
90
214
  commandSpec({
91
215
  command: ["clips", "reselect"],
92
- usage: "blitzreels clips reselect --clip-id ID [--suggestion-id ID | --start-seconds 52 --end-seconds 64]",
216
+ 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]",
93
217
  flags: [
94
218
  "clip-id",
95
219
  "suggestion-id",
@@ -102,8 +226,54 @@ export const CLIPS_COMMAND_SPECS = [
102
226
  },
103
227
  { name: "min-duration", hidden: true },
104
228
  { name: "max-duration", hidden: true },
229
+ {
230
+ name: "layout-mode",
231
+ enum: [
232
+ "auto",
233
+ "people_first",
234
+ "screen_first",
235
+ "preserve_full_source",
236
+ "prefer_split",
237
+ "prefer_focus",
238
+ "prefer_tutorial",
239
+ "prefer_demo",
240
+ ],
241
+ },
242
+ {
243
+ name: "content-type-hint",
244
+ enum: ["auto", "podcast", "tutorial", "demo", "generic"],
245
+ },
246
+ {
247
+ name: "tutorial-stack-ratio",
248
+ enum: ["screen_70_face_30", "screen_50_face_50"],
249
+ hidden: true,
250
+ },
251
+ "captions-enabled",
252
+ "caption-style-id",
253
+ {
254
+ name: "caption-word-reveal-animation",
255
+ enum: [
256
+ "none",
257
+ "fade",
258
+ "scale",
259
+ "slide-up",
260
+ "slide-left",
261
+ "slide-mix",
262
+ "pop",
263
+ "drop",
264
+ "slam",
265
+ "typewriter",
266
+ "glitch",
267
+ "mask-reveal",
268
+ "bounce-up",
269
+ "split-reveal",
270
+ ],
271
+ },
272
+ "caption-max-lines",
273
+ "caption-words-per-line",
274
+ "caption-page-combine-ms",
105
275
  ],
106
- description: "Choose a different suggestion or time range for a clip.",
276
+ description: "Regenerate a clip from a suggestion or time range with optional layout and caption settings.",
107
277
  mutates: true,
108
278
  }),
109
279
  commandSpec({
@@ -145,10 +315,101 @@ export const CLIPS_COMMAND_SPECS = [
145
315
  mutates: true,
146
316
  }),
147
317
  ];
318
+ function parseClipShareVisibility({ value, }) {
319
+ if (value === "visible" || value === "hidden") {
320
+ return value;
321
+ }
322
+ throw new CliUsageError({
323
+ code: "invalid_flag_value",
324
+ message: "--visibility must be visible or hidden.",
325
+ });
326
+ }
327
+ function printClipBatchResult({ context, result, helpers, }) {
328
+ helpers.printResult({
329
+ json: context.json,
330
+ value: result,
331
+ human: [
332
+ `Batch ${result.batch.batch_id}: ${result.batch.status}`,
333
+ `Studio Sound: ${result.batch.studio_sound.status}`,
334
+ `Accepted: ${result.batch.rendered_count}/${result.batch.estimated_clip_count}`,
335
+ ...(result.batch.last_error_message === null
336
+ ? []
337
+ : [`Error: ${result.batch.last_error_message}`]),
338
+ ],
339
+ });
340
+ }
341
+ async function waitForClipBatch({ context, helpers, }) {
342
+ const batchId = helpers.requireStringFlag({
343
+ flags: context.args.flags,
344
+ name: "batch-id",
345
+ });
346
+ const intervalMs = helpers.getNumberFlag({
347
+ flags: context.args.flags,
348
+ name: "interval-ms",
349
+ }) ?? 3000;
350
+ const timeoutMs = helpers.getNumberFlag({
351
+ flags: context.args.flags,
352
+ name: "timeout-ms",
353
+ }) ?? 20 * 60 * 1000;
354
+ const startedAt = Date.now();
355
+ while (true) {
356
+ const result = await context.client.clipBatches.get({ batchId });
357
+ if (result.batch.studio_sound.status === "failed") {
358
+ throw new CliOperationError({
359
+ code: "studio_sound_failed",
360
+ exitCode: 5,
361
+ message: `Clip batch ${batchId} failed Studio Sound: ${result.batch.studio_sound.error_message ?? result.batch.last_error_message ?? "treatment was not applied"}`,
362
+ retryable: false,
363
+ suggestions: [
364
+ `blitzreels clip-batches recover --batch-id ${batchId} --idempotency-key NEW_KEY --json`,
365
+ ],
366
+ });
367
+ }
368
+ if (result.batch.status === "failed") {
369
+ throw new CliOperationError({
370
+ code: "clip_batch_failed",
371
+ exitCode: 5,
372
+ message: `Clip batch ${batchId} failed: ${result.batch.last_error_message ?? result.batch.studio_sound.error_message ?? "clip processing failed"}`,
373
+ retryable: false,
374
+ suggestions: [
375
+ `blitzreels clip-batches recover --batch-id ${batchId} --idempotency-key NEW_KEY --json`,
376
+ ],
377
+ });
378
+ }
379
+ if (result.batch.status === "completed_with_errors") {
380
+ throw new CliOperationError({
381
+ code: "clip_batch_incomplete",
382
+ exitCode: 5,
383
+ message: `Clip batch ${batchId} completed with ${result.batch.failed_count + result.batch.review_count} clips needing recovery.`,
384
+ retryable: false,
385
+ suggestions: [
386
+ `blitzreels clip-batches recover --batch-id ${batchId} --idempotency-key NEW_KEY --json`,
387
+ ],
388
+ });
389
+ }
390
+ if (result.batch.status === "completed") {
391
+ return result;
392
+ }
393
+ if (Date.now() - startedAt >= timeoutMs) {
394
+ throw new CliOperationError({
395
+ code: "clip_batch_wait_timeout",
396
+ exitCode: 7,
397
+ message: `Timed out waiting for clip batch ${batchId}.`,
398
+ retryable: true,
399
+ suggestions: [
400
+ `blitzreels clip-batches get --batch-id ${batchId} --json`,
401
+ ],
402
+ });
403
+ }
404
+ await helpers.sleep({ ms: intervalMs });
405
+ }
406
+ }
148
407
  export function createClipsCommandOperation({ helpers, }) {
149
408
  return {
150
409
  name: "clips",
151
- matches: ({ command }) => command[0] === "clips",
410
+ matches: ({ command }) => command[0] === "clips" ||
411
+ command[0] === "clip-batches" ||
412
+ command[0] === "clip-share-pages",
152
413
  handle: ({ context }) => handleClipsCommand({ context, helpers }),
153
414
  buildDryRun: ({ context, spec }) => buildClipsDryRun({ context, spec, helpers }),
154
415
  };
@@ -156,6 +417,139 @@ export function createClipsCommandOperation({ helpers, }) {
156
417
  async function handleClipsCommand({ context, helpers, }) {
157
418
  const action = context.args.command[1];
158
419
  const flags = context.args.flags;
420
+ if (context.args.command[0] === "clip-share-pages") {
421
+ const assetId = helpers.requireStringFlag({ flags, name: "asset-id" });
422
+ if (action === "get") {
423
+ const result = await context.client.clipSharePages.get({ assetId });
424
+ helpers.printResult({
425
+ json: context.json,
426
+ value: result,
427
+ human: [
428
+ result.enabled
429
+ ? `Published: ${result.share_url}`
430
+ : "Share page is not published.",
431
+ `Hidden clips: ${result.hidden_clip_ids.length}`,
432
+ ],
433
+ });
434
+ return true;
435
+ }
436
+ if (action === "publish" || action === "unpublish") {
437
+ const result = await context.client.clipSharePages.setEnabled({
438
+ assetId,
439
+ enabled: action === "publish",
440
+ idempotencyKey: helpers.requireStringFlag({
441
+ flags,
442
+ name: "idempotency-key",
443
+ }),
444
+ });
445
+ helpers.printResult({
446
+ json: context.json,
447
+ value: result,
448
+ human: [
449
+ result.enabled
450
+ ? `Published: ${result.share_url}`
451
+ : "Share page unpublished.",
452
+ ],
453
+ });
454
+ return true;
455
+ }
456
+ if (action === "set-clip-visibility") {
457
+ const visibility = parseClipShareVisibility({
458
+ value: helpers.requireStringFlag({
459
+ flags,
460
+ name: "visibility",
461
+ }),
462
+ });
463
+ const result = await context.client.clipSharePages.setClipVisibility({
464
+ assetId,
465
+ clipId: helpers.requireStringFlag({ flags, name: "clip-id" }),
466
+ visibility,
467
+ idempotencyKey: helpers.requireStringFlag({
468
+ flags,
469
+ name: "idempotency-key",
470
+ }),
471
+ });
472
+ helpers.printResult({
473
+ json: context.json,
474
+ value: result,
475
+ human: [`Clip ${result.clip_id}: ${result.visibility}`],
476
+ });
477
+ return true;
478
+ }
479
+ }
480
+ if (context.args.command[0] === "clip-batches" && action === "get") {
481
+ const result = await context.client.clipBatches.get({
482
+ batchId: helpers.requireStringFlag({ flags, name: "batch-id" }),
483
+ });
484
+ printClipBatchResult({ context, result, helpers });
485
+ return true;
486
+ }
487
+ if (context.args.command[0] === "clip-batches" && action === "wait") {
488
+ const result = await waitForClipBatch({ context, helpers });
489
+ printClipBatchResult({ context, result, helpers });
490
+ return true;
491
+ }
492
+ if (context.args.command[0] === "clip-batches" && action === "recover") {
493
+ const result = await context.client.clipBatches.recover({
494
+ batchId: helpers.requireStringFlag({ flags, name: "batch-id" }),
495
+ targetClipCount: helpers.getNumberFlag({ flags, name: "target-count" }) ?? null,
496
+ idempotencyKey: helpers.requireStringFlag({
497
+ flags,
498
+ name: "idempotency-key",
499
+ }),
500
+ });
501
+ if (result.status === "failed") {
502
+ throw new CliOperationError({
503
+ code: "clip_batch_recovery_failed",
504
+ exitCode: 5,
505
+ message: `Recovery batch ${result.recovery_batch_id ?? result.source_batch_id} failed: ${result.queue_reason ?? "clip recovery failed"}`,
506
+ retryable: false,
507
+ suggestions: [
508
+ 'blitzreels agent-context --command "clip-batches recover" --json',
509
+ ],
510
+ });
511
+ }
512
+ const statusLine = result.status === "satisfied"
513
+ ? `Batch ${result.source_batch_id} already has ${result.retained_clip_count}/${result.target_clip_count} accepted clips.`
514
+ : result.status === "queued"
515
+ ? `Recovery batch ${result.recovery_batch_id} queued for ${result.refill_clip_count} missing clips.`
516
+ : `Recovery batch ${result.recovery_batch_id} created; waiting: ${result.queue_reason ?? "not ready to queue"}.`;
517
+ helpers.printResult({
518
+ json: context.json,
519
+ value: result,
520
+ human: [
521
+ statusLine,
522
+ `Folder: ${result.project_folder_id}`,
523
+ "Duration policy: quality first; suggestion boundaries preserved.",
524
+ ],
525
+ });
526
+ return true;
527
+ }
528
+ if (context.args.command[0] === "clip-batches" && action === "delete") {
529
+ if (helpers.getOptionalBooleanFlag({ flags, name: "confirm-delete" }) !== true) {
530
+ throw new Error("Pass --confirm-delete to delete a clip batch.");
531
+ }
532
+ const result = await context.client.clipBatches.delete({
533
+ batchId: helpers.requireStringFlag({ flags, name: "batch-id" }),
534
+ retention: parseClipBatchDeleteRetention({
535
+ value: helpers.getStringFlag({ flags, name: "retention" }),
536
+ }),
537
+ dryRun: false,
538
+ confirmDelete: true,
539
+ idempotencyKey: helpers.requireStringFlag({
540
+ flags,
541
+ name: "idempotency-key",
542
+ }),
543
+ });
544
+ helpers.printResult({
545
+ json: context.json,
546
+ value: result,
547
+ human: [
548
+ `Deleted batch ${result.batch_id}: ${result.deleted_clip_count} clips, ${result.deleted_project_count} projects, ${result.deleted_export_count} exports.`,
549
+ ],
550
+ });
551
+ return true;
552
+ }
159
553
  if (action === "create") {
160
554
  const result = await createClipWorkflow({ context, helpers });
161
555
  helpers.printResult({
@@ -173,6 +567,66 @@ async function handleClipsCommand({ context, helpers, }) {
173
567
  });
174
568
  return true;
175
569
  }
570
+ if (action === "attach-export") {
571
+ if (helpers.getOptionalBooleanFlag({
572
+ flags,
573
+ name: "confirm-visual-qa",
574
+ }) !== true) {
575
+ throw new CliUsageError({
576
+ code: "missing_visual_qa_confirmation",
577
+ message: "Pass --confirm-visual-qa only after reviewing the completed MP4.",
578
+ usage: "blitzreels clips attach-export --asset-id ID --project-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY",
579
+ });
580
+ }
581
+ const result = await context.client.clips.attachExport({
582
+ assetId: helpers.requireStringFlag({ flags, name: "asset-id" }),
583
+ projectId: helpers.requireStringFlag({ flags, name: "project-id" }),
584
+ exportId: helpers.requireStringFlag({ flags, name: "export-id" }),
585
+ confirmVisualQa: true,
586
+ idempotencyKey: helpers.requireStringFlag({
587
+ flags,
588
+ name: "idempotency-key",
589
+ }),
590
+ });
591
+ helpers.printResult({
592
+ json: context.json,
593
+ value: result,
594
+ human: [
595
+ `Attached export ${result.export_id} to managed clip ${result.clip_id}.`,
596
+ ],
597
+ });
598
+ return true;
599
+ }
600
+ if (action === "promote-export") {
601
+ if (helpers.getOptionalBooleanFlag({
602
+ flags,
603
+ name: "confirm-visual-qa",
604
+ }) !== true) {
605
+ throw new CliUsageError({
606
+ code: "missing_visual_qa_confirmation",
607
+ message: "Pass --confirm-visual-qa only after reviewing the completed MP4.",
608
+ usage: "blitzreels clips promote-export --clip-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY",
609
+ });
610
+ }
611
+ const result = await context.client.clips.promoteExport({
612
+ clipId: helpers.requireStringFlag({ flags, name: "clip-id" }),
613
+ exportId: helpers.requireStringFlag({ flags, name: "export-id" }),
614
+ confirmVisualQa: true,
615
+ idempotencyKey: helpers.requireStringFlag({
616
+ flags,
617
+ name: "idempotency-key",
618
+ }),
619
+ });
620
+ helpers.printResult({
621
+ json: context.json,
622
+ value: result,
623
+ human: [
624
+ `Promoted export ${result.export_id} for managed clip ${result.clip_id}.`,
625
+ `Previous export retained: ${result.previous_export_id}.`,
626
+ ],
627
+ });
628
+ return true;
629
+ }
176
630
  if (action === "list") {
177
631
  const result = await context.client.clips.list({
178
632
  assetId: helpers.getStringFlag({ flags, name: "asset-id" }),
@@ -216,6 +670,8 @@ async function handleClipsCommand({ context, helpers, }) {
216
670
  flags,
217
671
  name: "selection-mode",
218
672
  });
673
+ const layout = buildReselectLayout({ flags, helpers });
674
+ const captions = buildReselectCaptions({ flags, helpers });
219
675
  const result = await context.client.clips.reselect({
220
676
  clipId,
221
677
  ...(selectionMode === undefined
@@ -224,10 +680,14 @@ async function handleClipsCommand({ context, helpers, }) {
224
680
  startSeconds: selection.startSeconds,
225
681
  endSeconds: selection.endSeconds,
226
682
  target,
683
+ layout,
684
+ captions,
227
685
  }
228
686
  : {
229
687
  selection,
230
688
  target,
689
+ layout,
690
+ captions,
231
691
  }),
232
692
  });
233
693
  helpers.printClipResult({ context, result });
@@ -299,6 +759,143 @@ function formatClipListLine({ clip }) {
299
759
  function buildClipsDryRun({ context, spec, helpers, }) {
300
760
  const action = context.args.command[1];
301
761
  const flags = context.args.flags;
762
+ if (context.args.command[0] === "clips" && action === "attach-export") {
763
+ if (helpers.getOptionalBooleanFlag({
764
+ flags,
765
+ name: "confirm-visual-qa",
766
+ }) !== true) {
767
+ throw new CliUsageError({
768
+ code: "missing_visual_qa_confirmation",
769
+ message: "Pass --confirm-visual-qa only after reviewing the completed MP4.",
770
+ usage: "blitzreels clips attach-export --asset-id ID --project-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY",
771
+ });
772
+ }
773
+ return helpers.dryRunRequest({
774
+ spec,
775
+ method: "POST",
776
+ path: "/clips/attach-export",
777
+ headers: {
778
+ "Idempotency-Key": helpers.requireStringFlag({
779
+ flags,
780
+ name: "idempotency-key",
781
+ }),
782
+ },
783
+ body: {
784
+ asset_id: helpers.requireStringFlag({ flags, name: "asset-id" }),
785
+ project_id: helpers.requireStringFlag({ flags, name: "project-id" }),
786
+ export_id: helpers.requireStringFlag({ flags, name: "export-id" }),
787
+ confirm_visual_qa: true,
788
+ },
789
+ });
790
+ }
791
+ if (context.args.command[0] === "clips" && action === "promote-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 promote-export --clip-id ID --export-id ID --confirm-visual-qa --idempotency-key KEY",
800
+ });
801
+ }
802
+ const clipId = helpers.requireStringFlag({ flags, name: "clip-id" });
803
+ return helpers.dryRunRequest({
804
+ spec,
805
+ method: "POST",
806
+ path: `/clips/${encodeURIComponent(clipId)}/promote-export`,
807
+ headers: {
808
+ "Idempotency-Key": helpers.requireStringFlag({
809
+ flags,
810
+ name: "idempotency-key",
811
+ }),
812
+ },
813
+ body: {
814
+ export_id: helpers.requireStringFlag({ flags, name: "export-id" }),
815
+ confirm_visual_qa: true,
816
+ },
817
+ });
818
+ }
819
+ if (context.args.command[0] === "clip-share-pages") {
820
+ const assetId = helpers.requireStringFlag({ flags, name: "asset-id" });
821
+ if (action === "publish" || action === "unpublish") {
822
+ return helpers.dryRunRequest({
823
+ spec,
824
+ method: "POST",
825
+ path: `/clip-share-pages/${encodeURIComponent(assetId)}`,
826
+ headers: {
827
+ "Idempotency-Key": helpers.requireStringFlag({
828
+ flags,
829
+ name: "idempotency-key",
830
+ }),
831
+ },
832
+ body: { enabled: action === "publish" },
833
+ });
834
+ }
835
+ if (action === "set-clip-visibility") {
836
+ const clipId = helpers.requireStringFlag({ flags, name: "clip-id" });
837
+ return helpers.dryRunRequest({
838
+ spec,
839
+ method: "POST",
840
+ path: `/clip-share-pages/${encodeURIComponent(assetId)}/clips/${encodeURIComponent(clipId)}/visibility`,
841
+ headers: {
842
+ "Idempotency-Key": helpers.requireStringFlag({
843
+ flags,
844
+ name: "idempotency-key",
845
+ }),
846
+ },
847
+ body: {
848
+ visibility: parseClipShareVisibility({
849
+ value: helpers.requireStringFlag({
850
+ flags,
851
+ name: "visibility",
852
+ }),
853
+ }),
854
+ },
855
+ });
856
+ }
857
+ }
858
+ if (context.args.command[0] === "clip-batches" && action === "recover") {
859
+ return helpers.dryRunRequest({
860
+ spec,
861
+ method: "POST",
862
+ path: `/clip-batches/${encodeURIComponent(helpers.requireStringFlag({ flags, name: "batch-id" }))}/recover`,
863
+ headers: {
864
+ "Idempotency-Key": helpers.requireStringFlag({
865
+ flags,
866
+ name: "idempotency-key",
867
+ }),
868
+ },
869
+ body: {
870
+ target_clip_count: helpers.getNumberFlag({ flags, name: "target-count" }) ?? null,
871
+ },
872
+ notes: [
873
+ "Accepted exports remain linked; only the missing accepted count is refilled.",
874
+ "Suggestion boundaries are preserved even when a coherent clip exceeds the target duration.",
875
+ ],
876
+ });
877
+ }
878
+ if (context.args.command[0] === "clip-batches" && action === "delete") {
879
+ return helpers.dryRunRequest({
880
+ spec,
881
+ method: "DELETE",
882
+ path: `/clip-batches/${encodeURIComponent(helpers.requireStringFlag({ flags, name: "batch-id" }))}`,
883
+ query: {
884
+ retention: parseClipBatchDeleteRetention({
885
+ value: helpers.getStringFlag({ flags, name: "retention" }),
886
+ }),
887
+ dry_run: true,
888
+ confirm_delete: false,
889
+ },
890
+ headers: {
891
+ "Idempotency-Key": helpers.requireStringFlag({
892
+ flags,
893
+ name: "idempotency-key",
894
+ }),
895
+ },
896
+ body: undefined,
897
+ });
898
+ }
302
899
  if (action === "list") {
303
900
  return helpers.dryRunRequest({
304
901
  spec,
@@ -328,8 +925,9 @@ function buildClipsDryRun({ context, spec, helpers, }) {
328
925
  quality: null,
329
926
  },
330
927
  clip_preset_id: options.clipPresetId,
928
+ max_clip_count: options.count,
331
929
  caption_theme_id: options.captionThemeId,
332
- create_flow_options: {},
930
+ create_flow_options: buildClipWorkflowCreateFlowOptions({ options }),
333
931
  branding_options: buildClipWorkflowBrandingOptions({
334
932
  logoAssetId: options.logoAssetId,
335
933
  }),
@@ -362,6 +960,8 @@ function buildClipsDryRun({ context, spec, helpers, }) {
362
960
  flags,
363
961
  name: "selection-mode",
364
962
  });
963
+ const layout = buildReselectLayout({ flags, helpers });
964
+ const captions = buildReselectCaptions({ flags, helpers });
365
965
  return helpers.dryRunRequest({
366
966
  spec,
367
967
  method: "POST",
@@ -385,6 +985,12 @@ function buildClipsDryRun({ context, spec, helpers, }) {
385
985
  max_duration_seconds: target.maxDurationSeconds,
386
986
  },
387
987
  }),
988
+ ...(layout === null
989
+ ? {}
990
+ : { layout: toApiClipLayout({ layout }) }),
991
+ ...(captions === null
992
+ ? {}
993
+ : { captions: toApiClipCaptions({ captions }) }),
388
994
  }
389
995
  : {
390
996
  selection: toApiClipSelection({ selection }),
@@ -394,6 +1000,8 @@ function buildClipsDryRun({ context, spec, helpers, }) {
394
1000
  min_duration_seconds: target.minDurationSeconds,
395
1001
  max_duration_seconds: target.maxDurationSeconds,
396
1002
  },
1003
+ layout: layout === null ? null : toApiClipLayout({ layout }),
1004
+ captions: captions === null ? null : toApiClipCaptions({ captions }),
397
1005
  },
398
1006
  });
399
1007
  }
@@ -428,6 +1036,25 @@ function buildClipsDryRun({ context, spec, helpers, }) {
428
1036
  }
429
1037
  return undefined;
430
1038
  }
1039
+ function parseClipBatchDeleteRetention({ value, }) {
1040
+ if (value === undefined || value === "retain_projects_and_exports") {
1041
+ return "retain_projects_and_exports";
1042
+ }
1043
+ if (value === "delete_projects_and_exports") {
1044
+ return value;
1045
+ }
1046
+ throw new Error("--retention must be retain_projects_and_exports or delete_projects_and_exports.");
1047
+ }
1048
+ function buildClipWorkflowCreateFlowOptions({ options, }) {
1049
+ return {
1050
+ ...(options.layoutMode === undefined
1051
+ ? {}
1052
+ : { reframeLayoutMode: options.layoutMode }),
1053
+ ...(options.contentTypeHint === undefined
1054
+ ? {}
1055
+ : { reframeContentTypeHint: options.contentTypeHint }),
1056
+ };
1057
+ }
431
1058
  function parseClipWorkflowOptions({ flags, helpers, }) {
432
1059
  const count = helpers.getNumberFlag({ flags, name: "count" }) ?? 5;
433
1060
  if (!Number.isInteger(count) || count < 1 || count > 10) {
@@ -467,6 +1094,12 @@ function parseClipWorkflowOptions({ flags, helpers, }) {
467
1094
  captionThemeId: parseNullableWorkflowString({
468
1095
  value: helpers.getStringFlag({ flags, name: "caption-theme-id" }),
469
1096
  }),
1097
+ layoutMode: parseClipLayoutMode({
1098
+ value: helpers.getStringFlag({ flags, name: "layout-mode" }),
1099
+ }),
1100
+ contentTypeHint: parseClipContentTypeHint({
1101
+ value: helpers.getStringFlag({ flags, name: "content-type-hint" }),
1102
+ }),
470
1103
  idempotencyKey: helpers.getStringFlag({ flags, name: "idempotency-key" }) ?? null,
471
1104
  };
472
1105
  }
@@ -501,8 +1134,9 @@ async function createClipWorkflow({ context, helpers, }) {
501
1134
  quality: null,
502
1135
  },
503
1136
  clipPresetId: options.clipPresetId,
1137
+ maxClipCount: options.count,
504
1138
  captionThemeId: options.captionThemeId,
505
- createFlowOptions: {},
1139
+ createFlowOptions: buildClipWorkflowCreateFlowOptions({ options }),
506
1140
  brandingOptions: buildClipWorkflowBrandingOptions({
507
1141
  logoAssetId: options.logoAssetId,
508
1142
  }),
@@ -622,4 +1256,138 @@ function toApiClipSelection({ selection, }) {
622
1256
  end_seconds: selection.endSeconds,
623
1257
  };
624
1258
  }
1259
+ function parseClipLayoutMode({ value, }) {
1260
+ if (value === undefined ||
1261
+ value === "auto" ||
1262
+ value === "people_first" ||
1263
+ value === "screen_first" ||
1264
+ value === "preserve_full_source" ||
1265
+ value === "prefer_split" ||
1266
+ value === "prefer_focus" ||
1267
+ value === "prefer_tutorial" ||
1268
+ value === "prefer_demo") {
1269
+ return value;
1270
+ }
1271
+ throw new Error(`Unsupported clip layout mode: ${value}`);
1272
+ }
1273
+ function parseClipContentTypeHint({ value, }) {
1274
+ if (value === undefined ||
1275
+ value === "auto" ||
1276
+ value === "podcast" ||
1277
+ value === "tutorial" ||
1278
+ value === "demo" ||
1279
+ value === "generic") {
1280
+ return value;
1281
+ }
1282
+ throw new Error(`Unsupported clip content type hint: ${value}`);
1283
+ }
1284
+ function parseTutorialStackRatio({ value, }) {
1285
+ if (value === undefined ||
1286
+ value === "screen_70_face_30" ||
1287
+ value === "screen_50_face_50") {
1288
+ return value;
1289
+ }
1290
+ throw new Error(`Unsupported tutorial stack ratio: ${value}`);
1291
+ }
1292
+ function buildReselectLayout({ flags, helpers, }) {
1293
+ const mode = parseClipLayoutMode({
1294
+ value: helpers.getStringFlag({ flags, name: "layout-mode" }),
1295
+ });
1296
+ const contentTypeHint = parseClipContentTypeHint({
1297
+ value: helpers.getStringFlag({ flags, name: "content-type-hint" }),
1298
+ });
1299
+ const tutorialStackRatio = parseTutorialStackRatio({
1300
+ value: helpers.getStringFlag({ flags, name: "tutorial-stack-ratio" }),
1301
+ });
1302
+ if (mode === undefined &&
1303
+ contentTypeHint === undefined &&
1304
+ tutorialStackRatio === undefined) {
1305
+ return null;
1306
+ }
1307
+ return { mode, contentTypeHint, tutorialStackRatio };
1308
+ }
1309
+ function buildReselectCaptions({ flags, helpers, }) {
1310
+ const enabled = helpers.getOptionalBooleanFlag({
1311
+ flags,
1312
+ name: "captions-enabled",
1313
+ });
1314
+ const styleId = helpers.getStringFlag({
1315
+ flags,
1316
+ name: "caption-style-id",
1317
+ });
1318
+ const wordRevealAnimation = parseCaptionWordRevealAnimation({
1319
+ value: helpers.getStringFlag({
1320
+ flags,
1321
+ name: "caption-word-reveal-animation",
1322
+ }),
1323
+ });
1324
+ const maxLinesPerCaption = helpers.getNumberFlag({
1325
+ flags,
1326
+ name: "caption-max-lines",
1327
+ });
1328
+ const wordsPerLineLimit = helpers.getNumberFlag({
1329
+ flags,
1330
+ name: "caption-words-per-line",
1331
+ });
1332
+ const pageCombineMs = helpers.getNumberFlag({
1333
+ flags,
1334
+ name: "caption-page-combine-ms",
1335
+ });
1336
+ if (enabled === undefined &&
1337
+ styleId === undefined &&
1338
+ wordRevealAnimation === undefined &&
1339
+ maxLinesPerCaption === undefined &&
1340
+ wordsPerLineLimit === undefined &&
1341
+ pageCombineMs === undefined) {
1342
+ return null;
1343
+ }
1344
+ return {
1345
+ enabled,
1346
+ styleId,
1347
+ wordRevealAnimation,
1348
+ maxLinesPerCaption,
1349
+ wordsPerLineLimit,
1350
+ pageCombineMs,
1351
+ };
1352
+ }
1353
+ function parseCaptionWordRevealAnimation({ value, }) {
1354
+ const supported = [
1355
+ "none",
1356
+ "fade",
1357
+ "scale",
1358
+ "slide-up",
1359
+ "slide-left",
1360
+ "slide-mix",
1361
+ "pop",
1362
+ "drop",
1363
+ "slam",
1364
+ "typewriter",
1365
+ "glitch",
1366
+ "mask-reveal",
1367
+ "bounce-up",
1368
+ "split-reveal",
1369
+ ];
1370
+ if (value === undefined ||
1371
+ supported.some((candidate) => candidate === value)) {
1372
+ return value;
1373
+ }
1374
+ throw new Error(`Unsupported caption word reveal animation: ${value}`);
1375
+ }
1376
+ function toApiClipLayout({ layout }) {
1377
+ return {
1378
+ layout_mode: layout.mode,
1379
+ content_type_hint: layout.contentTypeHint,
1380
+ tutorial_stack_ratio: layout.tutorialStackRatio,
1381
+ };
1382
+ }
1383
+ function toApiClipCaptions({ captions }) {
1384
+ return {
1385
+ enabled: captions.enabled,
1386
+ style_id: captions.styleId,
1387
+ word_reveal_animation: captions.wordRevealAnimation,
1388
+ max_lines_per_caption: captions.maxLinesPerCaption,
1389
+ words_per_line_limit: captions.wordsPerLineLimit,
1390
+ page_combine_ms: captions.pageCombineMs,
1391
+ };
1392
+ }
625
1393
  //# sourceMappingURL=clips.js.map