makaron-cli 0.1.0 → 0.3.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 +341 -0
  2. package/bin/makaron.mjs +357 -21
  3. package/package.json +9 -2
package/README.md ADDED
@@ -0,0 +1,341 @@
1
+ # makaron-cli
2
+
3
+ Talk to **Makaron Agent** from the terminal — create projects, edit images, generate videos, and compose music.
4
+
5
+ Zero dependencies. Single file. Works with `npx`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npx makaron-cli # run directly (always latest)
11
+ # or
12
+ npm install -g makaron-cli
13
+ ```
14
+
15
+ ## Authentication
16
+
17
+ ### API Key (recommended for agents)
18
+
19
+ Set the `MAKARON_API_KEY` environment variable. That's it — no login step needed.
20
+
21
+ ```bash
22
+ export MAKARON_API_KEY=mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
23
+ npx makaron-cli list
24
+ ```
25
+
26
+ Get your API key from the Makaron dashboard (Settings → API Keys), or ask your admin to generate one.
27
+
28
+ ### Interactive login (humans)
29
+
30
+ ```bash
31
+ npx makaron-cli login
32
+ # Enter email + password → token saved to ~/.makaron/auth.json
33
+ ```
34
+
35
+ The CLI checks `MAKARON_API_KEY` first, then falls back to the saved session in `~/.makaron/auth.json`.
36
+
37
+ ## Commands
38
+
39
+ ### `list` — Show all projects
40
+
41
+ ```bash
42
+ npx makaron-cli list
43
+ ```
44
+
45
+ Output:
46
+ ```
47
+ 📁 12 projects
48
+
49
+ abc123def My Photo 3 snaps 2h ago
50
+ xyz789ghi Sunset Edit 5 snaps 1d ago
51
+ ```
52
+
53
+ ### `create` — Create a new project
54
+
55
+ ```bash
56
+ # From local image file(s)
57
+ npx makaron-cli create --image photo.jpg
58
+ npx makaron-cli create --image img1.jpg --image img2.jpg
59
+
60
+ # From URL(s)
61
+ npx makaron-cli create --image-url https://example.com/photo.jpg
62
+
63
+ # Empty project (for text-to-image)
64
+ npx makaron-cli create --title "My New Project"
65
+ ```
66
+
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.
79
+
80
+ ```bash
81
+ npx makaron-cli chat --project <id> "make it look cinematic"
82
+ ```
83
+
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
88
+
89
+ ```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
+ ```
93
+
94
+ After the Agent finishes, the CLI automatically polls for any pending video/music tasks until they complete.
95
+
96
+ ### `abort` — Stop a running Agent
97
+
98
+ ```bash
99
+ npx makaron-cli abort <runId>
100
+ ```
101
+
102
+ Press `Ctrl+C` during `chat` to abort automatically.
103
+
104
+ ### `edit` — AI image editing (direct MCP tool call)
105
+
106
+ Unlike `chat` (which uses the Agent with project context), `edit` directly calls the image generation model for one-shot results.
107
+
108
+ ```bash
109
+ # Text-to-image (no input image)
110
+ npx makaron-cli edit "a cyberpunk cityscape at night, neon reflections"
111
+
112
+ # Edit an existing image
113
+ npx makaron-cli edit --image photo.jpg "add cinematic warm lighting"
114
+
115
+ # With model and skill
116
+ npx makaron-cli edit --image photo.jpg --model openai --skill captions "add elegant title"
117
+
118
+ # With reference images (up to 3)
119
+ npx makaron-cli edit --image photo.jpg --ref style1.jpg --ref style2.jpg "match this style"
120
+
121
+ # Specify output path and aspect ratio
122
+ npx makaron-cli edit --out result.jpg --aspect 9:16 "vertical poster design"
123
+ ```
124
+
125
+ Options:
126
+ - `--image <file|url>` — input image (omit for text-to-image)
127
+ - `--model gemini|qwen|openai|pony|wai` — model selection (default: auto)
128
+ - `--skill enhance|creative|wild|captions` — activate skill template
129
+ - `--ref <file|url>` — reference image (repeatable, up to 3)
130
+ - `--aspect <ratio>` — target aspect ratio (e.g. `4:5`, `1:1`, `16:9`)
131
+ - `--out <path>` — output file path (default: `makaron-output-{timestamp}.jpg`)
132
+
133
+ Output: saves image to local file, prints the file path to stdout.
134
+
135
+ ### `video` — Video generation
136
+
137
+ ```bash
138
+ # Write a video script from images
139
+ npx makaron-cli video script --image img1.jpg --image img2.jpg "cinematic story"
140
+ npx makaron-cli video script --image img1.jpg --image img2.jpg --lang zh "电影感故事"
141
+
142
+ # Submit video rendering (images must be public URLs)
143
+ npx makaron-cli video create --script "Shot 1..." --image https://...img1.jpg --duration 10
144
+ npx makaron-cli video create --script-file script.txt --image https://...jpg --model seedance
145
+
146
+ # Check status
147
+ npx makaron-cli video status <taskId>
148
+ ```
149
+
150
+ Options for `video create`:
151
+ - `--script "..."` or `--script-file <path>` — video script
152
+ - `--image <url>` — public image URL (repeatable, up to 7)
153
+ - `--duration 3|5|7|10|15` — seconds (omit for smart mode)
154
+ - `--aspect 9:16|16:9|1:1` — aspect ratio
155
+ - `--model kling|seedance` — video model (default: kling)
156
+
157
+ ### `music` — Music generation
158
+
159
+ ```bash
160
+ # Generate instrumental music
161
+ npx makaron-cli music create "gentle piano, warm strings, cinematic"
162
+
163
+ # With vocals and style
164
+ npx makaron-cli music create --vocals --style "lo-fi, ambient" "rainy day vibes"
165
+
166
+ # Check status
167
+ npx makaron-cli music status <taskId>
168
+ ```
169
+
170
+ Options for `music create`:
171
+ - `--vocals` — include vocals (default: instrumental only)
172
+ - `--style "genre"` — genre/mood tags
173
+
174
+ ## Example: Full workflow
175
+
176
+ ```bash
177
+ # 1. Login (once)
178
+ npx makaron-cli login
179
+
180
+ # 2. Create a project from a photo
181
+ npx makaron-cli create --image my-photo.jpg
182
+ # → ID: proj_abc123
183
+
184
+ # 3. Edit the image
185
+ npx makaron-cli chat --project proj_abc123 "add dramatic lighting and film grain"
186
+
187
+ # 4. Generate a video from the result
188
+ npx makaron-cli chat --project proj_abc123 "make a 5 second cinematic video"
189
+
190
+ # 5. Add background music
191
+ npx makaron-cli chat --project proj_abc123 "add epic orchestral background music"
192
+ ```
193
+
194
+ ## Agent Integration Guide
195
+
196
+ For AI agents (Claude Code, Cursor, etc.) calling this CLI programmatically:
197
+
198
+ ### Setup (one-time)
199
+
200
+ ```bash
201
+ export MAKARON_API_KEY=mk_live_your_key_here
202
+ npx makaron-cli list # verify it works
203
+ ```
204
+
205
+ ### Best practices
206
+
207
+ 1. **Always capture the project ID** from `create` output — you need it for all `chat` commands
208
+ 2. **One instruction per `chat` call** — the Agent handles complex requests, but single clear instructions work best
209
+ 3. **Check results via the URL** — every project has a web URL at `https://www.makaron.app/projects/<id>`
210
+ 4. **Video generation takes 2-5 minutes** — the CLI auto-polls and prints the URL when done
211
+ 5. **Music generation takes ~60 seconds** — also auto-polled
212
+
213
+ ### What the Agent can do
214
+
215
+ | Capability | Example prompt |
216
+ |-----------|---------------|
217
+ | Image editing | "make the sky more dramatic" |
218
+ | Style transfer | "convert to oil painting style" |
219
+ | Add elements | "add a cat sitting on the table" |
220
+ | Remove elements | "remove the person in the background" |
221
+ | Text-to-image | "generate a cyberpunk cityscape" (on empty project) |
222
+ | Video from image | "create a 5 second video" |
223
+ | Video with script | "make a video: camera slowly zooms in while leaves fall" |
224
+ | Background music | "add calm piano music" |
225
+ | Design/motion graphics | "create an Instagram story with animated text" |
226
+
227
+ ### Output parsing
228
+
229
+ - **stdout** = Agent's text response (markdown)
230
+ - **stderr** = progress, tool calls, result URLs
231
+ - Key patterns in stderr:
232
+ - `🖼️ Image: <url>` — generated/edited image URL
233
+ - `🎬 Video done (<seconds>s): <url>` — completed video URL
234
+ - `🎵 Music done (<seconds>s): <url>` — completed music URL
235
+ - `🎨 Design published: <description>` — motion graphic/design created
236
+ - `🔗 <url>` — project web URL (always last)
237
+
238
+ ### Environment variables
239
+
240
+ | Variable | Purpose | Default |
241
+ |----------|---------|---------|
242
+ | `MAKARON_API_KEY` | API key (`mk_live_xxx`) | — |
243
+ | `MAKARON_URL` | API endpoint override | `https://www.makaron.app` |
244
+
245
+ ## Viewing results
246
+
247
+ All generated content (images, videos, designs) is saved to the project and visible at:
248
+
249
+ ```
250
+ https://www.makaron.app/projects/<project-id>
251
+ ```
252
+
253
+ Open this URL in a browser to see the timeline of all edits, play videos, and download assets.
254
+
255
+ ## Admin: Skill Marketplace Operations
256
+
257
+ Admin commands require an API key with admin privileges. Ask your admin to run `makaron admin set-admin <your-email>` to grant access.
258
+
259
+ ### List all marketplace skills
260
+
261
+ ```bash
262
+ npx makaron-cli admin skills
263
+ ```
264
+
265
+ ### Upload assets to Storage
266
+
267
+ ```bash
268
+ # Upload cover image (3:4 aspect ratio recommended)
269
+ npx makaron-cli admin upload cover.jpg marketplace/covers/skill-name.jpg
270
+
271
+ # Upload before photo
272
+ npx makaron-cli admin upload before.jpg marketplace/before/before-name.jpg
273
+
274
+ # Upload skill zip
275
+ npx makaron-cli admin upload skill.zip marketplace/skills/skill-name.zip
276
+ ```
277
+
278
+ Storage paths follow this convention:
279
+ - Covers: `marketplace/covers/<skill-name>.jpg` (or `.mp4` for video covers)
280
+ - Before images: `marketplace/before/<name>.jpg`
281
+ - Skill zips: `marketplace/skills/<skill-name>.zip`
282
+
283
+ ### Add a new skill to marketplace
284
+
285
+ ```bash
286
+ npx makaron-cli admin skills add '{
287
+ "labels": {"zh": "中文名", "en": "English Name"},
288
+ "image": "https://sdyrtztrjgmmpnirswxt.supabase.co/storage/v1/object/public/images/marketplace/covers/skill-name.jpg",
289
+ "prompt": "Default prompt shown to users",
290
+ "skill_path": "https://sdyrtztrjgmmpnirswxt.supabase.co/storage/v1/object/public/images/marketplace/skills/skill-name.zip",
291
+ "image_count": 2,
292
+ "sort_order": 10,
293
+ "is_active": true,
294
+ "before_images": ["https://sdyrtztrjgmmpnirswxt.supabase.co/storage/v1/object/public/images/marketplace/before/before-name.jpg"]
295
+ }'
296
+ ```
297
+
298
+ ### Update / delete a skill
299
+
300
+ ```bash
301
+ npx makaron-cli admin skills update <id> '{"sort_order": 5, "is_active": false}'
302
+ npx makaron-cli admin skills delete <id>
303
+ ```
304
+
305
+ ### Download skill from share link
306
+
307
+ ```bash
308
+ # From share code
309
+ npx makaron-cli admin fetch-skill 4c4cbd57
310
+
311
+ # From full URL
312
+ npx makaron-cli admin fetch-skill https://www.makaron.app/s/4c4cbd57
313
+
314
+ # → Creates ./skill-name/ directory with SKILL.md + assets/
315
+ ```
316
+
317
+ ### End-to-end skill launch workflow
318
+
319
+ 0. **Get skill from share link** — `npx makaron-cli admin fetch-skill <code>`
320
+ 1. **Read SKILL.md** — understand the skill's variables, prompt template, and output format
321
+ 2. **Generate before photo** — use Makaron MCP `makaron_edit_image` to generate a plain selfie (no phone in frame, natural light)
322
+ 3. **Generate cover photo** — use `makaron_edit_image` following the SKILL.md variables/style closely. Match gender pairing (female idol → male fan, male idol → female fan)
323
+ 4. **Package zip** — `zip skill-name.zip SKILL.md` (video reference URLs go in SKILL.md metadata, not in the zip)
324
+ 5. **Upload all assets**:
325
+ ```bash
326
+ npx makaron-cli admin upload cover.jpg marketplace/covers/skill-name.jpg
327
+ npx makaron-cli admin upload before.jpg marketplace/before/skill-name-before.jpg
328
+ npx makaron-cli admin upload skill-name.zip marketplace/skills/skill-name.zip
329
+ ```
330
+ 6. **Add to marketplace**:
331
+ ```bash
332
+ npx makaron-cli admin skills add '{"labels":{"zh":"...","en":"..."},"image":"<cover_url>","skill_path":"<zip_url>","before_images":["<before_url>"],"image_count":2,"sort_order":10,"is_active":true}'
333
+ ```
334
+ 7. **Verify** — visit https://www.makaron.app/home and confirm the skill appears, can be clicked, and works
335
+
336
+ ### Cover image guidelines
337
+
338
+ - Target aspect ratio: **3:4** (marketplace cards use `object-fit: cover`)
339
+ - 16:9 images will be severely cropped — resize or stack two 16:9 images vertically
340
+ - Video covers (`.mp4`) are supported — auto-play in the card
341
+ - Before photo should match the person in the cover (hair, clothing, accessories)
package/bin/makaron.mjs CHANGED
@@ -83,23 +83,54 @@ function getAuthCookie() {
83
83
  console.error('Not logged in. Run: npx makaron-cli login');
84
84
  process.exit(1);
85
85
  }
86
- return { cookie: buildCookie(auth), baseUrl: auth._baseUrl || BASE_URL };
86
+ return { cookie: buildCookie(auth), baseUrl: process.env.MAKARON_URL || auth._baseUrl || BASE_URL };
87
+ }
88
+
89
+ function getAuth() {
90
+ const apiKey = process.env.MAKARON_API_KEY;
91
+ if (apiKey) {
92
+ return {
93
+ headers: { 'Authorization': `Bearer ${apiKey}` },
94
+ baseUrl: process.env.MAKARON_URL || DEFAULT_URL,
95
+ };
96
+ }
97
+ const auth = loadAuth();
98
+ if (!auth) {
99
+ console.error('Not logged in. Set MAKARON_API_KEY or run: npx makaron-cli login');
100
+ process.exit(1);
101
+ }
102
+ return {
103
+ headers: { 'Cookie': buildCookie(auth) },
104
+ baseUrl: process.env.MAKARON_URL || auth._baseUrl || BASE_URL,
105
+ };
87
106
  }
88
107
 
89
108
  // ─── SSE Consumer ────────────────────────────────────────────────────────────
90
109
 
91
- async function streamAgent(baseUrl, cookie, projectId, prompt) {
110
+ async function abortRun(baseUrl, headers, runId) {
111
+ try {
112
+ await fetch(`${baseUrl}/api/agent/abort`, {
113
+ method: 'POST',
114
+ headers: { 'Content-Type': 'application/json', ...headers },
115
+ body: JSON.stringify({ runId }),
116
+ });
117
+ } catch { /* best effort */ }
118
+ }
119
+
120
+ async function streamAgent(baseUrl, headers, projectId, prompt) {
121
+ const controller = new AbortController();
92
122
  const res = await fetch(`${baseUrl}/api/agent`, {
93
123
  method: 'POST',
94
124
  headers: {
95
125
  'Content-Type': 'application/json',
96
- 'Cookie': cookie,
126
+ ...headers,
97
127
  },
98
128
  body: JSON.stringify({
99
129
  projectId,
100
130
  prompt,
101
131
  headless: true,
102
132
  }),
133
+ signal: controller.signal,
103
134
  });
104
135
 
105
136
  if (!res.ok) {
@@ -112,6 +143,15 @@ async function streamAgent(baseUrl, cookie, projectId, prompt) {
112
143
  const decoder = new TextDecoder();
113
144
  let buffer = '';
114
145
 
146
+ // Ctrl+C → abort agent on server, then exit
147
+ const sigintHandler = async () => {
148
+ process.stderr.write('\n⏹️ Aborting...\n');
149
+ controller.abort();
150
+ if (runId) await abortRun(baseUrl, headers, runId);
151
+ process.exit(0);
152
+ };
153
+ process.on('SIGINT', sigintHandler);
154
+
115
155
  const results = { images: [], designs: [], animationTasks: [], musicTasks: [], text: '' };
116
156
 
117
157
  while (true) {
@@ -139,7 +179,9 @@ async function streamAgent(baseUrl, cookie, projectId, prompt) {
139
179
 
140
180
  case 'tool_call':
141
181
  process.stderr.write(`\n🔧 ${event.tool}`);
142
- if (event.input?.editPrompt) process.stderr.write(`: ${event.input.editPrompt.substring(0, 80)}`);
182
+ if (event.input?.editPrompt) process.stderr.write(`\n editPrompt: ${event.input.editPrompt}`);
183
+ if (event.input?.model) process.stderr.write(`\n model: ${event.input.model}`);
184
+ if (event.input?.skill) process.stderr.write(`\n skill: ${event.input.skill}`);
143
185
  if (event.input?.description) process.stderr.write(`: ${event.input.description.substring(0, 80)}`);
144
186
  process.stderr.write('\n');
145
187
  break;
@@ -179,20 +221,21 @@ async function streamAgent(baseUrl, cookie, projectId, prompt) {
179
221
  }
180
222
  }
181
223
 
224
+ process.removeListener('SIGINT', sigintHandler);
182
225
  if (results.text) process.stdout.write('\n');
183
226
  return { runId, results };
184
227
  }
185
228
 
186
229
  // ─── Async Task Polling ──────────────────────────────────────────────────────
187
230
 
188
- async function pollVideo(baseUrl, cookie, taskId) {
231
+ async function pollVideo(baseUrl, headers, taskId) {
189
232
  process.stderr.write(`🎬 Waiting for video ${taskId}...\n`);
190
233
  const start = Date.now();
191
234
  while (true) {
192
235
  await new Promise(r => setTimeout(r, 10_000));
193
236
  const elapsed = Math.round((Date.now() - start) / 1000);
194
237
  try {
195
- const res = await fetch(`${baseUrl}/api/animate/${taskId}`, { headers: { 'Cookie': cookie } });
238
+ const res = await fetch(`${baseUrl}/api/animate/${taskId}`, { headers });
196
239
  if (!res.ok) continue;
197
240
  const data = await res.json();
198
241
  if (data.videoUrl) { process.stderr.write(`\r🎬 Video done (${elapsed}s): ${data.videoUrl}\n`); return data.videoUrl; }
@@ -203,14 +246,14 @@ async function pollVideo(baseUrl, cookie, taskId) {
203
246
  }
204
247
  }
205
248
 
206
- async function pollMusic(baseUrl, cookie, taskId) {
249
+ async function pollMusic(baseUrl, headers, taskId) {
207
250
  process.stderr.write(`🎵 Waiting for music ${taskId}...\n`);
208
251
  const start = Date.now();
209
252
  while (true) {
210
253
  await new Promise(r => setTimeout(r, 5_000));
211
254
  const elapsed = Math.round((Date.now() - start) / 1000);
212
255
  try {
213
- const res = await fetch(`${baseUrl}/api/music/${taskId}`, { headers: { 'Cookie': cookie } });
256
+ const res = await fetch(`${baseUrl}/api/music/${taskId}`, { headers });
214
257
  if (!res.ok) continue;
215
258
  const data = await res.json();
216
259
  const trackUrl = data.audioUrl || data.tracks?.[0]?.audioUrl;
@@ -235,7 +278,7 @@ async function pollMusic(baseUrl, cookie, taskId) {
235
278
 
236
279
  // ─── Create Project ──────────────────────────────────────────────────────────
237
280
 
238
- async function createProject(baseUrl, cookie, opts) {
281
+ async function createProject(baseUrl, headers, opts) {
239
282
  const body = {};
240
283
  if (opts.imageUrls?.length) {
241
284
  body.imageUrls = opts.imageUrls;
@@ -254,7 +297,7 @@ async function createProject(baseUrl, cookie, opts) {
254
297
 
255
298
  const res = await fetch(`${baseUrl}/api/projects/create`, {
256
299
  method: 'POST',
257
- headers: { 'Content-Type': 'application/json', 'Cookie': cookie },
300
+ headers: { 'Content-Type': 'application/json', ...headers },
258
301
  body: JSON.stringify(body),
259
302
  });
260
303
 
@@ -273,8 +316,8 @@ async function createProject(baseUrl, cookie, opts) {
273
316
 
274
317
  // ─── List Projects ───────────────────────────────────────────────────────────
275
318
 
276
- async function listProjects(baseUrl, cookie) {
277
- const res = await fetch(`${baseUrl}/api/projects/list`, { headers: { 'Cookie': cookie } });
319
+ async function listProjects(baseUrl, headers) {
320
+ const res = await fetch(`${baseUrl}/api/projects/list`, { headers });
278
321
  if (!res.ok) { console.error('List failed:', await res.text()); process.exit(1); }
279
322
  const { projects } = await res.json();
280
323
  if (!projects.length) { console.log('No projects yet. Create one with: makaron create --image <file>'); return; }
@@ -294,6 +337,43 @@ function timeSince(date) {
294
337
  return `${Math.floor(s / 86400)}d ago`;
295
338
  }
296
339
 
340
+ // ─── MCP Tool Caller ─────────────────────────────────────────────────────────
341
+
342
+ async function callMcpTool(baseUrl, headers, toolName, args) {
343
+ const res = await fetch(`${baseUrl}/api/mcp`, {
344
+ method: 'POST',
345
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', ...headers },
346
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: toolName, arguments: args } }),
347
+ });
348
+ if (!res.ok) { console.error(`MCP error ${res.status}:`, await res.text()); process.exit(1); }
349
+ const data = await res.json();
350
+ if (data.error) { console.error(`MCP error:`, data.error.message); process.exit(1); }
351
+ return data.result;
352
+ }
353
+
354
+ function imageToArg(imgPath) {
355
+ if (imgPath.startsWith('http://') || imgPath.startsWith('https://')) return imgPath;
356
+ const buf = fs.readFileSync(imgPath);
357
+ const ext = imgPath.split('.').pop()?.toLowerCase();
358
+ const mime = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp' }[ext] || 'image/jpeg';
359
+ return `data:${mime};base64,${buf.toString('base64')}`;
360
+ }
361
+
362
+ function saveMcpImage(result, outputPath) {
363
+ const content = result?.content || [];
364
+ const textBlock = content.find(c => c.type === 'text');
365
+ const imageBlock = content.find(c => c.type === 'image');
366
+ if (textBlock) process.stderr.write(`${textBlock.text}\n`);
367
+ if (imageBlock) {
368
+ const out = outputPath || `makaron-output-${Date.now()}.jpg`;
369
+ fs.writeFileSync(out, Buffer.from(imageBlock.data, 'base64'));
370
+ console.log(out);
371
+ return out;
372
+ }
373
+ if (textBlock) console.log(textBlock.text);
374
+ return null;
375
+ }
376
+
297
377
  // ─── Main ────────────────────────────────────────────────────────────────────
298
378
 
299
379
  const args = process.argv.slice(2);
@@ -302,7 +382,7 @@ const command = args[0];
302
382
  if (command === 'login') {
303
383
  await login();
304
384
  } else if (command === 'create') {
305
- const { cookie, baseUrl } = getAuthCookie();
385
+ const { headers, baseUrl } = getAuth();
306
386
  const opts = { images: [], imageUrls: [] };
307
387
  for (let i = 1; i < args.length; i++) {
308
388
  if (args[i] === '--image' && args[i + 1]) opts.images.push(args[++i]);
@@ -315,9 +395,9 @@ if (command === 'login') {
315
395
  console.error('Usage: makaron create --image <file> [--image <file2>] or --title "name"');
316
396
  process.exit(1);
317
397
  }
318
- await createProject(baseUrl, cookie, opts);
398
+ await createProject(baseUrl, headers, opts);
319
399
  } else if (command === 'chat') {
320
- const { cookie, baseUrl } = getAuthCookie();
400
+ const { headers, baseUrl } = getAuth();
321
401
  let projectId = null;
322
402
  const chatImages = [];
323
403
  const promptParts = [];
@@ -339,7 +419,7 @@ if (command === 'login') {
339
419
  });
340
420
  const res = await fetch(`${baseUrl}/api/projects/create`, {
341
421
  method: 'POST',
342
- headers: { 'Content-Type': 'application/json', 'Cookie': cookie },
422
+ headers: { 'Content-Type': 'application/json', ...headers },
343
423
  body: JSON.stringify({ imageBase64s: base64s, _addToProject: projectId }),
344
424
  });
345
425
  if (res.ok) {
@@ -349,16 +429,264 @@ if (command === 'login') {
349
429
  process.stderr.write(`⚠️ Failed to upload images: ${await res.text()}\n`);
350
430
  }
351
431
  }
352
- const { results } = await streamAgent(baseUrl, cookie, projectId, prompt);
432
+ const { results } = await streamAgent(baseUrl, headers, projectId, prompt);
353
433
  process.stderr.write('\n━━━ Results ━━━\n');
354
434
  for (const img of results.images) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
355
435
  for (const d of results.designs) process.stderr.write(`🎨 ${d.desc}\n`);
356
436
  process.stderr.write(`🔗 ${APP_URL}/projects/${projectId}\n`);
357
- for (const task of results.animationTasks) await pollVideo(baseUrl, cookie, task.taskId);
358
- for (const task of results.musicTasks) await pollMusic(baseUrl, cookie, task.taskId);
437
+ for (const task of results.animationTasks) await pollVideo(baseUrl, headers, task.taskId);
438
+ for (const task of results.musicTasks) await pollMusic(baseUrl, headers, task.taskId);
359
439
  } else if (command === 'list' || command === 'ls') {
360
- const { cookie, baseUrl } = getAuthCookie();
361
- await listProjects(baseUrl, cookie);
440
+ const { headers, baseUrl } = getAuth();
441
+ await listProjects(baseUrl, headers);
442
+ } else if (command === 'abort') {
443
+ const { headers, baseUrl } = getAuth();
444
+ const runId = args[1];
445
+ if (!runId) {
446
+ console.error('Usage: makaron abort <runId>');
447
+ process.exit(1);
448
+ }
449
+ const res = await fetch(`${baseUrl}/api/agent/abort`, {
450
+ method: 'POST',
451
+ headers: { 'Content-Type': 'application/json', ...headers },
452
+ body: JSON.stringify({ runId }),
453
+ });
454
+ if (res.ok) {
455
+ console.log(`✅ Run ${runId} aborted`);
456
+ } else {
457
+ console.error(`❌ Abort failed:`, await res.text());
458
+ }
459
+ } else if (command === 'edit') {
460
+ const { headers, baseUrl } = getAuth();
461
+ const editArgs = {};
462
+ const promptParts = [];
463
+ let outputPath = null;
464
+ for (let i = 1; i < args.length; i++) {
465
+ if (args[i] === '--image' && args[i + 1]) editArgs.image = imageToArg(args[++i]);
466
+ else if (args[i] === '--model' && args[i + 1]) editArgs.model = args[++i];
467
+ else if (args[i] === '--skill' && args[i + 1]) editArgs.skill = args[++i];
468
+ else if (args[i] === '--ref' && args[i + 1]) {
469
+ editArgs.referenceImages = editArgs.referenceImages || [];
470
+ editArgs.referenceImages.push(imageToArg(args[++i]));
471
+ }
472
+ else if (args[i] === '--aspect' && args[i + 1]) editArgs.aspectRatio = args[++i];
473
+ else if (args[i] === '--out' && args[i + 1]) outputPath = args[++i];
474
+ else promptParts.push(args[i]);
475
+ }
476
+ editArgs.editPrompt = promptParts.join(' ');
477
+ 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); }
478
+ process.stderr.write('🎨 Generating...\n');
479
+ const result = await callMcpTool(baseUrl, headers, 'makaron_edit_image', editArgs);
480
+ saveMcpImage(result, outputPath);
481
+
482
+ } else if (command === 'video') {
483
+ const { headers, baseUrl } = getAuth();
484
+ const sub = args[1];
485
+
486
+ if (sub === 'script') {
487
+ const images = [];
488
+ const promptParts = [];
489
+ let language = 'en';
490
+ for (let i = 2; i < args.length; i++) {
491
+ if (args[i] === '--image' && args[i + 1]) images.push(imageToArg(args[++i]));
492
+ else if (args[i] === '--lang' && args[i + 1]) language = args[++i];
493
+ else promptParts.push(args[i]);
494
+ }
495
+ if (!images.length) { console.error('Usage: makaron video script --image <file> [--image <file>] [--lang en|zh] "direction"'); process.exit(1); }
496
+ process.stderr.write('🎬 Writing script...\n');
497
+ const result = await callMcpTool(baseUrl, headers, 'makaron_write_video_script', { images, userRequest: promptParts.join(' ') || undefined, language });
498
+ const text = result?.content?.find(c => c.type === 'text')?.text;
499
+ if (text) console.log(text);
500
+
501
+ } else if (sub === 'create') {
502
+ const images = [];
503
+ let script = '', duration = undefined, aspectRatio = undefined, videoModel = undefined;
504
+ for (let i = 2; i < args.length; i++) {
505
+ if (args[i] === '--image' && args[i + 1]) images.push(args[++i]);
506
+ else if (args[i] === '--script' && args[i + 1]) script = args[++i];
507
+ else if (args[i] === '--script-file' && args[i + 1]) script = fs.readFileSync(args[++i], 'utf-8');
508
+ else if (args[i] === '--duration' && args[i + 1]) duration = Number(args[++i]);
509
+ else if (args[i] === '--aspect' && args[i + 1]) aspectRatio = args[++i];
510
+ else if (args[i] === '--model' && args[i + 1]) videoModel = args[++i];
511
+ }
512
+ if (!images.length || !script) { console.error('Usage: makaron video create --script "..." --image <url> [--duration 10] [--aspect 9:16] [--model kling|seedance]'); process.exit(1); }
513
+ process.stderr.write('🎬 Submitting video...\n');
514
+ const vArgs = { script, images };
515
+ if (duration) vArgs.duration = duration;
516
+ if (aspectRatio) vArgs.aspectRatio = aspectRatio;
517
+ if (videoModel) vArgs.videoModel = videoModel;
518
+ const result = await callMcpTool(baseUrl, headers, 'makaron_create_video', vArgs);
519
+ const text = result?.content?.find(c => c.type === 'text')?.text;
520
+ if (text) console.log(text);
521
+
522
+ } else if (sub === 'status') {
523
+ const taskId = args[2];
524
+ if (!taskId) { console.error('Usage: makaron video status <taskId>'); process.exit(1); }
525
+ const result = await callMcpTool(baseUrl, headers, 'makaron_get_video_status', { taskId });
526
+ const text = result?.content?.find(c => c.type === 'text')?.text;
527
+ if (text) console.log(text);
528
+
529
+ } else {
530
+ console.log(`Video commands:
531
+ video script --image <file> [--image <file>] "direction" Write video script
532
+ video create --script "..." --image <url> [--duration 10] Submit video task
533
+ video status <taskId> Check video status
534
+ `);
535
+ }
536
+
537
+ } else if (command === 'music') {
538
+ const { headers, baseUrl } = getAuth();
539
+ const sub = args[1];
540
+
541
+ if (sub === 'create') {
542
+ const promptParts = [];
543
+ let instrumental = true, style = undefined;
544
+ for (let i = 2; i < args.length; i++) {
545
+ if (args[i] === '--vocals') instrumental = false;
546
+ else if (args[i] === '--style' && args[i + 1]) style = args[++i];
547
+ else promptParts.push(args[i]);
548
+ }
549
+ const prompt = promptParts.join(' ');
550
+ if (!prompt) { console.error('Usage: makaron music create [--vocals] [--style "lo-fi"] "gentle piano"'); process.exit(1); }
551
+ process.stderr.write('🎵 Generating music...\n');
552
+ const mArgs = { prompt, instrumental };
553
+ if (style) mArgs.style = style;
554
+ const result = await callMcpTool(baseUrl, headers, 'makaron_create_music', mArgs);
555
+ const text = result?.content?.find(c => c.type === 'text')?.text;
556
+ if (text) console.log(text);
557
+
558
+ } else if (sub === 'status') {
559
+ const taskId = args[2];
560
+ if (!taskId) { console.error('Usage: makaron music status <taskId>'); process.exit(1); }
561
+ const result = await callMcpTool(baseUrl, headers, 'makaron_get_music_status', { taskId });
562
+ const text = result?.content?.find(c => c.type === 'text')?.text;
563
+ if (text) console.log(text);
564
+
565
+ } else {
566
+ console.log(`Music commands:
567
+ music create [--vocals] [--style "genre"] "description" Generate music
568
+ music status <taskId> Check music status
569
+ `);
570
+ }
571
+
572
+ } else if (command === 'admin') {
573
+ const { headers, baseUrl } = getAuth();
574
+ const sub = args[1];
575
+
576
+ if (sub === 'skills') {
577
+ const action = args[2]; // add, update, delete, or none (list)
578
+ if (!action) {
579
+ // List all skills
580
+ const res = await fetch(`${baseUrl}/api/admin/home-skills`, { headers });
581
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
582
+ const skills = await res.json();
583
+ console.log(`📋 ${skills.length} skills\n`);
584
+ for (const s of skills) {
585
+ const label = s.labels?.en || s.labels?.zh || '(no label)';
586
+ const active = s.is_active ? '✅' : '❌';
587
+ const hasZip = s.skill_path ? '📦' : ' ';
588
+ console.log(` ${active} ${hasZip} ${String(s.sort_order).padStart(3)} ${s.id} ${label}`);
589
+ }
590
+ } else if (action === 'add') {
591
+ const json = args.slice(3).join(' ');
592
+ if (!json) { console.error('Usage: makaron admin skills add \'{"labels":...,"image":"..."}\''); process.exit(1); }
593
+ let body;
594
+ try { body = JSON.parse(json); } catch { console.error('Invalid JSON'); process.exit(1); }
595
+ const res = await fetch(`${baseUrl}/api/admin/home-skills`, {
596
+ method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify(body),
597
+ });
598
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
599
+ const data = await res.json();
600
+ console.log(`✅ Skill created: ${data.id}`);
601
+ } else if (action === 'update') {
602
+ const id = args[3];
603
+ const json = args.slice(4).join(' ');
604
+ if (!id || !json) { console.error('Usage: makaron admin skills update <id> \'{"field":"value"}\''); process.exit(1); }
605
+ let body;
606
+ try { body = JSON.parse(json); } catch { console.error('Invalid JSON'); process.exit(1); }
607
+ body.id = id;
608
+ const res = await fetch(`${baseUrl}/api/admin/home-skills`, {
609
+ method: 'PUT', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify(body),
610
+ });
611
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
612
+ console.log(`✅ Skill ${id} updated`);
613
+ } else if (action === 'delete') {
614
+ const id = args[3];
615
+ if (!id) { console.error('Usage: makaron admin skills delete <id>'); process.exit(1); }
616
+ const res = await fetch(`${baseUrl}/api/admin/home-skills`, {
617
+ method: 'DELETE', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ id }),
618
+ });
619
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
620
+ console.log(`✅ Skill ${id} deleted`);
621
+ } else {
622
+ console.error(`Unknown action: ${action}. Use: add, update, delete, or omit to list.`);
623
+ process.exit(1);
624
+ }
625
+
626
+ } else if (sub === 'upload') {
627
+ const filePath = args[2];
628
+ const storagePath = args[3];
629
+ if (!filePath || !storagePath) { console.error('Usage: makaron admin upload <local-file> <storage-path>'); process.exit(1); }
630
+ if (!fs.existsSync(filePath)) { console.error(`File not found: ${filePath}`); process.exit(1); }
631
+
632
+ const buf = fs.readFileSync(filePath);
633
+ const ext = filePath.split('.').pop()?.toLowerCase();
634
+ const mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', mp4: 'video/mp4', zip: 'application/zip' };
635
+ const mime = mimeMap[ext] || 'application/octet-stream';
636
+
637
+ const formData = new FormData();
638
+ formData.append('file', new Blob([buf], { type: mime }), path.basename(filePath));
639
+ formData.append('path', storagePath);
640
+
641
+ const res = await fetch(`${baseUrl}/api/admin/upload`, { method: 'POST', headers, body: formData });
642
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
643
+ const { url } = await res.json();
644
+ console.log(`✅ Uploaded: ${url}`);
645
+
646
+ } else if (sub === 'fetch-skill') {
647
+ const code = args[2];
648
+ if (!code) { console.error('Usage: makaron admin fetch-skill <share-code>'); process.exit(1); }
649
+ // Extract code from full URL if given (e.g., https://www.makaron.app/s/4c4cbd57)
650
+ const shareCode = code.includes('/s/') ? code.split('/s/').pop() : code;
651
+
652
+ const res = await fetch(`${baseUrl}/api/skills/share/${shareCode}/download?format=json`, { headers });
653
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
654
+ const { skillName, files } = await res.json();
655
+
656
+ // Create output directory
657
+ const outDir = path.resolve(skillName);
658
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
659
+
660
+ for (const f of files) {
661
+ const filePath = path.join(outDir, f.path);
662
+ const dir = path.dirname(filePath);
663
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
664
+ fs.writeFileSync(filePath, Buffer.from(f.data, 'base64'));
665
+ }
666
+
667
+ console.log(`✅ Skill "${skillName}" downloaded to ./${skillName}/`);
668
+ console.log(` Files: ${files.map(f => f.path).join(', ')}`);
669
+
670
+ } else if (sub === 'set-admin') {
671
+ const email = args[2];
672
+ if (!email) { console.error('Usage: makaron admin set-admin <email>'); process.exit(1); }
673
+ const res = await fetch(`${baseUrl}/api/admin/set-admin`, {
674
+ method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ email }),
675
+ });
676
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
677
+ console.log(`✅ ${email} is now admin`);
678
+
679
+ } else {
680
+ console.log(`Admin commands:
681
+ admin skills List all marketplace skills
682
+ admin skills add '<json>' Add a new skill
683
+ admin skills update <id> '<json>' Update a skill
684
+ admin skills delete <id> Delete a skill
685
+ admin upload <file> <storage-path> Upload file to Storage
686
+ admin fetch-skill <code|url> Download skill from share link
687
+ admin set-admin <email> Grant admin access to a user
688
+ `);
689
+ }
362
690
  } else {
363
691
  console.log(`Makaron CLI — Talk to Makaron Agent from the terminal
364
692
 
@@ -370,8 +698,16 @@ Commands:
370
698
  create --title "name" Create empty project (text-to-image)
371
699
  chat --project <id> "message" Chat with Makaron Agent
372
700
  chat --project <id> --image <file> "message" Add image + chat
701
+ abort <runId> Abort a running Agent
702
+
703
+ edit [--image <file>] "prompt" AI image edit / text-to-image
704
+ video script|create|status Video generation
705
+ music create|status Music generation
706
+
707
+ admin Admin commands (skills, upload, set-admin)
373
708
 
374
709
  Environment:
710
+ MAKARON_API_KEY API key (mk_live_xxx) — recommended for agents
375
711
  MAKARON_URL API base (default: ${DEFAULT_URL})
376
712
  `);
377
713
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,14 @@
9
9
  "files": [
10
10
  "bin/"
11
11
  ],
12
- "keywords": ["makaron", "ai", "image-editing", "video", "cli", "agent"],
12
+ "keywords": [
13
+ "makaron",
14
+ "ai",
15
+ "image-editing",
16
+ "video",
17
+ "cli",
18
+ "agent"
19
+ ],
13
20
  "license": "MIT",
14
21
  "repository": {
15
22
  "type": "git",