makaron-cli 0.6.2 → 0.7.1

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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "makaron-cli",
3
+ "version": "0.6.2",
4
+ "description": "AI image editing, video generation, and music creation via CLI. Agents can self-register, create projects, and produce creative media.",
5
+ "author": {
6
+ "name": "Makaron AI",
7
+ "url": "https://www.makaron.app"
8
+ },
9
+ "homepage": "https://www.makaron.app/agent",
10
+ "repository": "https://github.com/vegekyd/ai-image-editor",
11
+ "skills": "./skills/"
12
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "makaron-cli",
3
+ "version": "0.6.2",
4
+ "description": "AI image editing, video generation, and music creation via CLI. Agents can self-register, create projects, and produce creative media.",
5
+ "displayName": "Makaron",
6
+ "shortDescription": "AI image/video/music creation from the terminal",
7
+ "longDescription": "makaron.app is for humans. makaron-cli is for AI agents. Talk to Makaron Agent from the terminal — create projects, edit images, generate videos, and compose music. Zero dependencies, single file, works with npx. Agents can self-register to get an API key without human intervention.",
8
+ "author": {
9
+ "name": "Makaron AI",
10
+ "url": "https://www.makaron.app"
11
+ },
12
+ "homepage": "https://www.makaron.app/agent",
13
+ "repository": "https://github.com/vegekyd/ai-image-editor",
14
+ "license": "MIT",
15
+ "keywords": ["ai", "image-editing", "video", "music", "agent", "creative"],
16
+ "category": "Creative Tools",
17
+ "capabilities": ["image-editing", "video-generation", "music-creation", "text-to-image"],
18
+ "skills": "./skills/",
19
+ "websiteURL": "https://www.makaron.app",
20
+ "brandColor": "#d946ef"
21
+ }
package/SKILL.md CHANGED
@@ -86,6 +86,21 @@ Returns immediately:
86
86
  npx makaron-cli chat --project <id> --image ref1.jpg --image ref2.jpg -b "use these as style reference"
87
87
  ```
88
88
 
89
+ ### With video input
90
+
91
+ ```bash
92
+ # Upload a video file — Agent can analyze, edit, or use it as reference
93
+ npx makaron-cli chat --project <id> --video clip.mp4 -b "edit this video to be more cinematic"
94
+
95
+ # Video URL (public, downloadable)
96
+ npx makaron-cli chat --project auto --video https://example.com/video.mp4 -b "make a 10s highlight reel"
97
+
98
+ # Mix images + video
99
+ npx makaron-cli chat --project auto --image photo.jpg --video clip.mp4 -b "combine the photo style with this video"
100
+ ```
101
+
102
+ Supported formats: MP4, MOV, WebM (max 200MB). Videos are uploaded to the project timeline and available to the Agent for analysis and editing.
103
+
89
104
  ### Check status (single query)
90
105
 
91
106
  ```bash
@@ -184,7 +199,7 @@ type MakaronOutput =
184
199
  | { id: string; type: "text"; status: "completed"; content: string }
185
200
  | { id: string; type: "image"; status: "completed"; url: string; snapshot_id: string }
186
201
  | { id: string; type: "design"; status: "completed"; url: string; width: number; height: number; animated: boolean; duration?: number }
187
- | { id: string; type: "video"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; url?: string; elapsed_seconds?: number }
202
+ | { id: string; type: "video"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; snapshot_id?: string; url?: string; elapsed_seconds?: number; width?: number; height?: number }
188
203
  | { id: string; type: "music"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; url?: string; elapsed_seconds?: number }
189
204
  ```
190
205
 
package/bin/makaron.mjs CHANGED
@@ -217,6 +217,11 @@ async function streamAgent(baseUrl, headers, projectId, prompt) {
217
217
  process.stderr.write(`\n🎬 Video submitted: ${event.taskId}\n`);
218
218
  break;
219
219
 
220
+ case 'video_snapshot':
221
+ results.animationTasks.push({ taskId: event.taskId, snapshotId: event.snapshotId });
222
+ process.stderr.write(`\n🎬 Video submitted: ${event.taskId} (snapshot: ${event.snapshotId})\n`);
223
+ break;
224
+
220
225
  case 'music_task':
221
226
  results.musicTasks.push({ taskId: event.taskId });
222
227
  process.stderr.write(`\n🎵 Music submitted: ${event.taskId}\n`);
@@ -325,6 +330,9 @@ async function pollRun(baseUrl, headers, runId, opts = {}) {
325
330
  case 'animation_task':
326
331
  process.stderr.write(`\n🎬 Video submitted: ${ev.data?.taskId}\n`);
327
332
  break;
333
+ case 'video_snapshot':
334
+ process.stderr.write(`\n🎬 Video submitted: ${ev.data?.taskId} (snapshot: ${ev.data?.snapshotId})\n`);
335
+ break;
328
336
  case 'music_task':
329
337
  process.stderr.write(`\n🎵 Music submitted: ${ev.data?.taskId}\n`);
330
338
  break;
@@ -444,18 +452,23 @@ async function watchRun(baseUrl, headers, runId, opts = {}) {
444
452
 
445
453
  // ─── Async Task Polling ──────────────────────────────────────────────────────
446
454
 
447
- async function pollVideo(baseUrl, headers, taskId) {
448
- process.stderr.write(`🎬 Waiting for video ${taskId}...\n`);
455
+ async function pollVideo(baseUrl, headers, taskId, snapshotId) {
456
+ const label = snapshotId ? (taskId ? `${taskId} (snapshot)` : snapshotId) : taskId;
457
+ process.stderr.write(`🎬 Waiting for video ${label}...\n`);
449
458
  const start = Date.now();
459
+ // v2: poll /api/video-snapshot/[snapshotId]; v1: poll /api/animate/[taskId]
460
+ const endpoint = snapshotId
461
+ ? `${baseUrl}/api/video-snapshot/${snapshotId}`
462
+ : `${baseUrl}/api/animate/${taskId}`;
450
463
  while (true) {
451
464
  await new Promise(r => setTimeout(r, 10_000));
452
465
  const elapsed = Math.round((Date.now() - start) / 1000);
453
466
  try {
454
- const res = await fetch(`${baseUrl}/api/animate/${taskId}`, { headers });
467
+ const res = await fetch(endpoint, { headers });
455
468
  if (!res.ok) continue;
456
469
  const data = await res.json();
457
470
  if (data.videoUrl) { process.stderr.write(`\r🎬 Video done (${elapsed}s): ${data.videoUrl}\n`); return data.videoUrl; }
458
- if (data.status === 'failed') { process.stderr.write(`\r🎬 Video failed (${elapsed}s)\n`); return null; }
471
+ if (data.status === 'failed' || data.status === 'abandoned') { process.stderr.write(`\r🎬 Video ${data.status} (${elapsed}s)\n`); return null; }
459
472
  process.stderr.write(`\r🎬 Video rendering... ${elapsed}s`);
460
473
  } catch { /* retry */ }
461
474
  if (elapsed > 600) { process.stderr.write(`\r🎬 Video timeout (${elapsed}s)\n`); return null; }
@@ -597,6 +610,43 @@ function readImageAsDataUrl(filePath) {
597
610
  return `data:${v.mime};base64,${buf.toString('base64')}`;
598
611
  }
599
612
 
613
+ /**
614
+ * Upload a local file via signed URL (works for both images and videos).
615
+ * 1. POST /api/storage/upload-url → get signed URL + public URL
616
+ * 2. PUT file directly to Supabase Storage (no Vercel body limit)
617
+ * Returns public URL on success, null on failure.
618
+ */
619
+ async function uploadFileViaSignedUrl(baseUrl, headers, projectId, filePath, contentType) {
620
+ const filename = path.basename(filePath);
621
+ // Step 1: get signed upload URL
622
+ const urlRes = await fetch(`${baseUrl}/api/storage/upload-url`, {
623
+ method: 'POST',
624
+ headers: { 'Content-Type': 'application/json', ...headers },
625
+ body: JSON.stringify({ projectId, filename, contentType }),
626
+ });
627
+ if (!urlRes.ok) {
628
+ process.stderr.write(`⚠️ Failed to get upload URL: ${await urlRes.text()}\n`);
629
+ return null;
630
+ }
631
+ const { uploadUrl, token, publicUrl } = await urlRes.json();
632
+
633
+ // Step 2: PUT file directly to Supabase Storage
634
+ const buf = fs.readFileSync(filePath);
635
+ const putRes = await fetch(uploadUrl, {
636
+ method: 'PUT',
637
+ headers: {
638
+ 'Content-Type': contentType,
639
+ 'Authorization': `Bearer ${token}`,
640
+ },
641
+ body: buf,
642
+ });
643
+ if (!putRes.ok) {
644
+ process.stderr.write(`⚠️ Failed to upload file: ${await putRes.text()}\n`);
645
+ return null;
646
+ }
647
+ return publicUrl;
648
+ }
649
+
600
650
  function imageToArg(imgPath) {
601
651
  if (imgPath.startsWith('http://') || imgPath.startsWith('https://')) return imgPath;
602
652
  return readImageAsDataUrl(imgPath);
@@ -643,6 +693,7 @@ if (command === 'login') {
643
693
  const { headers, baseUrl } = getAuth();
644
694
  let projectId = null;
645
695
  const chatImages = [];
696
+ const chatVideos = [];
646
697
  const promptParts = [];
647
698
  let useStream = false;
648
699
  let background = false;
@@ -651,11 +702,12 @@ if (command === 'login') {
651
702
  let preferredModel = undefined;
652
703
  for (let i = 1; i < args.length; i++) {
653
704
  if (args[i] === '--help' || args[i] === '-h') {
654
- console.error('Usage: makaron chat --project <id|auto> [--image <file>] [--stream] [--background|-b] [--json] "your message"');
705
+ console.error('Usage: makaron chat --project <id|auto> [--image <file>] [--video <file|url>] [--stream] [--background|-b] [--json] "your message"');
655
706
  process.exit(0);
656
707
  }
657
708
  else if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
658
709
  else if (args[i] === '--image' && args[i + 1]) chatImages.push(args[++i]);
710
+ else if (args[i] === '--video' && args[i + 1]) chatVideos.push(args[++i]);
659
711
  else if (args[i] === '--stream') useStream = true;
660
712
  else if (args[i] === '--background' || args[i] === '-b') background = true;
661
713
  else if (args[i] === '--json') jsonOutput = true;
@@ -665,13 +717,17 @@ if (command === 'login') {
665
717
  }
666
718
  const prompt = promptParts.join(' ');
667
719
  if (!prompt) {
668
- console.error('Usage: makaron chat --project <id|auto> [--image <file>] [--stream] [--background|-b] [--json] "your message"');
720
+ console.error('Usage: makaron chat --project <id|auto> [--image <file>] [--video <file|url>] [--stream] [--background|-b] [--json] "your message"');
669
721
  process.exit(1);
670
722
  }
671
- // --project auto: create a new project (with images if provided)
723
+ // Split images into URLs vs local files
724
+ const imageUrlList = chatImages.filter(p => p.startsWith('http://') || p.startsWith('https://'));
725
+ const imageFileList = chatImages.filter(p => !p.startsWith('http://') && !p.startsWith('https://'));
726
+
727
+ // --project auto: create a new project (with images/videos if provided)
672
728
  if (!projectId || projectId === 'auto') {
673
729
  if (chatImages.length === 0) {
674
- // Create empty project
730
+ // Create empty project (videos will be uploaded separately after)
675
731
  process.stderr.write(`📦 Creating new project...\n`);
676
732
  const res = await fetch(`${baseUrl}/api/projects/create`, {
677
733
  method: 'POST',
@@ -683,33 +739,43 @@ if (command === 'login') {
683
739
  projectId = data.projectId;
684
740
  process.stderr.write(`📦 Project created: ${projectId}\n`);
685
741
  } else {
686
- // Create project with images
687
- const base64s = chatImages.map(imgPath => {
742
+ // Create project with images (URLs and/or local files)
743
+ const base64s = imageFileList.map(imgPath => {
688
744
  process.stderr.write(`📤 Uploading ${path.basename(imgPath)}...\n`);
689
745
  return readImageAsDataUrl(imgPath);
690
746
  });
747
+ if (imageUrlList.length) process.stderr.write(`📤 Attaching ${imageUrlList.length} URL image(s)...\n`);
748
+ const body = {};
749
+ if (base64s.length) body.imageBase64s = base64s;
750
+ if (imageUrlList.length) body.imageUrls = imageUrlList;
691
751
  const res = await fetch(`${baseUrl}/api/projects/create`, {
692
752
  method: 'POST',
693
753
  headers: { 'Content-Type': 'application/json', ...headers },
694
- body: JSON.stringify({ imageBase64s: base64s }),
754
+ body: JSON.stringify(body),
695
755
  });
696
756
  if (!res.ok) { process.stderr.write(`❌ Failed to create project: ${await res.text()}\n`); process.exit(1); }
697
757
  const data = await res.json();
698
758
  projectId = data.projectId;
699
759
  process.stderr.write(`📦 Project created: ${projectId} (${data.snapshots?.length || 0} images)\n`);
700
760
  }
701
- chatImages.length = 0; // already uploaded
761
+ chatImages.length = 0;
762
+ imageUrlList.length = 0;
763
+ imageFileList.length = 0;
702
764
  }
703
765
  // Upload additional images to existing project
704
- if (chatImages.length > 0) {
705
- const base64s = chatImages.map(imgPath => {
766
+ if (imageFileList.length > 0 || imageUrlList.length > 0) {
767
+ const base64s = imageFileList.map(imgPath => {
706
768
  process.stderr.write(`📤 Uploading ${path.basename(imgPath)}...\n`);
707
769
  return readImageAsDataUrl(imgPath);
708
770
  });
771
+ if (imageUrlList.length) process.stderr.write(`📤 Attaching ${imageUrlList.length} URL image(s)...\n`);
772
+ const body = { _addToProject: projectId };
773
+ if (base64s.length) body.imageBase64s = base64s;
774
+ if (imageUrlList.length) body.imageUrls = imageUrlList;
709
775
  const res = await fetch(`${baseUrl}/api/projects/create`, {
710
776
  method: 'POST',
711
777
  headers: { 'Content-Type': 'application/json', ...headers },
712
- body: JSON.stringify({ imageBase64s: base64s, _addToProject: projectId }),
778
+ body: JSON.stringify(body),
713
779
  });
714
780
  if (res.ok) {
715
781
  const data = await res.json();
@@ -719,18 +785,70 @@ if (command === 'login') {
719
785
  }
720
786
  }
721
787
 
788
+ // Upload videos to project timeline (via /api/projects/create with videoUrls)
789
+ let finalPrompt = prompt;
790
+ if (chatVideos.length > 0) {
791
+ const videoUrlList = chatVideos.filter(p => p.startsWith('http://') || p.startsWith('https://'));
792
+ const videoFileList = chatVideos.filter(p => !p.startsWith('http://') && !p.startsWith('https://'));
793
+
794
+ // Validate local video files
795
+ const validVideoFiles = [];
796
+ for (const videoPath of videoFileList) {
797
+ if (!fs.existsSync(videoPath)) { process.stderr.write(`⚠️ Video file not found: ${videoPath}\n`); continue; }
798
+ const stat = fs.statSync(videoPath);
799
+ if (stat.size > 200 * 1024 * 1024) { process.stderr.write(`⚠️ Video too large: ${(stat.size/1024/1024).toFixed(1)}MB (max 200MB)\n`); continue; }
800
+ const ext = path.extname(videoPath).slice(1).toLowerCase();
801
+ if (!['mp4', 'mov', 'webm'].includes(ext)) { process.stderr.write(`⚠️ Unsupported video format: .${ext}. Use MP4, MOV, or WebM.\n`); continue; }
802
+ validVideoFiles.push(videoPath);
803
+ }
804
+
805
+ // Upload local files via signed URL (no size limit, works with API key auth)
806
+ const uploadedVideoUrls = [...videoUrlList];
807
+ for (const videoPath of validVideoFiles) {
808
+ process.stderr.write(`📹 Uploading ${path.basename(videoPath)} (${(fs.statSync(videoPath).size/1024/1024).toFixed(1)}MB)...\n`);
809
+ const ext = path.extname(videoPath).slice(1).toLowerCase();
810
+ const mime = ext === 'mov' ? 'video/quicktime' : ext === 'webm' ? 'video/webm' : 'video/mp4';
811
+ const url = await uploadFileViaSignedUrl(baseUrl, headers, projectId, videoPath, mime);
812
+ if (url) {
813
+ uploadedVideoUrls.push(url);
814
+ process.stderr.write(`📹 Uploaded: ${path.basename(videoPath)}\n`);
815
+ }
816
+ }
817
+
818
+ // Add videos to project via projects/create (same as images)
819
+ if (uploadedVideoUrls.length > 0) {
820
+ if (videoUrlList.length) process.stderr.write(`📹 Adding ${uploadedVideoUrls.length} video(s) to timeline...\n`);
821
+ const res = await fetch(`${baseUrl}/api/projects/create`, {
822
+ method: 'POST',
823
+ headers: { 'Content-Type': 'application/json', ...headers },
824
+ body: JSON.stringify({ _addToProject: projectId, videoUrls: uploadedVideoUrls }),
825
+ });
826
+ if (res.ok) {
827
+ const data = await res.json();
828
+ const videoSnaps = (data.snapshots || []).filter(s => s.type === 'video');
829
+ process.stderr.write(`📹 Added ${videoSnaps.length} video(s) to timeline\n`);
830
+ } else {
831
+ process.stderr.write(`⚠️ Failed to add videos: ${await res.text()}\n`);
832
+ }
833
+ }
834
+
835
+ // Inject hint so Agent knows videos are available
836
+ const hint = `[User uploaded ${chatVideos.length === 1 ? 'a video' : `${chatVideos.length} videos`}. Use analyze_video to understand the content.]`;
837
+ finalPrompt = `${prompt}\n\n${hint}`;
838
+ }
839
+
722
840
  if (useStream) {
723
841
  // Legacy SSE mode
724
- const { results } = await streamAgent(baseUrl, headers, projectId, prompt);
842
+ const { results } = await streamAgent(baseUrl, headers, projectId, finalPrompt);
725
843
  process.stderr.write('\n━━━ Results ━━━\n');
726
844
  for (const img of results.images) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
727
845
  for (const d of results.designs) process.stderr.write(`🎨 ${d.desc}\n`);
728
846
  process.stderr.write(`🔗 ${APP_URL}/projects/${projectId}\n`);
729
- for (const task of results.animationTasks) await pollVideo(baseUrl, headers, task.taskId);
847
+ for (const task of results.animationTasks) await pollVideo(baseUrl, headers, task.taskId, task.snapshotId);
730
848
  for (const task of results.musicTasks) await pollMusic(baseUrl, headers, task.taskId);
731
849
  } else {
732
850
  // Default: fire-and-forget + poll
733
- const { runId } = await submitRun(baseUrl, headers, projectId, prompt, { videoModel, preferredModel });
851
+ const { runId } = await submitRun(baseUrl, headers, projectId, finalPrompt, { videoModel, preferredModel });
734
852
  if (background) {
735
853
  // Just print runId and exit
736
854
  if (jsonOutput) {
@@ -873,7 +991,7 @@ if (command === 'login') {
873
991
 
874
992
  } else if (sub === 'create') {
875
993
  const images = [];
876
- let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined;
994
+ let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined, projectId = null, wait = false;
877
995
  for (let i = 2; i < args.length; i++) {
878
996
  if (args[i] === '--image' && args[i + 1]) images.push(args[++i]);
879
997
  else if (args[i] === '--script' && args[i + 1]) script = args[++i];
@@ -881,29 +999,83 @@ if (command === 'login') {
881
999
  else if (args[i] === '--duration' && args[i + 1]) duration = Number(args[++i]);
882
1000
  else if (args[i] === '--aspect' && args[i + 1]) aspectRatio = args[++i];
883
1001
  else if (args[i] === '--model' && args[i + 1]) videoModel = args[++i];
1002
+ else if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
1003
+ else if (args[i] === '--wait') wait = true;
1004
+ }
1005
+ if (!images.length || !script) { console.error('Usage: makaron video create --script "..." --image <url> [--project <id>] [--duration 10] [--aspect 9:16] [--model kling|seedance] [--wait]'); process.exit(1); }
1006
+
1007
+ if (projectId) {
1008
+ // v2: submit to /api/video-snapshot → writes into timeline
1009
+ process.stderr.write('🎬 Submitting video to timeline...\n');
1010
+ const body = { projectId, imageUrls: images, prompt: script };
1011
+ if (duration) body.duration = duration;
1012
+ if (aspectRatio) body.aspectRatio = aspectRatio;
1013
+ if (videoModel) body.videoModel = videoModel;
1014
+ const res = await fetch(`${baseUrl}/api/video-snapshot`, {
1015
+ method: 'POST',
1016
+ headers: { 'Content-Type': 'application/json', ...headers },
1017
+ body: JSON.stringify(body),
1018
+ });
1019
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
1020
+ const data = await res.json();
1021
+ process.stderr.write(`✅ Video snapshot: ${data.snapshotId} (task: ${data.taskId})\n`);
1022
+ if (wait) {
1023
+ const url = await pollVideo(baseUrl, headers, data.taskId, data.snapshotId);
1024
+ if (url) console.log(url);
1025
+ } else {
1026
+ console.log(JSON.stringify(data, null, 2));
1027
+ }
1028
+ } else {
1029
+ // No project: use MCP tool (standalone, no timeline write)
1030
+ process.stderr.write('🎬 Submitting video...\n');
1031
+ const vArgs = { script, images };
1032
+ if (duration) vArgs.duration = duration;
1033
+ if (aspectRatio) vArgs.aspectRatio = aspectRatio;
1034
+ if (videoModel) vArgs.videoModel = videoModel;
1035
+ const result = await callMcpTool(baseUrl, headers, 'makaron_create_video', vArgs);
1036
+ const text = result?.content?.find(c => c.type === 'text')?.text;
1037
+ if (text) console.log(text);
884
1038
  }
885
- if (!images.length || !script) { console.error('Usage: makaron video create --script "..." --image <url> [--duration 10] [--aspect 9:16] [--model kling|seedance]'); process.exit(1); }
886
- process.stderr.write('🎬 Submitting video...\n');
887
- const vArgs = { script, images };
888
- if (duration) vArgs.duration = duration;
889
- if (aspectRatio) vArgs.aspectRatio = aspectRatio;
890
- if (videoModel) vArgs.videoModel = videoModel;
891
- const result = await callMcpTool(baseUrl, headers, 'makaron_create_video', vArgs);
892
- const text = result?.content?.find(c => c.type === 'text')?.text;
893
- if (text) console.log(text);
894
1039
 
895
1040
  } else if (sub === 'status') {
896
- const taskId = args[2];
897
- if (!taskId) { console.error('Usage: makaron video status <taskId>'); process.exit(1); }
898
- const result = await callMcpTool(baseUrl, headers, 'makaron_get_video_status', { taskId });
899
- const text = result?.content?.find(c => c.type === 'text')?.text;
900
- if (text) console.log(text);
1041
+ let taskId = null, snapshotId = null, wait = false;
1042
+ for (let i = 2; i < args.length; i++) {
1043
+ if (args[i] === '--snapshot' && args[i + 1]) snapshotId = args[++i];
1044
+ else if (args[i] === '--wait') wait = true;
1045
+ else if (!taskId) taskId = args[i];
1046
+ }
1047
+ if (!taskId && !snapshotId) { console.error('Usage: makaron video status <taskId> | --snapshot <snapshotId> [--wait]'); process.exit(1); }
1048
+
1049
+ if (snapshotId || (taskId && taskId.length === 36 && taskId.includes('-'))) {
1050
+ // v2: poll /api/video-snapshot/[snapshotId]
1051
+ const id = snapshotId || taskId;
1052
+ if (wait) {
1053
+ const url = await pollVideo(baseUrl, headers, null, id);
1054
+ if (url) console.log(url);
1055
+ } else {
1056
+ const res = await fetch(`${baseUrl}/api/video-snapshot/${id}`, { headers });
1057
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
1058
+ const data = await res.json();
1059
+ console.log(JSON.stringify(data, null, 2));
1060
+ }
1061
+ } else {
1062
+ // v1: MCP get_video_status
1063
+ if (wait) {
1064
+ const url = await pollVideo(baseUrl, headers, taskId);
1065
+ if (url) console.log(url);
1066
+ } else {
1067
+ const result = await callMcpTool(baseUrl, headers, 'makaron_get_video_status', { taskId });
1068
+ const text = result?.content?.find(c => c.type === 'text')?.text;
1069
+ if (text) console.log(text);
1070
+ }
1071
+ }
901
1072
 
902
1073
  } else {
903
1074
  console.log(`Video commands:
904
1075
  video script --image <file> [--image <file>] "direction" Write video script
905
1076
  video create --script "..." --image <url> [--duration 10] Submit video task
906
1077
  video status <taskId> Check video status
1078
+ video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
907
1079
  `);
908
1080
  }
909
1081
 
@@ -1158,6 +1330,7 @@ Commands:
1158
1330
  create --title "name" Create empty project (text-to-image)
1159
1331
 
1160
1332
  chat --project <id> "message" Chat (non-blocking, polls for result)
1333
+ chat --project <id> --video <file> Attach video to conversation
1161
1334
  chat --project <id> -b "message" Background: submit and print runId
1162
1335
  chat --project <id> --stream "msg" Legacy: stream SSE in real-time
1163
1336
  chat --project <id> --json "msg" Output structured JSON result
@@ -1168,7 +1341,7 @@ Commands:
1168
1341
  abort <runId> Abort a running Agent
1169
1342
 
1170
1343
  edit [--image <file>] "prompt" AI image edit / text-to-image
1171
- video script|create|status Video generation
1344
+ video script|create|status Video generation (v2 timeline support)
1172
1345
  music create|status Music generation
1173
1346
 
1174
1347
  admin Admin commands (skills, upload, set-admin)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,6 +8,9 @@
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
+ "skills/",
12
+ ".codex-plugin/",
13
+ ".claude-plugin/",
11
14
  "SKILL.md",
12
15
  "README.md"
13
16
  ],
@@ -0,0 +1,271 @@
1
+ ---
2
+ name: makaron
3
+ description: Use Makaron CLI to generate AI images, videos, music, and motion designs. Trigger when user needs creative media production — photo editing, video generation, music composition, or design creation. Requires `npx makaron-cli` and MAKARON_API_KEY env var.
4
+ ---
5
+
6
+ # Makaron CLI — Agent Integration Skill
7
+
8
+ > **makaron.app** is for humans. **makaron-cli** is for AI agents.
9
+
10
+ Makaron is a multimodal AI creative agent. You talk to it via `makaron chat`, and it produces images, videos, music, and animated designs — all saved to a persistent project.
11
+
12
+ ## Setup
13
+
14
+ ### Get your API key
15
+
16
+ **Option A: Human login**
17
+ 1. Go to [makaron.app](https://makaron.app) and log in
18
+ 2. Open the menu (top-right) → **Get API Key**
19
+ 3. Copy your `mk_live_...` key
20
+
21
+ **Option B: Self-Registration (no human required)**
22
+ ```bash
23
+ # Step 1: Get challenge
24
+ npx makaron-cli register --json
25
+ # → { "challenge_id": "...", "challenge": "...", "expected_format": "numeric, round to 2 decimal places" }
26
+
27
+ # Step 2: Solve and verify
28
+ npx makaron-cli register --verify --challenge-id <id> --answer 34.5
29
+ # → Key saved to ~/.makaron/auth.json
30
+ # → { "api_key": "mk_live_...", "credits": N, "claim_url": "..." }
31
+
32
+ # (Optional) Let a human claim this account
33
+ npx makaron-cli claim
34
+ # → { "claim_url": "..." } — share with human to link key to their account (valid 7 days)
35
+ ```
36
+
37
+ Discovery endpoint: `GET https://www.makaron.app/api/agent/register` — returns full registration flow + CLI usage as JSON.
38
+
39
+ After self-registration the key is saved locally — no need to export `MAKARON_API_KEY`.
40
+
41
+ ```bash
42
+ export MAKARON_API_KEY=mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
43
+ ```
44
+
45
+ Verify: `npx makaron-cli list` should show projects.
46
+
47
+ ## Core Workflow
48
+
49
+ ```bash
50
+ # One-shot: create project + upload image + submit prompt — all in one command
51
+ RUN_ID=$(npx makaron-cli chat --project auto --image photo.jpg -b "make it cinematic and create a 5s video")
52
+
53
+ # Watch until all artifacts are ready
54
+ npx makaron-cli responses watch $RUN_ID --jsonl
55
+ ```
56
+
57
+ Or with an existing project:
58
+ ```bash
59
+ RUN_ID=$(npx makaron-cli chat --project $PROJECT_ID -b "make a 5s video")
60
+ npx makaron-cli responses watch $RUN_ID --jsonl
61
+ ```
62
+
63
+ ## Primary: `chat` (Agent-driven creative work)
64
+
65
+ Use `chat` for all creative tasks. Makaron Agent decides how to execute — it can edit images, generate videos, compose music, and create designs in a single conversation.
66
+
67
+ ### Submit a request
68
+
69
+ ```bash
70
+ # With existing project
71
+ npx makaron-cli chat --project <id> --json -b "<prompt>"
72
+
73
+ # Auto-create project (with or without images)
74
+ npx makaron-cli chat --project auto --image photo.jpg --json -b "make it cinematic"
75
+ npx makaron-cli chat --project auto --image img1.jpg --image img2.jpg --json -b "combine these"
76
+ ```
77
+
78
+ Returns immediately:
79
+ ```json
80
+ {"runId": "xxx", "projectId": "...", "projectUrl": "https://www.makaron.app/projects/...", "status": "running"}
81
+ ```
82
+
83
+ ### With additional images (existing project)
84
+
85
+ ```bash
86
+ npx makaron-cli chat --project <id> --image ref1.jpg --image ref2.jpg -b "use these as style reference"
87
+ ```
88
+
89
+ ### Check status (single query)
90
+
91
+ ```bash
92
+ npx makaron-cli responses get <runId> --json
93
+ ```
94
+
95
+ ### Watch until done (streaming events)
96
+
97
+ ```bash
98
+ npx makaron-cli responses watch <runId> --jsonl
99
+ ```
100
+
101
+ Outputs one JSON per line as artifacts appear:
102
+ ```
103
+ {"event":"output.added","item":{"id":"out_1","type":"image","status":"completed","url":"https://..."}}
104
+ {"event":"output.added","item":{"id":"out_2","type":"video","status":"rendering","task_id":"xxx"}}
105
+ {"event":"output.updated","item":{"id":"out_2","type":"video","status":"completed","url":"https://..."}}
106
+ {"event":"done","status":"completed"}
107
+ ```
108
+
109
+ ### Extract specific results
110
+
111
+ ```bash
112
+ npx makaron-cli responses get <runId> --pick first_image_url
113
+ npx makaron-cli responses get <runId> --pick image_urls # all images (JSON array)
114
+ npx makaron-cli responses get <runId> --pick first_video_url
115
+ npx makaron-cli responses get <runId> --pick video_urls # all videos
116
+ npx makaron-cli responses get <runId> --pick project_url
117
+ npx makaron-cli responses get <runId> --pick text # agent's text reply
118
+ npx makaron-cli responses get <runId> --pick output # full output array
119
+ npx makaron-cli responses get <runId> --pick status
120
+ ```
121
+
122
+ ## Fallback: Direct tool calls (no project context)
123
+
124
+ Use these only when `chat` is unavailable or you need raw model access without project/conversation context.
125
+
126
+ ### `edit` — One-shot image editing
127
+
128
+ ```bash
129
+ # Edit an existing image
130
+ npx makaron-cli edit --image photo.jpg "add cinematic warm lighting"
131
+
132
+ # Text-to-image (no input)
133
+ npx makaron-cli edit "a cyberpunk cityscape at night"
134
+
135
+ # With model/skill/reference
136
+ npx makaron-cli edit --image photo.jpg --model openai --skill captions "add title"
137
+ npx makaron-cli edit --image photo.jpg --ref style.jpg "match this style"
138
+
139
+ # Output to file
140
+ npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
141
+ ```
142
+
143
+ Options: `--image`, `--model gemini|qwen|openai|pony|wai`, `--skill enhance|creative|wild|captions`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
144
+
145
+ ### `video` — Video generation (3 steps)
146
+
147
+ ```bash
148
+ # 1. Write script from images
149
+ npx makaron-cli video script --image img1.jpg "cinematic story"
150
+
151
+ # 2. Submit rendering (images must be public URLs from step 1 or uploaded)
152
+ npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
153
+
154
+ # 3. Check status
155
+ npx makaron-cli video status <taskId>
156
+ ```
157
+
158
+ Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--duration 3|5|7|10|15`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`
159
+
160
+ ### `music` — Music generation
161
+
162
+ ```bash
163
+ npx makaron-cli music create "gentle piano, warm strings, cinematic"
164
+ npx makaron-cli music create --vocals --style "lo-fi" "rainy day vibes"
165
+ npx makaron-cli music status <taskId>
166
+ ```
167
+
168
+ Options: `--vocals` (include vocals), `--style "genre"`
169
+
170
+ ## Response Schema
171
+
172
+ ```typescript
173
+ type MakaronRunResponse = {
174
+ id: string
175
+ status: "in_progress" | "completed" | "failed" | "aborted"
176
+ incomplete: boolean // true = keep polling
177
+ project_id: string
178
+ project_url: string
179
+ next_poll_after_ms?: number // suggested poll interval
180
+ output: MakaronOutput[]
181
+ }
182
+
183
+ type MakaronOutput =
184
+ | { id: string; type: "text"; status: "completed"; content: string }
185
+ | { id: string; type: "image"; status: "completed"; url: string; snapshot_id: string }
186
+ | { id: string; type: "design"; status: "completed"; url: string; width: number; height: number; animated: boolean; duration?: number }
187
+ | { id: string; type: "video"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; url?: string; elapsed_seconds?: number }
188
+ | { id: string; type: "music"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; url?: string; elapsed_seconds?: number }
189
+ ```
190
+
191
+ ## Polling Rules
192
+
193
+ 1. Poll while `incomplete: true` or `status` is `"in_progress"`
194
+ 2. Use `next_poll_after_ms` as interval (default 5000ms)
195
+ 3. Stop when `status` is `"completed"`, `"failed"`, or `"aborted"`
196
+ 4. Top-level `status: "completed"` means ALL artifacts are ready (including rendered videos)
197
+
198
+ ## Exit Codes
199
+
200
+ | Code | Meaning |
201
+ |------|---------|
202
+ | 0 | Success (completed) or valid in-progress response |
203
+ | 1 | Failed, aborted, or HTTP error |
204
+ | 2 | Timeout (partial response still printed to stdout) |
205
+
206
+ ## What Makaron Agent Can Do
207
+
208
+ | Task | Example prompt |
209
+ |------|---------------|
210
+ | Edit photo | "make it cinematic with warm tones" |
211
+ | Style transfer | "convert to oil painting style" |
212
+ | Add/remove elements | "add a cat on the table" / "remove background person" |
213
+ | Text-to-image | "generate a cyberpunk cityscape" |
214
+ | Video from image | "create a 5 second video of her walking" |
215
+ | Video with model | "use seedance model, make a 5s video" |
216
+ | Background music | "add calm piano music" |
217
+ | Motion design | "create an Instagram story with animated text" |
218
+ | Multi-step | "edit the photo then make a video from it" |
219
+
220
+ ## Recommended Pattern: Service Flow (Feishu/OpenClaw/Group Chat)
221
+
222
+ When serving end-users in a chat environment (Feishu, Slack, Discord), use this proactive message pattern:
223
+
224
+ ```bash
225
+ # 1. Immediately acknowledge the user
226
+ send_message "Got it! Working on it now..."
227
+
228
+ # 2. Create project + submit (one command)
229
+ RUN_ID=$(npx makaron-cli chat --project auto --image photo.jpg -b "make it cinematic and create a 5s video")
230
+
231
+ # 3. Send project link proactively
232
+ PROJECT_URL=$(npx makaron-cli responses get $RUN_ID --pick project_url)
233
+ send_message "Project created: $PROJECT_URL"
234
+
235
+ # 4. Watch and send each artifact as it appears
236
+ npx makaron-cli responses watch $RUN_ID --jsonl | while read -r line; do
237
+ EVENT=$(echo "$line" | jq -r '.event')
238
+ TYPE=$(echo "$line" | jq -r '.item.type // empty')
239
+ URL=$(echo "$line" | jq -r '.item.url // empty')
240
+ STATUS=$(echo "$line" | jq -r '.item.status // empty')
241
+
242
+ if [ "$EVENT" = "output.added" ] && [ "$TYPE" = "image" ]; then
243
+ # Send image immediately as media (not just link)
244
+ send_image "$URL"
245
+ elif [ "$EVENT" = "output.updated" ] && [ "$TYPE" = "video" ] && [ "$STATUS" = "completed" ]; then
246
+ # Video ready — send as media
247
+ send_video "$URL"
248
+ elif [ "$EVENT" = "done" ]; then
249
+ send_message "All done!"
250
+ fi
251
+ done
252
+ ```
253
+
254
+ **Key principles for service agents:**
255
+ - **Proactive, not reactive**: Don't wait for the full run to finish. Send progress messages and artifacts as they appear.
256
+ - **Media over links**: When possible, send images/videos as native media in the chat (download URL and upload as attachment), not just paste the URL.
257
+ - **Immediate acknowledgment**: Reply within 1 second of receiving user request. Don't make users wait for project creation.
258
+ - **Project link early**: Send the project URL right after creation so users can check anytime.
259
+ - **Stream artifacts**: Use `watch --jsonl` to push each artifact the moment it's ready. An image at 15s should reach the user at 15s, not after the video finishes at 5 minutes.
260
+
261
+ ## Important Notes
262
+
263
+ - One project = one conversation thread. All history is preserved.
264
+ - One run at a time per project. New message interrupts previous run.
265
+ - Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
266
+ - Videos take 2-5 minutes to render. Use `watch` to get URL when ready.
267
+ - Music takes ~60 seconds. Appears in output when done.
268
+ - Images are typically ready in 15-30 seconds.
269
+ - stdout is always machine-readable JSON/text. Human-friendly logs go to stderr.
270
+ - Always use `chat` as the primary interface — even for single image edits.
271
+ - `edit`/`video`/`music` are fallback tools for when `chat` is unavailable or you need raw model access without project context.