makaron-cli 0.11.5 → 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.
- package/README.md +42 -1
- package/bin/makaron.mjs +299 -18
- package/package.json +1 -1
- package/skills/makaron/SKILL.md +22 -1
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
|
|
@@ -386,7 +427,7 @@ send_message "All done!"
|
|
|
386
427
|
- One project = one conversation thread. All history is preserved.
|
|
387
428
|
- One run at a time per project. New message interrupts previous run.
|
|
388
429
|
- Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
|
|
389
|
-
-
|
|
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`.
|
|
390
431
|
- Music takes ~60 seconds. Appears in output when done.
|
|
391
432
|
- Images are typically ready in 15-30 seconds.
|
|
392
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 {
|
|
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, {
|
|
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
|
-
|
|
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
package/skills/makaron/SKILL.md
CHANGED
|
@@ -340,6 +340,27 @@ type CompletionAction = {
|
|
|
340
340
|
| Motion design | "create an Instagram story with animated text" |
|
|
341
341
|
| Multi-step | "edit the photo then make a video from it" |
|
|
342
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
|
+
|
|
343
364
|
## Recommended Pattern: Service Flow (Feishu/OpenClaw/Group Chat)
|
|
344
365
|
|
|
345
366
|
When serving end-users in a chat environment (Feishu, Slack, Discord), use this proactive message pattern:
|
|
@@ -381,7 +402,7 @@ send_message "All done!"
|
|
|
381
402
|
- One project = one conversation thread. All history is preserved.
|
|
382
403
|
- One run at a time per project. New message interrupts previous run.
|
|
383
404
|
- Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
|
|
384
|
-
-
|
|
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`.
|
|
385
406
|
- Music takes ~60 seconds. Appears in output when done.
|
|
386
407
|
- Images are typically ready in 15-30 seconds.
|
|
387
408
|
- stdout is always machine-readable JSON/text. Human-friendly logs go to stderr.
|