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.
- package/README.md +271 -0
- package/bin/makaron.mjs +202 -21
- 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
|
|
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; }
|
|
@@ -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 {
|
|
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,
|
|
361
|
+
await createProject(baseUrl, headers, opts);
|
|
319
362
|
} else if (command === 'chat') {
|
|
320
|
-
const {
|
|
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',
|
|
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,
|
|
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,
|
|
358
|
-
for (const task of results.musicTasks) await pollMusic(baseUrl,
|
|
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 {
|
|
361
|
-
await listProjects(baseUrl,
|
|
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.
|
|
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": [
|
|
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",
|