makaron-cli 0.13.8 → 0.14.0

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.8",
3
+ "version": "0.14.0",
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.8",
3
+ "version": "0.14.0",
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
@@ -170,26 +170,37 @@ npx makaron-cli project media <projectId> --json
170
170
 
171
171
  This is project-scoped. `responses get <runId> --pick output` only returns artifacts from one run; `project media` returns the whole project timeline: original uploads, references, generated images, video snapshots, and editable compositions.
172
172
 
173
- External source ranges can be added without uploading either the original video or a derivative clip:
173
+ Typed external images and video ranges can be added without uploading the original media:
174
174
 
175
175
  ```bash
176
176
  npx makaron-cli project media add <projectId> \
177
+ --type image \
178
+ --source-url "https://cdn.example.com/product.jpg" \
179
+ --description "Hero product image"
180
+
181
+ npx makaron-cli project media add <projectId> \
182
+ --type video \
177
183
  --source-url "https://cdn.example.com/source.mp4" \
178
184
  --start 12.5 --end 19 \
179
185
  --description "Racket frame molding"
180
186
 
181
187
  # Batch form: a JSON array or {"clips": [...]}
182
- npx makaron-cli project media add <projectId> --input ranges.json --json
188
+ npx makaron-cli project media add <projectId> --input media.json --json
183
189
  ```
184
190
 
185
- The returned `<<<media_N>>>` is immediately usable by a later Agent run. Its range remains non-destructive metadata; Remotion must trim the original URL to these exact source bounds.
191
+ Every item must declare `type` as `image` or `video`. Images contain
192
+ `source_url + type + description` and do not have a time range. Videos contain
193
+ `source_url + type + start + end + description`; their ranges remain
194
+ non-destructive metadata, and Remotion must trim the original URL to those exact
195
+ source bounds. The returned `<<<media_N>>>` is immediately usable by a later
196
+ Agent run.
186
197
  Use `description` as the provider-neutral media-understanding field. Put any
187
198
  already-known summary, editorial purpose, concrete scene evidence, confidence,
188
199
  and limitations there. Makaron exposes the full description in Media List
189
200
  context so the Agent can edit from it without repeating image/video analysis
190
201
  unless a required detail is missing or uncertain.
191
202
 
192
- For an agent-to-agent handoff, create the project, import the external ranges,
203
+ For an agent-to-agent handoff, create the project, import the typed external media,
193
204
  and start the Agent in one command:
194
205
 
195
206
  ```bash
@@ -200,9 +211,9 @@ npx makaron-cli chat --project auto \
200
211
  ```
201
212
 
202
213
  The manifest is a JSON array or `{ "title": "...", "clips": [...] }`. Every
203
- clip contains exactly `source_url`, `start`, `end`, and `description`; the time
204
- values are seconds and array order is edit order. It is validated before project
205
- creation and supports up to 20 clips for one Makaron task. Batch planning remains
214
+ item declares `type`. Images omit `start` and `end`; videos require both values
215
+ in seconds. Array order is edit order. It is validated before project creation
216
+ and supports up to 20 media items for one Makaron task. Batch planning remains
206
217
  the upstream orchestrator's responsibility: convert each plan into one manifest
207
218
  and start one independent Makaron task.
208
219
 
@@ -341,9 +352,12 @@ npx makaron-cli edit --image photo.jpg --ref style.jpg "match this style"
341
352
 
342
353
  # Output to file
343
354
  npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
355
+
356
+ # Strict transparent PNG/WebP output through GPT Image 2 (fails rather than returning opaque)
357
+ npx makaron-cli edit --image-model openai --background transparent --out sticker.png "a magenta star sticker"
344
358
  ```
345
359
 
346
- Options: `--image`, `--image-model gemini|gemini-lite|qwen|openai|pony|wai`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
360
+ Options: `--image`, `--image-model gemini|gemini-lite|qwen|openai|pony|wai`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--background auto|opaque|transparent`, `--out <path>`. Transparent output routes strictly to GPT Image 2 and is returned only when the provider supplies real PNG/WebP alpha.
347
361
 
348
362
  ### `video` — Standalone video tools (no project timeline)
349
363
 
@@ -377,7 +391,7 @@ For project/timeline video editing, use:
377
391
  npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
378
392
  ```
379
393
 
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`, `--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.
394
+ 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.
381
395
 
382
396
  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.
383
397
 
@@ -428,6 +442,7 @@ type CompletionAction = {
428
442
  3. Stop when `status` is `"completed"`, `"failed"`, or `"aborted"`
429
443
  4. Top-level `status: "completed"` means ALL artifacts are ready (including rendered videos)
430
444
  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.
445
+ 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.
431
446
 
432
447
  ## Exit Codes
433
448
 
@@ -508,6 +523,15 @@ send_message "All done!"
508
523
 
509
524
  Admin commands require an API key with admin privileges. Ask your admin to run `makaron admin set-admin <your-email>` to grant access.
510
525
 
526
+ ### Add credits to a user
527
+
528
+ Use either the account email address or the Supabase user UUID. Credits must be a positive integer.
529
+
530
+ ```bash
531
+ npx makaron-cli admin add-credits user@example.com 1000
532
+ npx makaron-cli admin add-credits <user-id> 1000 --json
533
+ ```
534
+
511
535
  ### List all marketplace skills
512
536
 
513
537
  ```bash
package/bin/makaron.mjs CHANGED
@@ -174,25 +174,19 @@ function readJsonInput(filePath) {
174
174
  return JSON.parse(raw);
175
175
  }
176
176
 
177
- const MAX_MEDIA_MANIFEST_RANGES = 20;
177
+ const MAX_MEDIA_MANIFEST_ITEMS = 20;
178
178
 
179
179
  function normalizeMediaManifest(input) {
180
180
  const manifest = Array.isArray(input) ? { clips: input } : input;
181
181
  if (!manifest || typeof manifest !== 'object') {
182
- throw new Error('Media manifest must be a JSON object or an array of source ranges.');
183
- }
184
- const rawRanges = Array.isArray(manifest.clips)
185
- ? manifest.clips
186
- : Array.isArray(manifest.source_ranges)
187
- ? manifest.source_ranges
188
- : Array.isArray(manifest.sourceRanges)
189
- ? manifest.sourceRanges
190
- : null;
182
+ throw new Error('Media manifest must be a JSON object or an array of media items.');
183
+ }
184
+ const rawRanges = Array.isArray(manifest.clips) ? manifest.clips : null;
191
185
  if (!rawRanges?.length) {
192
186
  throw new Error('Media manifest must contain a non-empty clips array.');
193
187
  }
194
- if (rawRanges.length > MAX_MEDIA_MANIFEST_RANGES) {
195
- throw new Error(`Media manifest supports at most ${MAX_MEDIA_MANIFEST_RANGES} source ranges per Makaron task.`);
188
+ if (rawRanges.length > MAX_MEDIA_MANIFEST_ITEMS) {
189
+ throw new Error(`Media manifest supports at most ${MAX_MEDIA_MANIFEST_ITEMS} media items per Makaron task.`);
196
190
  }
197
191
 
198
192
  const sourceRanges = rawRanges.map((raw, index) => {
@@ -207,8 +201,19 @@ function normalizeMediaManifest(input) {
207
201
  if (!['http:', 'https:'].includes(parsed.protocol)) {
208
202
  throw new Error(`clips[${index}].source_url must use HTTP or HTTPS.`);
209
203
  }
210
- const start = Number(raw.start ?? raw.start_sec);
211
- const end = Number(raw.end ?? raw.end_sec);
204
+ const declaredType = raw.type;
205
+ if (declaredType !== 'image' && declaredType !== 'video') {
206
+ throw new Error(`clips[${index}].type must be image or video.`);
207
+ }
208
+ const description = typeof raw.description === 'string' ? raw.description.trim() : '';
209
+ if (declaredType === 'image') {
210
+ if (raw.start !== undefined || raw.end !== undefined || raw.start_sec !== undefined || raw.end_sec !== undefined) {
211
+ throw new Error(`clips[${index}] image items must not include start or end.`);
212
+ }
213
+ return { source_url: sourceUrl, type: 'image', description };
214
+ }
215
+ const start = Number(raw.start);
216
+ const end = Number(raw.end);
212
217
  if (!Number.isFinite(start) || start < 0) {
213
218
  throw new Error(`clips[${index}].start must be a finite number >= 0.`);
214
219
  }
@@ -217,9 +222,10 @@ function normalizeMediaManifest(input) {
217
222
  }
218
223
  return {
219
224
  source_url: sourceUrl,
225
+ type: declaredType,
220
226
  start,
221
227
  end,
222
- description: typeof raw.description === 'string' ? raw.description.trim() : '',
228
+ description,
223
229
  };
224
230
  });
225
231
 
@@ -328,6 +334,86 @@ function normalizeRunResponse(data) {
328
334
  return data;
329
335
  }
330
336
 
337
+ function projectMediaIdentity(item) {
338
+ const snapshotId = item?.snapshot_id || item?.snapshotId;
339
+ const taskId = item?.task_id || item?.taskId;
340
+ return {
341
+ snapshotId: typeof snapshotId === 'string' && snapshotId ? snapshotId : null,
342
+ taskId: typeof taskId === 'string' && taskId ? taskId : null,
343
+ };
344
+ }
345
+
346
+ function findMatchingCompletedProjectVideo(item, media) {
347
+ const identity = projectMediaIdentity(item);
348
+ if (!identity.snapshotId && !identity.taskId) return null;
349
+ return (media || []).find(candidate => {
350
+ if (candidate?.type !== 'video' || candidate?.status !== 'completed' || !candidate?.url) return false;
351
+ const candidateIdentity = projectMediaIdentity(candidate);
352
+ return (identity.snapshotId && candidateIdentity.snapshotId === identity.snapshotId)
353
+ || (identity.taskId && candidateIdentity.taskId === identity.taskId);
354
+ }) || null;
355
+ }
356
+
357
+ async function reconcileRunWithProjectMedia(baseUrl, headers, data) {
358
+ normalizeRunResponse(data);
359
+ const output = Array.isArray(data.output) ? data.output : [];
360
+ const pendingVideos = output.filter(item =>
361
+ item?.type === 'video' && (item.status === 'queued' || item.status === 'rendering')
362
+ );
363
+ const agentDone = data.agent_status === 'completed'
364
+ || (data.status === 'in_progress' && data.incomplete === true);
365
+ if (!agentDone || pendingVideos.length === 0 || !data.projectId) return data;
366
+
367
+ let projectMedia;
368
+ try {
369
+ const res = await fetch(`${baseUrl}/api/projects/${data.projectId}/media`, { headers });
370
+ if (!res.ok) return data;
371
+ projectMedia = await res.json();
372
+ } catch {
373
+ return data;
374
+ }
375
+
376
+ let reconciled = false;
377
+ for (const item of pendingVideos) {
378
+ const completed = findMatchingCompletedProjectVideo(item, projectMedia.media);
379
+ if (!completed) continue;
380
+ const identity = projectMediaIdentity(completed);
381
+ item.status = 'completed';
382
+ item.url = completed.url;
383
+ if (identity.snapshotId) item.snapshot_id = identity.snapshotId;
384
+ if (identity.taskId) item.task_id = identity.taskId;
385
+ if (completed.posterUrl) item.poster_url = completed.posterUrl;
386
+ for (const field of ['duration', 'width', 'height']) {
387
+ if (typeof completed[field] === 'number') item[field] = completed[field];
388
+ }
389
+
390
+ const legacyVideos = Array.isArray(data.result?.videos) ? data.result.videos : [];
391
+ const legacy = legacyVideos.find(video => {
392
+ const legacyIdentity = projectMediaIdentity(video);
393
+ return (identity.snapshotId && legacyIdentity.snapshotId === identity.snapshotId)
394
+ || (identity.taskId && legacyIdentity.taskId === identity.taskId);
395
+ });
396
+ if (legacy) {
397
+ legacy.status = 'completed';
398
+ legacy.videoUrl = completed.url;
399
+ if (identity.snapshotId && !legacy.snapshotId) legacy.snapshotId = identity.snapshotId;
400
+ }
401
+ reconciled = true;
402
+ }
403
+
404
+ if (!reconciled) return data;
405
+ const pendingArtifacts = output.some(item =>
406
+ (item?.type === 'video' || item?.type === 'music')
407
+ && (item.status === 'queued' || item.status === 'rendering')
408
+ );
409
+ if (!pendingArtifacts) {
410
+ data.status = 'completed';
411
+ data.incomplete = false;
412
+ delete data.next_poll_after_ms;
413
+ }
414
+ return data;
415
+ }
416
+
331
417
  function collectCompletionActions(data) {
332
418
  const items = [];
333
419
  const add = (action, source) => {
@@ -367,7 +453,7 @@ Options:
367
453
  --image <file|url> Attach a reference image or screenshot. Repeatable.
368
454
  --video <file|url> Attach a video to the project timeline. Repeatable.
369
455
  --audio <file|url> Attach a song, beat, or voice reference. MP3/WAV, repeatable.
370
- --media-manifest <file|-> Import source_url + start + end + description clips before this run.
456
+ --media-manifest <file|-> Import typed image/video media before this run.
371
457
  --skill <id|label|name> Use an installed skill or auto-install a matched marketplace skill.
372
458
  --agent-model <id> Agent LLM only: auto, gpt-5.6-terra, gpt-5.6-sol,
373
459
  gpt-5.6-luna, grok-4.5, or deepseek-v4-pro.
@@ -612,6 +698,7 @@ async function pollRun(baseUrl, headers, runId, opts = {}) {
612
698
  continue;
613
699
  }
614
700
  data = await res.json();
701
+ data = await reconcileRunWithProjectMedia(baseUrl, headers, data);
615
702
  } catch {
616
703
  continue;
617
704
  }
@@ -764,6 +851,7 @@ async function watchRun(baseUrl, headers, runId, opts = {}) {
764
851
  continue;
765
852
  }
766
853
  data = await res.json();
854
+ data = await reconcileRunWithProjectMedia(baseUrl, headers, data);
767
855
  } catch {
768
856
  await new Promise(r => setTimeout(r, interval));
769
857
  continue;
@@ -949,7 +1037,10 @@ async function addProjectMediaSourceRanges(baseUrl, headers, projectId, ranges,
949
1037
  }
950
1038
  if (!opts.silent) {
951
1039
  for (const item of data.media || []) {
952
- console.log(`${item.ref} ${item.source_url} ${formatSeconds(item.start_sec)}-${formatSeconds(item.end_sec)}s${item.created ? '' : ' (existing)'}`);
1040
+ const range = item.type === 'video' && Number.isFinite(item.start_sec) && Number.isFinite(item.end_sec)
1041
+ ? ` ${formatSeconds(item.start_sec)}-${formatSeconds(item.end_sec)}s`
1042
+ : '';
1043
+ console.log(`${item.ref} [${item.type || 'media'}] ${item.source_url}${range}${item.created ? '' : ' (existing)'}`);
953
1044
  }
954
1045
  }
955
1046
  return data;
@@ -1705,7 +1796,22 @@ function saveMcpImage(result, outputPath) {
1705
1796
  const imageBlock = content.find(c => c.type === 'image');
1706
1797
  if (textBlock) process.stderr.write(`${textBlock.text}\n`);
1707
1798
  if (imageBlock) {
1708
- const out = outputPath || `makaron-output-${Date.now()}.jpg`;
1799
+ const extension = imageBlock.mimeType === 'image/png'
1800
+ ? 'png'
1801
+ : imageBlock.mimeType === 'image/webp'
1802
+ ? 'webp'
1803
+ : 'jpg';
1804
+ let out = outputPath || `makaron-output-${Date.now()}.${extension}`;
1805
+ if (outputPath) {
1806
+ const requestedExtension = path.extname(outputPath).slice(1).toLowerCase();
1807
+ const compatible = extension === 'jpg'
1808
+ ? ['jpg', 'jpeg'].includes(requestedExtension)
1809
+ : requestedExtension === extension;
1810
+ if (!compatible) {
1811
+ out = `${outputPath.slice(0, outputPath.length - path.extname(outputPath).length)}.${extension}`;
1812
+ process.stderr.write(`Output is ${imageBlock.mimeType}; saving as ${out} so bytes and filename agree.\n`);
1813
+ }
1814
+ }
1709
1815
  fs.writeFileSync(out, Buffer.from(imageBlock.data, 'base64'));
1710
1816
  console.log(out);
1711
1817
  return out;
@@ -1756,8 +1862,10 @@ Commands:
1756
1862
  credits Show current credit balance
1757
1863
  list (ls) List all projects
1758
1864
  project media <projectId> --json List timeline media for a project
1759
- project media add <projectId> --source-url <url> --start <n> --end <n>
1760
- Add an external source range without uploading video
1865
+ project media add <projectId> --type image --source-url <url>
1866
+ Add an external image without uploading it
1867
+ project media add <projectId> --type video --source-url <url> --start <n> --end <n>
1868
+ Add an external video range without uploading it
1761
1869
  create --image <file> Create project from local image
1762
1870
  create --image-url <url> Create project from URL
1763
1871
  create --title "name" Create empty project (text-to-image)
@@ -1769,7 +1877,7 @@ Commands:
1769
1877
  chat --project <id> --video <file> Attach video to conversation
1770
1878
  chat --project <id> --audio <file> Attach song/beat/voice reference
1771
1879
  chat --project auto --media-manifest <file> "message"
1772
- Create, import external ranges, and run Agent
1880
+ Create, import typed external media, and run Agent
1773
1881
  chat --project <id> -b "message" Background: submit and print runId
1774
1882
  chat --project <id> --stream "msg" Legacy: stream SSE in real-time
1775
1883
  chat --project <id> --json "msg" Output structured JSON result
@@ -1791,7 +1899,7 @@ Commands:
1791
1899
  video script|create|status Video generation
1792
1900
  music create|status Music generation
1793
1901
 
1794
- admin Admin commands (skills, upload, set-admin)
1902
+ admin Admin commands (skills, credits, upload, set-admin)
1795
1903
 
1796
1904
  Examples:
1797
1905
  makaron chat --project auto "plan a launch poster"
@@ -1879,11 +1987,12 @@ function printHelp(topic, subtopic) {
1879
1987
  console.log('Usage: makaron credits [--json]');
1880
1988
  } else if (topic === 'project' || topic === 'projects') {
1881
1989
  if (subtopic === 'media') console.log(`Usage: makaron project media <projectId> [--json]
1882
- makaron project media add <projectId> --source-url <url> --start <n> --end <n> [--description <text>] [--json]
1883
- makaron project media add <projectId> --input <ranges.json> [--json]`);
1990
+ makaron project media add <projectId> --type image --source-url <url> [--description <text>] [--json]
1991
+ makaron project media add <projectId> --type video --source-url <url> --start <n> --end <n> [--description <text>] [--json]
1992
+ makaron project media add <projectId> --input <media.json> [--json]`);
1884
1993
  else console.log(`Project commands:
1885
1994
  project media <projectId> --json List timeline media for a project
1886
- project media add <projectId> ... Add external source_url + start + end media
1995
+ project media add <projectId> ... Add typed external image/video media
1887
1996
  `);
1888
1997
  } else if (topic === 'abort') {
1889
1998
  console.log('Usage: makaron abort <runId>');
@@ -1923,12 +2032,12 @@ Not sure which built-in skill to use? Start with:
1923
2032
  composition status <jobId> [--wait] [--json]
1924
2033
  `);
1925
2034
  } else if (topic === 'edit') {
1926
- console.log('Usage: makaron edit [--image <file|url>] [--image-model gemini|gemini-lite|qwen|openai|pony|wai] [--skill enhance|creative|wild|captions] [--ref <file>] [--out <file>] "prompt"');
2035
+ console.log('Usage: makaron edit [--image <file|url>] [--image-model gemini|gemini-lite|qwen|openai|pony|wai] [--skill enhance|creative|wild|captions] [--ref <file>] [--aspect <ratio>] [--background auto|opaque|transparent] [--out <file>] "prompt"');
1927
2036
  } else if (topic === 'analyze') {
1928
2037
  console.log('Usage: makaron analyze --video <file|url> ["question"]');
1929
2038
  } else if (topic === 'video') {
1930
2039
  if (subtopic === 'script') console.log('Usage: makaron video script --image <file> [--image <file>] [--lang en|zh] "direction"');
1931
- 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]');
2040
+ 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]');
1932
2041
  else if (subtopic === 'status') console.log('Usage: makaron video status <taskId> | --snapshot <snapshotId> [--wait]');
1933
2042
  else console.log(`Video commands:
1934
2043
  video script --image <file> [--image <file>] "direction" Write video script
@@ -1936,6 +2045,7 @@ Not sure which built-in skill to use? Start with:
1936
2045
  video create --script "..." --video-model minimax-h3 MiniMax H3 text-to-video (default 768P)
1937
2046
  video create --script "..." --image <url> [--duration 10] Submit video task
1938
2047
  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)
2048
+ video create --script "Use the supplied audio" --video <url> --audio <url> --video-model sync-lipsync-v3 Lip-sync exact replacement audio
1939
2049
  video status <taskId> Check video status
1940
2050
  video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
1941
2051
  `);
@@ -1951,6 +2061,7 @@ Not sure which built-in skill to use? Start with:
1951
2061
  else if (subtopic === 'skill-categories') console.log('Usage: makaron admin skill-categories [--json|add|update|delete] ...');
1952
2062
  else if (subtopic === 'upload') console.log('Usage: makaron admin upload <local-file> <storage-path>');
1953
2063
  else if (subtopic === 'fetch-skill') console.log('Usage: makaron admin fetch-skill <share-code|url>');
2064
+ else if (subtopic === 'add-credits') console.log('Usage: makaron admin add-credits <email-or-user-id> <credits> [--json]');
1954
2065
  else if (subtopic === 'set-admin') console.log('Usage: makaron admin set-admin <email>');
1955
2066
  else console.log(`Admin commands:
1956
2067
  admin skills List all marketplace skills
@@ -1963,6 +2074,7 @@ Not sure which built-in skill to use? Start with:
1963
2074
  admin skill-categories delete <id> Delete a category
1964
2075
  admin upload <file> <storage-path> Upload file to Storage
1965
2076
  admin fetch-skill <code|url> Download skill from share link
2077
+ admin add-credits <user> <credits> Add credits by email or user ID
1966
2078
  admin set-admin <email> Grant admin access to a user
1967
2079
  `);
1968
2080
  } else if (topic === 'register') {
@@ -2184,12 +2296,12 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2184
2296
  );
2185
2297
  importedManifestMedia = imported.media || [];
2186
2298
  if (importedManifestMedia.length !== mediaManifest.sourceRanges.length) {
2187
- process.stderr.write(`❌ Imported ${importedManifestMedia.length}/${mediaManifest.sourceRanges.length} source range(s); aborting run.\n`);
2299
+ process.stderr.write(`❌ Imported ${importedManifestMedia.length}/${mediaManifest.sourceRanges.length} media item(s); aborting run.\n`);
2188
2300
  process.exit(1);
2189
2301
  }
2190
2302
  uploadedTurnMediaCount += importedManifestMedia.length;
2191
- uploadedTurnVideoCount += importedManifestMedia.length;
2192
- process.stderr.write(`📎 Imported ${importedManifestMedia.length} external source range(s) from media manifest\n`);
2303
+ uploadedTurnVideoCount += importedManifestMedia.filter(item => item.type === 'video').length;
2304
+ process.stderr.write(`📎 Imported ${importedManifestMedia.length} typed external media item(s) from media manifest\n`);
2193
2305
  }
2194
2306
  // Upload additional images to existing project
2195
2307
  if (imageFileList.length > 0 || imageUrlList.length > 0) {
@@ -2347,9 +2459,12 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2347
2459
  ...(importedManifestMedia.length ? {
2348
2460
  importedMedia: importedManifestMedia.map(item => ({
2349
2461
  ref: item.ref,
2462
+ type: item.type,
2350
2463
  source_url: item.source_url,
2351
- start_sec: item.start_sec,
2352
- end_sec: item.end_sec,
2464
+ ...(item.type === 'video' ? {
2465
+ start_sec: item.start_sec,
2466
+ end_sec: item.end_sec,
2467
+ } : {}),
2353
2468
  })),
2354
2469
  } : {}),
2355
2470
  }));
@@ -2398,7 +2513,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2398
2513
  const res = await fetch(`${baseUrl}/api/agent/run/${runId}`, { headers });
2399
2514
  if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
2400
2515
  let data = await res.json();
2401
- normalizeRunResponse(data);
2516
+ data = await reconcileRunWithProjectMedia(baseUrl, headers, data);
2402
2517
  if (exportCompositions && data.status === 'completed') {
2403
2518
  data = await exportAnimatedCompositionsFromRun(baseUrl, headers, data, { publish: publishExports, quiet: jsonOutput || !!pick });
2404
2519
  }
@@ -2614,7 +2729,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2614
2729
  const jsonOutput = args.includes('--json');
2615
2730
  if (args[2] === 'add') {
2616
2731
  const projectId = args[3];
2617
- if (!projectId) { console.error('Usage: makaron project media add <projectId> --source-url <url> --start <n> --end <n>'); process.exit(1); }
2732
+ if (!projectId) { console.error('Usage: makaron project media add <projectId> --type <image|video> --source-url <url> [--start <n> --end <n>]'); process.exit(1); }
2618
2733
  const readOption = (name) => {
2619
2734
  const index = args.indexOf(name);
2620
2735
  return index >= 0 ? args[index + 1] : undefined;
@@ -2622,26 +2737,25 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2622
2737
  const inputPath = readOption('--input');
2623
2738
  let ranges;
2624
2739
  if (inputPath) {
2625
- const input = readJsonInput(inputPath);
2626
- ranges = Array.isArray(input) ? input : input.clips || input.source_ranges || input.sourceRanges;
2740
+ const normalized = normalizeMediaManifest(readJsonInput(inputPath));
2741
+ ranges = normalized.sourceRanges;
2627
2742
  } else {
2628
2743
  const sourceUrl = readOption('--source-url');
2629
- const start = Number(readOption('--start') ?? readOption('--start-sec'));
2630
- const end = Number(readOption('--end') ?? readOption('--end-sec'));
2631
- if (!sourceUrl || !Number.isFinite(start) || !Number.isFinite(end)) {
2632
- console.error('Provide --source-url, --start, and --end, or --input <manifest.json>.');
2744
+ const type = readOption('--type');
2745
+ const start = readOption('--start');
2746
+ const end = readOption('--end');
2747
+ if (!sourceUrl || (type !== 'image' && type !== 'video')) {
2748
+ console.error('Provide --type <image|video> and --source-url <url>, or --input <manifest.json>.');
2633
2749
  process.exit(1);
2634
2750
  }
2635
- ranges = [{
2751
+ const normalized = normalizeMediaManifest([{
2636
2752
  source_url: sourceUrl,
2637
- start,
2638
- end,
2753
+ type,
2754
+ ...(start !== undefined ? { start } : {}),
2755
+ ...(end !== undefined ? { end } : {}),
2639
2756
  ...(readOption('--description') ? { description: readOption('--description') } : {}),
2640
- }];
2641
- }
2642
- if (!Array.isArray(ranges) || !ranges.length) {
2643
- console.error('Input must contain a non-empty clips array.');
2644
- process.exit(1);
2757
+ }]);
2758
+ ranges = normalized.sourceRanges;
2645
2759
  }
2646
2760
  await addProjectMediaSourceRanges(baseUrl, headers, projectId, ranges, { json: jsonOutput });
2647
2761
  } else {
@@ -2652,7 +2766,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2652
2766
  } else {
2653
2767
  console.log(`Project commands:
2654
2768
  project media <projectId> --json List timeline media for a project
2655
- project media add <projectId> ... Add external source_url + start + end media
2769
+ project media add <projectId> ... Add typed external image/video media
2656
2770
  `);
2657
2771
  }
2658
2772
  } else if (command === 'abort') {
@@ -2690,11 +2804,19 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2690
2804
  editArgs.referenceImages.push(imageToArg(args[++i]));
2691
2805
  }
2692
2806
  else if (args[i] === '--aspect' && args[i + 1]) editArgs.aspectRatio = args[++i];
2807
+ else if (args[i] === '--background' && args[i + 1]) {
2808
+ const background = args[++i];
2809
+ if (!['auto', 'opaque', 'transparent'].includes(background)) {
2810
+ console.error('Invalid --background. Use auto, opaque, or transparent.');
2811
+ process.exit(1);
2812
+ }
2813
+ editArgs.background = background;
2814
+ }
2693
2815
  else if (args[i] === '--out' && args[i + 1]) outputPath = args[++i];
2694
2816
  else promptParts.push(args[i]);
2695
2817
  }
2696
2818
  editArgs.editPrompt = promptParts.join(' ');
2697
- if (!editArgs.editPrompt) { console.error('Usage: makaron edit [--image <file|url>] [--image-model gemini|gemini-lite|qwen|openai|pony|wai] [--ref <file>] [--out <file>] "prompt"'); process.exit(1); }
2819
+ if (!editArgs.editPrompt) { console.error('Usage: makaron edit [--image <file|url>] [--image-model gemini|gemini-lite|qwen|openai|pony|wai] [--ref <file>] [--aspect <ratio>] [--background auto|opaque|transparent] [--out <file>] "prompt"'); process.exit(1); }
2698
2820
  process.stderr.write('🎨 Generating...\n');
2699
2821
  const result = await callMcpTool(baseUrl, headers, 'makaron_edit_image', editArgs);
2700
2822
  saveMcpImage(result, outputPath);
@@ -2768,6 +2890,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2768
2890
  const isSeedance25 = selectedVideoModel === 'seedance-2.5';
2769
2891
  const isSeedanceModel = selectedVideoModel === 'seedance-fast' || selectedVideoModel === 'seedance-mini' || selectedVideoModel === 'seedance' || isSeedance25;
2770
2892
  const isMinimaxH3 = selectedVideoModel === 'minimax-h3';
2893
+ const isSyncLipsync = selectedVideoModel === 'sync-lipsync-v3';
2771
2894
  const supportsNativeTextToVideo = isSeedanceModel || isMinimaxH3;
2772
2895
  if (!script || (!images.length && !videos.length && !audios.length && !supportsNativeTextToVideo)) {
2773
2896
  console.error('Usage: makaron video create --script "..." [--image <url>] [--video <file|url>] [--audio <file|url>] [--duration 30] [--video-model seedance-2.5|minimax-h3]');
@@ -2779,6 +2902,10 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2779
2902
  if (isMinimaxH3 && images.length > 9) { console.error('MiniMax H3 supports at most 9 image references.'); process.exit(1); }
2780
2903
  if (isMinimaxH3 && videos.length > 3) { console.error('MiniMax H3 supports at most 3 video references.'); process.exit(1); }
2781
2904
  if (isMinimaxH3 && audios.length > 3) { console.error('MiniMax H3 supports at most 3 audio references.'); process.exit(1); }
2905
+ 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); }
2906
+ if (isSyncLipsync && !/<<<audio_1>>>/i.test(script)) {
2907
+ script += '\nUse <<<audio_1>>> as the exact replacement soundtrack.';
2908
+ }
2782
2909
  if (videoOperation && !['generate', 'edit', 'extend'].includes(videoOperation)) { console.error('--video-operation must be generate, edit, or extend.'); process.exit(1); }
2783
2910
  if (extendDirection && !['forward', 'backward'].includes(extendDirection)) { console.error('--extend-direction must be forward or backward.'); process.exit(1); }
2784
2911
  if (outputFormat && !['mp4', 'mov'].includes(outputFormat)) { console.error('--output-format must be mp4 or mov.'); process.exit(1); }
@@ -2788,8 +2915,9 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2788
2915
  process.exit(1);
2789
2916
  }
2790
2917
 
2791
- const providerMaxDuration = isSeedance25 ? SEEDANCE25_MAX_VIDEO_REFERENCE_DURATION : MAX_VIDEO_PROVIDER_REFERENCE_DURATION;
2792
- const providerMaxPixels = isMinimaxH3 ? Infinity : isSeedance25 ? SEEDANCE25_MAX_VIDEO_FRAME_PIXELS : MAX_VIDEO_FRAME_PIXELS;
2918
+ let providerMaxDuration = isSeedance25 ? SEEDANCE25_MAX_VIDEO_REFERENCE_DURATION : MAX_VIDEO_PROVIDER_REFERENCE_DURATION;
2919
+ if (isSyncLipsync) providerMaxDuration = 60;
2920
+ const providerMaxPixels = isSyncLipsync || isMinimaxH3 ? Infinity : isSeedance25 ? SEEDANCE25_MAX_VIDEO_FRAME_PIXELS : MAX_VIDEO_FRAME_PIXELS;
2793
2921
  const localImages = images.filter(image => !isHttpUrl(image));
2794
2922
  if (localImages.length) {
2795
2923
  const uploadedImages = await uploadImageFilesViaSignedUrl(baseUrl, headers, undefined, localImages);
@@ -2804,7 +2932,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2804
2932
  maxDuration: providerMaxDuration,
2805
2933
  durationTolerance: MAX_VIDEO_PROVIDER_REFERENCE_DURATION_TOLERANCE,
2806
2934
  maxFramePixels: providerMaxPixels,
2807
- maxFileSize: isSeedance25 ? 200 * 1024 * 1024 : MAX_VIDEO_UPLOAD_FILE_SIZE,
2935
+ maxFileSize: isSeedance25 || isSyncLipsync ? 200 * 1024 * 1024 : MAX_VIDEO_UPLOAD_FILE_SIZE,
2808
2936
  ...(selectedVideoModel === 'minimax-h3' ? {
2809
2937
  allowedExtensions: ['mp4', 'mov'],
2810
2938
  minSide: MINIMAX_H3_MIN_VIDEO_SIDE,
@@ -2830,7 +2958,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2830
2958
  const audioUrls = [];
2831
2959
  for (const audio of audios) {
2832
2960
  if (isHttpUrl(audio)) { audioUrls.push(audio); continue; }
2833
- const valid = validateAudioReferenceFile(audio, { maxDuration: isSeedance25 ? 30 : MAX_AUDIO_REFERENCE_DURATION });
2961
+ const valid = validateAudioReferenceFile(audio, { maxDuration: isSyncLipsync ? 60 : isSeedance25 ? 30 : MAX_AUDIO_REFERENCE_DURATION });
2834
2962
  if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
2835
2963
  process.stderr.write(`🎵 Uploading ${path.basename(audio)}...\n`);
2836
2964
  const uploaded = await uploadFileViaSignedUrl(baseUrl, headers, undefined, audio, valid.mime, { uploadKind: 'audio' });
@@ -2840,7 +2968,9 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2840
2968
  // Standalone MCP tool (no project timeline write)
2841
2969
  process.stderr.write('🎬 Submitting video...\n');
2842
2970
  const resolvedOperation = videoOperation || (isSeedance25 && videoUrls.length ? 'edit' : 'generate');
2843
- const vArgs = isSeedance25
2971
+ const vArgs = isSyncLipsync
2972
+ ? { script, images, videoUrls, audioUrls, videoModel: selectedVideoModel, videoResolution }
2973
+ : isSeedance25
2844
2974
  ? { script, images, videoUrls, audioUrls, videoModel: selectedVideoModel, videoResolution, operation: resolvedOperation, extendDirection, outputFormat, generateAudio, contentFilter, webSearch }
2845
2975
  : isMinimaxH3
2846
2976
  ? { script, images, videoUrls, audioUrls, videoModel: selectedVideoModel, videoResolution }
@@ -2851,7 +2981,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2851
2981
  if (effectiveDuration) vArgs.duration = effectiveDuration;
2852
2982
  if (aspectRatio) vArgs.aspectRatio = aspectRatio;
2853
2983
  if (keepOriginalSound && videoUrls.length && !isSeedance25) vArgs.keepOriginalSound = true;
2854
- const result = await callMcpTool(baseUrl, headers, videoUrls.length && !isSeedance25 && !isMinimaxH3 ? 'makaron_edit_video' : 'makaron_create_video', vArgs);
2984
+ const result = await callMcpTool(baseUrl, headers, videoUrls.length && !isSeedance25 && !isMinimaxH3 && !isSyncLipsync ? 'makaron_edit_video' : 'makaron_create_video', vArgs);
2855
2985
  const text = result?.content?.find(c => c.type === 'text')?.text;
2856
2986
  if (text) {
2857
2987
  console.log(text);
@@ -2898,6 +3028,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2898
3028
  video create --script "..." --video-model minimax-h3 MiniMax H3 text-to-video (default 768P)
2899
3029
  video create --script "..." --image <url> [--duration 10] Submit video task
2900
3030
  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)
3031
+ video create --script "Use the supplied audio" --video <url> --audio <url> --video-model sync-lipsync-v3 Lip-sync exact replacement audio
2901
3032
  video status <taskId> Check video status
2902
3033
  video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
2903
3034
  `);
@@ -3103,6 +3234,31 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
3103
3234
  if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
3104
3235
  console.log(`✅ ${email} is now admin`);
3105
3236
 
3237
+ } else if (sub === 'add-credits') {
3238
+ const user = args[2]?.trim();
3239
+ const credits = Number(args[3]);
3240
+ if (!user || !Number.isSafeInteger(credits) || credits <= 0) {
3241
+ console.error('Usage: makaron admin add-credits <email-or-user-id> <credits> [--json]');
3242
+ console.error('Credits must be a positive integer.');
3243
+ process.exit(1);
3244
+ }
3245
+
3246
+ const res = await fetch(`${baseUrl}/api/admin/add-credits`, {
3247
+ method: 'POST',
3248
+ headers: { 'Content-Type': 'application/json', ...headers },
3249
+ body: JSON.stringify({ email: user, credits }),
3250
+ });
3251
+ const data = await res.json().catch(() => null);
3252
+ if (!res.ok) {
3253
+ console.error(`Error ${res.status}:`, data?.error || 'Failed to add credits');
3254
+ process.exit(1);
3255
+ }
3256
+ if (args.includes('--json')) {
3257
+ console.log(JSON.stringify(data));
3258
+ } else {
3259
+ console.log(`✅ Added ${credits} credits to ${user}. New balance: ${data.newBalance}`);
3260
+ }
3261
+
3106
3262
  } else {
3107
3263
  console.log(`Admin commands:
3108
3264
  admin skills List all marketplace skills
@@ -3115,6 +3271,7 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
3115
3271
  admin skill-categories delete <id> Delete a category
3116
3272
  admin upload <file> <storage-path> Upload file to Storage
3117
3273
  admin fetch-skill <code|url> Download skill from share link
3274
+ admin add-credits <user> <credits> Add credits by email or user ID
3118
3275
  admin set-admin <email> Grant admin access to a user
3119
3276
  `);
3120
3277
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.13.8",
3
+ "version": "0.14.0",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -162,7 +162,7 @@ npx makaron-cli chat --project auto \
162
162
 
163
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.
164
164
 
165
- 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.
166
166
 
167
167
  ### With additional images (existing project)
168
168
 
@@ -180,16 +180,17 @@ npx makaron-cli project media <projectId> --json
180
180
 
181
181
  This is project-scoped. `responses get <runId> --pick output` only returns artifacts from one run; `project media` returns the whole project timeline: original uploads, references, generated images, video snapshots, and editable compositions.
182
182
 
183
- Publish an external video interval directly into that Media List without uploading the original or a derivative MP4:
183
+ Publish typed external images and video intervals directly into that Media List without uploading the original media:
184
184
 
185
185
  ```bash
186
- npx makaron-cli project media add <projectId> --source-url "https://cdn.example.com/source.mp4" --start 12.5 --end 19 --description "Racket frame molding"
187
- npx makaron-cli project media add <projectId> --input ranges.json --json
186
+ npx makaron-cli project media add <projectId> --type image --source-url "https://cdn.example.com/product.jpg" --description "Hero product image"
187
+ npx makaron-cli project media add <projectId> --type video --source-url "https://cdn.example.com/source.mp4" --start 12.5 --end 19 --description "Racket frame molding"
188
+ npx makaron-cli project media add <projectId> --input media.json --json
188
189
  ```
189
190
 
190
- The JSON input may be an array or `{ "clips": [...] }`. Each clip has exactly `source_url + start + end + description`; `start` and `end` are seconds, array order is edit order, and `source_url` is opaque. Do not add or request provider-specific identity fields. Put existing media understanding (summary, editorial purpose, scene evidence, confidence, and limitations) in `description`. Makaron reads that provider-neutral Media List field before deciding whether any additional image/video analysis is needed.
191
+ The JSON input may be an array or `{ "clips": [...] }`. Every item declares `type` as `image` or `video`. Images have `source_url + type + description` and no time range. Videos have `source_url + type + start + end + description`; `start` and `end` are seconds. Array order is edit order and `source_url` is opaque. Do not add or request provider-specific identity fields. Put existing media understanding (summary, editorial purpose, scene evidence, confidence, and limitations) in `description`. Makaron reads that provider-neutral Media List field before deciding whether any additional image/video analysis is needed.
191
192
 
192
- For one-call orchestration, use `chat --project auto --media-manifest plan.json`. Makaron validates the manifest, creates the project, imports its ranges, and starts the Agent. If an upstream service returns multiple plans, the caller should start one independent Makaron task per plan instead of passing the provider-specific batch response into Makaron.
193
+ For one-call orchestration, use `chat --project auto --media-manifest plan.json`. Makaron validates the manifest, creates the project, imports its media, and starts the Agent. If an upstream service returns multiple plans, the caller should start one independent Makaron task per plan instead of passing the provider-specific batch response into Makaron.
193
194
 
194
195
  ```bash
195
196
  npx makaron-cli chat --project auto --media-manifest set-01.json --json -b "Make a 30-second 9:16 TikTok with English VO and captions"
@@ -292,9 +293,12 @@ npx makaron-cli edit --image photo.jpg --ref style.jpg "match this style"
292
293
 
293
294
  # Output to file
294
295
  npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
296
+
297
+ # Strict transparent output through GPT Image 2
298
+ npx makaron-cli edit --image-model openai --background transparent --out sticker.png "a magenta star sticker"
295
299
  ```
296
300
 
297
- Options: `--image`, `--image-model gemini|gemini-lite|qwen|openai|pony|wai`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
301
+ Options: `--image`, `--image-model gemini|gemini-lite|qwen|openai|pony|wai`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--background auto|opaque|transparent`, `--out <path>`. Transparent output routes strictly to GPT Image 2 and fails instead of returning an opaque fallback.
298
302
 
299
303
  ### `video` — Standalone video tools (no project timeline)
300
304
 
@@ -327,7 +331,7 @@ npx makaron-cli video status <taskId>
327
331
  npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
328
332
  ```
329
333
 
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`, `--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.
334
+ 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.
331
335
 
332
336
  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.
333
337
 
@@ -378,6 +382,7 @@ type CompletionAction = {
378
382
  3. Stop when `status` is `"completed"`, `"failed"`, or `"aborted"`
379
383
  4. Top-level `status: "completed"` means ALL artifacts are ready (including rendered videos)
380
384
  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.
385
+ 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.
381
386
 
382
387
  ## Exit Codes
383
388