makaron-cli 0.11.4 → 0.12.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.11.4",
3
+ "version": "0.11.5",
4
4
  "description": "AI image editing, video generation, music creation, and marketplace skill workflows via CLI. Agents can self-register, install skills, create projects, and produce creative media.",
5
5
  "author": {
6
6
  "name": "Makaron AI",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.11.4",
3
+ "version": "0.11.5",
4
4
  "description": "AI image editing, video generation, music creation, and marketplace skill workflows via CLI. Agents can self-register, install skills, create projects, and produce creative media.",
5
5
  "displayName": "Makaron",
6
6
  "shortDescription": "AI image/video/music creation from the terminal",
package/README.md CHANGED
@@ -143,6 +143,47 @@ npx makaron-cli project media <projectId> --json
143
143
 
144
144
  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.
145
145
 
146
+ ### Export editable Remotion compositions
147
+
148
+ Animated Remotion compositions are saved as editable timeline/code artifacts first. To materialize one into an MP4 that CLI, V, or another service can read, call the backend export worker:
149
+
150
+ ```bash
151
+ npx makaron-cli materialize --project <projectId> --media <N> --pick url
152
+ npx makaron-cli materialize --project <projectId> --design-json composition.json --pick url
153
+ npx makaron-cli composition export --project <projectId> --media <N> --wait
154
+ npx makaron-cli composition export --project <projectId> --snapshot <snapshotId> --wait
155
+ npx makaron-cli composition status <jobId> --wait
156
+ ```
157
+
158
+ `materialize` is the preferred high-level command for Remotion-to-MP4. It defaults to `--wait`, `--publish`, and the `fast_720p` profile (short side 720, no upscale), so the completed MP4 is also added back to the project timeline like CUI. Use `--no-publish` only when you need a file URL without a new timeline video. Use `--profile source` only when full source resolution is required.
159
+
160
+ For a run that produced an animated composition, materialize before picking the video URL:
161
+
162
+ ```bash
163
+ npx makaron-cli responses get <runId> --materialize --wait --pick first_video_url
164
+ npx makaron-cli responses get <runId> --export-compositions --wait --pick first_video_url
165
+ ```
166
+
167
+ To turn a Makaron Remotion design JSON file directly into an MP4, use `--design-json`. The JSON must be a Makaron/Remotion composition payload, not a provider-video task response. Always pass the destination project because published exports and storage paths are project-scoped:
168
+
169
+ ```bash
170
+ npx makaron-cli materialize --project <projectId> --design-json composition.json --pick url
171
+ cat composition.json | npx makaron-cli materialize --project <projectId> --design-json - --pick url
172
+ ```
173
+
174
+ This JSON-to-MP4 path uses the same defaults as timeline materialize: `--wait`, `--publish`, and `fast_720p`. Add `--no-publish` only when another agent needs the MP4 URL but should not add a timeline video.
175
+
176
+ The completed export reports `duration_seconds`, `render_seconds`, and `realtime_ratio` so agents can compare video length against export time. Do not apply provider-video ETA rules to Remotion materialize; with a warm exporter it is often near video length to tens of seconds, while cold starts can be longer.
177
+
178
+ In production, run the exporter as a separate warm worker:
179
+
180
+ ```bash
181
+ REMOTION_EXPORT_INLINE_AFTER=false npm run worker:remotion-export:check
182
+ REMOTION_EXPORT_INLINE_AFTER=false npm run worker:remotion-export
183
+ ```
184
+
185
+ Keeping this worker warm avoids paying sandbox cold-start cost on every CLI or service call.
186
+
146
187
  ### With video input (MP4/MOV/WebM)
147
188
 
148
189
  ```bash
@@ -162,23 +203,17 @@ Use `chat --project <id|auto> --video ...` for any project/timeline video work.
162
203
 
163
204
  ### With reference audio (MP3/WAV)
164
205
 
165
- Use `--audio` when a song, beat, or voice recording should guide the video generation. Audio is imported into the project music library and passed to the agent as `audio_1`, `audio_2`, etc.; it does not become a timeline media item.
206
+ Attach a short song, beat, or voice recording when the video should follow audio pacing:
166
207
 
167
208
  ```bash
168
- # Local audio: validate, upload, import, then generate with Seedance
169
209
  npx makaron-cli chat --project auto \
170
210
  --audio beat.mp3 \
171
211
  --video-model seedance-fast \
172
212
  --video-resolution 480p \
173
- -b "make a 15s beat-synced video from this music"
174
-
175
- # Public audio URL: store the URL directly, no mirroring
176
- npx makaron-cli chat --project <id> \
177
- --audio https://example.com/voice.wav \
178
- -b "use this voice recording as the pacing reference for the video"
213
+ -b "make a 15s beat-synced video"
179
214
  ```
180
215
 
181
- Local audio supports MP3/WAV, 2-15s duration, and max 15MB. Reference audio is currently supported for Seedance video generation; Kling and Grok will return a clear unsupported-model error if audio refs are used.
216
+ `--audio` accepts repeatable local files or public URLs. Local MP3/WAV files must be 2-15s and <=15MB; reference audio currently works with Seedance video generation.
182
217
 
183
218
  ### Fix one video moment from a screenshot
184
219
 
@@ -392,7 +427,7 @@ send_message "All done!"
392
427
  - One project = one conversation thread. All history is preserved.
393
428
  - One run at a time per project. New message interrupts previous run.
394
429
  - Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
395
- - Most videos take 3-5 minutes; Grok is usually around 30-40 seconds. Use `responses get <runId> --wait --json` for the default customer-service path.
430
+ - Provider-generated videos can take 3-5 minutes; Grok is usually around 30-40 seconds. Remotion compositions should be converted with `materialize` / `responses get --materialize`, and timing should be read from `duration_seconds`, `render_seconds`, and `realtime_ratio`.
396
431
  - Music takes ~60 seconds. Appears in output when done.
397
432
  - Images are typically ready in 15-30 seconds.
398
433
  - stdout is always machine-readable JSON/text. Human-friendly logs go to stderr.
package/bin/makaron.mjs CHANGED
@@ -142,6 +142,11 @@ function formatSeconds(seconds) {
142
142
  return Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(1).replace(/\.0$/, '');
143
143
  }
144
144
 
145
+ function readJsonInput(filePath) {
146
+ const raw = filePath === '-' ? fs.readFileSync(0, 'utf-8') : fs.readFileSync(filePath, 'utf-8');
147
+ return JSON.parse(raw);
148
+ }
149
+
145
150
  // ─── Auth ────────────────────────────────────────────────────────────────────
146
151
 
147
152
  function loadAuth() {
@@ -195,15 +200,6 @@ async function login() {
195
200
  console.error(` Token saved to ${AUTH_FILE}`);
196
201
  }
197
202
 
198
- function getAuthCookie() {
199
- const auth = loadAuth();
200
- if (!auth) {
201
- console.error('Not logged in. Run: npx makaron-cli login');
202
- process.exit(1);
203
- }
204
- return { cookie: buildCookie(auth), baseUrl: process.env.MAKARON_URL || auth._baseUrl || BASE_URL };
205
- }
206
-
207
203
  function getAuth() {
208
204
  const apiKey = process.env.MAKARON_API_KEY;
209
205
  if (apiKey) {
@@ -478,7 +474,14 @@ async function submitRun(baseUrl, headers, projectId, prompt, opts = {}) {
478
474
  }
479
475
 
480
476
  async function pollRun(baseUrl, headers, runId, opts = {}) {
481
- const { json = false, waitForArtifacts = false, background = false } = opts;
477
+ const {
478
+ json = false,
479
+ waitForArtifacts = false,
480
+ background = false,
481
+ exportCompositions = false,
482
+ publishExports = false,
483
+ returnDataOnly = false,
484
+ } = opts;
482
485
  if (background) return;
483
486
 
484
487
  let lastSeq = -1;
@@ -559,11 +562,17 @@ async function pollRun(baseUrl, headers, runId, opts = {}) {
559
562
  // Check terminal status
560
563
  if (data.status === 'completed' || data.status === 'failed' || data.status === 'aborted') {
561
564
  if (printedText && !json) process.stdout.write('\n');
565
+ if (data.status === 'completed' && exportCompositions) {
566
+ data = await exportAnimatedCompositionsFromRun(baseUrl, headers, data, {
567
+ publish: publishExports,
568
+ quiet: json || returnDataOnly,
569
+ });
570
+ }
562
571
 
563
- if (json) {
572
+ if (json && !returnDataOnly) {
564
573
  // Structured JSON output — add projectUrl
565
574
  console.log(JSON.stringify(normalizeRunResponse(data), null, 2));
566
- } else {
575
+ } else if (!returnDataOnly) {
567
576
  process.stderr.write('\n━━━ Results ━━━\n');
568
577
  if (data.result) {
569
578
  for (const img of data.result.images || []) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
@@ -777,7 +786,7 @@ async function listProjectMedia(baseUrl, headers, projectId, opts = {}) {
777
786
  if (!res.ok) { console.error('Project media failed:', await res.text()); process.exit(1); }
778
787
  const data = await res.json();
779
788
  if (opts.json) {
780
- console.log(JSON.stringify(data, null, 2));
789
+ if (!opts.silent) console.log(JSON.stringify(data, null, 2));
781
790
  return data;
782
791
  }
783
792
 
@@ -800,6 +809,158 @@ async function listProjectMedia(baseUrl, headers, projectId, opts = {}) {
800
809
  return data;
801
810
  }
802
811
 
812
+ async function pollRemotionExport(baseUrl, headers, jobId, opts = {}) {
813
+ const start = Date.now();
814
+ while (true) {
815
+ const res = await fetch(`${baseUrl}/api/remotion/export/${jobId}`, { headers });
816
+ if (!res.ok) { process.stderr.write(`Export status failed ${res.status}: ${await res.text()}\n`); process.exit(1); }
817
+ const data = await res.json();
818
+ if (data.status === 'completed' || data.status === 'failed') {
819
+ if (opts.json) {
820
+ console.log(JSON.stringify(data, null, 2));
821
+ } else if (data.status === 'completed') {
822
+ const duration = typeof data.duration_seconds === 'number' ? `${data.duration_seconds.toFixed(2)}s` : 'unknown';
823
+ const render = typeof data.render_seconds === 'number' ? `${data.render_seconds.toFixed(2)}s` : 'unknown';
824
+ const ratio = typeof data.realtime_ratio === 'number' ? data.realtime_ratio.toFixed(2) : 'unknown';
825
+ if (!opts.quiet) {
826
+ process.stderr.write(`✅ Export complete\n`);
827
+ process.stderr.write(` Video duration: ${duration}\n`);
828
+ process.stderr.write(` Export time: ${render}\n`);
829
+ process.stderr.write(` Duration/export ratio: ${ratio}:1\n`);
830
+ console.log(data.url || data.storageUrl || '');
831
+ }
832
+ } else if (!opts.quiet) {
833
+ process.stderr.write(`❌ Export failed: ${data.error || 'unknown error'}\n`);
834
+ }
835
+ if (data.status === 'failed') process.exit(1);
836
+ return data;
837
+ }
838
+
839
+ const elapsed = Math.round((Date.now() - start) / 1000);
840
+ const pct = typeof data.progress === 'number' ? ` ${(data.progress * 100).toFixed(0)}%` : '';
841
+ if (!opts.quiet) process.stderr.write(`\r🎬 Export ${data.status}${pct} (${elapsed}s)`);
842
+ await new Promise(r => setTimeout(r, data.next_poll_after_ms || 3000));
843
+ }
844
+ }
845
+
846
+ async function exportComposition(baseUrl, headers, opts = {}) {
847
+ const body = {
848
+ projectId: opts.projectId,
849
+ snapshotId: opts.snapshotId,
850
+ designPath: opts.designPath,
851
+ design: opts.design,
852
+ outputType: opts.outputType || 'video',
853
+ renderProfile: opts.renderProfile || 'fast_720p',
854
+ publish: opts.publish === true,
855
+ name: opts.name,
856
+ };
857
+
858
+ if (opts.mediaIndex) {
859
+ if (!body.projectId) {
860
+ process.stderr.write('Usage: makaron materialize --project <id> --media <N> [--wait] [--publish] [--name <slug>] [--json]\n');
861
+ process.exit(1);
862
+ }
863
+ const mediaData = await listProjectMedia(baseUrl, headers, opts.projectId, { json: true, silent: true });
864
+ const item = (mediaData.media || []).find(m => Number(m.index) === Number(opts.mediaIndex));
865
+ if (!item) {
866
+ process.stderr.write(`No media item at index ${opts.mediaIndex}\n`);
867
+ process.exit(1);
868
+ }
869
+ if (item.type !== 'composition' && !item.codePath) {
870
+ process.stderr.write(`Media ${opts.mediaIndex} is ${item.type}, not a Remotion composition.\n`);
871
+ process.exit(1);
872
+ }
873
+ body.snapshotId = item.snapshotId || item.snapshot_id;
874
+ body.designPath = item.codePath || item.designPath || body.designPath;
875
+ }
876
+
877
+ if (!body.projectId || (!body.snapshotId && !body.designPath && !body.design)) {
878
+ process.stderr.write('Usage: makaron materialize --project <id> (--media <N> | --snapshot <snapshotId> | --design-path <path> | --design-json <file|->) [--wait] [--publish] [--name <slug>] [--json]\n');
879
+ process.exit(1);
880
+ }
881
+
882
+ const res = await fetch(`${baseUrl}/api/remotion/export`, {
883
+ method: 'POST',
884
+ headers: { 'Content-Type': 'application/json', ...headers },
885
+ body: JSON.stringify(body),
886
+ });
887
+ const data = await res.json();
888
+ if (!res.ok) {
889
+ process.stderr.write(`Export failed ${res.status}: ${JSON.stringify(data)}\n`);
890
+ process.exit(1);
891
+ }
892
+
893
+ if (opts.wait) {
894
+ if (!opts.quiet) process.stderr.write(`🎬 Export queued: ${data.jobId || data.id}\n`);
895
+ return pollRemotionExport(baseUrl, headers, data.jobId || data.id, { json: opts.json, quiet: opts.quiet });
896
+ }
897
+
898
+ if (!opts.quiet) {
899
+ if (opts.json) console.log(JSON.stringify(data, null, 2));
900
+ else console.log(data.jobId || data.id);
901
+ }
902
+ return data;
903
+ }
904
+
905
+ function pickExportValue(data, pick) {
906
+ if (!pick) return undefined;
907
+ if (pick === 'url' || pick === 'video_url' || pick === 'first_video_url') return data.url || data.storageUrl || data.storage_url;
908
+ if (pick === 'job_id' || pick === 'id') return data.jobId || data.id;
909
+ if (pick === 'workspace_path') return data.workspacePath || data.workspace_path;
910
+ if (pick === 'status') return data.status;
911
+ if (pick === 'output') return data;
912
+ return data[pick];
913
+ }
914
+
915
+ async function exportAnimatedCompositionsFromRun(baseUrl, headers, data, opts = {}) {
916
+ const projectId = data.projectId || data.project_id;
917
+ if (!projectId) return data;
918
+ const designs = (data.output || []).filter(o => o.type === 'design' && o.animated && o.snapshot_id);
919
+ if (!designs.length) return data;
920
+
921
+ const exported = [];
922
+ for (const design of designs) {
923
+ if (!opts.quiet) process.stderr.write(`\n🎬 Exporting composition snapshot ${design.snapshot_id}...\n`);
924
+ const job = await exportComposition(baseUrl, headers, {
925
+ projectId,
926
+ snapshotId: design.snapshot_id,
927
+ wait: true,
928
+ publish: opts.publish === true,
929
+ name: `run-${data.id || 'composition'}-${design.snapshot_id}`,
930
+ quiet: opts.quiet,
931
+ });
932
+ exported.push(job);
933
+ if (job?.url || job?.storageUrl) {
934
+ data.output = data.output || [];
935
+ data.output.push({
936
+ id: `export_${job.id}`,
937
+ type: 'video',
938
+ status: 'completed',
939
+ url: job.url || job.storageUrl,
940
+ export_job_id: job.id,
941
+ source_snapshot_id: design.snapshot_id,
942
+ duration: job.duration_seconds,
943
+ render_seconds: job.render_seconds,
944
+ realtime_ratio: job.realtime_ratio,
945
+ width: job.width,
946
+ height: job.height,
947
+ });
948
+ data.result = data.result || {};
949
+ data.result.videos = data.result.videos || [];
950
+ data.result.videos.push({
951
+ taskId: `remotion-export-${job.id}`,
952
+ status: 'completed',
953
+ videoUrl: job.url || job.storageUrl,
954
+ duration: job.duration_seconds,
955
+ renderSeconds: job.render_seconds,
956
+ realtimeRatio: job.realtime_ratio,
957
+ });
958
+ }
959
+ }
960
+ data.remotion_exports = exported;
961
+ return data;
962
+ }
963
+
803
964
  function timeSince(date) {
804
965
  const s = Math.floor((Date.now() - date.getTime()) / 1000);
805
966
  if (s < 60) return 'just now';
@@ -1373,6 +1534,12 @@ Commands:
1373
1534
 
1374
1535
  responses get <runId> Get run status and results
1375
1536
  responses get <runId> --wait Poll until completed
1537
+ responses get <runId> --materialize --wait --pick first_video_url
1538
+ Export and publish Remotion compositions as MP4
1539
+ materialize --project <id> --media <N> --pick url
1540
+ Convert editable composition/JSON to MP4
1541
+ composition export --project <id> --media <N> --wait
1542
+ Export editable Remotion composition to MP4
1376
1543
  responses list --project <id> List runs for a project
1377
1544
  abort <runId> Abort a running Agent
1378
1545
  skills list|search|show|install Browse and install marketplace skills
@@ -1441,13 +1608,17 @@ function printHelp(topic, subtopic) {
1441
1608
  } else if (topic === 'chat') {
1442
1609
  printChatHelp();
1443
1610
  } else if (topic === 'responses' || topic === 'run') {
1444
- if (subtopic === 'get') console.log('Usage: makaron responses get <runId> [--wait] [--json] [--pick <field>]');
1611
+ if (subtopic === 'get') console.log('Usage: makaron responses get <runId> [--wait] [--json] [--pick <field>] [--materialize|--export-compositions] [--publish-exports]');
1445
1612
  else if (subtopic === 'watch') console.log('Usage: makaron responses watch <runId> [--jsonl] [--interval <ms>]');
1446
1613
  else if (subtopic === 'list') console.log('Usage: makaron responses list --project <id>');
1447
1614
  else console.log(`Responses commands:
1448
1615
  responses get <runId> Get status and output (JSON)
1449
1616
  responses get <runId> --wait Poll until completed
1450
1617
  responses get <runId> --pick <field> Extract: first_image_url, first_video_url, project_url, output
1618
+ responses get <runId> --export-compositions --wait --pick first_video_url
1619
+ Export animated compositions before picking video URL
1620
+ responses get <runId> --materialize --wait --pick first_video_url
1621
+ Export and publish animated compositions as MP4
1451
1622
  responses watch <runId> --jsonl Watch until done (incremental events)
1452
1623
  responses list --project <id> List runs for a project
1453
1624
  `);
@@ -1477,6 +1648,16 @@ function printHelp(topic, subtopic) {
1477
1648
 
1478
1649
  Use with chat:
1479
1650
  makaron chat --project auto --skill <id|label> "your request"
1651
+ `);
1652
+ } else if (topic === 'materialize') {
1653
+ 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]`);
1654
+ } else if (topic === 'composition' || topic === 'compositions') {
1655
+ console.log(`Composition commands:
1656
+ composition export --project <id> --media <N> --wait
1657
+ composition export --project <id> --snapshot <snapshotId> --wait
1658
+ composition export --project <id> --design-path <path> --wait
1659
+ composition export --project <id> --design-json composition.json --wait
1660
+ composition status <jobId> [--wait] [--json]
1480
1661
  `);
1481
1662
  } else if (topic === 'edit') {
1482
1663
  console.log('Usage: makaron edit [--image <file|url>] [--model gemini|qwen|openai] [--skill enhance|creative|wild|captions] [--ref <file>] [--out <file>] "prompt"');
@@ -1809,20 +1990,41 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1809
1990
 
1810
1991
  if (sub === 'get') {
1811
1992
  const runId = args[2];
1812
- if (!runId) { console.error('Usage: makaron responses get <runId> [--wait] [--json] [--pick <field>]'); process.exit(1); }
1813
- let wait = false, pick = null;
1993
+ if (!runId) { console.error('Usage: makaron responses get <runId> [--wait] [--json] [--pick <field>] [--materialize|--export-compositions]'); process.exit(1); }
1994
+ let wait = false, jsonOutput = false, pick = null, exportCompositions = false, publishExports = false;
1814
1995
  for (let i = 3; i < args.length; i++) {
1815
1996
  if (args[i] === '--wait') wait = true;
1997
+ if (args[i] === '--json') jsonOutput = true;
1998
+ if (args[i] === '--export-compositions') exportCompositions = true;
1999
+ if (args[i] === '--materialize') {
2000
+ exportCompositions = true;
2001
+ publishExports = true;
2002
+ }
2003
+ if (args[i] === '--publish-exports') publishExports = true;
1816
2004
  if (args[i] === '--pick' && args[i + 1]) pick = args[++i];
1817
2005
  }
1818
2006
 
1819
2007
  if (wait) {
1820
- await pollRun(baseUrl, headers, runId, { json: true });
2008
+ const data = await pollRun(baseUrl, headers, runId, {
2009
+ json: true,
2010
+ exportCompositions,
2011
+ publishExports,
2012
+ returnDataOnly: !!pick || !jsonOutput,
2013
+ });
2014
+ if (pick) {
2015
+ const picked = applyPick(data, pick);
2016
+ if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
2017
+ } else if (!jsonOutput) {
2018
+ console.log(JSON.stringify(data, null, 2));
2019
+ }
1821
2020
  } else {
1822
2021
  const res = await fetch(`${baseUrl}/api/agent/run/${runId}`, { headers });
1823
2022
  if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
1824
- const data = await res.json();
2023
+ let data = await res.json();
1825
2024
  normalizeRunResponse(data);
2025
+ if (exportCompositions && data.status === 'completed') {
2026
+ data = await exportAnimatedCompositionsFromRun(baseUrl, headers, data, { publish: publishExports, quiet: jsonOutput || !!pick });
2027
+ }
1826
2028
  if (pick) {
1827
2029
  const picked = applyPick(data, pick);
1828
2030
  if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
@@ -1865,6 +2067,10 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1865
2067
  responses get <runId> Get status and output (JSON)
1866
2068
  responses get <runId> --wait Poll until completed
1867
2069
  responses get <runId> --pick <field> Extract: first_image_url, first_video_url, project_url, output
2070
+ responses get <runId> --export-compositions --wait --pick first_video_url
2071
+ Export animated compositions before picking video URL
2072
+ responses get <runId> --materialize --wait --pick first_video_url
2073
+ Export and publish animated compositions as MP4
1868
2074
  responses watch <runId> --jsonl Watch until done (incremental events)
1869
2075
  responses list --project <id> List runs for a project
1870
2076
  `);
@@ -1872,6 +2078,81 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1872
2078
  } else if (command === 'list' || command === 'ls') {
1873
2079
  const { headers, baseUrl } = getAuth();
1874
2080
  await listProjects(baseUrl, headers);
2081
+ } else if (command === 'materialize') {
2082
+ const { headers, baseUrl } = getAuth();
2083
+ const opts = { wait: true, publish: true, json: false, outputType: 'video', renderProfile: 'fast_720p', pick: null, quiet: false };
2084
+ for (let i = 1; i < args.length; i++) {
2085
+ if (args[i] === '--project' && args[i + 1]) opts.projectId = args[++i];
2086
+ else if (args[i] === '--media' && args[i + 1]) opts.mediaIndex = Number(args[++i]);
2087
+ else if (args[i] === '--snapshot' && args[i + 1]) opts.snapshotId = args[++i];
2088
+ else if (args[i] === '--design-path' && args[i + 1]) opts.designPath = args[++i];
2089
+ else if (args[i] === '--design-json' && args[i + 1]) opts.design = readJsonInput(args[++i]);
2090
+ else if (args[i] === '--name' && args[i + 1]) opts.name = args[++i];
2091
+ else if (args[i] === '--type' && args[i + 1]) opts.outputType = args[++i];
2092
+ else if (args[i] === '--profile' && args[i + 1]) opts.renderProfile = args[++i];
2093
+ else if (args[i] === '--render-profile' && args[i + 1]) opts.renderProfile = args[++i];
2094
+ else if (args[i] === '--no-publish') opts.publish = false;
2095
+ else if (args[i] === '--publish') opts.publish = true;
2096
+ else if (args[i] === '--no-wait') opts.wait = false;
2097
+ else if (args[i] === '--wait') opts.wait = true;
2098
+ else if (args[i] === '--json') opts.json = true;
2099
+ else if (args[i] === '--pick' && args[i + 1]) opts.pick = args[++i];
2100
+ }
2101
+ if (opts.pick) opts.quiet = true;
2102
+ const data = await exportComposition(baseUrl, headers, opts);
2103
+ if (opts.pick) {
2104
+ const picked = pickExportValue(data, opts.pick);
2105
+ if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
2106
+ }
2107
+ } else if (command === 'composition' || command === 'compositions') {
2108
+ const { headers, baseUrl } = getAuth();
2109
+ const sub = args[1];
2110
+ if (sub === 'export') {
2111
+ const opts = { wait: false, publish: false, json: false, outputType: 'video', renderProfile: 'fast_720p', pick: null, quiet: false };
2112
+ for (let i = 2; i < args.length; i++) {
2113
+ if (args[i] === '--project' && args[i + 1]) opts.projectId = args[++i];
2114
+ else if (args[i] === '--media' && args[i + 1]) opts.mediaIndex = Number(args[++i]);
2115
+ else if (args[i] === '--snapshot' && args[i + 1]) opts.snapshotId = args[++i];
2116
+ else if (args[i] === '--design-path' && args[i + 1]) opts.designPath = args[++i];
2117
+ else if (args[i] === '--design-json' && args[i + 1]) opts.design = readJsonInput(args[++i]);
2118
+ else if (args[i] === '--name' && args[i + 1]) opts.name = args[++i];
2119
+ else if (args[i] === '--type' && args[i + 1]) opts.outputType = args[++i];
2120
+ else if (args[i] === '--profile' && args[i + 1]) opts.renderProfile = args[++i];
2121
+ else if (args[i] === '--render-profile' && args[i + 1]) opts.renderProfile = args[++i];
2122
+ else if (args[i] === '--publish') opts.publish = true;
2123
+ else if (args[i] === '--wait') opts.wait = true;
2124
+ else if (args[i] === '--json') opts.json = true;
2125
+ else if (args[i] === '--pick' && args[i + 1]) opts.pick = args[++i];
2126
+ }
2127
+ if (opts.pick) opts.quiet = true;
2128
+ const data = await exportComposition(baseUrl, headers, opts);
2129
+ if (opts.pick) {
2130
+ const picked = pickExportValue(data, opts.pick);
2131
+ if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
2132
+ }
2133
+ } else if (sub === 'status') {
2134
+ const jobId = args[2];
2135
+ if (!jobId) { console.error('Usage: makaron composition status <jobId> [--wait] [--json]'); process.exit(1); }
2136
+ const wait = args.includes('--wait');
2137
+ const json = args.includes('--json');
2138
+ if (wait) await pollRemotionExport(baseUrl, headers, jobId, { json });
2139
+ else {
2140
+ const res = await fetch(`${baseUrl}/api/remotion/export/${jobId}`, { headers });
2141
+ if (!res.ok) { process.stderr.write(`Export status failed ${res.status}: ${await res.text()}\n`); process.exit(1); }
2142
+ const data = await res.json();
2143
+ if (json) console.log(JSON.stringify(data, null, 2));
2144
+ else console.log(`${data.id} ${data.status} ${data.url || ''}`);
2145
+ if (data.status === 'failed') process.exit(1);
2146
+ }
2147
+ } else {
2148
+ console.log(`Composition commands:
2149
+ composition export --project <id> --media <N> --wait
2150
+ composition export --project <id> --snapshot <snapshotId> --wait
2151
+ composition export --project <id> --design-path <path> --wait
2152
+ composition export --project <id> --design-json composition.json --wait
2153
+ composition status <jobId> [--wait] [--json]
2154
+ `);
2155
+ }
1875
2156
  } else if (command === 'skills') {
1876
2157
  const sub = args[1] || 'list';
1877
2158
  const baseUrl = process.env.MAKARON_URL || DEFAULT_URL;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.11.4",
3
+ "version": "0.12.0",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -161,23 +161,17 @@ Use `chat --project <id|auto> --video ...` for any project/timeline video work.
161
161
 
162
162
  ### With reference audio (MP3/WAV)
163
163
 
164
- Use `--audio` when a song, beat, or voice recording should guide the video generation. Audio is imported into the project music library and passed to the agent as `audio_1`, `audio_2`, etc.; it does not become a timeline media item.
164
+ Attach a short song, beat, or voice recording when the video should follow audio pacing:
165
165
 
166
166
  ```bash
167
- # Local audio: validate, upload, import, then generate with Seedance
168
167
  npx makaron-cli chat --project auto \
169
168
  --audio beat.mp3 \
170
169
  --video-model seedance-fast \
171
170
  --video-resolution 480p \
172
- -b "make a 15s beat-synced video from this music"
173
-
174
- # Public audio URL: store the URL directly, no mirroring
175
- npx makaron-cli chat --project <id> \
176
- --audio https://example.com/voice.wav \
177
- -b "use this voice recording as the pacing reference for the video"
171
+ -b "make a 15s beat-synced video"
178
172
  ```
179
173
 
180
- Local audio supports MP3/WAV, 2-15s duration, and max 15MB. Reference audio is currently supported for Seedance video generation; Kling and Grok will return a clear unsupported-model error if audio refs are used.
174
+ `--audio` accepts repeatable local files or public URLs. Local MP3/WAV files must be 2-15s and <=15MB; reference audio currently works with Seedance video generation.
181
175
 
182
176
  ### Fix one video moment from a screenshot
183
177
 
@@ -346,6 +340,27 @@ type CompletionAction = {
346
340
  | Motion design | "create an Instagram story with animated text" |
347
341
  | Multi-step | "edit the photo then make a video from it" |
348
342
 
343
+ ## Export editable Remotion compositions
344
+
345
+ Animated Remotion compositions are saved as editable timeline/code artifacts first. Use `materialize` as the preferred high-level Remotion-to-MP4 command:
346
+
347
+ ```bash
348
+ npx makaron-cli materialize --project <projectId> --media <N> --pick url
349
+ npx makaron-cli materialize --project <projectId> --design-json composition.json --pick url
350
+ npx makaron-cli responses get <runId> --materialize --wait --pick first_video_url
351
+ ```
352
+
353
+ `materialize` defaults to `--wait`, `--publish`, and `fast_720p`, so the completed MP4 is added back to the timeline like CUI. Use `--no-publish` only when you need a file URL without a new timeline video. The completed export reports `duration_seconds`, `render_seconds`, and `realtime_ratio`; use those metrics instead of provider-video ETA rules.
354
+
355
+ For JSON-to-MP4, pass a Makaron/Remotion composition JSON with `--design-json`. This is the correct CLI path when another agent already has the composition JSON and only needs the exported video:
356
+
357
+ ```bash
358
+ npx makaron-cli materialize --project <projectId> --design-json composition.json --pick url
359
+ cat composition.json | npx makaron-cli materialize --project <projectId> --design-json - --pick url
360
+ ```
361
+
362
+ Keep `--project` because exports are project-scoped and publish back to the timeline by default. Use `--no-publish` only when you want the MP4 URL without adding a timeline video.
363
+
349
364
  ## Recommended Pattern: Service Flow (Feishu/OpenClaw/Group Chat)
350
365
 
351
366
  When serving end-users in a chat environment (Feishu, Slack, Discord), use this proactive message pattern:
@@ -387,7 +402,7 @@ send_message "All done!"
387
402
  - One project = one conversation thread. All history is preserved.
388
403
  - One run at a time per project. New message interrupts previous run.
389
404
  - Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
390
- - Most videos take 3-5 minutes; Grok is usually around 30-40 seconds. Use `responses get <runId> --wait --json` for the default customer-service path.
405
+ - Provider-generated videos can take 3-5 minutes; Grok is usually around 30-40 seconds. Remotion compositions should be converted with `materialize` / `responses get --materialize`, and timing should be read from `duration_seconds`, `render_seconds`, and `realtime_ratio`.
391
406
  - Music takes ~60 seconds. Appears in output when done.
392
407
  - Images are typically ready in 15-30 seconds.
393
408
  - stdout is always machine-readable JSON/text. Human-friendly logs go to stderr.