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.
- package/README.md +341 -0
- package/bin/makaron.mjs +357 -21
- 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
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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',
|
|
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,
|
|
277
|
-
const res = await fetch(`${baseUrl}/api/projects/list`, { headers
|
|
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 {
|
|
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,
|
|
398
|
+
await createProject(baseUrl, headers, opts);
|
|
319
399
|
} else if (command === 'chat') {
|
|
320
|
-
const {
|
|
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',
|
|
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,
|
|
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,
|
|
358
|
-
for (const task of results.musicTasks) await pollMusic(baseUrl,
|
|
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 {
|
|
361
|
-
await listProjects(baseUrl,
|
|
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.
|
|
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": [
|
|
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",
|