makaron-cli 0.3.0 → 0.5.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 (4) hide show
  1. package/README.md +118 -176
  2. package/SKILL.md +197 -0
  3. package/bin/makaron.mjs +319 -10
  4. package/package.json +4 -2
package/README.md CHANGED
@@ -1,256 +1,198 @@
1
- # makaron-cli
1
+ # Makaron CLI — Agent Integration Skill
2
2
 
3
- Talk to **Makaron Agent** from the terminal create projects, edit images, generate videos, and compose music.
3
+ Makaron is a multimodal AI creative agent. You talk to it via `makaron chat`, and it produces images, videos, music, and animated designs — all saved to a persistent project.
4
4
 
5
- Zero dependencies. Single file. Works with `npx`.
6
-
7
- ## Install
5
+ ## Setup
8
6
 
9
7
  ```bash
10
- npx makaron-cli # run directly (always latest)
11
- # or
12
8
  npm install -g makaron-cli
13
- ```
9
+ # or use directly: npx makaron-cli
14
10
 
15
- ## Authentication
11
+ export MAKARON_API_KEY=mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
12
+ ```
16
13
 
17
- ### API Key (recommended for agents)
14
+ Verify: `npx makaron-cli list` should show projects.
18
15
 
19
- Set the `MAKARON_API_KEY` environment variable. That's it — no login step needed.
16
+ ## Core Workflow
20
17
 
21
18
  ```bash
22
- export MAKARON_API_KEY=mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
23
- npx makaron-cli list
24
- ```
19
+ # 1. Create project with image(s)
20
+ PROJECT_ID=$(npx makaron-cli create --image photo.jpg 2>/dev/null | grep "ID:" | awk '{print $2}')
25
21
 
26
- Get your API key from the Makaron dashboard (Settings → API Keys), or ask your admin to generate one.
22
+ # Multi-image project:
23
+ PROJECT_ID=$(npx makaron-cli create --image img1.jpg --image img2.jpg 2>/dev/null | grep "ID:" | awk '{print $2}')
27
24
 
28
- ### Interactive login (humans)
25
+ # 2. Submit creative request (non-blocking)
26
+ RUN_ID=$(npx makaron-cli chat --project $PROJECT_ID -b "make it cinematic and create a 5s video")
29
27
 
30
- ```bash
31
- npx makaron-cli login
32
- # Enter email + password → token saved to ~/.makaron/auth.json
28
+ # 3. Watch until all artifacts are ready
29
+ npx makaron-cli responses watch $RUN_ID --jsonl
33
30
  ```
34
31
 
35
- The CLI checks `MAKARON_API_KEY` first, then falls back to the saved session in `~/.makaron/auth.json`.
32
+ ## Primary: `chat` (Agent-driven creative work)
36
33
 
37
- ## Commands
34
+ Use `chat` for all creative tasks. Makaron Agent decides how to execute — it can edit images, generate videos, compose music, and create designs in a single conversation.
38
35
 
39
- ### `list` Show all projects
36
+ ### Submit a request
40
37
 
41
38
  ```bash
42
- npx makaron-cli list
39
+ npx makaron-cli chat --project <id> --json -b "<prompt>"
43
40
  ```
44
41
 
45
- Output:
42
+ Returns immediately:
43
+ ```json
44
+ {"runId": "xxx", "projectId": "...", "projectUrl": "https://www.makaron.app/projects/...", "status": "running"}
46
45
  ```
47
- 📁 12 projects
48
46
 
49
- abc123def My Photo 3 snaps 2h ago
50
- xyz789ghi Sunset Edit 5 snaps 1d ago
51
- ```
52
-
53
- ### `create` — Create a new project
47
+ ### With additional images
54
48
 
55
49
  ```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:
50
+ # Add reference images to project before chatting
51
+ npx makaron-cli chat --project <id> --image ref1.jpg --image ref2.jpg -b "use these as style reference"
68
52
  ```
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
53
 
78
- This is the main command. The Agent can edit images, generate videos, compose music, and create designs.
54
+ ### Check status (single query)
79
55
 
80
56
  ```bash
81
- npx makaron-cli chat --project <id> "make it look cinematic"
57
+ npx makaron-cli responses get <runId> --json
82
58
  ```
83
59
 
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
60
+ ### Watch until done (streaming events)
88
61
 
89
62
  ```bash
90
- # Add extra reference images to the project before chatting
91
- npx makaron-cli chat --project <id> --image ref.jpg "use this style"
63
+ npx makaron-cli responses watch <runId> --jsonl
92
64
  ```
93
65
 
94
- After the Agent finishes, the CLI automatically polls for any pending video/music tasks until they complete.
66
+ Outputs one JSON per line as artifacts appear:
67
+ ```
68
+ {"event":"output.added","item":{"id":"out_1","type":"image","status":"completed","url":"https://..."}}
69
+ {"event":"output.added","item":{"id":"out_2","type":"video","status":"rendering","task_id":"xxx"}}
70
+ {"event":"output.updated","item":{"id":"out_2","type":"video","status":"completed","url":"https://..."}}
71
+ {"event":"done","status":"completed"}
72
+ ```
95
73
 
96
- ### `abort` Stop a running Agent
74
+ ### Extract specific results
97
75
 
98
76
  ```bash
99
- npx makaron-cli abort <runId>
77
+ npx makaron-cli responses get <runId> --pick first_image_url
78
+ npx makaron-cli responses get <runId> --pick image_urls # all images (JSON array)
79
+ npx makaron-cli responses get <runId> --pick first_video_url
80
+ npx makaron-cli responses get <runId> --pick video_urls # all videos
81
+ npx makaron-cli responses get <runId> --pick project_url
82
+ npx makaron-cli responses get <runId> --pick text # agent's text reply
83
+ npx makaron-cli responses get <runId> --pick output # full output array
84
+ npx makaron-cli responses get <runId> --pick status
100
85
  ```
101
86
 
102
- Press `Ctrl+C` during `chat` to abort automatically.
87
+ ## Fallback: Direct tool calls (no project context)
103
88
 
104
- ### `edit` AI image editing (direct MCP tool call)
89
+ Use these only when `chat` is unavailable or you need raw model access without project/conversation context.
105
90
 
106
- Unlike `chat` (which uses the Agent with project context), `edit` directly calls the image generation model for one-shot results.
91
+ ### `edit` One-shot image editing
107
92
 
108
93
  ```bash
109
- # Text-to-image (no input image)
110
- npx makaron-cli edit "a cyberpunk cityscape at night, neon reflections"
111
-
112
94
  # Edit an existing image
113
95
  npx makaron-cli edit --image photo.jpg "add cinematic warm lighting"
114
96
 
115
- # With model and skill
116
- npx makaron-cli edit --image photo.jpg --model openai --skill captions "add elegant title"
97
+ # Text-to-image (no input)
98
+ npx makaron-cli edit "a cyberpunk cityscape at night"
117
99
 
118
- # With reference images (up to 3)
119
- npx makaron-cli edit --image photo.jpg --ref style1.jpg --ref style2.jpg "match this style"
100
+ # With model/skill/reference
101
+ npx makaron-cli edit --image photo.jpg --model openai --skill captions "add title"
102
+ npx makaron-cli edit --image photo.jpg --ref style.jpg "match this style"
120
103
 
121
- # Specify output path and aspect ratio
122
- npx makaron-cli edit --out result.jpg --aspect 9:16 "vertical poster design"
104
+ # Output to file
105
+ npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
123
106
  ```
124
107
 
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`)
108
+ Options: `--image`, `--model gemini|qwen|openai|pony|wai`, `--skill enhance|creative|wild|captions`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
132
109
 
133
- Output: saves image to local file, prints the file path to stdout.
134
-
135
- ### `video` — Video generation
110
+ ### `video` Video generation (3 steps)
136
111
 
137
112
  ```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 "电影感故事"
113
+ # 1. Write script from images
114
+ npx makaron-cli video script --image img1.jpg "cinematic story"
141
115
 
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
116
+ # 2. Submit rendering (images must be public URLs from step 1 or uploaded)
117
+ npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
145
118
 
146
- # Check status
119
+ # 3. Check status
147
120
  npx makaron-cli video status <taskId>
148
121
  ```
149
122
 
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)
123
+ Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--duration 3|5|7|10|15`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`
156
124
 
157
125
  ### `music` — Music generation
158
126
 
159
127
  ```bash
160
- # Generate instrumental music
161
128
  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
129
+ npx makaron-cli music create --vocals --style "lo-fi" "rainy day vibes"
167
130
  npx makaron-cli music status <taskId>
168
131
  ```
169
132
 
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"
133
+ Options: `--vocals` (include vocals), `--style "genre"`
134
+
135
+ ## Response Schema
136
+
137
+ ```typescript
138
+ type MakaronRunResponse = {
139
+ id: string
140
+ status: "in_progress" | "completed" | "failed" | "aborted"
141
+ incomplete: boolean // true = keep polling
142
+ project_id: string
143
+ project_url: string
144
+ next_poll_after_ms?: number // suggested poll interval
145
+ output: MakaronOutput[]
146
+ }
147
+
148
+ type MakaronOutput =
149
+ | { id: string; type: "text"; status: "completed"; content: string }
150
+ | { id: string; type: "image"; status: "completed"; url: string; snapshot_id: string }
151
+ | { id: string; type: "design"; status: "completed"; url: string; width: number; height: number; animated: boolean; duration?: number }
152
+ | { id: string; type: "video"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; url?: string; elapsed_seconds?: number }
153
+ | { id: string; type: "music"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; url?: string; elapsed_seconds?: number }
192
154
  ```
193
155
 
194
- ## Agent Integration Guide
195
-
196
- For AI agents (Claude Code, Cursor, etc.) calling this CLI programmatically:
197
-
198
- ### Setup (one-time)
156
+ ## Polling Rules
199
157
 
200
- ```bash
201
- export MAKARON_API_KEY=mk_live_your_key_here
202
- npx makaron-cli list # verify it works
203
- ```
158
+ 1. Poll while `incomplete: true` or `status` is `"in_progress"`
159
+ 2. Use `next_poll_after_ms` as interval (default 5000ms)
160
+ 3. Stop when `status` is `"completed"`, `"failed"`, or `"aborted"`
161
+ 4. Top-level `status: "completed"` means ALL artifacts are ready (including rendered videos)
204
162
 
205
- ### Best practices
163
+ ## Exit Codes
206
164
 
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
165
+ | Code | Meaning |
166
+ |------|---------|
167
+ | 0 | Success (completed) or valid in-progress response |
168
+ | 1 | Failed, aborted, or HTTP error |
169
+ | 2 | Timeout (partial response still printed to stdout) |
212
170
 
213
- ### What the Agent can do
171
+ ## What Makaron Agent Can Do
214
172
 
215
- | Capability | Example prompt |
216
- |-----------|---------------|
217
- | Image editing | "make the sky more dramatic" |
173
+ | Task | Example prompt |
174
+ |------|---------------|
175
+ | Edit photo | "make it cinematic with warm tones" |
218
176
  | 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" |
177
+ | Add/remove elements | "add a cat on the table" / "remove background person" |
178
+ | Text-to-image | "generate a cyberpunk cityscape" |
179
+ | Video from image | "create a 5 second video of her walking" |
180
+ | Video with model | "用seedance模型做5秒视频" |
224
181
  | 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
182
+ | Motion design | "create an Instagram story with animated text" |
183
+ | Multi-step | "edit the photo then make a video from it" |
239
184
 
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
- ```
185
+ ## Important Notes
252
186
 
253
- Open this URL in a browser to see the timeline of all edits, play videos, and download assets.
187
+ - One project = one conversation thread. All history is preserved.
188
+ - One run at a time per project. New message interrupts previous run.
189
+ - Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
190
+ - Videos take 2-5 minutes to render. Use `watch` to get URL when ready.
191
+ - Music takes ~60 seconds. Appears in output when done.
192
+ - Images are typically ready in 15-30 seconds.
193
+ - stdout is always machine-readable JSON/text. Human-friendly logs go to stderr.
194
+ - Always use `chat` as the primary interface — even for single image edits.
195
+ - `edit`/`video`/`music` are fallback tools for when `chat` is unavailable or you need raw model access without project context.
254
196
 
255
197
  ## Admin: Skill Marketplace Operations
256
198
 
@@ -272,7 +214,7 @@ npx makaron-cli admin upload cover.jpg marketplace/covers/skill-name.jpg
272
214
  npx makaron-cli admin upload before.jpg marketplace/before/before-name.jpg
273
215
 
274
216
  # Upload skill zip
275
- npx makaron-cli admin upload skill.zip marketplace/skills/skill-name.zip
217
+ npx makaron-cli admin upload skill-name.zip marketplace/skills/skill-name.zip
276
218
  ```
277
219
 
278
220
  Storage paths follow this convention:
package/SKILL.md ADDED
@@ -0,0 +1,197 @@
1
+ ---
2
+ name: makaron
3
+ description: Use Makaron CLI to generate AI images, videos, music, and motion designs. Trigger when user needs creative media production — photo editing, video generation, music composition, or design creation. Requires `npx makaron-cli` and MAKARON_API_KEY env var.
4
+ ---
5
+
6
+ # Makaron CLI — Agent Integration Skill
7
+
8
+ Makaron is a multimodal AI creative agent. You talk to it via `makaron chat`, and it produces images, videos, music, and animated designs — all saved to a persistent project.
9
+
10
+ ## Setup
11
+
12
+ ```bash
13
+ export MAKARON_API_KEY=mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
14
+ ```
15
+
16
+ Verify: `npx makaron-cli list` should show projects.
17
+
18
+ ## Core Workflow
19
+
20
+ ```bash
21
+ # 1. Create project with image(s)
22
+ PROJECT_ID=$(npx makaron-cli create --image photo.jpg 2>/dev/null | grep "ID:" | awk '{print $2}')
23
+
24
+ # Multi-image project:
25
+ PROJECT_ID=$(npx makaron-cli create --image img1.jpg --image img2.jpg 2>/dev/null | grep "ID:" | awk '{print $2}')
26
+
27
+ # 2. Submit creative request (non-blocking)
28
+ RUN_ID=$(npx makaron-cli chat --project $PROJECT_ID -b "make it cinematic and create a 5s video")
29
+
30
+ # 3. Watch until all artifacts are ready
31
+ npx makaron-cli responses watch $RUN_ID --jsonl
32
+ ```
33
+
34
+ ## Primary: `chat` (Agent-driven creative work)
35
+
36
+ Use `chat` for all creative tasks. Makaron Agent decides how to execute — it can edit images, generate videos, compose music, and create designs in a single conversation.
37
+
38
+ ### Submit a request
39
+
40
+ ```bash
41
+ npx makaron-cli chat --project <id> --json -b "<prompt>"
42
+ ```
43
+
44
+ Returns immediately:
45
+ ```json
46
+ {"runId": "xxx", "projectId": "...", "projectUrl": "https://www.makaron.app/projects/...", "status": "running"}
47
+ ```
48
+
49
+ ### With additional images
50
+
51
+ ```bash
52
+ # Add reference images to project before chatting
53
+ npx makaron-cli chat --project <id> --image ref1.jpg --image ref2.jpg -b "use these as style reference"
54
+ ```
55
+
56
+ ### Check status (single query)
57
+
58
+ ```bash
59
+ npx makaron-cli responses get <runId> --json
60
+ ```
61
+
62
+ ### Watch until done (streaming events)
63
+
64
+ ```bash
65
+ npx makaron-cli responses watch <runId> --jsonl
66
+ ```
67
+
68
+ Outputs one JSON per line as artifacts appear:
69
+ ```
70
+ {"event":"output.added","item":{"id":"out_1","type":"image","status":"completed","url":"https://..."}}
71
+ {"event":"output.added","item":{"id":"out_2","type":"video","status":"rendering","task_id":"xxx"}}
72
+ {"event":"output.updated","item":{"id":"out_2","type":"video","status":"completed","url":"https://..."}}
73
+ {"event":"done","status":"completed"}
74
+ ```
75
+
76
+ ### Extract specific results
77
+
78
+ ```bash
79
+ npx makaron-cli responses get <runId> --pick first_image_url
80
+ npx makaron-cli responses get <runId> --pick image_urls # all images (JSON array)
81
+ npx makaron-cli responses get <runId> --pick first_video_url
82
+ npx makaron-cli responses get <runId> --pick video_urls # all videos
83
+ npx makaron-cli responses get <runId> --pick project_url
84
+ npx makaron-cli responses get <runId> --pick text # agent's text reply
85
+ npx makaron-cli responses get <runId> --pick output # full output array
86
+ npx makaron-cli responses get <runId> --pick status
87
+ ```
88
+
89
+ ## Fallback: Direct tool calls (no project context)
90
+
91
+ Use these only when `chat` is unavailable or you need raw model access without project/conversation context.
92
+
93
+ ### `edit` — One-shot image editing
94
+
95
+ ```bash
96
+ # Edit an existing image
97
+ npx makaron-cli edit --image photo.jpg "add cinematic warm lighting"
98
+
99
+ # Text-to-image (no input)
100
+ npx makaron-cli edit "a cyberpunk cityscape at night"
101
+
102
+ # With model/skill/reference
103
+ npx makaron-cli edit --image photo.jpg --model openai --skill captions "add title"
104
+ npx makaron-cli edit --image photo.jpg --ref style.jpg "match this style"
105
+
106
+ # Output to file
107
+ npx makaron-cli edit --image photo.jpg --out result.jpg "make it dramatic"
108
+ ```
109
+
110
+ Options: `--image`, `--model gemini|qwen|openai|pony|wai`, `--skill enhance|creative|wild|captions`, `--ref <file>` (up to 3), `--aspect <ratio>`, `--out <path>`
111
+
112
+ ### `video` — Video generation (3 steps)
113
+
114
+ ```bash
115
+ # 1. Write script from images
116
+ npx makaron-cli video script --image img1.jpg "cinematic story"
117
+
118
+ # 2. Submit rendering (images must be public URLs from step 1 or uploaded)
119
+ npx makaron-cli video create --script "Shot 1 (5s): <<<image_1>>> ..." --image https://...jpg --duration 5 --model kling
120
+
121
+ # 3. Check status
122
+ npx makaron-cli video status <taskId>
123
+ ```
124
+
125
+ Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--duration 3|5|7|10|15`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`
126
+
127
+ ### `music` — Music generation
128
+
129
+ ```bash
130
+ npx makaron-cli music create "gentle piano, warm strings, cinematic"
131
+ npx makaron-cli music create --vocals --style "lo-fi" "rainy day vibes"
132
+ npx makaron-cli music status <taskId>
133
+ ```
134
+
135
+ Options: `--vocals` (include vocals), `--style "genre"`
136
+
137
+ ## Response Schema
138
+
139
+ ```typescript
140
+ type MakaronRunResponse = {
141
+ id: string
142
+ status: "in_progress" | "completed" | "failed" | "aborted"
143
+ incomplete: boolean // true = keep polling
144
+ project_id: string
145
+ project_url: string
146
+ next_poll_after_ms?: number // suggested poll interval
147
+ output: MakaronOutput[]
148
+ }
149
+
150
+ type MakaronOutput =
151
+ | { id: string; type: "text"; status: "completed"; content: string }
152
+ | { id: string; type: "image"; status: "completed"; url: string; snapshot_id: string }
153
+ | { id: string; type: "design"; status: "completed"; url: string; width: number; height: number; animated: boolean; duration?: number }
154
+ | { id: string; type: "video"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; url?: string; elapsed_seconds?: number }
155
+ | { id: string; type: "music"; status: "queued"|"rendering"|"completed"|"failed"; task_id: string; url?: string; elapsed_seconds?: number }
156
+ ```
157
+
158
+ ## Polling Rules
159
+
160
+ 1. Poll while `incomplete: true` or `status` is `"in_progress"`
161
+ 2. Use `next_poll_after_ms` as interval (default 5000ms)
162
+ 3. Stop when `status` is `"completed"`, `"failed"`, or `"aborted"`
163
+ 4. Top-level `status: "completed"` means ALL artifacts are ready (including rendered videos)
164
+
165
+ ## Exit Codes
166
+
167
+ | Code | Meaning |
168
+ |------|---------|
169
+ | 0 | Success (completed) or valid in-progress response |
170
+ | 1 | Failed, aborted, or HTTP error |
171
+ | 2 | Timeout (partial response still printed to stdout) |
172
+
173
+ ## What Makaron Agent Can Do
174
+
175
+ | Task | Example prompt |
176
+ |------|---------------|
177
+ | Edit photo | "make it cinematic with warm tones" |
178
+ | Style transfer | "convert to oil painting style" |
179
+ | Add/remove elements | "add a cat on the table" / "remove background person" |
180
+ | Text-to-image | "generate a cyberpunk cityscape" |
181
+ | Video from image | "create a 5 second video of her walking" |
182
+ | Video with model | "用seedance模型做5秒视频" |
183
+ | Background music | "add calm piano music" |
184
+ | Motion design | "create an Instagram story with animated text" |
185
+ | Multi-step | "edit the photo then make a video from it" |
186
+
187
+ ## Important Notes
188
+
189
+ - One project = one conversation thread. All history is preserved.
190
+ - One run at a time per project. New message interrupts previous run.
191
+ - Multi-image: `create --image a.jpg --image b.jpg` or `chat --image ref.jpg`.
192
+ - Videos take 2-5 minutes to render. Use `watch` to get URL when ready.
193
+ - Music takes ~60 seconds. Appears in output when done.
194
+ - Images are typically ready in 15-30 seconds.
195
+ - stdout is always machine-readable JSON/text. Human-friendly logs go to stderr.
196
+ - Always use `chat` as the primary interface — even for single image edits.
197
+ - `edit`/`video`/`music` are fallback tools for when `chat` is unavailable or you need raw model access without project context.
package/bin/makaron.mjs CHANGED
@@ -6,6 +6,8 @@
6
6
  * npx makaron-cli login
7
7
  * npx makaron-cli create --image photo.jpg
8
8
  * npx makaron-cli chat --project <id> "make it look cinematic"
9
+ * npx makaron-cli chat --project <id> -b "message" # background, returns runId
10
+ * npx makaron-cli responses get <runId> --wait # poll until done
9
11
  * npx makaron-cli list
10
12
  */
11
13
 
@@ -226,6 +228,211 @@ async function streamAgent(baseUrl, headers, projectId, prompt) {
226
228
  return { runId, results };
227
229
  }
228
230
 
231
+ // ─── Run + Poll (non-blocking) ──────────────────────────────────────────────
232
+
233
+ async function submitRun(baseUrl, headers, projectId, prompt, opts = {}) {
234
+ const body = { projectId, prompt };
235
+ if (opts.preferredModel) body.preferredModel = opts.preferredModel;
236
+ if (opts.videoModel) body.videoModel = opts.videoModel;
237
+ if (opts.currentSnapshotIndex != null) body.currentSnapshotIndex = opts.currentSnapshotIndex;
238
+ if (opts.isNsfw) body.isNsfw = opts.isNsfw;
239
+
240
+ const res = await fetch(`${baseUrl}/api/agent/run`, {
241
+ method: 'POST',
242
+ headers: { 'Content-Type': 'application/json', ...headers },
243
+ body: JSON.stringify(body),
244
+ });
245
+
246
+ if (!res.ok) {
247
+ const text = await res.text();
248
+ console.error(`Error ${res.status}: ${text}`);
249
+ process.exit(1);
250
+ }
251
+
252
+ return await res.json();
253
+ }
254
+
255
+ async function pollRun(baseUrl, headers, runId, opts = {}) {
256
+ const { json = false, waitForArtifacts = false, background = false } = opts;
257
+ if (background) return;
258
+
259
+ let lastSeq = -1;
260
+ let printedText = '';
261
+ const start = Date.now();
262
+
263
+ while (true) {
264
+ await new Promise(r => setTimeout(r, 3000));
265
+ const elapsed = Math.round((Date.now() - start) / 1000);
266
+
267
+ const params = new URLSearchParams({ events: 'true' });
268
+ if (lastSeq >= 0) params.set('after', String(lastSeq));
269
+ if (waitForArtifacts) params.set('wait_for_artifacts', 'true');
270
+
271
+ let data;
272
+ try {
273
+ const res = await fetch(`${baseUrl}/api/agent/run/${runId}?${params}`, { headers });
274
+ if (!res.ok) {
275
+ if (elapsed > 800) { process.stderr.write(`\n❌ Timeout after ${elapsed}s\n`); process.exit(1); }
276
+ continue;
277
+ }
278
+ data = await res.json();
279
+ } catch {
280
+ continue;
281
+ }
282
+
283
+ // Process incremental events
284
+ if (data.events?.length) {
285
+ for (const ev of data.events) {
286
+ if (ev.seq > lastSeq) lastSeq = ev.seq;
287
+ if (json) continue; // skip printing in json mode
288
+ switch (ev.type) {
289
+ case 'content': {
290
+ const newText = ev.data?.text || '';
291
+ process.stdout.write(newText);
292
+ printedText += newText;
293
+ break;
294
+ }
295
+ case 'status':
296
+ process.stderr.write(`\r⏳ ${ev.data?.text || ''}`);
297
+ break;
298
+ case 'tool_call':
299
+ process.stderr.write(`\n🔧 ${ev.data?.tool || ''}`);
300
+ if (ev.data?.input?.editPrompt) process.stderr.write(`\n editPrompt: ${ev.data.input.editPrompt}`);
301
+ if (ev.data?.input?.model) process.stderr.write(`\n model: ${ev.data.input.model}`);
302
+ if (ev.data?.input?.description) process.stderr.write(`: ${ev.data.input.description.substring(0, 80)}`);
303
+ process.stderr.write('\n');
304
+ break;
305
+ case 'image':
306
+ process.stderr.write(`\n🖼️ Image: ${ev.data?.imageUrl || '(uploading...)'}\n`);
307
+ break;
308
+ case 'render':
309
+ if (ev.data?.published) {
310
+ const desc = ev.data.animation
311
+ ? `${ev.data.animation.durationInSeconds}s video (${ev.data.width}x${ev.data.height})`
312
+ : `still design (${ev.data.width}x${ev.data.height})`;
313
+ process.stderr.write(`\n🎨 Design published: ${desc}\n`);
314
+ }
315
+ break;
316
+ case 'animation_task':
317
+ process.stderr.write(`\n🎬 Video submitted: ${ev.data?.taskId}\n`);
318
+ break;
319
+ case 'music_task':
320
+ process.stderr.write(`\n🎵 Music submitted: ${ev.data?.taskId}\n`);
321
+ break;
322
+ case 'error':
323
+ process.stderr.write(`\n❌ Error: ${ev.data?.message}\n`);
324
+ break;
325
+ }
326
+ }
327
+ } else if (!json) {
328
+ process.stderr.write(`\r⏳ Working... ${elapsed}s (${data.eventCount || 0} events)`);
329
+ }
330
+
331
+ // Check terminal status
332
+ if (data.status === 'completed' || data.status === 'failed' || data.status === 'aborted') {
333
+ if (printedText && !json) process.stdout.write('\n');
334
+
335
+ if (json) {
336
+ // Structured JSON output — add projectUrl
337
+ data.projectUrl = `${APP_URL}/projects/${data.projectId}`;
338
+ console.log(JSON.stringify(data, null, 2));
339
+ } else {
340
+ process.stderr.write('\n━━━ Results ━━━\n');
341
+ if (data.result) {
342
+ for (const img of data.result.images || []) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
343
+ for (const d of data.result.designs || []) process.stderr.write(`🎨 Design (${d.width}x${d.height})\n`);
344
+ for (const v of data.result.videos || []) {
345
+ if (v.videoUrl) process.stderr.write(`🎬 Video: ${v.videoUrl}\n`);
346
+ else process.stderr.write(`🎬 Video ${v.taskId}: ${v.status || 'submitted'}\n`);
347
+ }
348
+ for (const m of data.result.music || []) {
349
+ if (m.audioUrl) process.stderr.write(`🎵 Music: ${m.audioUrl}\n`);
350
+ else process.stderr.write(`🎵 Music ${m.taskId}: ${m.status || 'submitted'}\n`);
351
+ }
352
+ if (data.result.error) process.stderr.write(`❌ ${data.result.error}\n`);
353
+ }
354
+ process.stderr.write(`🔗 ${APP_URL}/projects/${data.projectId}\n`);
355
+ }
356
+
357
+ if (data.status === 'failed') process.exit(1);
358
+ return data;
359
+ }
360
+ }
361
+ }
362
+
363
+ // ─── Pick Helper ────────────────────────────────────────────────────────────
364
+
365
+ function applyPick(data, field) {
366
+ switch (field) {
367
+ case 'first_image_url': return data.output?.find(o => o.type === 'image')?.url || null;
368
+ case 'image_urls': return (data.output || []).filter(o => o.type === 'image' && o.url).map(o => o.url);
369
+ case 'first_video_url': return data.output?.find(o => o.type === 'video' && o.url)?.url || null;
370
+ case 'video_urls': return (data.output || []).filter(o => o.type === 'video' && o.url).map(o => o.url);
371
+ case 'first_design_url': return data.output?.find(o => o.type === 'design')?.url || null;
372
+ case 'design_urls': return (data.output || []).filter(o => o.type === 'design' && o.url).map(o => o.url);
373
+ case 'first_music_url': return data.output?.find(o => o.type === 'music' && o.url)?.url || null;
374
+ case 'music_urls': return (data.output || []).filter(o => o.type === 'music' && o.url).map(o => o.url);
375
+ case 'project_url': return data.project_url || null;
376
+ case 'output': return data.output || [];
377
+ case 'text': return data.output?.find(o => o.type === 'text')?.content || null;
378
+ case 'status': return data.status;
379
+ default: return data[field] !== undefined ? data[field] : null;
380
+ }
381
+ }
382
+
383
+ // ─── Watch (incremental event stream) ───────────────────────────────────────
384
+
385
+ async function watchRun(baseUrl, headers, runId, opts = {}) {
386
+ const { interval = 5000, jsonl = true } = opts;
387
+ let lastOutput = [];
388
+ const start = Date.now();
389
+
390
+ while (true) {
391
+ const elapsed = Math.round((Date.now() - start) / 1000);
392
+ let data;
393
+ try {
394
+ const res = await fetch(`${baseUrl}/api/agent/run/${runId}`, { headers });
395
+ if (!res.ok) {
396
+ if (elapsed > 800) { process.stderr.write(`Timeout after ${elapsed}s\n`); process.exit(2); }
397
+ await new Promise(r => setTimeout(r, interval));
398
+ continue;
399
+ }
400
+ data = await res.json();
401
+ } catch {
402
+ await new Promise(r => setTimeout(r, interval));
403
+ continue;
404
+ }
405
+
406
+ const currentOutput = data.output || [];
407
+
408
+ // Diff: find new or updated items
409
+ for (const item of currentOutput) {
410
+ const prev = lastOutput.find(o => o.id === item.id);
411
+ if (!prev) {
412
+ // New item
413
+ if (jsonl) console.log(JSON.stringify({ event: 'output.added', item }));
414
+ else process.stderr.write(`+ [${item.type}] ${item.url || item.content?.slice(0, 60) || item.status}\n`);
415
+ } else if (JSON.stringify(prev) !== JSON.stringify(item)) {
416
+ // Updated item (e.g. video status changed)
417
+ if (jsonl) console.log(JSON.stringify({ event: 'output.updated', item }));
418
+ else process.stderr.write(`~ [${item.type}] ${item.status} ${item.url || ''}\n`);
419
+ }
420
+ }
421
+
422
+ lastOutput = currentOutput;
423
+
424
+ // Check terminal status
425
+ if (!data.incomplete && (data.status === 'completed' || data.status === 'failed' || data.status === 'aborted')) {
426
+ if (jsonl) console.log(JSON.stringify({ event: 'done', status: data.status }));
427
+ if (data.status === 'failed' || data.status === 'aborted') process.exit(1);
428
+ process.exit(0);
429
+ }
430
+
431
+ const waitMs = data.next_poll_after_ms || interval;
432
+ await new Promise(r => setTimeout(r, waitMs));
433
+ }
434
+ }
435
+
229
436
  // ─── Async Task Polling ──────────────────────────────────────────────────────
230
437
 
231
438
  async function pollVideo(baseUrl, headers, taskId) {
@@ -401,16 +608,27 @@ if (command === 'login') {
401
608
  let projectId = null;
402
609
  const chatImages = [];
403
610
  const promptParts = [];
611
+ let useStream = false;
612
+ let background = false;
613
+ let jsonOutput = false;
614
+ let videoModel = undefined;
615
+ let preferredModel = undefined;
404
616
  for (let i = 1; i < args.length; i++) {
405
617
  if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
406
618
  else if (args[i] === '--image' && args[i + 1]) chatImages.push(args[++i]);
619
+ else if (args[i] === '--stream') useStream = true;
620
+ else if (args[i] === '--background' || args[i] === '-b') background = true;
621
+ else if (args[i] === '--json') jsonOutput = true;
622
+ else if (args[i] === '--video-model' && args[i + 1]) videoModel = args[++i];
623
+ else if (args[i] === '--model' && args[i + 1]) preferredModel = args[++i];
407
624
  else promptParts.push(args[i]);
408
625
  }
409
626
  const prompt = promptParts.join(' ');
410
627
  if (!projectId || !prompt) {
411
- console.error('Usage: makaron chat --project <id> [--image <file>] "your message"');
628
+ console.error('Usage: makaron chat --project <id> [--image <file>] [--stream] [--background|-b] [--json] "your message"');
412
629
  process.exit(1);
413
630
  }
631
+ // Upload images if provided
414
632
  if (chatImages.length > 0) {
415
633
  const base64s = chatImages.map(imgPath => {
416
634
  process.stderr.write(`📤 Uploading ${path.basename(imgPath)}...\n`);
@@ -429,13 +647,97 @@ if (command === 'login') {
429
647
  process.stderr.write(`⚠️ Failed to upload images: ${await res.text()}\n`);
430
648
  }
431
649
  }
432
- const { results } = await streamAgent(baseUrl, headers, projectId, prompt);
433
- process.stderr.write('\n━━━ Results ━━━\n');
434
- for (const img of results.images) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
435
- for (const d of results.designs) process.stderr.write(`🎨 ${d.desc}\n`);
436
- process.stderr.write(`🔗 ${APP_URL}/projects/${projectId}\n`);
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);
650
+
651
+ if (useStream) {
652
+ // Legacy SSE mode
653
+ const { results } = await streamAgent(baseUrl, headers, projectId, prompt);
654
+ process.stderr.write('\n━━━ Results ━━━\n');
655
+ for (const img of results.images) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
656
+ for (const d of results.designs) process.stderr.write(`🎨 ${d.desc}\n`);
657
+ process.stderr.write(`🔗 ${APP_URL}/projects/${projectId}\n`);
658
+ for (const task of results.animationTasks) await pollVideo(baseUrl, headers, task.taskId);
659
+ for (const task of results.musicTasks) await pollMusic(baseUrl, headers, task.taskId);
660
+ } else {
661
+ // Default: fire-and-forget + poll
662
+ const { runId } = await submitRun(baseUrl, headers, projectId, prompt, { videoModel, preferredModel });
663
+ if (background) {
664
+ // Just print runId and exit
665
+ if (jsonOutput) {
666
+ console.log(JSON.stringify({ runId, projectId, projectUrl: `${APP_URL}/projects/${projectId}`, status: 'running' }));
667
+ } else {
668
+ console.log(runId);
669
+ }
670
+ } else {
671
+ process.stderr.write(`🚀 Run started: ${runId}\n`);
672
+ await pollRun(baseUrl, headers, runId, { json: jsonOutput });
673
+ }
674
+ }
675
+ } else if (command === 'responses' || command === 'run') {
676
+ const { headers, baseUrl } = getAuth();
677
+ const sub = args[1];
678
+
679
+ if (sub === 'get') {
680
+ const runId = args[2];
681
+ if (!runId) { console.error('Usage: makaron responses get <runId> [--wait] [--json] [--pick <field>]'); process.exit(1); }
682
+ let wait = false, jsonOutput = false, pick = null;
683
+ for (let i = 3; i < args.length; i++) {
684
+ if (args[i] === '--wait') wait = true;
685
+ if (args[i] === '--json') jsonOutput = true;
686
+ if (args[i] === '--pick' && args[i + 1]) pick = args[++i];
687
+ }
688
+
689
+ if (wait) {
690
+ await pollRun(baseUrl, headers, runId, { json: true });
691
+ } else {
692
+ const res = await fetch(`${baseUrl}/api/agent/run/${runId}`, { headers });
693
+ if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
694
+ const data = await res.json();
695
+ if (pick) {
696
+ const picked = applyPick(data, pick);
697
+ if (picked !== undefined) console.log(typeof picked === 'string' ? picked : JSON.stringify(picked));
698
+ } else {
699
+ console.log(JSON.stringify(data, null, 2));
700
+ }
701
+ if (data.status === 'failed' || data.status === 'aborted') process.exit(1);
702
+ }
703
+
704
+ } else if (sub === 'watch') {
705
+ const runId = args[2];
706
+ if (!runId) { console.error('Usage: makaron responses watch <runId> [--jsonl] [--interval <ms>]'); process.exit(1); }
707
+ let interval = 5000, jsonl = false;
708
+ for (let i = 3; i < args.length; i++) {
709
+ if (args[i] === '--jsonl') jsonl = true;
710
+ if (args[i] === '--interval' && args[i + 1]) interval = parseInt(args[++i]);
711
+ }
712
+ await watchRun(baseUrl, headers, runId, { interval, jsonl });
713
+
714
+ } else if (sub === 'list') {
715
+ let projectId = null;
716
+ for (let i = 2; i < args.length; i++) {
717
+ if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
718
+ }
719
+ if (!projectId) { console.error('Usage: makaron responses list --project <id>'); process.exit(1); }
720
+ const res = await fetch(`${baseUrl}/api/agent/run?projectId=${projectId}`, { headers });
721
+ if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
722
+ const data = await res.json();
723
+ if (data.runs?.length) {
724
+ for (const r of data.runs) {
725
+ const age = timeSince(new Date(r.started_at));
726
+ console.log(` ${r.id} ${r.status.padEnd(10)} ${age} ${(r.prompt || '').slice(0, 50)}`);
727
+ }
728
+ } else {
729
+ console.log('No runs found for this project.');
730
+ }
731
+
732
+ } else {
733
+ console.log(`Responses commands:
734
+ responses get <runId> Get status and output (JSON)
735
+ responses get <runId> --wait Poll until completed
736
+ responses get <runId> --pick <field> Extract: first_image_url, first_video_url, project_url, output
737
+ responses watch <runId> --jsonl Watch until done (incremental events)
738
+ responses list --project <id> List runs for a project
739
+ `);
740
+ }
439
741
  } else if (command === 'list' || command === 'ls') {
440
742
  const { headers, baseUrl } = getAuth();
441
743
  await listProjects(baseUrl, headers);
@@ -696,8 +998,15 @@ Commands:
696
998
  create --image <file> Create project from local image
697
999
  create --image-url <url> Create project from URL
698
1000
  create --title "name" Create empty project (text-to-image)
699
- chat --project <id> "message" Chat with Makaron Agent
700
- chat --project <id> --image <file> "message" Add image + chat
1001
+
1002
+ chat --project <id> "message" Chat (non-blocking, polls for result)
1003
+ chat --project <id> -b "message" Background: submit and print runId
1004
+ chat --project <id> --stream "msg" Legacy: stream SSE in real-time
1005
+ chat --project <id> --json "msg" Output structured JSON result
1006
+
1007
+ responses get <runId> Get run status and results
1008
+ responses get <runId> --wait Poll until completed
1009
+ responses list --project <id> List runs for a project
701
1010
  abort <runId> Abort a running Agent
702
1011
 
703
1012
  edit [--image <file>] "prompt" AI image edit / text-to-image
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "makaron": "./bin/makaron.mjs"
8
8
  },
9
9
  "files": [
10
- "bin/"
10
+ "bin/",
11
+ "SKILL.md",
12
+ "README.md"
11
13
  ],
12
14
  "keywords": [
13
15
  "makaron",