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.
- package/README.md +166 -65
- package/bin/makaron.mjs +387 -10
- 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
|
-
### `
|
|
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
|
|
46
|
+
npx makaron-cli chat --project <id> "make it look cinematic"
|
|
43
47
|
```
|
|
44
48
|
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
|
148
|
+
npx makaron-cli abort <runId>
|
|
82
149
|
```
|
|
83
150
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
#
|
|
91
|
-
npx makaron-cli
|
|
92
|
-
```
|
|
156
|
+
# Text-to-image (no input image)
|
|
157
|
+
npx makaron-cli edit "a cyberpunk cityscape at night, neon reflections"
|
|
93
158
|
|
|
94
|
-
|
|
159
|
+
# Edit an existing image
|
|
160
|
+
npx makaron-cli edit --image photo.jpg "add cinematic warm lighting"
|
|
95
161
|
|
|
96
|
-
|
|
162
|
+
# With model and skill
|
|
163
|
+
npx makaron-cli edit --image photo.jpg --model openai --skill captions "add elegant title"
|
|
97
164
|
|
|
98
|
-
|
|
99
|
-
npx makaron-cli
|
|
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
|
-
|
|
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
|
-
|
|
180
|
+
### `video` — Video generation
|
|
105
181
|
|
|
106
182
|
```bash
|
|
107
|
-
#
|
|
108
|
-
npx makaron-cli
|
|
183
|
+
# Write a video script from images
|
|
184
|
+
npx makaron-cli video script --image img1.jpg --image img2.jpg "cinematic story"
|
|
109
185
|
|
|
110
|
-
#
|
|
111
|
-
npx makaron-cli create --image
|
|
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
|
-
#
|
|
115
|
-
npx makaron-cli
|
|
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
|
-
#
|
|
118
|
-
npx makaron-cli
|
|
199
|
+
# With vocals and style
|
|
200
|
+
npx makaron-cli music create --vocals --style "lo-fi, ambient" "rainy day vibes"
|
|
119
201
|
|
|
120
|
-
#
|
|
121
|
-
npx makaron-cli
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
npx makaron-cli
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
|
260
|
+
### Output types in result
|
|
158
261
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
550
|
-
chat --project <id>
|
|
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:
|