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.
- package/.claude-plugin/plugin.json +12 -0
- package/.codex-plugin/plugin.json +21 -0
- package/SKILL.md +16 -1
- package/bin/makaron.mjs +246 -48
- package/package.json +4 -1
- package/skills/makaron/SKILL.md +271 -0
|
@@ -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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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;
|
|
761
|
+
chatImages.length = 0;
|
|
762
|
+
imageUrlList.length = 0;
|
|
763
|
+
imageFileList.length = 0;
|
|
676
764
|
}
|
|
677
765
|
// Upload additional images to existing project
|
|
678
|
-
if (
|
|
679
|
-
const base64s =
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
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.
|
|
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.
|