makaron-cli 0.1.0 → 0.2.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 +271 -0
  2. package/bin/makaron.mjs +202 -21
  3. package/package.json +9 -2
package/README.md ADDED
@@ -0,0 +1,271 @@
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
+ ## Example: Full workflow
105
+
106
+ ```bash
107
+ # 1. Login (once)
108
+ npx makaron-cli login
109
+
110
+ # 2. Create a project from a photo
111
+ npx makaron-cli create --image my-photo.jpg
112
+ # → ID: proj_abc123
113
+
114
+ # 3. Edit the image
115
+ npx makaron-cli chat --project proj_abc123 "add dramatic lighting and film grain"
116
+
117
+ # 4. Generate a video from the result
118
+ npx makaron-cli chat --project proj_abc123 "make a 5 second cinematic video"
119
+
120
+ # 5. Add background music
121
+ npx makaron-cli chat --project proj_abc123 "add epic orchestral background music"
122
+ ```
123
+
124
+ ## Agent Integration Guide
125
+
126
+ For AI agents (Claude Code, Cursor, etc.) calling this CLI programmatically:
127
+
128
+ ### Setup (one-time)
129
+
130
+ ```bash
131
+ export MAKARON_API_KEY=mk_live_your_key_here
132
+ npx makaron-cli list # verify it works
133
+ ```
134
+
135
+ ### Best practices
136
+
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
142
+
143
+ ### What the Agent can do
144
+
145
+ | Capability | Example prompt |
146
+ |-----------|---------------|
147
+ | Image editing | "make the sky more dramatic" |
148
+ | Style transfer | "convert to oil painting style" |
149
+ | Add elements | "add a cat sitting on the table" |
150
+ | Remove elements | "remove the person in the background" |
151
+ | Text-to-image | "generate a cyberpunk cityscape" (on empty project) |
152
+ | Video from image | "create a 5 second video" |
153
+ | Video with script | "make a video: camera slowly zooms in while leaves fall" |
154
+ | Background music | "add calm piano music" |
155
+ | Design/motion graphics | "create an Instagram story with animated text" |
156
+
157
+ ### Output parsing
158
+
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)
167
+
168
+ ### Environment variables
169
+
170
+ | Variable | Purpose | Default |
171
+ |----------|---------|---------|
172
+ | `MAKARON_API_KEY` | API key (`mk_live_xxx`) | — |
173
+ | `MAKARON_URL` | API endpoint override | `https://www.makaron.app` |
174
+
175
+ ## Viewing results
176
+
177
+ All generated content (images, videos, designs) is saved to the project and visible at:
178
+
179
+ ```
180
+ https://www.makaron.app/projects/<project-id>
181
+ ```
182
+
183
+ Open this URL in a browser to see the timeline of all edits, play videos, and download assets.
184
+
185
+ ## Admin: Skill Marketplace Operations
186
+
187
+ Admin commands require an API key with admin privileges. Ask your admin to run `makaron admin set-admin <your-email>` to grant access.
188
+
189
+ ### List all marketplace skills
190
+
191
+ ```bash
192
+ npx makaron-cli admin skills
193
+ ```
194
+
195
+ ### Upload assets to Storage
196
+
197
+ ```bash
198
+ # Upload cover image (3:4 aspect ratio recommended)
199
+ npx makaron-cli admin upload cover.jpg marketplace/covers/skill-name.jpg
200
+
201
+ # Upload before photo
202
+ npx makaron-cli admin upload before.jpg marketplace/before/before-name.jpg
203
+
204
+ # Upload skill zip
205
+ npx makaron-cli admin upload skill.zip marketplace/skills/skill-name.zip
206
+ ```
207
+
208
+ Storage paths follow this convention:
209
+ - Covers: `marketplace/covers/<skill-name>.jpg` (or `.mp4` for video covers)
210
+ - Before images: `marketplace/before/<name>.jpg`
211
+ - Skill zips: `marketplace/skills/<skill-name>.zip`
212
+
213
+ ### Add a new skill to marketplace
214
+
215
+ ```bash
216
+ npx makaron-cli admin skills add '{
217
+ "labels": {"zh": "中文名", "en": "English Name"},
218
+ "image": "https://sdyrtztrjgmmpnirswxt.supabase.co/storage/v1/object/public/images/marketplace/covers/skill-name.jpg",
219
+ "prompt": "Default prompt shown to users",
220
+ "skill_path": "https://sdyrtztrjgmmpnirswxt.supabase.co/storage/v1/object/public/images/marketplace/skills/skill-name.zip",
221
+ "image_count": 2,
222
+ "sort_order": 10,
223
+ "is_active": true,
224
+ "before_images": ["https://sdyrtztrjgmmpnirswxt.supabase.co/storage/v1/object/public/images/marketplace/before/before-name.jpg"]
225
+ }'
226
+ ```
227
+
228
+ ### Update / delete a skill
229
+
230
+ ```bash
231
+ npx makaron-cli admin skills update <id> '{"sort_order": 5, "is_active": false}'
232
+ npx makaron-cli admin skills delete <id>
233
+ ```
234
+
235
+ ### Download skill from share link
236
+
237
+ ```bash
238
+ # From share code
239
+ npx makaron-cli admin fetch-skill 4c4cbd57
240
+
241
+ # From full URL
242
+ npx makaron-cli admin fetch-skill https://www.makaron.app/s/4c4cbd57
243
+
244
+ # → Creates ./skill-name/ directory with SKILL.md + assets/
245
+ ```
246
+
247
+ ### End-to-end skill launch workflow
248
+
249
+ 0. **Get skill from share link** — `npx makaron-cli admin fetch-skill <code>`
250
+ 1. **Read SKILL.md** — understand the skill's variables, prompt template, and output format
251
+ 2. **Generate before photo** — use Makaron MCP `makaron_edit_image` to generate a plain selfie (no phone in frame, natural light)
252
+ 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)
253
+ 4. **Package zip** — `zip skill-name.zip SKILL.md` (video reference URLs go in SKILL.md metadata, not in the zip)
254
+ 5. **Upload all assets**:
255
+ ```bash
256
+ npx makaron-cli admin upload cover.jpg marketplace/covers/skill-name.jpg
257
+ npx makaron-cli admin upload before.jpg marketplace/before/skill-name-before.jpg
258
+ npx makaron-cli admin upload skill-name.zip marketplace/skills/skill-name.zip
259
+ ```
260
+ 6. **Add to marketplace**:
261
+ ```bash
262
+ 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}'
263
+ ```
264
+ 7. **Verify** — visit https://www.makaron.app/home and confirm the skill appears, can be clicked, and works
265
+
266
+ ### Cover image guidelines
267
+
268
+ - Target aspect ratio: **3:4** (marketplace cards use `object-fit: cover`)
269
+ - 16:9 images will be severely cropped — resize or stack two 16:9 images vertically
270
+ - Video covers (`.mp4`) are supported — auto-play in the card
271
+ - 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; }
@@ -302,7 +345,7 @@ const command = args[0];
302
345
  if (command === 'login') {
303
346
  await login();
304
347
  } else if (command === 'create') {
305
- const { cookie, baseUrl } = getAuthCookie();
348
+ const { headers, baseUrl } = getAuth();
306
349
  const opts = { images: [], imageUrls: [] };
307
350
  for (let i = 1; i < args.length; i++) {
308
351
  if (args[i] === '--image' && args[i + 1]) opts.images.push(args[++i]);
@@ -315,9 +358,9 @@ if (command === 'login') {
315
358
  console.error('Usage: makaron create --image <file> [--image <file2>] or --title "name"');
316
359
  process.exit(1);
317
360
  }
318
- await createProject(baseUrl, cookie, opts);
361
+ await createProject(baseUrl, headers, opts);
319
362
  } else if (command === 'chat') {
320
- const { cookie, baseUrl } = getAuthCookie();
363
+ const { headers, baseUrl } = getAuth();
321
364
  let projectId = null;
322
365
  const chatImages = [];
323
366
  const promptParts = [];
@@ -339,7 +382,7 @@ if (command === 'login') {
339
382
  });
340
383
  const res = await fetch(`${baseUrl}/api/projects/create`, {
341
384
  method: 'POST',
342
- headers: { 'Content-Type': 'application/json', 'Cookie': cookie },
385
+ headers: { 'Content-Type': 'application/json', ...headers },
343
386
  body: JSON.stringify({ imageBase64s: base64s, _addToProject: projectId }),
344
387
  });
345
388
  if (res.ok) {
@@ -349,16 +392,151 @@ if (command === 'login') {
349
392
  process.stderr.write(`⚠️ Failed to upload images: ${await res.text()}\n`);
350
393
  }
351
394
  }
352
- const { results } = await streamAgent(baseUrl, cookie, projectId, prompt);
395
+ const { results } = await streamAgent(baseUrl, headers, projectId, prompt);
353
396
  process.stderr.write('\n━━━ Results ━━━\n');
354
397
  for (const img of results.images) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
355
398
  for (const d of results.designs) process.stderr.write(`🎨 ${d.desc}\n`);
356
399
  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);
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);
359
402
  } else if (command === 'list' || command === 'ls') {
360
- const { cookie, baseUrl } = getAuthCookie();
361
- await listProjects(baseUrl, cookie);
403
+ const { headers, baseUrl } = getAuth();
404
+ await listProjects(baseUrl, headers);
405
+ } else if (command === 'abort') {
406
+ const { headers, baseUrl } = getAuth();
407
+ const runId = args[1];
408
+ if (!runId) {
409
+ console.error('Usage: makaron abort <runId>');
410
+ process.exit(1);
411
+ }
412
+ const res = await fetch(`${baseUrl}/api/agent/abort`, {
413
+ method: 'POST',
414
+ headers: { 'Content-Type': 'application/json', ...headers },
415
+ body: JSON.stringify({ runId }),
416
+ });
417
+ if (res.ok) {
418
+ console.log(`✅ Run ${runId} aborted`);
419
+ } else {
420
+ console.error(`❌ Abort failed:`, await res.text());
421
+ }
422
+ } else if (command === 'admin') {
423
+ const { headers, baseUrl } = getAuth();
424
+ const sub = args[1];
425
+
426
+ if (sub === 'skills') {
427
+ const action = args[2]; // add, update, delete, or none (list)
428
+ if (!action) {
429
+ // List all skills
430
+ const res = await fetch(`${baseUrl}/api/admin/home-skills`, { headers });
431
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
432
+ const skills = await res.json();
433
+ console.log(`📋 ${skills.length} skills\n`);
434
+ for (const s of skills) {
435
+ const label = s.labels?.en || s.labels?.zh || '(no label)';
436
+ const active = s.is_active ? '✅' : '❌';
437
+ const hasZip = s.skill_path ? '📦' : ' ';
438
+ console.log(` ${active} ${hasZip} ${String(s.sort_order).padStart(3)} ${s.id} ${label}`);
439
+ }
440
+ } else if (action === 'add') {
441
+ const json = args.slice(3).join(' ');
442
+ if (!json) { console.error('Usage: makaron admin skills add \'{"labels":...,"image":"..."}\''); process.exit(1); }
443
+ let body;
444
+ try { body = JSON.parse(json); } catch { console.error('Invalid JSON'); process.exit(1); }
445
+ const res = await fetch(`${baseUrl}/api/admin/home-skills`, {
446
+ method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify(body),
447
+ });
448
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
449
+ const data = await res.json();
450
+ console.log(`✅ Skill created: ${data.id}`);
451
+ } else if (action === 'update') {
452
+ const id = args[3];
453
+ const json = args.slice(4).join(' ');
454
+ if (!id || !json) { console.error('Usage: makaron admin skills update <id> \'{"field":"value"}\''); process.exit(1); }
455
+ let body;
456
+ try { body = JSON.parse(json); } catch { console.error('Invalid JSON'); process.exit(1); }
457
+ body.id = id;
458
+ const res = await fetch(`${baseUrl}/api/admin/home-skills`, {
459
+ method: 'PUT', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify(body),
460
+ });
461
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
462
+ console.log(`✅ Skill ${id} updated`);
463
+ } else if (action === 'delete') {
464
+ const id = args[3];
465
+ if (!id) { console.error('Usage: makaron admin skills delete <id>'); process.exit(1); }
466
+ const res = await fetch(`${baseUrl}/api/admin/home-skills`, {
467
+ method: 'DELETE', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ id }),
468
+ });
469
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
470
+ console.log(`✅ Skill ${id} deleted`);
471
+ } else {
472
+ console.error(`Unknown action: ${action}. Use: add, update, delete, or omit to list.`);
473
+ process.exit(1);
474
+ }
475
+
476
+ } else if (sub === 'upload') {
477
+ const filePath = args[2];
478
+ const storagePath = args[3];
479
+ if (!filePath || !storagePath) { console.error('Usage: makaron admin upload <local-file> <storage-path>'); process.exit(1); }
480
+ if (!fs.existsSync(filePath)) { console.error(`File not found: ${filePath}`); process.exit(1); }
481
+
482
+ const buf = fs.readFileSync(filePath);
483
+ const ext = filePath.split('.').pop()?.toLowerCase();
484
+ const mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', mp4: 'video/mp4', zip: 'application/zip' };
485
+ const mime = mimeMap[ext] || 'application/octet-stream';
486
+
487
+ const formData = new FormData();
488
+ formData.append('file', new Blob([buf], { type: mime }), path.basename(filePath));
489
+ formData.append('path', storagePath);
490
+
491
+ const res = await fetch(`${baseUrl}/api/admin/upload`, { method: 'POST', headers, body: formData });
492
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
493
+ const { url } = await res.json();
494
+ console.log(`✅ Uploaded: ${url}`);
495
+
496
+ } else if (sub === 'fetch-skill') {
497
+ const code = args[2];
498
+ if (!code) { console.error('Usage: makaron admin fetch-skill <share-code>'); process.exit(1); }
499
+ // Extract code from full URL if given (e.g., https://www.makaron.app/s/4c4cbd57)
500
+ const shareCode = code.includes('/s/') ? code.split('/s/').pop() : code;
501
+
502
+ const res = await fetch(`${baseUrl}/api/skills/share/${shareCode}/download?format=json`, { headers });
503
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
504
+ const { skillName, files } = await res.json();
505
+
506
+ // Create output directory
507
+ const outDir = path.resolve(skillName);
508
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
509
+
510
+ for (const f of files) {
511
+ const filePath = path.join(outDir, f.path);
512
+ const dir = path.dirname(filePath);
513
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
514
+ fs.writeFileSync(filePath, Buffer.from(f.data, 'base64'));
515
+ }
516
+
517
+ console.log(`✅ Skill "${skillName}" downloaded to ./${skillName}/`);
518
+ console.log(` Files: ${files.map(f => f.path).join(', ')}`);
519
+
520
+ } else if (sub === 'set-admin') {
521
+ const email = args[2];
522
+ if (!email) { console.error('Usage: makaron admin set-admin <email>'); process.exit(1); }
523
+ const res = await fetch(`${baseUrl}/api/admin/set-admin`, {
524
+ method: 'POST', headers: { 'Content-Type': 'application/json', ...headers }, body: JSON.stringify({ email }),
525
+ });
526
+ if (!res.ok) { console.error(`Error ${res.status}:`, await res.text()); process.exit(1); }
527
+ console.log(`✅ ${email} is now admin`);
528
+
529
+ } else {
530
+ console.log(`Admin commands:
531
+ admin skills List all marketplace skills
532
+ admin skills add '<json>' Add a new skill
533
+ admin skills update <id> '<json>' Update a skill
534
+ admin skills delete <id> Delete a skill
535
+ admin upload <file> <storage-path> Upload file to Storage
536
+ admin fetch-skill <code|url> Download skill from share link
537
+ admin set-admin <email> Grant admin access to a user
538
+ `);
539
+ }
362
540
  } else {
363
541
  console.log(`Makaron CLI — Talk to Makaron Agent from the terminal
364
542
 
@@ -370,8 +548,11 @@ Commands:
370
548
  create --title "name" Create empty project (text-to-image)
371
549
  chat --project <id> "message" Chat with Makaron Agent
372
550
  chat --project <id> --image <file> "message" Add image + chat
551
+ abort <runId> Abort a running Agent
552
+ admin Admin commands (skills, upload, set-admin)
373
553
 
374
554
  Environment:
555
+ MAKARON_API_KEY API key (mk_live_xxx) — recommended for agents
375
556
  MAKARON_URL API base (default: ${DEFAULT_URL})
376
557
  `);
377
558
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",