makaron-cli 0.6.1 → 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; }
@@ -499,15 +512,11 @@ async function createProject(baseUrl, headers, opts) {
499
512
  if (opts.imageUrls?.length) {
500
513
  body.imageUrls = opts.imageUrls;
501
514
  } else if (opts.images?.length) {
502
- body.imageBase64s = opts.images.map(f => {
503
- const buf = fs.readFileSync(f);
504
- return `data:image/jpeg;base64,${buf.toString('base64')}`;
505
- });
515
+ body.imageBase64s = opts.images.map(f => readImageAsDataUrl(f));
506
516
  } else if (opts.imageUrl) {
507
517
  body.imageUrl = opts.imageUrl;
508
518
  } else if (opts.image) {
509
- const buf = fs.readFileSync(opts.image);
510
- body.imageBase64 = `data:image/jpeg;base64,${buf.toString('base64')}`;
519
+ body.imageBase64 = readImageAsDataUrl(opts.image);
511
520
  }
512
521
  if (opts.title) body.title = opts.title;
513
522
 
@@ -567,12 +576,80 @@ async function callMcpTool(baseUrl, headers, toolName, args) {
567
576
  return data.result;
568
577
  }
569
578
 
579
+ // ─── Image Validation ────────────────────────────────────────────────────────
580
+
581
+ function detectMime(buf) {
582
+ if (buf[0]===0xFF && buf[1]===0xD8) return 'image/jpeg';
583
+ if (buf[0]===0x89 && buf[1]===0x50 && buf[2]===0x4E && buf[3]===0x47) return 'image/png';
584
+ if (buf[0]===0x52 && buf[1]===0x49 && buf[2]===0x46 && buf[3]===0x46 && buf[8]===0x57 && buf[9]===0x45 && buf[10]===0x42 && buf[11]===0x50) return 'image/webp';
585
+ if (buf[0]===0x47 && buf[1]===0x49 && buf[2]===0x46) return 'image/gif';
586
+ if (buf.length >= 8 && buf[4]===0x66 && buf[5]===0x74 && buf[6]===0x79 && buf[7]===0x70) return 'image/heic';
587
+ return null;
588
+ }
589
+
590
+ function validateImage(filePath) {
591
+ if (!fs.existsSync(filePath)) return { error: `File not found: ${filePath}` };
592
+ const stat = fs.statSync(filePath);
593
+ if (stat.size === 0) return { error: `File is empty: ${filePath}` };
594
+ if (stat.size > 10 * 1024 * 1024) return { error: `File too large: ${(stat.size/1024/1024).toFixed(1)}MB (max 10MB). Resize before uploading.` };
595
+ const header = Buffer.alloc(12);
596
+ const fd = fs.openSync(filePath, 'r');
597
+ fs.readSync(fd, header, 0, 12, 0);
598
+ fs.closeSync(fd);
599
+ const mime = detectMime(header);
600
+ if (!mime) return { error: `Unsupported format: ${path.extname(filePath)}. Supported: JPEG, PNG, WebP` };
601
+ if (mime === 'image/heic') return { error: `HEIC format not supported via CLI. Convert first:\n sips -s format jpeg "${filePath}" --out output.jpg` };
602
+ if (mime === 'image/gif') return { error: `GIF format not supported. Convert to JPEG or PNG first.` };
603
+ return { ok: true, mime };
604
+ }
605
+
606
+ function readImageAsDataUrl(filePath) {
607
+ const v = validateImage(filePath);
608
+ if (!v.ok) { console.error(`❌ Cannot upload: ${path.basename(filePath)}\n ${v.error}`); process.exit(1); }
609
+ const buf = fs.readFileSync(filePath);
610
+ return `data:${v.mime};base64,${buf.toString('base64')}`;
611
+ }
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
+
570
650
  function imageToArg(imgPath) {
571
651
  if (imgPath.startsWith('http://') || imgPath.startsWith('https://')) return imgPath;
572
- const buf = fs.readFileSync(imgPath);
573
- const ext = imgPath.split('.').pop()?.toLowerCase();
574
- const mime = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp' }[ext] || 'image/jpeg';
575
- return `data:${mime};base64,${buf.toString('base64')}`;
652
+ return readImageAsDataUrl(imgPath);
576
653
  }
577
654
 
578
655
  function saveMcpImage(result, outputPath) {
@@ -616,6 +693,7 @@ if (command === 'login') {
616
693
  const { headers, baseUrl } = getAuth();
617
694
  let projectId = null;
618
695
  const chatImages = [];
696
+ const chatVideos = [];
619
697
  const promptParts = [];
620
698
  let useStream = false;
621
699
  let background = false;
@@ -624,11 +702,12 @@ if (command === 'login') {
624
702
  let preferredModel = undefined;
625
703
  for (let i = 1; i < args.length; i++) {
626
704
  if (args[i] === '--help' || args[i] === '-h') {
627
- 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"');
628
706
  process.exit(0);
629
707
  }
630
708
  else if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
631
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]);
632
711
  else if (args[i] === '--stream') useStream = true;
633
712
  else if (args[i] === '--background' || args[i] === '-b') background = true;
634
713
  else if (args[i] === '--json') jsonOutput = true;
@@ -638,13 +717,17 @@ if (command === 'login') {
638
717
  }
639
718
  const prompt = promptParts.join(' ');
640
719
  if (!prompt) {
641
- 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"');
642
721
  process.exit(1);
643
722
  }
644
- // --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)
645
728
  if (!projectId || projectId === 'auto') {
646
729
  if (chatImages.length === 0) {
647
- // Create empty project
730
+ // Create empty project (videos will be uploaded separately after)
648
731
  process.stderr.write(`📦 Creating new project...\n`);
649
732
  const res = await fetch(`${baseUrl}/api/projects/create`, {
650
733
  method: 'POST',
@@ -656,35 +739,43 @@ if (command === 'login') {
656
739
  projectId = data.projectId;
657
740
  process.stderr.write(`📦 Project created: ${projectId}\n`);
658
741
  } else {
659
- // Create project with images
660
- const base64s = chatImages.map(imgPath => {
742
+ // Create project with images (URLs and/or local files)
743
+ const base64s = imageFileList.map(imgPath => {
661
744
  process.stderr.write(`📤 Uploading ${path.basename(imgPath)}...\n`);
662
- const buf = fs.readFileSync(imgPath);
663
- return `data:image/jpeg;base64,${buf.toString('base64')}`;
745
+ return readImageAsDataUrl(imgPath);
664
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;
665
751
  const res = await fetch(`${baseUrl}/api/projects/create`, {
666
752
  method: 'POST',
667
753
  headers: { 'Content-Type': 'application/json', ...headers },
668
- body: JSON.stringify({ imageBase64s: base64s }),
754
+ body: JSON.stringify(body),
669
755
  });
670
756
  if (!res.ok) { process.stderr.write(`❌ Failed to create project: ${await res.text()}\n`); process.exit(1); }
671
757
  const data = await res.json();
672
758
  projectId = data.projectId;
673
759
  process.stderr.write(`📦 Project created: ${projectId} (${data.snapshots?.length || 0} images)\n`);
674
760
  }
675
- chatImages.length = 0; // already uploaded
761
+ chatImages.length = 0;
762
+ imageUrlList.length = 0;
763
+ imageFileList.length = 0;
676
764
  }
677
765
  // Upload additional images to existing project
678
- if (chatImages.length > 0) {
679
- const base64s = chatImages.map(imgPath => {
766
+ if (imageFileList.length > 0 || imageUrlList.length > 0) {
767
+ const base64s = imageFileList.map(imgPath => {
680
768
  process.stderr.write(`📤 Uploading ${path.basename(imgPath)}...\n`);
681
- const buf = fs.readFileSync(imgPath);
682
- return `data:image/jpeg;base64,${buf.toString('base64')}`;
769
+ return readImageAsDataUrl(imgPath);
683
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;
684
775
  const res = await fetch(`${baseUrl}/api/projects/create`, {
685
776
  method: 'POST',
686
777
  headers: { 'Content-Type': 'application/json', ...headers },
687
- body: JSON.stringify({ imageBase64s: base64s, _addToProject: projectId }),
778
+ body: JSON.stringify(body),
688
779
  });
689
780
  if (res.ok) {
690
781
  const data = await res.json();
@@ -694,18 +785,70 @@ if (command === 'login') {
694
785
  }
695
786
  }
696
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
+
697
840
  if (useStream) {
698
841
  // Legacy SSE mode
699
- const { results } = await streamAgent(baseUrl, headers, projectId, prompt);
842
+ const { results } = await streamAgent(baseUrl, headers, projectId, finalPrompt);
700
843
  process.stderr.write('\n━━━ Results ━━━\n');
701
844
  for (const img of results.images) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
702
845
  for (const d of results.designs) process.stderr.write(`🎨 ${d.desc}\n`);
703
846
  process.stderr.write(`🔗 ${APP_URL}/projects/${projectId}\n`);
704
- 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);
705
848
  for (const task of results.musicTasks) await pollMusic(baseUrl, headers, task.taskId);
706
849
  } else {
707
850
  // Default: fire-and-forget + poll
708
- const { runId } = await submitRun(baseUrl, headers, projectId, prompt, { videoModel, preferredModel });
851
+ const { runId } = await submitRun(baseUrl, headers, projectId, finalPrompt, { videoModel, preferredModel });
709
852
  if (background) {
710
853
  // Just print runId and exit
711
854
  if (jsonOutput) {
@@ -848,7 +991,7 @@ if (command === 'login') {
848
991
 
849
992
  } else if (sub === 'create') {
850
993
  const images = [];
851
- let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined;
994
+ let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined, projectId = null, wait = false;
852
995
  for (let i = 2; i < args.length; i++) {
853
996
  if (args[i] === '--image' && args[i + 1]) images.push(args[++i]);
854
997
  else if (args[i] === '--script' && args[i + 1]) script = args[++i];
@@ -856,29 +999,83 @@ if (command === 'login') {
856
999
  else if (args[i] === '--duration' && args[i + 1]) duration = Number(args[++i]);
857
1000
  else if (args[i] === '--aspect' && args[i + 1]) aspectRatio = args[++i];
858
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);
859
1038
  }
860
- if (!images.length || !script) { console.error('Usage: makaron video create --script "..." --image <url> [--duration 10] [--aspect 9:16] [--model kling|seedance]'); process.exit(1); }
861
- process.stderr.write('🎬 Submitting video...\n');
862
- const vArgs = { script, images };
863
- if (duration) vArgs.duration = duration;
864
- if (aspectRatio) vArgs.aspectRatio = aspectRatio;
865
- if (videoModel) vArgs.videoModel = videoModel;
866
- const result = await callMcpTool(baseUrl, headers, 'makaron_create_video', vArgs);
867
- const text = result?.content?.find(c => c.type === 'text')?.text;
868
- if (text) console.log(text);
869
1039
 
870
1040
  } else if (sub === 'status') {
871
- const taskId = args[2];
872
- if (!taskId) { console.error('Usage: makaron video status <taskId>'); process.exit(1); }
873
- const result = await callMcpTool(baseUrl, headers, 'makaron_get_video_status', { taskId });
874
- const text = result?.content?.find(c => c.type === 'text')?.text;
875
- 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
+ }
876
1072
 
877
1073
  } else {
878
1074
  console.log(`Video commands:
879
1075
  video script --image <file> [--image <file>] "direction" Write video script
880
1076
  video create --script "..." --image <url> [--duration 10] Submit video task
881
1077
  video status <taskId> Check video status
1078
+ video status --snapshot <snapshotId> [--wait] Check v2 video snapshot
882
1079
  `);
883
1080
  }
884
1081
 
@@ -1133,6 +1330,7 @@ Commands:
1133
1330
  create --title "name" Create empty project (text-to-image)
1134
1331
 
1135
1332
  chat --project <id> "message" Chat (non-blocking, polls for result)
1333
+ chat --project <id> --video <file> Attach video to conversation
1136
1334
  chat --project <id> -b "message" Background: submit and print runId
1137
1335
  chat --project <id> --stream "msg" Legacy: stream SSE in real-time
1138
1336
  chat --project <id> --json "msg" Output structured JSON result
@@ -1143,7 +1341,7 @@ Commands:
1143
1341
  abort <runId> Abort a running Agent
1144
1342
 
1145
1343
  edit [--image <file>] "prompt" AI image edit / text-to-image
1146
- video script|create|status Video generation
1344
+ video script|create|status Video generation (v2 timeline support)
1147
1345
  music create|status Music generation
1148
1346
 
1149
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.1",
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.