makaron-cli 0.13.7 → 0.13.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.13.7",
3
+ "version": "0.13.9",
4
4
  "description": "Give Claude Code a creative agent. Pass complete creative requests and source media to Makaron Chat.",
5
5
  "author": {
6
6
  "name": "Versa AI",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.13.7",
3
+ "version": "0.13.9",
4
4
  "description": "Give Codex a creative agent. Pass complete creative requests and source media to Makaron Chat.",
5
5
  "author": {
6
6
  "name": "Versa AI",
package/README.md CHANGED
@@ -140,6 +140,20 @@ npx makaron-cli chat --project auto --image selfie.jpg --skill <marketplace-id-o
140
140
 
141
141
  `--skill` accepts an installed skill name, a marketplace UUID, or a unique marketplace label. If a marketplace skill is matched, the CLI installs or reuses it and sends `[Active skill: <installed-skill-name>]` to Makaron Agent. Ordinary users do not need admin commands, and the CLI intentionally does not expose skill deletion.
142
142
 
143
+ ### Built-in production skills
144
+
145
+ Discover the available production workflows before choosing `--skill`:
146
+
147
+ ```bash
148
+ npx makaron-cli skills list --built-in
149
+ npx makaron-cli skills search "talking head captions" --built-in
150
+ npx makaron-cli skills show talking-head --built-in
151
+ npx makaron-cli chat --project auto --video talk.mp4 --skill talking-head -b "make a tight captioned edit"
152
+ ```
153
+
154
+ The default list shows callable/discoverable skills with their purpose and input
155
+ requirements. Add `--all` to include internal adapters and helper skills.
156
+
143
157
  ### With additional images (existing project)
144
158
 
145
159
  ```bash
@@ -246,7 +260,7 @@ npx makaron-cli chat --project <id> --video party.mp4 --image kid.jpg -b "make t
246
260
  npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "splice these into one seamless video"
247
261
  ```
248
262
 
249
- Video files are uploaded via signed URL. CLI local video uploads support `.mp4`, `.mov`, or `.webm`, max 50MB, max 120s with 1s metadata tolerance, and <=1080p / 2,086,876 frame pixels. The frontend can transcode larger videos before upload; the CLI uploads directly to Storage and rejects videos above those limits.
263
+ Video files are uploaded via signed URL. CLI local video uploads support `.mp4`, `.mov`, or `.webm`, max 50MB, max 900s (15 minutes) with 1s metadata tolerance, and <=1080p / 2,086,876 frame pixels. The frontend can transcode larger videos before upload; the CLI uploads directly to Storage and rejects videos above those limits.
250
264
  The agent understands video content natively — it can analyze scenes, edit, extend, and compose videos. Seedance video-reference editing is still limited to ~15s provider references, so longer uploaded videos should be split/prepared by the agent before model submission; Kling remains the base/direct edit path.
251
265
  Use `chat --project <id|auto> --video ...` for any project/timeline video work. Direct `video create` is standalone and does not write timeline entries.
252
266
 
@@ -363,7 +377,7 @@ For project/timeline video editing, use:
363
377
  npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
364
378
  ```
365
379
 
366
- Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to the selected model limit), `--video <file|url>` and `--audio <file|url>` (repeatable where supported), `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|grok|google-omni|minimax-h3`, `--video-resolution auto|480p|720p|768p|1080p|2k|4k`. Default model is `seedance-fast`. SeeDance accepts native text-to-video with no image and integer output duration 4-15s (default 5s); `seedance-mini` supports 480p/720p and is best for cheaper drafts/multi-size tests; MiniMax H3 accepts native text-to-video, 4-15s output, public 768p/2k resolution, and up to 9 image, up to 3 video, and up to 3 audio references through Makaron Agent/chat. H3 defaults to 768p; request 2k explicitly for maximum/final quality. Kling supports 5-15s; Grok 1.5 supports 1-15s single-image-to-video only; Gemini Omni supports 3-10s fast 720p image/video generation and editing with native generated audio, including up to 6 image references when no video reference is provided. For `--video-model grok`, forced `--aspect` is ignored to avoid xAI stretching the source image; pad/create the image at the target shape first or use another model.
380
+ Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to the selected model limit), `--video <file|url>` and `--audio <file|url>` (repeatable where supported), `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|grok|google-omni|minimax-h3|sync-lipsync-v3`, `--video-resolution auto|480p|720p|768p|1080p|2k|4k`. Default model is `seedance-fast`. SeeDance accepts native text-to-video with no image and integer output duration 4-15s (default 5s); `seedance-mini` supports 480p/720p and is best for cheaper drafts/multi-size tests; MiniMax H3 accepts native text-to-video, 4-15s output, public 768p/2k resolution, and up to 9 image, up to 3 video, and up to 3 audio references through Makaron Agent/chat. `sync-lipsync-v3` requires exactly one video plus one MP3/WAV and preserves that replacement audio while aligning the mouth. H3 defaults to 768p; request 2k explicitly for maximum/final quality. Kling supports 5-15s; Grok 1.5 supports 1-15s single-image-to-video only; Gemini Omni supports 3-10s fast 720p image/video generation and editing with native generated audio, including up to 6 image references when no video reference is provided. For `--video-model grok`, forced `--aspect` is ignored to avoid xAI stretching the source image; pad/create the image at the target shape first or use another model.
367
381
 
368
382
  Seedance 2.5: use `--video-model seedance-2.5` for 4-30 second output at 480p/720p. `--image` accepts local files or URLs (up to 30), while repeatable `--video` and `--audio` accept up to 10 each. Use `--video-operation generate|edit|extend`, `--extend-direction forward|backward`, `--output-format mp4|mov`, `--web-search`, `--generated-audio` / `--no-generated-audio`, and `--relaxed-content-filter`. Edit/extend require a video reference. The Evolink route does not currently expose 4K output.
369
383
 
@@ -414,6 +428,7 @@ type CompletionAction = {
414
428
  3. Stop when `status` is `"completed"`, `"failed"`, or `"aborted"`
415
429
  4. Top-level `status: "completed"` means ALL artifacts are ready (including rendered videos)
416
430
  5. If an async video fails, top-level `status` is `"failed"` and the failed video may include `completion_actions` for a safe retry or diagnosis. Agents can surface these as the next user-confirmed step.
431
+ 6. `responses get --wait` reconciles pending video output against completed Project Media with the same `snapshot_id` or `task_id`. A lagging Run row therefore does not block delivery after the durable project video is ready.
417
432
 
418
433
  ## Exit Codes
419
434
 
@@ -494,6 +509,15 @@ send_message "All done!"
494
509
 
495
510
  Admin commands require an API key with admin privileges. Ask your admin to run `makaron admin set-admin <your-email>` to grant access.
496
511
 
512
+ ### Add credits to a user
513
+
514
+ Use either the account email address or the Supabase user UUID. Credits must be a positive integer.
515
+
516
+ ```bash
517
+ npx makaron-cli admin add-credits user@example.com 1000
518
+ npx makaron-cli admin add-credits <user-id> 1000 --json
519
+ ```
520
+
497
521
  ### List all marketplace skills
498
522
 
499
523
  ```bash
package/bin/makaron.mjs CHANGED
@@ -43,7 +43,7 @@ const SUPABASE_ANON_KEY = 'sb_publishable_FJFN2YYaWaQjABUKLqxQcA_fhxPLFDY';
43
43
 
44
44
  const MAX_VIDEO_UPLOAD_FILE_SIZE_MB = 50;
45
45
  const MAX_VIDEO_UPLOAD_FILE_SIZE = MAX_VIDEO_UPLOAD_FILE_SIZE_MB * 1024 * 1024;
46
- const MAX_VIDEO_UPLOAD_DURATION = 120;
46
+ const MAX_VIDEO_UPLOAD_DURATION = 900;
47
47
  const MAX_VIDEO_UPLOAD_DURATION_TOLERANCE = 1;
48
48
  const MAX_VIDEO_PROVIDER_REFERENCE_DURATION = 15;
49
49
  const MAX_VIDEO_PROVIDER_REFERENCE_DURATION_TOLERANCE = 0.5;
@@ -328,6 +328,86 @@ function normalizeRunResponse(data) {
328
328
  return data;
329
329
  }
330
330
 
331
+ function projectMediaIdentity(item) {
332
+ const snapshotId = item?.snapshot_id || item?.snapshotId;
333
+ const taskId = item?.task_id || item?.taskId;
334
+ return {
335
+ snapshotId: typeof snapshotId === 'string' && snapshotId ? snapshotId : null,
336
+ taskId: typeof taskId === 'string' && taskId ? taskId : null,
337
+ };
338
+ }
339
+
340
+ function findMatchingCompletedProjectVideo(item, media) {
341
+ const identity = projectMediaIdentity(item);
342
+ if (!identity.snapshotId && !identity.taskId) return null;
343
+ return (media || []).find(candidate => {
344
+ if (candidate?.type !== 'video' || candidate?.status !== 'completed' || !candidate?.url) return false;
345
+ const candidateIdentity = projectMediaIdentity(candidate);
346
+ return (identity.snapshotId && candidateIdentity.snapshotId === identity.snapshotId)
347
+ || (identity.taskId && candidateIdentity.taskId === identity.taskId);
348
+ }) || null;
349
+ }
350
+
351
+ async function reconcileRunWithProjectMedia(baseUrl, headers, data) {
352
+ normalizeRunResponse(data);
353
+ const output = Array.isArray(data.output) ? data.output : [];
354
+ const pendingVideos = output.filter(item =>
355
+ item?.type === 'video' && (item.status === 'queued' || item.status === 'rendering')
356
+ );
357
+ const agentDone = data.agent_status === 'completed'
358
+ || (data.status === 'in_progress' && data.incomplete === true);
359
+ if (!agentDone || pendingVideos.length === 0 || !data.projectId) return data;
360
+
361
+ let projectMedia;
362
+ try {
363
+ const res = await fetch(`${baseUrl}/api/projects/${data.projectId}/media`, { headers });
364
+ if (!res.ok) return data;
365
+ projectMedia = await res.json();
366
+ } catch {
367
+ return data;
368
+ }
369
+
370
+ let reconciled = false;
371
+ for (const item of pendingVideos) {
372
+ const completed = findMatchingCompletedProjectVideo(item, projectMedia.media);
373
+ if (!completed) continue;
374
+ const identity = projectMediaIdentity(completed);
375
+ item.status = 'completed';
376
+ item.url = completed.url;
377
+ if (identity.snapshotId) item.snapshot_id = identity.snapshotId;
378
+ if (identity.taskId) item.task_id = identity.taskId;
379
+ if (completed.posterUrl) item.poster_url = completed.posterUrl;
380
+ for (const field of ['duration', 'width', 'height']) {
381
+ if (typeof completed[field] === 'number') item[field] = completed[field];
382
+ }
383
+
384
+ const legacyVideos = Array.isArray(data.result?.videos) ? data.result.videos : [];
385
+ const legacy = legacyVideos.find(video => {
386
+ const legacyIdentity = projectMediaIdentity(video);
387
+ return (identity.snapshotId && legacyIdentity.snapshotId === identity.snapshotId)
388
+ || (identity.taskId && legacyIdentity.taskId === identity.taskId);
389
+ });
390
+ if (legacy) {
391
+ legacy.status = 'completed';
392
+ legacy.videoUrl = completed.url;
393
+ if (identity.snapshotId && !legacy.snapshotId) legacy.snapshotId = identity.snapshotId;
394
+ }
395
+ reconciled = true;
396
+ }
397
+
398
+ if (!reconciled) return data;
399
+ const pendingArtifacts = output.some(item =>
400
+ (item?.type === 'video' || item?.type === 'music')
401
+ && (item.status === 'queued' || item.status === 'rendering')
402
+ );
403
+ if (!pendingArtifacts) {
404
+ data.status = 'completed';
405
+ data.incomplete = false;
406
+ delete data.next_poll_after_ms;
407
+ }
408
+ return data;
409
+ }
410
+
331
411
  function collectCompletionActions(data) {
332
412
  const items = [];
333
413
  const add = (action, source) => {
@@ -392,6 +472,11 @@ What you can ask:
392
472
  Marketplace skill
393
473
  makaron chat --project auto --image selfie.jpg --skill "Football Captain" "make this cinematic"
394
474
 
475
+ Built-in production skill
476
+ makaron skills list --built-in
477
+ makaron skills show talking-head --built-in
478
+ makaron chat --project auto --video talk.mp4 --skill talking-head "make a tight captioned edit"
479
+
395
480
  Fix one video moment from a screenshot
396
481
  makaron chat --project <id> --image screenshot.png "@4 this frame should be Paris; only fix this moment"
397
482
 
@@ -607,6 +692,7 @@ async function pollRun(baseUrl, headers, runId, opts = {}) {
607
692
  continue;
608
693
  }
609
694
  data = await res.json();
695
+ data = await reconcileRunWithProjectMedia(baseUrl, headers, data);
610
696
  } catch {
611
697
  continue;
612
698
  }
@@ -759,6 +845,7 @@ async function watchRun(baseUrl, headers, runId, opts = {}) {
759
845
  continue;
760
846
  }
761
847
  data = await res.json();
848
+ data = await reconcileRunWithProjectMedia(baseUrl, headers, data);
762
849
  } catch {
763
850
  await new Promise(r => setTimeout(r, interval));
764
851
  continue;
@@ -1174,12 +1261,29 @@ async function fetchBuiltInSkills(baseUrl) {
1174
1261
  return (data.skills || []).filter(skill => skill.builtIn);
1175
1262
  }
1176
1263
 
1177
- function printBuiltInSkills(skills) {
1264
+ function isDiscoverableBuiltInSkill(skill) {
1265
+ return skill.userSelectable !== false || skill.manifestVisible === true;
1266
+ }
1267
+
1268
+ function builtInSkillSearchText(skill) {
1269
+ return [
1270
+ skill.name,
1271
+ skill.label,
1272
+ skill.description,
1273
+ skill.studioRunRecipe,
1274
+ skill.studioRunProfile,
1275
+ skill.canonicalSkill,
1276
+ ...(Array.isArray(skill.tags) ? skill.tags : []),
1277
+ ].filter(Boolean).join(' ').toLowerCase();
1278
+ }
1279
+
1280
+ function printBuiltInSkills(skills, opts = {}) {
1178
1281
  if (!skills.length) {
1179
1282
  console.log('No built-in skills found.');
1180
1283
  return;
1181
1284
  }
1182
- console.log(`Built-in skills: ${skills.length}\n`);
1285
+ const heading = opts.heading || 'Built-in skills';
1286
+ console.log(`${heading}: ${skills.length}\n`);
1183
1287
  for (const skill of skills) {
1184
1288
  const recipe = skill.studioRunRecipe ? ` [Studio Run: ${skill.studioRunRecipe}]` : '';
1185
1289
  const source = skill.sourceMediaRequired ? ' [source media required]' : '';
@@ -1189,6 +1293,30 @@ function printBuiltInSkills(skills) {
1189
1293
  console.log(` ${skill.name}${recipe}${source}${adapter}`);
1190
1294
  if (skill.description) console.log(` ${String(skill.description).replace(/\s+/g, ' ').trim()}`);
1191
1295
  }
1296
+ if (opts.hint !== false) {
1297
+ console.log('\nInspect and use a skill:');
1298
+ console.log(' makaron skills show <name> --built-in');
1299
+ console.log(' makaron chat --project auto --skill <name> "your request"');
1300
+ }
1301
+ }
1302
+
1303
+ function printBuiltInSkill(skill) {
1304
+ const description = String(skill.description || '').replace(/\s+/g, ' ').trim();
1305
+ console.log(`${skill.label || skill.name} (${skill.name})`);
1306
+ if (description) console.log(`\nPurpose:\n ${description}`);
1307
+ console.log('\nBest input:');
1308
+ console.log(` ${skill.inputHint || (skill.sourceMediaRequired ? 'Source media is required. Attach it with --video, --image, or --audio as appropriate.' : 'Start from a clear brief; attach source media when the request depends on existing footage or assets.')}`);
1309
+ if (skill.studioRunRecipe || skill.studioRunProfile) {
1310
+ console.log('\nWorkflow:');
1311
+ if (skill.studioRunRecipe) console.log(` Studio Run recipe: ${skill.studioRunRecipe}`);
1312
+ if (skill.studioRunProfile) console.log(` Profile: ${skill.studioRunProfile}`);
1313
+ }
1314
+ if (Array.isArray(skill.tags) && skill.tags.length) {
1315
+ console.log(`\nKeywords:\n ${skill.tags.join(', ')}`);
1316
+ }
1317
+ console.log('\nUse with chat:');
1318
+ const media = skill.sourceMediaRequired ? ' --video <file>' : '';
1319
+ console.log(` makaron chat --project auto${media} --skill ${skill.name} "describe the result you want"`);
1192
1320
  }
1193
1321
 
1194
1322
  function marketplaceSearchText(skill) {
@@ -1745,7 +1873,7 @@ Commands:
1745
1873
  video script|create|status Video generation
1746
1874
  music create|status Music generation
1747
1875
 
1748
- admin Admin commands (skills, upload, set-admin)
1876
+ admin Admin commands (skills, credits, upload, set-admin)
1749
1877
 
1750
1878
  Examples:
1751
1879
  makaron chat --project auto "plan a launch poster"
@@ -1846,20 +1974,25 @@ function printHelp(topic, subtopic) {
1846
1974
  } else if (topic === 'install-skill') {
1847
1975
  console.log('Usage: makaron install-skill [--global] [--agent <agent>] [--yes]');
1848
1976
  } else if (topic === 'skills') {
1849
- if (subtopic === 'list') console.log('Usage: makaron skills list [--built-in] [--json]');
1850
- else if (subtopic === 'search') console.log('Usage: makaron skills search <query> [--json]');
1851
- else if (subtopic === 'show') console.log('Usage: makaron skills show <marketplace-id|label> [--json]');
1977
+ if (subtopic === 'list') console.log('Usage: makaron skills list [--built-in] [--all] [--json]');
1978
+ else if (subtopic === 'search') console.log('Usage: makaron skills search <query> [--built-in] [--all] [--json]');
1979
+ else if (subtopic === 'show') console.log('Usage: makaron skills show <id|label|name> [--built-in] [--json]');
1852
1980
  else if (subtopic === 'install') console.log('Usage: makaron skills install <marketplace-id|label> [--json]');
1853
1981
  else console.log(`Skill commands:
1854
- skills list --built-in List all built-in Makaron skills and Studio Run recipes
1982
+ skills list --built-in List user-facing built-in skills and what they do
1983
+ skills list --built-in --all Include internal/adapted helper skills
1855
1984
  skills list List marketplace skills
1856
1985
  skills search <query> Search marketplace skills
1986
+ skills search <query> --built-in Find a built-in skill by task or keyword
1857
1987
  skills show <id|label> --built-in Show a built-in skill
1858
1988
  skills show <id|label> Show a marketplace skill
1859
1989
  skills install <id|label> Install a marketplace skill to your workspace
1860
1990
 
1861
1991
  Use with chat:
1862
1992
  makaron chat --project auto --skill <id|label> "your request"
1993
+
1994
+ Not sure which built-in skill to use? Start with:
1995
+ makaron skills list --built-in
1863
1996
  `);
1864
1997
  } else if (topic === 'materialize') {
1865
1998
  console.log(`Usage: makaron materialize --project <id> (--media <N> | --snapshot <snapshotId> | --design-path <path> | --design-json <file|->) [--wait] [--publish|--no-publish] [--profile fast_720p|source] [--pick url|job_id|status]`);
@@ -1877,7 +2010,7 @@ Use with chat:
1877
2010
  console.log('Usage: makaron analyze --video <file|url> ["question"]');
1878
2011
  } else if (topic === 'video') {
1879
2012
  if (subtopic === 'script') console.log('Usage: makaron video script --image <file> [--image <file>] [--lang en|zh] "direction"');
1880
- else if (subtopic === 'create') console.log('Usage: makaron video create --script "..." [--image <url> ...] [--video <url> ...] [--audio <url> ...] [--duration 10] [--aspect 9:16] [--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|grok|google-omni|minimax-h3] [--operation generate|edit|extend] [--video-resolution auto|480p|720p|768p|1080p|2k|4k] [--keep-original-sound]');
2013
+ else if (subtopic === 'create') console.log('Usage: makaron video create --script "..." [--image <url> ...] [--video <url> ...] [--audio <url> ...] [--duration 10] [--aspect 9:16] [--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|grok|google-omni|minimax-h3|sync-lipsync-v3] [--operation generate|edit|extend] [--video-resolution auto|480p|720p|768p|1080p|2k|4k] [--keep-original-sound]');
1881
2014
  else if (subtopic === 'status') console.log('Usage: makaron video status <taskId> | --snapshot <snapshotId> [--wait]');
1882
2015
  else console.log(`Video commands:
1883
2016
  video script --image <file> [--image <file>] "direction" Write video script
@@ -1885,6 +2018,7 @@ Use with chat:
1885
2018
  video create --script "..." --video-model minimax-h3 MiniMax H3 text-to-video (default 768P)
1886
2019
  video create --script "..." --image <url> [--duration 10] Submit video task
1887
2020
  video create --script "..." --video <public-url> [--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|google-omni|minimax-h3] Edit/reference a video (Grok does not support video refs)
2021
+ video create --script "Use the supplied audio" --video <url> --audio <url> --video-model sync-lipsync-v3 Lip-sync exact replacement audio
1888
2022
  video status <taskId> Check video status
1889
2023
  video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
1890
2024
  `);
@@ -1900,6 +2034,7 @@ Use with chat:
1900
2034
  else if (subtopic === 'skill-categories') console.log('Usage: makaron admin skill-categories [--json|add|update|delete] ...');
1901
2035
  else if (subtopic === 'upload') console.log('Usage: makaron admin upload <local-file> <storage-path>');
1902
2036
  else if (subtopic === 'fetch-skill') console.log('Usage: makaron admin fetch-skill <share-code|url>');
2037
+ else if (subtopic === 'add-credits') console.log('Usage: makaron admin add-credits <email-or-user-id> <credits> [--json]');
1903
2038
  else if (subtopic === 'set-admin') console.log('Usage: makaron admin set-admin <email>');
1904
2039
  else console.log(`Admin commands:
1905
2040
  admin skills List all marketplace skills
@@ -1912,6 +2047,7 @@ Use with chat:
1912
2047
  admin skill-categories delete <id> Delete a category
1913
2048
  admin upload <file> <storage-path> Upload file to Storage
1914
2049
  admin fetch-skill <code|url> Download skill from share link
2050
+ admin add-credits <user> <credits> Add credits by email or user ID
1915
2051
  admin set-admin <email> Grant admin access to a user
1916
2052
  `);
1917
2053
  } else if (topic === 'register') {
@@ -2257,7 +2393,8 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2257
2393
  uploadedTurnVideoCount += videoSnaps.length;
2258
2394
  uploadedTurnMediaCount += videoSnaps.length;
2259
2395
  } else {
2260
- process.stderr.write(`⚠️ Failed to add videos: ${await res.text()}\n`);
2396
+ process.stderr.write(`❌ Failed to add videos to the project timeline: ${await res.text()}\n`);
2397
+ process.exit(1);
2261
2398
  }
2262
2399
  }
2263
2400
 
@@ -2346,7 +2483,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2346
2483
  const res = await fetch(`${baseUrl}/api/agent/run/${runId}`, { headers });
2347
2484
  if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
2348
2485
  let data = await res.json();
2349
- normalizeRunResponse(data);
2486
+ data = await reconcileRunWithProjectMedia(baseUrl, headers, data);
2350
2487
  if (exportCompositions && data.status === 'completed') {
2351
2488
  data = await exportAnimatedCompositionsFromRun(baseUrl, headers, data, { publish: publishExports, quiet: jsonOutput || !!pick });
2352
2489
  }
@@ -2485,22 +2622,41 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2485
2622
 
2486
2623
  if (sub === 'list') {
2487
2624
  const builtIn = args.includes('--built-in');
2488
- const skills = builtIn ? await fetchBuiltInSkills(baseUrl) : await fetchMarketplaceSkills(baseUrl);
2625
+ const allBuiltIn = args.includes('--all');
2626
+ const fetchedSkills = builtIn ? await fetchBuiltInSkills(baseUrl) : await fetchMarketplaceSkills(baseUrl);
2627
+ const skills = builtIn && !allBuiltIn
2628
+ ? fetchedSkills.filter(isDiscoverableBuiltInSkill)
2629
+ : fetchedSkills;
2489
2630
  if (jsonOutput) console.log(JSON.stringify({ skills }, null, 2));
2490
- else if (builtIn) printBuiltInSkills(skills);
2631
+ else if (builtIn) printBuiltInSkills(skills, {
2632
+ heading: allBuiltIn ? 'All built-in skills' : 'Built-in skills available to use',
2633
+ });
2491
2634
  else printMarketplaceSkills(skills);
2492
2635
  } else if (sub === 'search') {
2493
- const query = args.filter((arg, index) => index > 1 && arg !== '--json').join(' ').trim();
2494
- if (!query) { console.error('Usage: makaron skills search <query> [--json]'); process.exit(1); }
2636
+ const builtIn = args.includes('--built-in');
2637
+ const allBuiltIn = args.includes('--all');
2638
+ const query = args.filter((arg, index) => index > 1 && !['--json', '--built-in', '--all'].includes(arg)).join(' ').trim();
2639
+ if (!query) { console.error('Usage: makaron skills search <query> [--built-in] [--all] [--json]'); process.exit(1); }
2495
2640
  const lowerQuery = query.toLowerCase();
2496
2641
  const slugQuery = slugifySkill(query);
2497
- const skills = (await fetchMarketplaceSkills(baseUrl))
2498
- .filter(skill => {
2499
- const rawMatch = marketplaceSkillTokens(skill).some(token => token.includes(lowerQuery));
2500
- const slugMatch = slugQuery ? marketplaceSearchText(skill).includes(slugQuery) : false;
2501
- return rawMatch || slugMatch;
2502
- });
2642
+ const builtInQueryTokens = slugQuery.split('-').filter(Boolean);
2643
+ const skills = builtIn
2644
+ ? (await fetchBuiltInSkills(baseUrl))
2645
+ .filter(skill => allBuiltIn || isDiscoverableBuiltInSkill(skill))
2646
+ .filter(skill => {
2647
+ const searchText = builtInSkillSearchText(skill);
2648
+ const slugText = slugifySkill(searchText);
2649
+ return searchText.includes(lowerQuery)
2650
+ || (builtInQueryTokens.length > 0 && builtInQueryTokens.every(token => slugText.includes(token)));
2651
+ })
2652
+ : (await fetchMarketplaceSkills(baseUrl))
2653
+ .filter(skill => {
2654
+ const rawMatch = marketplaceSkillTokens(skill).some(token => token.includes(lowerQuery));
2655
+ const slugMatch = slugQuery ? marketplaceSearchText(skill).includes(slugQuery) : false;
2656
+ return rawMatch || slugMatch;
2657
+ });
2503
2658
  if (jsonOutput) console.log(JSON.stringify({ skills }, null, 2));
2659
+ else if (builtIn) printBuiltInSkills(skills, { heading: `Built-in skill matches for "${query}"` });
2504
2660
  else printMarketplaceSkills(skills);
2505
2661
  } else if (sub === 'show') {
2506
2662
  const identifier = args[2];
@@ -2512,7 +2668,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2512
2668
  : findMarketplaceSkill(skills, identifier);
2513
2669
  if (!skill) { console.error(`Skill not found: ${identifier}`); process.exit(1); }
2514
2670
  if (jsonOutput) console.log(JSON.stringify(skill, null, 2));
2515
- else if (builtIn) printBuiltInSkills([skill]);
2671
+ else if (builtIn) printBuiltInSkill(skill);
2516
2672
  else printMarketplaceSkill(skill);
2517
2673
  } else if (sub === 'install') {
2518
2674
  const identifier = args[2];
@@ -2526,9 +2682,11 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2526
2682
  else console.log(data.skillName);
2527
2683
  } else {
2528
2684
  console.log(`Skill commands:
2529
- skills list --built-in List all built-in Makaron skills and Studio Run recipes
2685
+ skills list --built-in List user-facing built-in skills and what they do
2686
+ skills list --built-in --all Include internal/adapted helper skills
2530
2687
  skills list List marketplace skills
2531
2688
  skills search <query> Search marketplace skills
2689
+ skills search <query> --built-in Find a built-in skill by task or keyword
2532
2690
  skills show <id|label> --built-in Show a built-in skill
2533
2691
  skills show <id|label> Show a marketplace skill
2534
2692
  skills install <id|label> Install a marketplace skill to your workspace
@@ -2695,6 +2853,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2695
2853
  const isSeedance25 = selectedVideoModel === 'seedance-2.5';
2696
2854
  const isSeedanceModel = selectedVideoModel === 'seedance-fast' || selectedVideoModel === 'seedance-mini' || selectedVideoModel === 'seedance' || isSeedance25;
2697
2855
  const isMinimaxH3 = selectedVideoModel === 'minimax-h3';
2856
+ const isSyncLipsync = selectedVideoModel === 'sync-lipsync-v3';
2698
2857
  const supportsNativeTextToVideo = isSeedanceModel || isMinimaxH3;
2699
2858
  if (!script || (!images.length && !videos.length && !audios.length && !supportsNativeTextToVideo)) {
2700
2859
  console.error('Usage: makaron video create --script "..." [--image <url>] [--video <file|url>] [--audio <file|url>] [--duration 30] [--video-model seedance-2.5|minimax-h3]');
@@ -2706,6 +2865,10 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2706
2865
  if (isMinimaxH3 && images.length > 9) { console.error('MiniMax H3 supports at most 9 image references.'); process.exit(1); }
2707
2866
  if (isMinimaxH3 && videos.length > 3) { console.error('MiniMax H3 supports at most 3 video references.'); process.exit(1); }
2708
2867
  if (isMinimaxH3 && audios.length > 3) { console.error('MiniMax H3 supports at most 3 audio references.'); process.exit(1); }
2868
+ if (isSyncLipsync && (images.length !== 0 || videos.length !== 1 || audios.length !== 1)) { console.error('Sync Lipsync v3 requires exactly one --video and one --audio, with no --image.'); process.exit(1); }
2869
+ if (isSyncLipsync && !/<<<audio_1>>>/i.test(script)) {
2870
+ script += '\nUse <<<audio_1>>> as the exact replacement soundtrack.';
2871
+ }
2709
2872
  if (videoOperation && !['generate', 'edit', 'extend'].includes(videoOperation)) { console.error('--video-operation must be generate, edit, or extend.'); process.exit(1); }
2710
2873
  if (extendDirection && !['forward', 'backward'].includes(extendDirection)) { console.error('--extend-direction must be forward or backward.'); process.exit(1); }
2711
2874
  if (outputFormat && !['mp4', 'mov'].includes(outputFormat)) { console.error('--output-format must be mp4 or mov.'); process.exit(1); }
@@ -2715,8 +2878,9 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2715
2878
  process.exit(1);
2716
2879
  }
2717
2880
 
2718
- const providerMaxDuration = isSeedance25 ? SEEDANCE25_MAX_VIDEO_REFERENCE_DURATION : MAX_VIDEO_PROVIDER_REFERENCE_DURATION;
2719
- const providerMaxPixels = isMinimaxH3 ? Infinity : isSeedance25 ? SEEDANCE25_MAX_VIDEO_FRAME_PIXELS : MAX_VIDEO_FRAME_PIXELS;
2881
+ let providerMaxDuration = isSeedance25 ? SEEDANCE25_MAX_VIDEO_REFERENCE_DURATION : MAX_VIDEO_PROVIDER_REFERENCE_DURATION;
2882
+ if (isSyncLipsync) providerMaxDuration = 60;
2883
+ const providerMaxPixels = isSyncLipsync || isMinimaxH3 ? Infinity : isSeedance25 ? SEEDANCE25_MAX_VIDEO_FRAME_PIXELS : MAX_VIDEO_FRAME_PIXELS;
2720
2884
  const localImages = images.filter(image => !isHttpUrl(image));
2721
2885
  if (localImages.length) {
2722
2886
  const uploadedImages = await uploadImageFilesViaSignedUrl(baseUrl, headers, undefined, localImages);
@@ -2731,7 +2895,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2731
2895
  maxDuration: providerMaxDuration,
2732
2896
  durationTolerance: MAX_VIDEO_PROVIDER_REFERENCE_DURATION_TOLERANCE,
2733
2897
  maxFramePixels: providerMaxPixels,
2734
- maxFileSize: isSeedance25 ? 200 * 1024 * 1024 : MAX_VIDEO_UPLOAD_FILE_SIZE,
2898
+ maxFileSize: isSeedance25 || isSyncLipsync ? 200 * 1024 * 1024 : MAX_VIDEO_UPLOAD_FILE_SIZE,
2735
2899
  ...(selectedVideoModel === 'minimax-h3' ? {
2736
2900
  allowedExtensions: ['mp4', 'mov'],
2737
2901
  minSide: MINIMAX_H3_MIN_VIDEO_SIDE,
@@ -2757,7 +2921,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2757
2921
  const audioUrls = [];
2758
2922
  for (const audio of audios) {
2759
2923
  if (isHttpUrl(audio)) { audioUrls.push(audio); continue; }
2760
- const valid = validateAudioReferenceFile(audio, { maxDuration: isSeedance25 ? 30 : MAX_AUDIO_REFERENCE_DURATION });
2924
+ const valid = validateAudioReferenceFile(audio, { maxDuration: isSyncLipsync ? 60 : isSeedance25 ? 30 : MAX_AUDIO_REFERENCE_DURATION });
2761
2925
  if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
2762
2926
  process.stderr.write(`🎵 Uploading ${path.basename(audio)}...\n`);
2763
2927
  const uploaded = await uploadFileViaSignedUrl(baseUrl, headers, undefined, audio, valid.mime, { uploadKind: 'audio' });
@@ -2767,7 +2931,9 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2767
2931
  // Standalone MCP tool (no project timeline write)
2768
2932
  process.stderr.write('🎬 Submitting video...\n');
2769
2933
  const resolvedOperation = videoOperation || (isSeedance25 && videoUrls.length ? 'edit' : 'generate');
2770
- const vArgs = isSeedance25
2934
+ const vArgs = isSyncLipsync
2935
+ ? { script, images, videoUrls, audioUrls, videoModel: selectedVideoModel, videoResolution }
2936
+ : isSeedance25
2771
2937
  ? { script, images, videoUrls, audioUrls, videoModel: selectedVideoModel, videoResolution, operation: resolvedOperation, extendDirection, outputFormat, generateAudio, contentFilter, webSearch }
2772
2938
  : isMinimaxH3
2773
2939
  ? { script, images, videoUrls, audioUrls, videoModel: selectedVideoModel, videoResolution }
@@ -2778,7 +2944,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2778
2944
  if (effectiveDuration) vArgs.duration = effectiveDuration;
2779
2945
  if (aspectRatio) vArgs.aspectRatio = aspectRatio;
2780
2946
  if (keepOriginalSound && videoUrls.length && !isSeedance25) vArgs.keepOriginalSound = true;
2781
- const result = await callMcpTool(baseUrl, headers, videoUrls.length && !isSeedance25 && !isMinimaxH3 ? 'makaron_edit_video' : 'makaron_create_video', vArgs);
2947
+ const result = await callMcpTool(baseUrl, headers, videoUrls.length && !isSeedance25 && !isMinimaxH3 && !isSyncLipsync ? 'makaron_edit_video' : 'makaron_create_video', vArgs);
2782
2948
  const text = result?.content?.find(c => c.type === 'text')?.text;
2783
2949
  if (text) {
2784
2950
  console.log(text);
@@ -2825,6 +2991,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2825
2991
  video create --script "..." --video-model minimax-h3 MiniMax H3 text-to-video (default 768P)
2826
2992
  video create --script "..." --image <url> [--duration 10] Submit video task
2827
2993
  video create --script "..." --video <public-url> [--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|google-omni|minimax-h3] Edit/reference a video (Grok does not support video refs)
2994
+ video create --script "Use the supplied audio" --video <url> --audio <url> --video-model sync-lipsync-v3 Lip-sync exact replacement audio
2828
2995
  video status <taskId> Check video status
2829
2996
  video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
2830
2997
  `);
@@ -3030,6 +3197,31 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
3030
3197
  if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
3031
3198
  console.log(`✅ ${email} is now admin`);
3032
3199
 
3200
+ } else if (sub === 'add-credits') {
3201
+ const user = args[2]?.trim();
3202
+ const credits = Number(args[3]);
3203
+ if (!user || !Number.isSafeInteger(credits) || credits <= 0) {
3204
+ console.error('Usage: makaron admin add-credits <email-or-user-id> <credits> [--json]');
3205
+ console.error('Credits must be a positive integer.');
3206
+ process.exit(1);
3207
+ }
3208
+
3209
+ const res = await fetch(`${baseUrl}/api/admin/add-credits`, {
3210
+ method: 'POST',
3211
+ headers: { 'Content-Type': 'application/json', ...headers },
3212
+ body: JSON.stringify({ email: user, credits }),
3213
+ });
3214
+ const data = await res.json().catch(() => null);
3215
+ if (!res.ok) {
3216
+ console.error(`Error ${res.status}:`, data?.error || 'Failed to add credits');
3217
+ process.exit(1);
3218
+ }
3219
+ if (args.includes('--json')) {
3220
+ console.log(JSON.stringify(data));
3221
+ } else {
3222
+ console.log(`✅ Added ${credits} credits to ${user}. New balance: ${data.newBalance}`);
3223
+ }
3224
+
3033
3225
  } else {
3034
3226
  console.log(`Admin commands:
3035
3227
  admin skills List all marketplace skills
@@ -3042,6 +3234,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
3042
3234
  admin skill-categories delete <id> Delete a category
3043
3235
  admin upload <file> <storage-path> Upload file to Storage
3044
3236
  admin fetch-skill <code|url> Download skill from share link
3237
+ admin add-credits <user> <credits> Add credits by email or user ID
3045
3238
  admin set-admin <email> Grant admin access to a user
3046
3239
  `);
3047
3240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.13.7",
3
+ "version": "0.13.9",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -109,6 +109,33 @@ Returns immediately:
109
109
  | Beat-sync video from audio | `npx makaron-cli chat --project auto --audio beat.mp3 "use Seedance Mini at 480p to make a beat-synced video"` |
110
110
  | Create motion design | `npx makaron-cli chat --project <id> "make an animated Instagram story with this image"` |
111
111
 
112
+ ### Built-in production skills
113
+
114
+ When the request names a production format or the correct workflow is unclear,
115
+ discover the current built-in skills before starting. Do not guess a skill slug
116
+ from memory: the server list is the source of truth.
117
+
118
+ ```bash
119
+ # Short list of callable/discoverable production skills and their purpose
120
+ npx makaron-cli skills list --built-in
121
+
122
+ # Search by task, format, or keyword
123
+ npx makaron-cli skills search "talking head captions" --built-in
124
+
125
+ # Inspect input requirements, workflow, keywords, and exact invocation
126
+ npx makaron-cli skills show talking-head --built-in
127
+
128
+ # Use the exact slug returned by list/show
129
+ npx makaron-cli chat --project auto --video talk.mp4 \
130
+ --skill talking-head -b "remove false starts, add synced captions and useful B-roll"
131
+ ```
132
+
133
+ Use `--all` only when debugging adapters or looking for an internal helper Skill.
134
+ For ordinary creative work, choose from the default built-in list. A named
135
+ destination takes priority over a generic source workflow: for example,
136
+ explicit TikTok/Douyin work uses `tiktok-video`; ordinary speech-led cleanup
137
+ uses `talking-head`; broader mixed-footage editing uses `source-video-studio`.
138
+
112
139
  ### Marketplace skills
113
140
 
114
141
  Use marketplace skills when the user asks for a named Makaron effect, template, or skill such as "Football Captain", "足球队长", "World Cup MVP", or a marketplace UUID.
@@ -135,7 +162,7 @@ npx makaron-cli chat --project auto \
135
162
 
136
163
  `--skill` accepts an installed skill name, a marketplace UUID, or a unique marketplace label. Marketplace search matches every localized title, every localized default prompt, and category ids. If a marketplace skill is matched, the CLI installs or reuses it and sends `[Active skill: <installed-skill-name>]` to Makaron Agent. Do not call admin skill commands for ordinary users. There is intentionally no user-facing CLI delete command for marketplace skills.
137
164
 
138
- For an explicitly authorized marketplace administrator, `admin skills` shows category assignment and 4-locale title/prompt completeness, while `admin skill-categories` supports list/add/update/delete. New skills must include `en`, `zh`, `zh-Hant`, and `ja` in both `labels` and `prompts`, plus at least one existing `categories` id. Partial localized updates are safely merged server-side.
165
+ For an explicitly authorized administrator, `admin add-credits <email-or-user-id> <positive-integer> [--json]` adds credits to a user. Marketplace administrators can use `admin skills` to see category assignment and 4-locale title/prompt completeness, while `admin skill-categories` supports list/add/update/delete. New skills must include `en`, `zh`, `zh-Hant`, and `ja` in both `labels` and `prompts`, plus at least one existing `categories` id. Partial localized updates are safely merged server-side.
139
166
 
140
167
  ### With additional images (existing project)
141
168
 
@@ -184,7 +211,7 @@ npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "comb
184
211
  npx makaron-cli chat --project auto --video https://example.com/dance.mp4 -b "extend this to 15 seconds"
185
212
  ```
186
213
 
187
- Supported formats: MP4, MOV, WebM. CLI local video uploads support max 50MB, max 120s with 1s metadata tolerance, and <=1080p / 2,086,876 frame pixels. The frontend can transcode larger videos before upload; the CLI uploads directly to Storage and rejects videos above those limits. Videos are uploaded to the project timeline. The Agent can analyze scenes, edit content, compose multiple clips, extend duration, and add effects — all via natural language. Seedance video-reference editing is still limited to ~15s provider references, so longer uploaded videos should be split/prepared by the agent before model submission; Kling remains the base/direct edit path.
214
+ Supported formats: MP4, MOV, WebM. CLI local video uploads support max 50MB, max 900s (15 minutes) with 1s metadata tolerance, and <=1080p / 2,086,876 frame pixels. The frontend can transcode larger videos before upload; the CLI uploads directly to Storage and rejects videos above those limits. Videos are uploaded to the project timeline. The Agent can analyze scenes, edit content, compose multiple clips, extend duration, and add effects — all via natural language. Seedance reference-video limits remain provider-specific, so longer uploaded videos should be split/prepared by the agent before model submission; Kling remains the base/direct edit path.
188
215
 
189
216
  Use `chat --project <id|auto> --video ...` for any project/timeline video work. Direct video commands are standalone raw-tool calls.
190
217
 
@@ -300,7 +327,7 @@ npx makaron-cli video status <taskId>
300
327
  npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
301
328
  ```
302
329
 
303
- Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to the selected model limit), `--video <file|url>` and `--audio <file|url>` (repeatable where supported), `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|grok|google-omni|minimax-h3`, `--video-resolution auto|480p|720p|768p|1080p|2k|4k`. Default model is `seedance-fast`. SeeDance accepts native text-to-video with no image and integer output duration 4-15s (default 5s); `seedance-mini` supports 480p/720p and is best for cheaper drafts/multi-size tests; MiniMax H3 accepts native text-to-video, 4-15s output, public 768p/2k resolution, and up to 9 image, up to 3 video, and up to 3 audio references through Makaron Agent/chat. H3 defaults to 768p; request 2k explicitly for maximum/final quality. Kling supports 5-15s; Grok 1.5 supports 1-15s single-image-to-video only; Gemini Omni supports 3-10s fast 720p image/video generation and editing with native generated audio, including up to 6 image references when no video reference is provided. For `--video-model grok`, forced `--aspect` is ignored to avoid xAI stretching the source image; pad/create the image at the target shape first or use another model.
330
+ Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to the selected model limit), `--video <file|url>` and `--audio <file|url>` (repeatable where supported), `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--video-model seedance-fast|seedance-mini|seedance|seedance-2.5|kling|grok|google-omni|minimax-h3|sync-lipsync-v3`, `--video-resolution auto|480p|720p|768p|1080p|2k|4k`. Default model is `seedance-fast`. SeeDance accepts native text-to-video with no image and integer output duration 4-15s (default 5s); `seedance-mini` supports 480p/720p and is best for cheaper drafts/multi-size tests; MiniMax H3 accepts native text-to-video, 4-15s output, public 768p/2k resolution, and up to 9 image, up to 3 video, and up to 3 audio references through Makaron Agent/chat. `sync-lipsync-v3` requires exactly one video plus one MP3/WAV and preserves that replacement audio while aligning the mouth. H3 defaults to 768p; request 2k explicitly for maximum/final quality. Kling supports 5-15s; Grok 1.5 supports 1-15s single-image-to-video only; Gemini Omni supports 3-10s fast 720p image/video generation and editing with native generated audio, including up to 6 image references when no video reference is provided. For `--video-model grok`, forced `--aspect` is ignored to avoid xAI stretching the source image; pad/create the image at the target shape first or use another model.
304
331
 
305
332
  Seedance 2.5 uses `--video-model seedance-2.5` and supports 4-30s at 480p/720p, up to 30 images + 10 videos + 10 audios, repeatable local/URL references, `--video-operation generate|edit|extend`, `--extend-direction`, `--output-format mp4|mov`, and `--web-search`. The Evolink route does not currently expose 4K output.
306
333
 
@@ -351,6 +378,7 @@ type CompletionAction = {
351
378
  3. Stop when `status` is `"completed"`, `"failed"`, or `"aborted"`
352
379
  4. Top-level `status: "completed"` means ALL artifacts are ready (including rendered videos)
353
380
  5. If an async video fails, top-level `status` is `"failed"` and the failed video may include `completion_actions` for a safe retry or diagnosis. Agents can surface these as the next user-confirmed step.
381
+ 6. `responses get --wait` reconciles pending video output against completed Project Media with the same `snapshot_id` or `task_id`. A lagging Run row therefore does not block delivery after the durable project video is ready.
354
382
 
355
383
  ## Exit Codes
356
384