makaron-cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +166 -65
  2. package/bin/makaron.mjs +387 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,18 +36,96 @@ The CLI checks `MAKARON_API_KEY` first, then falls back to the saved session in
36
36
 
37
37
  ## Commands
38
38
 
39
- ### `list` — Show all projects
39
+ ### `chat` — Send a message to Makaron Agent
40
+
41
+ This is the main command. The Agent can edit images, generate videos, compose music, and create designs.
42
+
43
+ **Default mode (non-blocking poll):**
40
44
 
41
45
  ```bash
42
- npx makaron-cli list
46
+ npx makaron-cli chat --project <id> "make it look cinematic"
43
47
  ```
44
48
 
45
- Output:
49
+ The CLI submits the request and polls every 3s for results. No long-lived connection — you can Ctrl+C and come back later with `responses get`.
50
+
51
+ **Background mode — submit and exit immediately:**
52
+
53
+ ```bash
54
+ npx makaron-cli chat --project <id> --background "make a 5s video"
55
+ # → prints runId and exits in <1s
56
+
57
+ # Later, check the result:
58
+ npx makaron-cli responses get <runId> --wait
59
+ ```
60
+
61
+ **Structured JSON output (for programmatic use):**
62
+
63
+ ```bash
64
+ npx makaron-cli chat --project <id> --json --background "edit the photo"
65
+ # → {"runId":"...","projectId":"...","projectUrl":"...","status":"running"}
46
66
  ```
47
- 📁 12 projects
48
67
 
49
- abc123def My Photo 3 snaps 2h ago
50
- xyz789ghi Sunset Edit 5 snaps 1d ago
68
+ **Legacy streaming mode (real-time SSE):**
69
+
70
+ ```bash
71
+ npx makaron-cli chat --project <id> --stream "hello"
72
+ ```
73
+
74
+ **Options:**
75
+ - `--project <id>` — target project (required)
76
+ - `--image <file>` — upload image to project before chatting
77
+ - `--background` / `-b` — submit and exit, print runId
78
+ - `--json` — structured JSON output
79
+ - `--stream` — legacy real-time SSE mode
80
+ - `--video-model kling|seedance` — preferred video model
81
+ - `--model <name>` — preferred image model
82
+
83
+ ### `responses` — Query run status and results
84
+
85
+ ```bash
86
+ # Get current status (single query)
87
+ npx makaron-cli responses get <runId>
88
+
89
+ # Poll until completed
90
+ npx makaron-cli responses get <runId> --wait
91
+
92
+ # Wait for video/music rendering too (not just Agent completion)
93
+ npx makaron-cli responses get <runId> --wait --wait-artifacts
94
+
95
+ # List runs for a project
96
+ npx makaron-cli responses list --project <id>
97
+ ```
98
+
99
+ **Result JSON structure:**
100
+
101
+ ```json
102
+ {
103
+ "runId": "...",
104
+ "projectId": "...",
105
+ "status": "completed",
106
+ "eventCount": 31,
107
+ "result": {
108
+ "text": "Agent's text response...",
109
+ "images": [{ "snapshotId": "...", "imageUrl": "https://..." }],
110
+ "designs": [{
111
+ "snapshotId": "...",
112
+ "imageUrl": "https://...",
113
+ "width": 1080,
114
+ "height": 1920,
115
+ "animation": { "fps": 30, "durationInSeconds": 3 },
116
+ "props": { "title": "..." },
117
+ "code": "function Design(props) { ... }"
118
+ }],
119
+ "videos": [{ "taskId": "...", "status": "completed", "videoUrl": "https://..." }],
120
+ "music": [{ "taskId": "...", "status": "completed", "audioUrl": "https://..." }]
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### `list` — Show all projects
126
+
127
+ ```bash
128
+ npx makaron-cli list
51
129
  ```
52
130
 
53
131
  ### `create` — Create a new project
@@ -64,81 +142,106 @@ npx makaron-cli create --image-url https://example.com/photo.jpg
64
142
  npx makaron-cli create --title "My New Project"
65
143
  ```
66
144
 
67
- Output:
68
- ```
69
- ✅ Project created
70
- ID: abc123def456
71
- Images: 1
72
- [1] https://sdyrtztrjgmmpnirswxt.supabase.co/storage/v1/object/public/images/...
73
- URL: https://www.makaron.app/projects/abc123def456
74
- ```
75
-
76
- ### `chat` — Send a message to Makaron Agent
77
-
78
- This is the main command. The Agent can edit images, generate videos, compose music, and create designs.
145
+ ### `abort` — Stop a running Agent
79
146
 
80
147
  ```bash
81
- npx makaron-cli chat --project <id> "make it look cinematic"
148
+ npx makaron-cli abort <runId>
82
149
  ```
83
150
 
84
- The Agent streams its response in real-time:
85
- - **Text** → stdout (pipe-friendly)
86
- - **Status/progress** stderr
87
- - **Images, videos, music** → URLs printed to stderr
151
+ ### `edit` AI image editing (direct MCP tool call)
152
+
153
+ Unlike `chat` (which uses the Agent with project context), `edit` directly calls the image generation model for one-shot results.
88
154
 
89
155
  ```bash
90
- # Add extra reference images to the project before chatting
91
- npx makaron-cli chat --project <id> --image ref.jpg "use this style"
92
- ```
156
+ # Text-to-image (no input image)
157
+ npx makaron-cli edit "a cyberpunk cityscape at night, neon reflections"
93
158
 
94
- After the Agent finishes, the CLI automatically polls for any pending video/music tasks until they complete.
159
+ # Edit an existing image
160
+ npx makaron-cli edit --image photo.jpg "add cinematic warm lighting"
95
161
 
96
- ### `abort` Stop a running Agent
162
+ # With model and skill
163
+ npx makaron-cli edit --image photo.jpg --model openai --skill captions "add elegant title"
97
164
 
98
- ```bash
99
- npx makaron-cli abort <runId>
165
+ # With reference images (up to 3)
166
+ npx makaron-cli edit --image photo.jpg --ref style1.jpg --ref style2.jpg "match this style"
167
+
168
+ # Specify output path and aspect ratio
169
+ npx makaron-cli edit --out result.jpg --aspect 9:16 "vertical poster design"
100
170
  ```
101
171
 
102
- Press `Ctrl+C` during `chat` to abort automatically.
172
+ Options:
173
+ - `--image <file|url>` — input image (omit for text-to-image)
174
+ - `--model gemini|qwen|openai|pony|wai` — model selection (default: auto)
175
+ - `--skill enhance|creative|wild|captions` — activate skill template
176
+ - `--ref <file|url>` — reference image (repeatable, up to 3)
177
+ - `--aspect <ratio>` — target aspect ratio (e.g. `4:5`, `1:1`, `16:9`)
178
+ - `--out <path>` — output file path (default: `makaron-output-{timestamp}.jpg`)
103
179
 
104
- ## Example: Full workflow
180
+ ### `video` Video generation
105
181
 
106
182
  ```bash
107
- # 1. Login (once)
108
- npx makaron-cli login
183
+ # Write a video script from images
184
+ npx makaron-cli video script --image img1.jpg --image img2.jpg "cinematic story"
109
185
 
110
- # 2. Create a project from a photo
111
- npx makaron-cli create --image my-photo.jpg
112
- # → ID: proj_abc123
186
+ # Submit video rendering (images must be public URLs)
187
+ npx makaron-cli video create --script "Shot 1..." --image https://...img1.jpg --duration 10
113
188
 
114
- # 3. Edit the image
115
- npx makaron-cli chat --project proj_abc123 "add dramatic lighting and film grain"
189
+ # Check status
190
+ npx makaron-cli video status <taskId>
191
+ ```
192
+
193
+ ### `music` — Music generation
194
+
195
+ ```bash
196
+ # Generate instrumental music
197
+ npx makaron-cli music create "gentle piano, warm strings, cinematic"
116
198
 
117
- # 4. Generate a video from the result
118
- npx makaron-cli chat --project proj_abc123 "make a 5 second cinematic video"
199
+ # With vocals and style
200
+ npx makaron-cli music create --vocals --style "lo-fi, ambient" "rainy day vibes"
119
201
 
120
- # 5. Add background music
121
- npx makaron-cli chat --project proj_abc123 "add epic orchestral background music"
202
+ # Check status
203
+ npx makaron-cli music status <taskId>
122
204
  ```
123
205
 
124
206
  ## Agent Integration Guide
125
207
 
126
- For AI agents (Claude Code, Cursor, etc.) calling this CLI programmatically:
208
+ For AI agents (Claude Code, OpenClaw, etc.) calling this CLI programmatically:
209
+
210
+ ### Non-blocking workflow (recommended)
211
+
212
+ ```bash
213
+ # 1. Submit task — returns immediately
214
+ RUN_ID=$(npx makaron-cli chat --project $PROJECT_ID --background "make a 5s video")
215
+
216
+ # 2. Do other work while Makaron processes...
127
217
 
128
- ### Setup (one-time)
218
+ # 3. Poll for result when ready
219
+ npx makaron-cli responses get $RUN_ID --wait
220
+ ```
221
+
222
+ ### JSON mode for structured parsing
129
223
 
130
224
  ```bash
131
- export MAKARON_API_KEY=mk_live_your_key_here
132
- npx makaron-cli list # verify it works
225
+ # Submit
226
+ RESULT=$(npx makaron-cli chat --project $ID --json --background "edit the photo")
227
+ RUN_ID=$(echo $RESULT | jq -r .runId)
228
+
229
+ # Poll
230
+ npx makaron-cli responses get $RUN_ID --wait
133
231
  ```
134
232
 
135
- ### Best practices
233
+ ### Sequential multi-turn
234
+
235
+ ```bash
236
+ # Turn 1: edit image
237
+ npx makaron-cli chat --project $ID "make it cinematic"
238
+ # Wait for completion (default mode polls automatically)
239
+
240
+ # Turn 2: make video from the edited image
241
+ npx makaron-cli chat --project $ID "now make a 5s video from this"
242
+ ```
136
243
 
137
- 1. **Always capture the project ID** from `create` output you need it for all `chat` commands
138
- 2. **One instruction per `chat` call** — the Agent handles complex requests, but single clear instructions work best
139
- 3. **Check results via the URL** — every project has a web URL at `https://www.makaron.app/projects/<id>`
140
- 4. **Video generation takes 2-5 minutes** — the CLI auto-polls and prints the URL when done
141
- 5. **Music generation takes ~60 seconds** — also auto-polled
244
+ Note: One project runs one Agent at a time. A new message while the previous is still running will interrupt the first.
142
245
 
143
246
  ### What the Agent can do
144
247
 
@@ -154,16 +257,16 @@ npx makaron-cli list # verify it works
154
257
  | Background music | "add calm piano music" |
155
258
  | Design/motion graphics | "create an Instagram story with animated text" |
156
259
 
157
- ### Output parsing
260
+ ### Output types in result
158
261
 
159
- - **stdout** = Agent's text response (markdown)
160
- - **stderr** = progress, tool calls, result URLs
161
- - Key patterns in stderr:
162
- - `🖼️ Image: <url>` generated/edited image URL
163
- - `🎬 Video done (<seconds>s): <url>` completed video URL
164
- - `🎵 Music done (<seconds>s): <url>` completed music URL
165
- - `🎨 Design published: <description>` motion graphic/design created
166
- - `🔗 <url>` project web URL (always last)
262
+ | Type | Field | Contains |
263
+ |------|-------|----------|
264
+ | Text | `result.text` | Agent's conversational response |
265
+ | Image | `result.images[].imageUrl` | Generated/edited image URL |
266
+ | Design (still) | `result.designs[].imageUrl` | Poster screenshot + code |
267
+ | Design (animated) | `result.designs[].animation` | fps + duration + code |
268
+ | Video | `result.videos[].videoUrl` | MP4 URL (after rendering) |
269
+ | Music | `result.music[].audioUrl` | MP3 URL (after generating) |
167
270
 
168
271
  ### Environment variables
169
272
 
@@ -180,8 +283,6 @@ All generated content (images, videos, designs) is saved to the project and visi
180
283
  https://www.makaron.app/projects/<project-id>
181
284
  ```
182
285
 
183
- Open this URL in a browser to see the timeline of all edits, play videos, and download assets.
184
-
185
286
  ## Admin: Skill Marketplace Operations
186
287
 
187
288
  Admin commands require an API key with admin privileges. Ask your admin to run `makaron admin set-admin <your-email>` to grant access.
@@ -202,7 +303,7 @@ npx makaron-cli admin upload cover.jpg marketplace/covers/skill-name.jpg
202
303
  npx makaron-cli admin upload before.jpg marketplace/before/before-name.jpg
203
304
 
204
305
  # Upload skill zip
205
- npx makaron-cli admin upload skill.zip marketplace/skills/skill-name.zip
306
+ npx makaron-cli admin upload skill-name.zip marketplace/skills/skill-name.zip
206
307
  ```
207
308
 
208
309
  Storage paths follow this convention:
package/bin/makaron.mjs CHANGED
@@ -6,6 +6,8 @@
6
6
  * npx makaron-cli login
7
7
  * npx makaron-cli create --image photo.jpg
8
8
  * npx makaron-cli chat --project <id> "make it look cinematic"
9
+ * npx makaron-cli chat --project <id> -b "message" # background, returns runId
10
+ * npx makaron-cli responses get <runId> --wait # poll until done
9
11
  * npx makaron-cli list
10
12
  */
11
13
 
@@ -226,6 +228,138 @@ async function streamAgent(baseUrl, headers, projectId, prompt) {
226
228
  return { runId, results };
227
229
  }
228
230
 
231
+ // ─── Run + Poll (non-blocking) ──────────────────────────────────────────────
232
+
233
+ async function submitRun(baseUrl, headers, projectId, prompt, opts = {}) {
234
+ const body = { projectId, prompt };
235
+ if (opts.preferredModel) body.preferredModel = opts.preferredModel;
236
+ if (opts.videoModel) body.videoModel = opts.videoModel;
237
+ if (opts.currentSnapshotIndex != null) body.currentSnapshotIndex = opts.currentSnapshotIndex;
238
+ if (opts.isNsfw) body.isNsfw = opts.isNsfw;
239
+
240
+ const res = await fetch(`${baseUrl}/api/agent/run`, {
241
+ method: 'POST',
242
+ headers: { 'Content-Type': 'application/json', ...headers },
243
+ body: JSON.stringify(body),
244
+ });
245
+
246
+ if (!res.ok) {
247
+ const text = await res.text();
248
+ console.error(`Error ${res.status}: ${text}`);
249
+ process.exit(1);
250
+ }
251
+
252
+ return await res.json();
253
+ }
254
+
255
+ async function pollRun(baseUrl, headers, runId, opts = {}) {
256
+ const { json = false, waitForArtifacts = false, background = false } = opts;
257
+ if (background) return;
258
+
259
+ let lastSeq = -1;
260
+ let printedText = '';
261
+ const start = Date.now();
262
+
263
+ while (true) {
264
+ await new Promise(r => setTimeout(r, 3000));
265
+ const elapsed = Math.round((Date.now() - start) / 1000);
266
+
267
+ const params = new URLSearchParams({ events: 'true' });
268
+ if (lastSeq >= 0) params.set('after', String(lastSeq));
269
+ if (waitForArtifacts) params.set('wait_for_artifacts', 'true');
270
+
271
+ let data;
272
+ try {
273
+ const res = await fetch(`${baseUrl}/api/agent/run/${runId}?${params}`, { headers });
274
+ if (!res.ok) {
275
+ if (elapsed > 800) { process.stderr.write(`\n❌ Timeout after ${elapsed}s\n`); process.exit(1); }
276
+ continue;
277
+ }
278
+ data = await res.json();
279
+ } catch {
280
+ continue;
281
+ }
282
+
283
+ // Process incremental events
284
+ if (data.events?.length) {
285
+ for (const ev of data.events) {
286
+ if (ev.seq > lastSeq) lastSeq = ev.seq;
287
+ if (json) continue; // skip printing in json mode
288
+ switch (ev.type) {
289
+ case 'content': {
290
+ const newText = ev.data?.text || '';
291
+ process.stdout.write(newText);
292
+ printedText += newText;
293
+ break;
294
+ }
295
+ case 'status':
296
+ process.stderr.write(`\r⏳ ${ev.data?.text || ''}`);
297
+ break;
298
+ case 'tool_call':
299
+ process.stderr.write(`\n🔧 ${ev.data?.tool || ''}`);
300
+ if (ev.data?.input?.editPrompt) process.stderr.write(`\n editPrompt: ${ev.data.input.editPrompt}`);
301
+ if (ev.data?.input?.model) process.stderr.write(`\n model: ${ev.data.input.model}`);
302
+ if (ev.data?.input?.description) process.stderr.write(`: ${ev.data.input.description.substring(0, 80)}`);
303
+ process.stderr.write('\n');
304
+ break;
305
+ case 'image':
306
+ process.stderr.write(`\n🖼️ Image: ${ev.data?.imageUrl || '(uploading...)'}\n`);
307
+ break;
308
+ case 'render':
309
+ if (ev.data?.published) {
310
+ const desc = ev.data.animation
311
+ ? `${ev.data.animation.durationInSeconds}s video (${ev.data.width}x${ev.data.height})`
312
+ : `still design (${ev.data.width}x${ev.data.height})`;
313
+ process.stderr.write(`\n🎨 Design published: ${desc}\n`);
314
+ }
315
+ break;
316
+ case 'animation_task':
317
+ process.stderr.write(`\n🎬 Video submitted: ${ev.data?.taskId}\n`);
318
+ break;
319
+ case 'music_task':
320
+ process.stderr.write(`\n🎵 Music submitted: ${ev.data?.taskId}\n`);
321
+ break;
322
+ case 'error':
323
+ process.stderr.write(`\n❌ Error: ${ev.data?.message}\n`);
324
+ break;
325
+ }
326
+ }
327
+ } else if (!json) {
328
+ process.stderr.write(`\r⏳ Working... ${elapsed}s (${data.eventCount || 0} events)`);
329
+ }
330
+
331
+ // Check terminal status
332
+ if (data.status === 'completed' || data.status === 'failed' || data.status === 'aborted') {
333
+ if (printedText && !json) process.stdout.write('\n');
334
+
335
+ if (json) {
336
+ // Structured JSON output — add projectUrl
337
+ data.projectUrl = `${APP_URL}/projects/${data.projectId}`;
338
+ console.log(JSON.stringify(data, null, 2));
339
+ } else {
340
+ process.stderr.write('\n━━━ Results ━━━\n');
341
+ if (data.result) {
342
+ for (const img of data.result.images || []) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
343
+ for (const d of data.result.designs || []) process.stderr.write(`🎨 Design (${d.width}x${d.height})\n`);
344
+ for (const v of data.result.videos || []) {
345
+ if (v.videoUrl) process.stderr.write(`🎬 Video: ${v.videoUrl}\n`);
346
+ else process.stderr.write(`🎬 Video ${v.taskId}: ${v.status || 'submitted'}\n`);
347
+ }
348
+ for (const m of data.result.music || []) {
349
+ if (m.audioUrl) process.stderr.write(`🎵 Music: ${m.audioUrl}\n`);
350
+ else process.stderr.write(`🎵 Music ${m.taskId}: ${m.status || 'submitted'}\n`);
351
+ }
352
+ if (data.result.error) process.stderr.write(`❌ ${data.result.error}\n`);
353
+ }
354
+ process.stderr.write(`🔗 ${APP_URL}/projects/${data.projectId}\n`);
355
+ }
356
+
357
+ if (data.status === 'failed') process.exit(1);
358
+ return data;
359
+ }
360
+ }
361
+ }
362
+
229
363
  // ─── Async Task Polling ──────────────────────────────────────────────────────
230
364
 
231
365
  async function pollVideo(baseUrl, headers, taskId) {
@@ -337,6 +471,43 @@ function timeSince(date) {
337
471
  return `${Math.floor(s / 86400)}d ago`;
338
472
  }
339
473
 
474
+ // ─── MCP Tool Caller ─────────────────────────────────────────────────────────
475
+
476
+ async function callMcpTool(baseUrl, headers, toolName, args) {
477
+ const res = await fetch(`${baseUrl}/api/mcp`, {
478
+ method: 'POST',
479
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', ...headers },
480
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: toolName, arguments: args } }),
481
+ });
482
+ if (!res.ok) { console.error(`MCP error ${res.status}:`, await res.text()); process.exit(1); }
483
+ const data = await res.json();
484
+ if (data.error) { console.error(`MCP error:`, data.error.message); process.exit(1); }
485
+ return data.result;
486
+ }
487
+
488
+ function imageToArg(imgPath) {
489
+ if (imgPath.startsWith('http://') || imgPath.startsWith('https://')) return imgPath;
490
+ const buf = fs.readFileSync(imgPath);
491
+ const ext = imgPath.split('.').pop()?.toLowerCase();
492
+ const mime = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp' }[ext] || 'image/jpeg';
493
+ return `data:${mime};base64,${buf.toString('base64')}`;
494
+ }
495
+
496
+ function saveMcpImage(result, outputPath) {
497
+ const content = result?.content || [];
498
+ const textBlock = content.find(c => c.type === 'text');
499
+ const imageBlock = content.find(c => c.type === 'image');
500
+ if (textBlock) process.stderr.write(`${textBlock.text}\n`);
501
+ if (imageBlock) {
502
+ const out = outputPath || `makaron-output-${Date.now()}.jpg`;
503
+ fs.writeFileSync(out, Buffer.from(imageBlock.data, 'base64'));
504
+ console.log(out);
505
+ return out;
506
+ }
507
+ if (textBlock) console.log(textBlock.text);
508
+ return null;
509
+ }
510
+
340
511
  // ─── Main ────────────────────────────────────────────────────────────────────
341
512
 
342
513
  const args = process.argv.slice(2);
@@ -364,16 +535,27 @@ if (command === 'login') {
364
535
  let projectId = null;
365
536
  const chatImages = [];
366
537
  const promptParts = [];
538
+ let useStream = false;
539
+ let background = false;
540
+ let jsonOutput = false;
541
+ let videoModel = undefined;
542
+ let preferredModel = undefined;
367
543
  for (let i = 1; i < args.length; i++) {
368
544
  if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
369
545
  else if (args[i] === '--image' && args[i + 1]) chatImages.push(args[++i]);
546
+ else if (args[i] === '--stream') useStream = true;
547
+ else if (args[i] === '--background' || args[i] === '-b') background = true;
548
+ else if (args[i] === '--json') jsonOutput = true;
549
+ else if (args[i] === '--video-model' && args[i + 1]) videoModel = args[++i];
550
+ else if (args[i] === '--model' && args[i + 1]) preferredModel = args[++i];
370
551
  else promptParts.push(args[i]);
371
552
  }
372
553
  const prompt = promptParts.join(' ');
373
554
  if (!projectId || !prompt) {
374
- console.error('Usage: makaron chat --project <id> [--image <file>] "your message"');
555
+ console.error('Usage: makaron chat --project <id> [--image <file>] [--stream] [--background|-b] [--json] "your message"');
375
556
  process.exit(1);
376
557
  }
558
+ // Upload images if provided
377
559
  if (chatImages.length > 0) {
378
560
  const base64s = chatImages.map(imgPath => {
379
561
  process.stderr.write(`📤 Uploading ${path.basename(imgPath)}...\n`);
@@ -392,13 +574,83 @@ if (command === 'login') {
392
574
  process.stderr.write(`⚠️ Failed to upload images: ${await res.text()}\n`);
393
575
  }
394
576
  }
395
- const { results } = await streamAgent(baseUrl, headers, projectId, prompt);
396
- process.stderr.write('\n━━━ Results ━━━\n');
397
- for (const img of results.images) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
398
- for (const d of results.designs) process.stderr.write(`🎨 ${d.desc}\n`);
399
- process.stderr.write(`🔗 ${APP_URL}/projects/${projectId}\n`);
400
- for (const task of results.animationTasks) await pollVideo(baseUrl, headers, task.taskId);
401
- for (const task of results.musicTasks) await pollMusic(baseUrl, headers, task.taskId);
577
+
578
+ if (useStream) {
579
+ // Legacy SSE mode
580
+ const { results } = await streamAgent(baseUrl, headers, projectId, prompt);
581
+ process.stderr.write('\n━━━ Results ━━━\n');
582
+ for (const img of results.images) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
583
+ for (const d of results.designs) process.stderr.write(`🎨 ${d.desc}\n`);
584
+ process.stderr.write(`🔗 ${APP_URL}/projects/${projectId}\n`);
585
+ for (const task of results.animationTasks) await pollVideo(baseUrl, headers, task.taskId);
586
+ for (const task of results.musicTasks) await pollMusic(baseUrl, headers, task.taskId);
587
+ } else {
588
+ // Default: fire-and-forget + poll
589
+ const { runId } = await submitRun(baseUrl, headers, projectId, prompt, { videoModel, preferredModel });
590
+ if (background) {
591
+ // Just print runId and exit
592
+ if (jsonOutput) {
593
+ console.log(JSON.stringify({ runId, projectId, projectUrl: `${APP_URL}/projects/${projectId}`, status: 'running' }));
594
+ } else {
595
+ console.log(runId);
596
+ }
597
+ } else {
598
+ process.stderr.write(`🚀 Run started: ${runId}\n`);
599
+ await pollRun(baseUrl, headers, runId, { json: jsonOutput });
600
+ }
601
+ }
602
+ } else if (command === 'responses' || command === 'run') {
603
+ const { headers, baseUrl } = getAuth();
604
+ const sub = args[1];
605
+
606
+ if (sub === 'get') {
607
+ const runId = args[2];
608
+ if (!runId) { console.error('Usage: makaron responses get <runId> [--wait] [--json]'); process.exit(1); }
609
+ let wait = false, jsonOutput = false, waitForArtifacts = false;
610
+ for (let i = 3; i < args.length; i++) {
611
+ if (args[i] === '--wait') wait = true;
612
+ if (args[i] === '--json') jsonOutput = true;
613
+ if (args[i] === '--wait-artifacts') waitForArtifacts = true;
614
+ }
615
+
616
+ if (wait) {
617
+ await pollRun(baseUrl, headers, runId, { json: jsonOutput, waitForArtifacts });
618
+ } else {
619
+ const params = new URLSearchParams();
620
+ if (waitForArtifacts) params.set('wait_for_artifacts', 'true');
621
+ const res = await fetch(`${baseUrl}/api/agent/run/${runId}?${params}`, { headers });
622
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
623
+ const data = await res.json();
624
+ console.log(JSON.stringify(data, null, 2));
625
+ }
626
+
627
+ } else if (sub === 'list') {
628
+ let projectId = null;
629
+ for (let i = 2; i < args.length; i++) {
630
+ if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
631
+ }
632
+ if (!projectId) { console.error('Usage: makaron responses list --project <id>'); process.exit(1); }
633
+ // Query runs for project
634
+ const res = await fetch(`${baseUrl}/api/agent/run?projectId=${projectId}`, { headers });
635
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
636
+ const data = await res.json();
637
+ if (data.runs?.length) {
638
+ for (const r of data.runs) {
639
+ const age = timeSince(new Date(r.started_at));
640
+ console.log(` ${r.id} ${r.status.padEnd(10)} ${age} ${(r.prompt || '').slice(0, 50)}`);
641
+ }
642
+ } else {
643
+ console.log('No runs found for this project.');
644
+ }
645
+
646
+ } else {
647
+ console.log(`Responses commands:
648
+ responses get <runId> Get run status and results
649
+ responses get <runId> --wait Poll until completed
650
+ responses get <runId> --wait-artifacts Wait for video/music too
651
+ responses list --project <id> List runs for a project
652
+ `);
653
+ }
402
654
  } else if (command === 'list' || command === 'ls') {
403
655
  const { headers, baseUrl } = getAuth();
404
656
  await listProjects(baseUrl, headers);
@@ -419,6 +671,119 @@ if (command === 'login') {
419
671
  } else {
420
672
  console.error(`❌ Abort failed:`, await res.text());
421
673
  }
674
+ } else if (command === 'edit') {
675
+ const { headers, baseUrl } = getAuth();
676
+ const editArgs = {};
677
+ const promptParts = [];
678
+ let outputPath = null;
679
+ for (let i = 1; i < args.length; i++) {
680
+ if (args[i] === '--image' && args[i + 1]) editArgs.image = imageToArg(args[++i]);
681
+ else if (args[i] === '--model' && args[i + 1]) editArgs.model = args[++i];
682
+ else if (args[i] === '--skill' && args[i + 1]) editArgs.skill = args[++i];
683
+ else if (args[i] === '--ref' && args[i + 1]) {
684
+ editArgs.referenceImages = editArgs.referenceImages || [];
685
+ editArgs.referenceImages.push(imageToArg(args[++i]));
686
+ }
687
+ else if (args[i] === '--aspect' && args[i + 1]) editArgs.aspectRatio = args[++i];
688
+ else if (args[i] === '--out' && args[i + 1]) outputPath = args[++i];
689
+ else promptParts.push(args[i]);
690
+ }
691
+ editArgs.editPrompt = promptParts.join(' ');
692
+ if (!editArgs.editPrompt) { console.error('Usage: makaron edit [--image <file|url>] [--model gemini|qwen|openai] [--skill enhance|creative|wild|captions] [--ref <file>] [--out <file>] "prompt"'); process.exit(1); }
693
+ process.stderr.write('🎨 Generating...\n');
694
+ const result = await callMcpTool(baseUrl, headers, 'makaron_edit_image', editArgs);
695
+ saveMcpImage(result, outputPath);
696
+
697
+ } else if (command === 'video') {
698
+ const { headers, baseUrl } = getAuth();
699
+ const sub = args[1];
700
+
701
+ if (sub === 'script') {
702
+ const images = [];
703
+ const promptParts = [];
704
+ let language = 'en';
705
+ for (let i = 2; i < args.length; i++) {
706
+ if (args[i] === '--image' && args[i + 1]) images.push(imageToArg(args[++i]));
707
+ else if (args[i] === '--lang' && args[i + 1]) language = args[++i];
708
+ else promptParts.push(args[i]);
709
+ }
710
+ if (!images.length) { console.error('Usage: makaron video script --image <file> [--image <file>] [--lang en|zh] "direction"'); process.exit(1); }
711
+ process.stderr.write('🎬 Writing script...\n');
712
+ const result = await callMcpTool(baseUrl, headers, 'makaron_write_video_script', { images, userRequest: promptParts.join(' ') || undefined, language });
713
+ const text = result?.content?.find(c => c.type === 'text')?.text;
714
+ if (text) console.log(text);
715
+
716
+ } else if (sub === 'create') {
717
+ const images = [];
718
+ let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined;
719
+ for (let i = 2; i < args.length; i++) {
720
+ if (args[i] === '--image' && args[i + 1]) images.push(args[++i]);
721
+ else if (args[i] === '--script' && args[i + 1]) script = args[++i];
722
+ else if (args[i] === '--script-file' && args[i + 1]) script = fs.readFileSync(args[++i], 'utf-8');
723
+ else if (args[i] === '--duration' && args[i + 1]) duration = Number(args[++i]);
724
+ else if (args[i] === '--aspect' && args[i + 1]) aspectRatio = args[++i];
725
+ else if (args[i] === '--model' && args[i + 1]) videoModel = args[++i];
726
+ }
727
+ if (!images.length || !script) { console.error('Usage: makaron video create --script "..." --image <url> [--duration 10] [--aspect 9:16] [--model kling|seedance]'); process.exit(1); }
728
+ process.stderr.write('🎬 Submitting video...\n');
729
+ const vArgs = { script, images };
730
+ if (duration) vArgs.duration = duration;
731
+ if (aspectRatio) vArgs.aspectRatio = aspectRatio;
732
+ if (videoModel) vArgs.videoModel = videoModel;
733
+ const result = await callMcpTool(baseUrl, headers, 'makaron_create_video', vArgs);
734
+ const text = result?.content?.find(c => c.type === 'text')?.text;
735
+ if (text) console.log(text);
736
+
737
+ } else if (sub === 'status') {
738
+ const taskId = args[2];
739
+ if (!taskId) { console.error('Usage: makaron video status <taskId>'); process.exit(1); }
740
+ const result = await callMcpTool(baseUrl, headers, 'makaron_get_video_status', { taskId });
741
+ const text = result?.content?.find(c => c.type === 'text')?.text;
742
+ if (text) console.log(text);
743
+
744
+ } else {
745
+ console.log(`Video commands:
746
+ video script --image <file> [--image <file>] "direction" Write video script
747
+ video create --script "..." --image <url> [--duration 10] Submit video task
748
+ video status <taskId> Check video status
749
+ `);
750
+ }
751
+
752
+ } else if (command === 'music') {
753
+ const { headers, baseUrl } = getAuth();
754
+ const sub = args[1];
755
+
756
+ if (sub === 'create') {
757
+ const promptParts = [];
758
+ let instrumental = true, style = undefined;
759
+ for (let i = 2; i < args.length; i++) {
760
+ if (args[i] === '--vocals') instrumental = false;
761
+ else if (args[i] === '--style' && args[i + 1]) style = args[++i];
762
+ else promptParts.push(args[i]);
763
+ }
764
+ const prompt = promptParts.join(' ');
765
+ if (!prompt) { console.error('Usage: makaron music create [--vocals] [--style "lo-fi"] "gentle piano"'); process.exit(1); }
766
+ process.stderr.write('🎵 Generating music...\n');
767
+ const mArgs = { prompt, instrumental };
768
+ if (style) mArgs.style = style;
769
+ const result = await callMcpTool(baseUrl, headers, 'makaron_create_music', mArgs);
770
+ const text = result?.content?.find(c => c.type === 'text')?.text;
771
+ if (text) console.log(text);
772
+
773
+ } else if (sub === 'status') {
774
+ const taskId = args[2];
775
+ if (!taskId) { console.error('Usage: makaron music status <taskId>'); process.exit(1); }
776
+ const result = await callMcpTool(baseUrl, headers, 'makaron_get_music_status', { taskId });
777
+ const text = result?.content?.find(c => c.type === 'text')?.text;
778
+ if (text) console.log(text);
779
+
780
+ } else {
781
+ console.log(`Music commands:
782
+ music create [--vocals] [--style "genre"] "description" Generate music
783
+ music status <taskId> Check music status
784
+ `);
785
+ }
786
+
422
787
  } else if (command === 'admin') {
423
788
  const { headers, baseUrl } = getAuth();
424
789
  const sub = args[1];
@@ -546,9 +911,21 @@ Commands:
546
911
  create --image <file> Create project from local image
547
912
  create --image-url <url> Create project from URL
548
913
  create --title "name" Create empty project (text-to-image)
549
- chat --project <id> "message" Chat with Makaron Agent
550
- chat --project <id> --image <file> "message" Add image + chat
914
+
915
+ chat --project <id> "message" Chat (non-blocking, polls for result)
916
+ chat --project <id> -b "message" Background: submit and print runId
917
+ chat --project <id> --stream "msg" Legacy: stream SSE in real-time
918
+ chat --project <id> --json "msg" Output structured JSON result
919
+
920
+ responses get <runId> Get run status and results
921
+ responses get <runId> --wait Poll until completed
922
+ responses list --project <id> List runs for a project
551
923
  abort <runId> Abort a running Agent
924
+
925
+ edit [--image <file>] "prompt" AI image edit / text-to-image
926
+ video script|create|status Video generation
927
+ music create|status Music generation
928
+
552
929
  admin Admin commands (skills, upload, set-admin)
553
930
 
554
931
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "bin": {