makaron-cli 0.8.2 → 0.8.3
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 +13 -96
- package/SKILL.md +12 -95
- package/bin/makaron.mjs +73 -609
- package/package.json +1 -1
- package/skills/makaron/SKILL.md +12 -95
package/README.md
CHANGED
|
@@ -97,6 +97,16 @@ Returns immediately:
|
|
|
97
97
|
npx makaron-cli chat --project <id> --image ref1.jpg --image ref2.jpg -b "use these as style reference"
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
### Inspect existing timeline media
|
|
101
|
+
|
|
102
|
+
Before starting a follow-up run on an existing project, list the current timeline media so you know what assets are available and which `<<<media_N>>>` references to use:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npx makaron-cli project media <projectId> --json
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
This is project-scoped. `responses get <runId> --pick output` only returns artifacts from one run; `project media` returns the whole project timeline: original uploads, references, generated images, video snapshots, and editable compositions.
|
|
109
|
+
|
|
100
110
|
### With video input (MP4/MOV/WebM)
|
|
101
111
|
|
|
102
112
|
```bash
|
|
@@ -110,8 +120,8 @@ npx makaron-cli chat --project <id> --video party.mp4 --image kid.jpg -b "make t
|
|
|
110
120
|
npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "splice these into one seamless video"
|
|
111
121
|
```
|
|
112
122
|
|
|
113
|
-
Video files are uploaded via signed URL. CLI local video uploads
|
|
114
|
-
The agent understands video content natively — it can analyze scenes, edit, extend, and compose videos. Seedance video-reference editing is
|
|
123
|
+
Video files are uploaded via signed URL. CLI local video uploads support `.mp4`, `.mov`, or `.webm`, max 50MB, max 120s with 1s metadata tolerance, and <=1080p / 2,086,876 frame pixels. The frontend can transcode larger videos before upload; the CLI uploads directly to Storage and rejects videos above those limits.
|
|
124
|
+
The agent understands video content natively — it can analyze scenes, edit, extend, and compose videos. Seedance video-reference editing is still limited to ~15s provider references, so longer uploaded videos should be split/prepared by the agent before model submission; Kling remains the base/direct edit path.
|
|
115
125
|
Use `chat --project <id|auto> --video ...` for any project/timeline video work. Direct `video create` is standalone and does not write timeline entries.
|
|
116
126
|
|
|
117
127
|
### Check status (single query)
|
|
@@ -134,64 +144,6 @@ Outputs one JSON per line as artifacts appear:
|
|
|
134
144
|
{"event":"done","status":"completed"}
|
|
135
145
|
```
|
|
136
146
|
|
|
137
|
-
### New Agent quickstart
|
|
138
|
-
|
|
139
|
-
For a new Agent, keep the default service flow to two response commands:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
npx makaron-cli responses next <runId> --json
|
|
143
|
-
npx makaron-cli responses handle <messageId> --run <runId> --choice approve
|
|
144
|
-
npx makaron-cli responses deliver <artifactId> --run <runId> --channel feishu --message-id <messageId>
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
`responses next` emits a compact timeline, detects text-only checkpoints for creative/service work, and returns `next_commands` when the Agent must handle a Makaron message before waiting for artifacts. It exits with code `3` on an unhandled checkpoint so wrappers stop instead of silently waiting. After handling a checkpoint, run the returned `next_commands.inspect` command to continue. While the run is still generating, it returns `status: "running"` and `blocking: false`. If completed image/video artifacts exist and have not been delivered, it returns `status: "has_artifacts"` and `blocking: true` with compact `undelivered_artifacts` entries containing `kind`, `status`, `url`, `fileName`, and `contentType` when available, plus `next_commands.deliver`; the Agent must send the artifact URL to the user, then record delivery with `responses deliver`. A non-checkpoint response returns `status: "ready"` or `status: "delivered"`. If a run failed, `responses next --json` prints `status: "failed"` and `blocking: true` plus `error.type`, `error.message`, `error.recoverable`, `error.detail`, and `next_command` to stdout before exiting `1`, so wrappers can parse the failure without scraping stderr. Use `--no-fail` to inspect checkpoint JSON without failing. For pure Q&A runs, use:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
npx makaron-cli responses next <runId> --json --checkpoint-mode off
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
`responses handle` is the single checkpoint action command. Valid choices are `approve`, `revise`, `ask_user`, and `continue`. `responses deliver` records that a completed artifact was actually delivered to the user, preventing repeated delivery prompts.
|
|
154
|
-
|
|
155
|
-
### Dialogue events for external Agents
|
|
156
|
-
|
|
157
|
-
Use this when another Agent needs to read what Makaron said, handle text checkpoints, and relay artifacts without inventing customer-service wording.
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
npx makaron-cli responses events <runId> --jsonl
|
|
161
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl
|
|
162
|
-
# compact view: npx makaron-cli responses timeline <runId> --jsonl --compact
|
|
163
|
-
# pure Q&A view: npx makaron-cli responses timeline <runId> --jsonl --checkpoint-mode off
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
`events` and `timeline` are lower-level commands. New Agents should start with `responses next` and use these only when they need raw event streams.
|
|
167
|
-
|
|
168
|
-
Events use only three factual types:
|
|
169
|
-
|
|
170
|
-
```json
|
|
171
|
-
{"type":"message","id":"msg_1","runId":"run_xxx","seq":1,"text":"Makaron original message","requires_approval":true,"approval_options":["approve","revise","ask_user","continue"]}
|
|
172
|
-
{"type":"approval","messageId":"msg_1","choice":"approve","status":"recorded"}
|
|
173
|
-
{"type":"artifact","kind":"image","status":"completed","url":"https://..."}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
Makaron uses a conservative checkpoint rule for creative/service Agents: if a run stops with substantive text, has no image/video/music/design artifact, and has no continuing execution action such as a tool call, the message is marked `requires_approval: true`. Pure status text such as queued, rendering, uploading, or completed is not treated as a checkpoint. Pure Q&A flows should pass `--checkpoint-mode off`. If a message has `requires_approval: true`, the external Agent must record an approval before continuing to wait for artifacts or claiming completion:
|
|
177
|
-
|
|
178
|
-
```bash
|
|
179
|
-
npx makaron-cli responses approve msg_1 --run <runId> --note "Proceed."
|
|
180
|
-
npx makaron-cli responses revise msg_1 --run <runId> "make it softer"
|
|
181
|
-
npx makaron-cli responses ask-user msg_1 --run <runId>
|
|
182
|
-
npx makaron-cli responses continue msg_1 --run <runId>
|
|
183
|
-
npx makaron-cli responses handle msg_1 --run <runId> --choice approve
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
Important: approvals are a local Agent gate in v0. They are recorded by the CLI so wrappers and Skills can fail fast, but they do not pause or resume the remote Makaron runtime yet. Wrappers can enforce the local gate with:
|
|
187
|
-
|
|
188
|
-
```bash
|
|
189
|
-
npx makaron-cli responses events <runId> --jsonl --checkpoint-mode service --fail-on-unapproved
|
|
190
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl --compact --fail-on-unapproved
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
Use `--compact` when relaying to another Agent or chat system; it merges consecutive Makaron content chunks into a single readable message.
|
|
194
|
-
|
|
195
147
|
### Extract specific results
|
|
196
148
|
|
|
197
149
|
```bash
|
|
@@ -254,7 +206,7 @@ For project/timeline video editing, use:
|
|
|
254
206
|
npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
|
|
255
207
|
```
|
|
256
208
|
|
|
257
|
-
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration
|
|
209
|
+
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`. SeeDance accepts integer output duration 4-15s (default 5s); Kling supports 5-10s.
|
|
258
210
|
|
|
259
211
|
Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance --video` uses the Seedance video-reference path and requires target <=15s, <=1080p input. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s.
|
|
260
212
|
|
|
@@ -321,41 +273,6 @@ type MakaronOutput =
|
|
|
321
273
|
| Motion design | "create an Instagram story with animated text" |
|
|
322
274
|
| Multi-step | "edit the photo then make a video from it" |
|
|
323
275
|
|
|
324
|
-
## Minimal Agent Wrapper
|
|
325
|
-
|
|
326
|
-
A new Agent can use this minimal flow:
|
|
327
|
-
|
|
328
|
-
```bash
|
|
329
|
-
RUN_JSON=$(npx makaron-cli chat --project auto --json -b "$USER_PROMPT")
|
|
330
|
-
RUN_ID=$(echo "$RUN_JSON" | jq -r .runId)
|
|
331
|
-
PROJECT_URL=$(echo "$RUN_JSON" | jq -r .projectUrl)
|
|
332
|
-
send_message "Project created: $PROJECT_URL"
|
|
333
|
-
|
|
334
|
-
if ! NEXT=$(npx makaron-cli responses next "$RUN_ID" --json); then
|
|
335
|
-
STATUS=$(echo "$NEXT" | jq -r .status)
|
|
336
|
-
if [ "$STATUS" = "needs_approval" ]; then
|
|
337
|
-
MSG_ID=$(echo "$NEXT" | jq -r .checkpoint.id)
|
|
338
|
-
TEXT=$(echo "$NEXT" | jq -r .checkpoint.text)
|
|
339
|
-
send_message "$TEXT"
|
|
340
|
-
npx makaron-cli responses handle "$MSG_ID" --run "$RUN_ID" --choice ask_user
|
|
341
|
-
exit 3
|
|
342
|
-
elif [ "$STATUS" = "failed" ]; then
|
|
343
|
-
send_message "$(echo "$NEXT" | jq -r .error.message)"
|
|
344
|
-
exit 1
|
|
345
|
-
fi
|
|
346
|
-
fi
|
|
347
|
-
|
|
348
|
-
NEXT=$(npx makaron-cli responses next "$RUN_ID" --json --no-fail)
|
|
349
|
-
if [ "$(echo "$NEXT" | jq -r .status)" = "has_artifacts" ]; then
|
|
350
|
-
ARTIFACT_ID=$(echo "$NEXT" | jq -r ".undelivered_artifacts[0].id")
|
|
351
|
-
ARTIFACT_URL=$(echo "$NEXT" | jq -r ".undelivered_artifacts[0].url")
|
|
352
|
-
DELIVERY_MESSAGE_ID=$(send_message "$ARTIFACT_URL")
|
|
353
|
-
npx makaron-cli responses deliver "$ARTIFACT_ID" --run "$RUN_ID" --channel feishu --message-id "$DELIVERY_MESSAGE_ID"
|
|
354
|
-
fi
|
|
355
|
-
|
|
356
|
-
RESULT=$(npx makaron-cli responses get "$RUN_ID" --wait --json)
|
|
357
|
-
```
|
|
358
|
-
|
|
359
276
|
## Recommended Pattern: Service Flow (Feishu/OpenClaw/Group Chat)
|
|
360
277
|
|
|
361
278
|
When serving end-users in a chat environment (Feishu, Slack, Discord), use this proactive message pattern:
|
package/SKILL.md
CHANGED
|
@@ -86,6 +86,16 @@ Returns immediately:
|
|
|
86
86
|
npx makaron-cli chat --project <id> --image ref1.jpg --image ref2.jpg -b "use these as style reference"
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### Inspect existing timeline media
|
|
90
|
+
|
|
91
|
+
Before starting a follow-up run on an existing project, list the current timeline media so you know what assets are available and which `<<<media_N>>>` references to use:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx makaron-cli project media <projectId> --json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This is project-scoped. `responses get <runId> --pick output` only returns artifacts from one run; `project media` returns the whole project timeline: original uploads, references, generated images, video snapshots, and editable compositions.
|
|
98
|
+
|
|
89
99
|
### With video input (edit, compose, extend)
|
|
90
100
|
|
|
91
101
|
```bash
|
|
@@ -102,7 +112,7 @@ npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "comb
|
|
|
102
112
|
npx makaron-cli chat --project auto --video https://example.com/dance.mp4 -b "extend this to 15 seconds"
|
|
103
113
|
```
|
|
104
114
|
|
|
105
|
-
Supported formats: MP4, MOV, WebM. CLI local video uploads
|
|
115
|
+
Supported formats: MP4, MOV, WebM. CLI local video uploads support max 50MB, max 120s with 1s metadata tolerance, and <=1080p / 2,086,876 frame pixels. The frontend can transcode larger videos before upload; the CLI uploads directly to Storage and rejects videos above those limits. Videos are uploaded to the project timeline. The Agent can analyze scenes, edit content, compose multiple clips, extend duration, and add effects — all via natural language. Seedance video-reference editing is still limited to ~15s provider references, so longer uploaded videos should be split/prepared by the agent before model submission; Kling remains the base/direct edit path.
|
|
106
116
|
|
|
107
117
|
Use `chat --project <id|auto> --video ...` for any project/timeline video work. Direct video commands are standalone raw-tool calls.
|
|
108
118
|
|
|
@@ -126,64 +136,6 @@ Outputs one JSON per line as artifacts appear:
|
|
|
126
136
|
{"event":"done","status":"completed"}
|
|
127
137
|
```
|
|
128
138
|
|
|
129
|
-
### New Agent quickstart
|
|
130
|
-
|
|
131
|
-
For a new Agent, keep the default service flow to two response commands:
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
npx makaron-cli responses next <runId> --json
|
|
135
|
-
npx makaron-cli responses handle <messageId> --run <runId> --choice approve
|
|
136
|
-
npx makaron-cli responses deliver <artifactId> --run <runId> --channel feishu --message-id <messageId>
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
`responses next` emits a compact timeline, detects text-only checkpoints for creative/service work, and returns `next_commands` when the Agent must handle a Makaron message before waiting for artifacts. It exits with code `3` on an unhandled checkpoint so wrappers stop instead of silently waiting. After handling a checkpoint, run the returned `next_commands.inspect` command to continue. While the run is still generating, it returns `status: "running"` and `blocking: false`. If completed image/video artifacts exist and have not been delivered, it returns `status: "has_artifacts"` and `blocking: true` with compact `undelivered_artifacts` entries containing `kind`, `status`, `url`, `fileName`, and `contentType` when available, plus `next_commands.deliver`; the Agent must send the artifact URL to the user, then record delivery with `responses deliver`. A non-checkpoint response returns `status: "ready"` or `status: "delivered"`. If a run failed, `responses next --json` prints `status: "failed"` and `blocking: true` plus `error.type`, `error.message`, `error.recoverable`, `error.detail`, and `next_command` to stdout before exiting `1`, so wrappers can parse the failure without scraping stderr. Use `--no-fail` to inspect checkpoint JSON without failing. For pure Q&A runs, use:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
npx makaron-cli responses next <runId> --json --checkpoint-mode off
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
`responses handle` is the single checkpoint action command. Valid choices are `approve`, `revise`, `ask_user`, and `continue`. `responses deliver` records that a completed artifact was actually delivered to the user, preventing repeated delivery prompts.
|
|
146
|
-
|
|
147
|
-
### Dialogue events for external Agents
|
|
148
|
-
|
|
149
|
-
Use this when another Agent needs to read what Makaron said, handle text checkpoints, and relay artifacts without inventing customer-service wording.
|
|
150
|
-
|
|
151
|
-
```bash
|
|
152
|
-
npx makaron-cli responses events <runId> --jsonl
|
|
153
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl
|
|
154
|
-
# compact view: npx makaron-cli responses timeline <runId> --jsonl --compact
|
|
155
|
-
# pure Q&A view: npx makaron-cli responses timeline <runId> --jsonl --checkpoint-mode off
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
`events` and `timeline` are lower-level commands. New Agents should start with `responses next` and use these only when they need raw event streams.
|
|
159
|
-
|
|
160
|
-
Events use only three factual types:
|
|
161
|
-
|
|
162
|
-
```json
|
|
163
|
-
{"type":"message","id":"msg_1","runId":"run_xxx","seq":1,"text":"Makaron original message","requires_approval":true,"approval_options":["approve","revise","ask_user","continue"]}
|
|
164
|
-
{"type":"approval","messageId":"msg_1","choice":"approve","status":"recorded"}
|
|
165
|
-
{"type":"artifact","kind":"image","status":"completed","url":"https://..."}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Makaron uses a conservative checkpoint rule for creative/service Agents: if a run stops with substantive text, has no image/video/music/design artifact, and has no continuing execution action such as a tool call, the message is marked `requires_approval: true`. Pure status text such as queued, rendering, uploading, or completed is not treated as a checkpoint. Pure Q&A flows should pass `--checkpoint-mode off`. If a message has `requires_approval: true`, the external Agent must record an approval before continuing to wait for artifacts or claiming completion:
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
npx makaron-cli responses approve msg_1 --run <runId> --note "Proceed."
|
|
172
|
-
npx makaron-cli responses revise msg_1 --run <runId> "make it softer"
|
|
173
|
-
npx makaron-cli responses ask-user msg_1 --run <runId>
|
|
174
|
-
npx makaron-cli responses continue msg_1 --run <runId>
|
|
175
|
-
npx makaron-cli responses handle msg_1 --run <runId> --choice approve
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Important: approvals are a local Agent gate in v0. They are recorded by the CLI so wrappers and Skills can fail fast, but they do not pause or resume the remote Makaron runtime yet. Wrappers can enforce the local gate with:
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
npx makaron-cli responses events <runId> --jsonl --checkpoint-mode service --fail-on-unapproved
|
|
182
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl --compact --fail-on-unapproved
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Use `--compact` when relaying to another Agent or chat system; it merges consecutive Makaron content chunks into a single readable message.
|
|
186
|
-
|
|
187
139
|
### Extract specific results
|
|
188
140
|
|
|
189
141
|
```bash
|
|
@@ -246,7 +198,7 @@ npx makaron-cli video status <taskId>
|
|
|
246
198
|
npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
|
|
247
199
|
```
|
|
248
200
|
|
|
249
|
-
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration
|
|
201
|
+
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`. SeeDance accepts integer output duration 4-15s (default 5s); Kling supports 5-10s.
|
|
250
202
|
|
|
251
203
|
Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance --video` uses the Seedance video-reference path and requires target <=15s, <=1080p input. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s.
|
|
252
204
|
|
|
@@ -310,41 +262,6 @@ type MakaronOutput =
|
|
|
310
262
|
| Motion design | "create an Instagram story with animated text" |
|
|
311
263
|
| Multi-step | "edit the photo then make a video from it" |
|
|
312
264
|
|
|
313
|
-
## Minimal Agent Wrapper
|
|
314
|
-
|
|
315
|
-
A new Agent can use this minimal flow:
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
RUN_JSON=$(npx makaron-cli chat --project auto --json -b "$USER_PROMPT")
|
|
319
|
-
RUN_ID=$(echo "$RUN_JSON" | jq -r .runId)
|
|
320
|
-
PROJECT_URL=$(echo "$RUN_JSON" | jq -r .projectUrl)
|
|
321
|
-
send_message "Project created: $PROJECT_URL"
|
|
322
|
-
|
|
323
|
-
if ! NEXT=$(npx makaron-cli responses next "$RUN_ID" --json); then
|
|
324
|
-
STATUS=$(echo "$NEXT" | jq -r .status)
|
|
325
|
-
if [ "$STATUS" = "needs_approval" ]; then
|
|
326
|
-
MSG_ID=$(echo "$NEXT" | jq -r .checkpoint.id)
|
|
327
|
-
TEXT=$(echo "$NEXT" | jq -r .checkpoint.text)
|
|
328
|
-
send_message "$TEXT"
|
|
329
|
-
npx makaron-cli responses handle "$MSG_ID" --run "$RUN_ID" --choice ask_user
|
|
330
|
-
exit 3
|
|
331
|
-
elif [ "$STATUS" = "failed" ]; then
|
|
332
|
-
send_message "$(echo "$NEXT" | jq -r .error.message)"
|
|
333
|
-
exit 1
|
|
334
|
-
fi
|
|
335
|
-
fi
|
|
336
|
-
|
|
337
|
-
NEXT=$(npx makaron-cli responses next "$RUN_ID" --json --no-fail)
|
|
338
|
-
if [ "$(echo "$NEXT" | jq -r .status)" = "has_artifacts" ]; then
|
|
339
|
-
ARTIFACT_ID=$(echo "$NEXT" | jq -r ".undelivered_artifacts[0].id")
|
|
340
|
-
ARTIFACT_URL=$(echo "$NEXT" | jq -r ".undelivered_artifacts[0].url")
|
|
341
|
-
DELIVERY_MESSAGE_ID=$(send_message "$ARTIFACT_URL")
|
|
342
|
-
npx makaron-cli responses deliver "$ARTIFACT_ID" --run "$RUN_ID" --channel feishu --message-id "$DELIVERY_MESSAGE_ID"
|
|
343
|
-
fi
|
|
344
|
-
|
|
345
|
-
RESULT=$(npx makaron-cli responses get "$RUN_ID" --wait --json)
|
|
346
|
-
```
|
|
347
|
-
|
|
348
265
|
## Recommended Pattern: Service Flow (Feishu/OpenClaw/Group Chat)
|
|
349
266
|
|
|
350
267
|
When serving end-users in a chat environment (Feishu, Slack, Discord), use this proactive message pattern:
|
package/bin/makaron.mjs
CHANGED
|
@@ -19,8 +19,6 @@ import { execFileSync } from 'child_process';
|
|
|
19
19
|
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
20
20
|
|
|
21
21
|
const AUTH_FILE = path.join(process.env.HOME || '~', '.makaron', 'auth.json');
|
|
22
|
-
const APPROVALS_FILE = path.join(path.dirname(AUTH_FILE), 'approvals.json');
|
|
23
|
-
const DELIVERIES_FILE = path.join(path.dirname(AUTH_FILE), 'deliveries.json');
|
|
24
22
|
const DEFAULT_URL = 'https://www.makaron.app';
|
|
25
23
|
const BASE_URL = process.env.MAKARON_URL || DEFAULT_URL;
|
|
26
24
|
const APP_URL = process.env.MAKARON_APP_URL || DEFAULT_URL;
|
|
@@ -29,9 +27,12 @@ const APP_URL = process.env.MAKARON_APP_URL || DEFAULT_URL;
|
|
|
29
27
|
const SUPABASE_URL = 'https://sdyrtztrjgmmpnirswxt.supabase.co';
|
|
30
28
|
const SUPABASE_ANON_KEY = 'sb_publishable_FJFN2YYaWaQjABUKLqxQcA_fhxPLFDY';
|
|
31
29
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
30
|
+
const MAX_VIDEO_UPLOAD_FILE_SIZE_MB = 50;
|
|
31
|
+
const MAX_VIDEO_UPLOAD_FILE_SIZE = MAX_VIDEO_UPLOAD_FILE_SIZE_MB * 1024 * 1024;
|
|
32
|
+
const MAX_VIDEO_UPLOAD_DURATION = 120;
|
|
33
|
+
const MAX_VIDEO_UPLOAD_DURATION_TOLERANCE = 1;
|
|
34
|
+
const MAX_VIDEO_PROVIDER_REFERENCE_DURATION = 15;
|
|
35
|
+
const MAX_VIDEO_PROVIDER_REFERENCE_DURATION_TOLERANCE = 0.5;
|
|
35
36
|
const MAX_VIDEO_FRAME_PIXELS = 2_086_876;
|
|
36
37
|
|
|
37
38
|
function getCliVersion() {
|
|
@@ -64,34 +65,6 @@ function saveAuth(data) {
|
|
|
64
65
|
fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2));
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
function loadApprovals() {
|
|
68
|
-
try {
|
|
69
|
-
return JSON.parse(fs.readFileSync(APPROVALS_FILE, 'utf-8'));
|
|
70
|
-
} catch {
|
|
71
|
-
return [];
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function saveApprovals(approvals) {
|
|
76
|
-
const dir = path.dirname(APPROVALS_FILE);
|
|
77
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
78
|
-
fs.writeFileSync(APPROVALS_FILE, JSON.stringify(approvals, null, 2));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function loadDeliveries() {
|
|
82
|
-
try {
|
|
83
|
-
return JSON.parse(fs.readFileSync(DELIVERIES_FILE, 'utf-8'));
|
|
84
|
-
} catch {
|
|
85
|
-
return [];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function saveDeliveries(deliveries) {
|
|
90
|
-
const dir = path.dirname(DELIVERIES_FILE);
|
|
91
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
92
|
-
fs.writeFileSync(DELIVERIES_FILE, JSON.stringify(deliveries, null, 2));
|
|
93
|
-
}
|
|
94
|
-
|
|
95
68
|
function buildCookie(tokenJson) {
|
|
96
69
|
const url = tokenJson._supabaseUrl || SUPABASE_URL;
|
|
97
70
|
const ref = url.match(/\/\/([^.]+)\./)?.[1] || '';
|
|
@@ -456,434 +429,6 @@ function applyPick(data, field) {
|
|
|
456
429
|
}
|
|
457
430
|
}
|
|
458
431
|
|
|
459
|
-
// ─── Dialogue Events (message / approval / artifact) ─────────────────────────
|
|
460
|
-
|
|
461
|
-
function stableEventId(prefix, runId, seq, fallback) {
|
|
462
|
-
return fallback || `${prefix}_${runId}_${seq ?? Date.now()}`;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function inferApprovalIntent(text) {
|
|
466
|
-
if (!text) return false;
|
|
467
|
-
const normalized = text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
468
|
-
const hasEnglishApprovalIntent = /\b(shall i|should i|do you want me to|confirm|approve|approval|permission)\b/.test(normalized);
|
|
469
|
-
const hasEnglishAction = /\b(go ahead|proceed|continue|generate|create|submit|start|run|render)\b/.test(normalized);
|
|
470
|
-
const hasChineseApprovalIntent = /(是否|要不要|是否要|需要我|请确认|确认|同意|批准|可以吗|是否可以)/.test(normalized);
|
|
471
|
-
const hasChineseAction = /(继续|开始|生成|创建|提交|执行|渲染|出图|出视频|制作)/.test(normalized);
|
|
472
|
-
return (hasEnglishApprovalIntent && hasEnglishAction) || (hasChineseApprovalIntent && hasChineseAction);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function isPureStatusMessage(text, sourceType, status) {
|
|
476
|
-
const normalized = String(text || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
477
|
-
if (sourceType === 'status') return true;
|
|
478
|
-
if (status && !normalized) return true;
|
|
479
|
-
if (!normalized) return false;
|
|
480
|
-
return /^(queued|running|rendering|uploading|processing|generating|completed|complete|done|failed|aborted|started|submitted|waiting|polling)(\.|…|\.\.\.)?$/.test(normalized)
|
|
481
|
-
|| /^(排队中|队列中|运行中|渲染中|上传中|处理中|生成中|已完成|完成|失败|已失败|已提交|等待中)$/.test(normalized);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function hasExplicitApprovalRequirement(data, text) {
|
|
485
|
-
return Boolean(
|
|
486
|
-
data.requires_approval
|
|
487
|
-
|| data.requiresApproval
|
|
488
|
-
|| data.action_required
|
|
489
|
-
|| data.actionRequired
|
|
490
|
-
|| data.proposal
|
|
491
|
-
|| inferApprovalIntent(text)
|
|
492
|
-
);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function isPureQaRun(data) {
|
|
496
|
-
const raw = [
|
|
497
|
-
data.intent,
|
|
498
|
-
data.mode,
|
|
499
|
-
data.workflow,
|
|
500
|
-
data.workflow_type,
|
|
501
|
-
data.workflowType,
|
|
502
|
-
data.run_type,
|
|
503
|
-
data.runType,
|
|
504
|
-
data.kind,
|
|
505
|
-
].filter(Boolean).join(' ').toLowerCase();
|
|
506
|
-
return /\b(qa|q&a|question_answer|question-answer|answer|pure_qa|pure-qa)\b/.test(raw);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
function normalizeDialogueMessage(runId, projectId, ev, context = {}) {
|
|
510
|
-
const data = ev.data || ev;
|
|
511
|
-
const text = data.text || data.message || data.content || data.statusText || '';
|
|
512
|
-
if (!text && ev.type !== 'tool_call') return null;
|
|
513
|
-
const explicitApproval = hasExplicitApprovalRequirement(data, text);
|
|
514
|
-
const textOnlyCheckpoint = Boolean(
|
|
515
|
-
context.stoppedWithoutArtifacts
|
|
516
|
-
&& ev.type !== 'error'
|
|
517
|
-
&& !explicitApproval
|
|
518
|
-
&& !isPureStatusMessage(text, ev.type, data.status)
|
|
519
|
-
);
|
|
520
|
-
const requiresApproval = explicitApproval || textOnlyCheckpoint;
|
|
521
|
-
const message = {
|
|
522
|
-
type: 'message',
|
|
523
|
-
id: stableEventId('msg', runId, ev.seq, data.id || ev.id),
|
|
524
|
-
runId,
|
|
525
|
-
projectId,
|
|
526
|
-
seq: ev.seq,
|
|
527
|
-
status: data.status || undefined,
|
|
528
|
-
text: text || `${data.tool || 'tool_call'}${data.input?.description ? `: ${data.input.description}` : ''}`,
|
|
529
|
-
};
|
|
530
|
-
if (ev.type) message.source_type = ev.type;
|
|
531
|
-
if (requiresApproval) {
|
|
532
|
-
message.requires_approval = true;
|
|
533
|
-
message.approval_options = data.approval_options || data.approvalOptions || ['approve', 'revise', 'ask_user', 'continue'];
|
|
534
|
-
if (textOnlyCheckpoint) message.approval_reason = 'text_only_checkpoint';
|
|
535
|
-
}
|
|
536
|
-
if (data.proposal) message.proposal = data.proposal;
|
|
537
|
-
return message;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
function normalizeDialogueArtifact(runId, projectId, item, seq) {
|
|
541
|
-
if (!item) return null;
|
|
542
|
-
const kind = item.type || item.kind || (item.imageUrl ? 'image' : item.videoUrl ? 'video' : undefined);
|
|
543
|
-
if (!kind) return null;
|
|
544
|
-
if (!['image', 'video', 'design', 'music', 'audio', 'file'].includes(kind)) return null;
|
|
545
|
-
const artifact = {
|
|
546
|
-
type: 'artifact',
|
|
547
|
-
id: stableEventId('artifact', runId, seq, item.id || item.snapshotId || item.taskId),
|
|
548
|
-
runId,
|
|
549
|
-
projectId,
|
|
550
|
-
seq,
|
|
551
|
-
kind,
|
|
552
|
-
status: item.status || (item.url || item.imageUrl || item.videoUrl || item.audioUrl ? 'completed' : 'running'),
|
|
553
|
-
};
|
|
554
|
-
const url = item.url || item.imageUrl || item.videoUrl || item.audioUrl;
|
|
555
|
-
if (url) artifact.url = url;
|
|
556
|
-
const fileName = item.fileName || item.filename || item.name || (url ? url.split('/').pop()?.split('?')[0] : null);
|
|
557
|
-
const contentType = item.contentType || item.content_type || item.mimeType || item.mime_type;
|
|
558
|
-
if (fileName) artifact.fileName = fileName;
|
|
559
|
-
if (contentType) artifact.contentType = contentType;
|
|
560
|
-
if (item.error) artifact.error = item.error;
|
|
561
|
-
if (item.taskId) artifact.taskId = item.taskId;
|
|
562
|
-
if (item.snapshotId) artifact.snapshotId = item.snapshotId;
|
|
563
|
-
return artifact;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function normalizeDialogueEvent(runId, projectId, ev, context = {}) {
|
|
567
|
-
const data = ev.data || {};
|
|
568
|
-
if (ev.type === 'message' || ev.type === 'approval' || ev.type === 'artifact') {
|
|
569
|
-
return { ...data, ...ev, runId: ev.runId || runId, projectId: ev.projectId || projectId };
|
|
570
|
-
}
|
|
571
|
-
switch (ev.type) {
|
|
572
|
-
case 'content':
|
|
573
|
-
case 'status':
|
|
574
|
-
case 'tool_call':
|
|
575
|
-
case 'error':
|
|
576
|
-
return normalizeDialogueMessage(runId, projectId, ev, context);
|
|
577
|
-
case 'image':
|
|
578
|
-
return normalizeDialogueArtifact(runId, projectId, { type: 'image', status: data.imageUrl ? 'completed' : 'running', imageUrl: data.imageUrl, snapshotId: data.snapshotId }, ev.seq);
|
|
579
|
-
case 'render':
|
|
580
|
-
return normalizeDialogueArtifact(runId, projectId, { type: data.animation ? 'video' : 'design', status: data.published ? 'completed' : 'running', url: data.url, snapshotId: data.snapshotId }, ev.seq);
|
|
581
|
-
case 'animation_task':
|
|
582
|
-
case 'video_snapshot':
|
|
583
|
-
return normalizeDialogueArtifact(runId, projectId, { type: 'video', status: 'running', taskId: data.taskId, snapshotId: data.snapshotId }, ev.seq);
|
|
584
|
-
case 'music_task':
|
|
585
|
-
return normalizeDialogueArtifact(runId, projectId, { type: 'music', status: 'running', taskId: data.taskId }, ev.seq);
|
|
586
|
-
default:
|
|
587
|
-
return null;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function buildDialogueEvents(runId, data) {
|
|
592
|
-
const projectId = data.projectId || data.project_id;
|
|
593
|
-
const events = [];
|
|
594
|
-
const hasArtifacts = Boolean(
|
|
595
|
-
(data.output || []).some(item => normalizeDialogueArtifact(runId, projectId, item, item.seq))
|
|
596
|
-
|| (data.events || []).some(ev => normalizeDialogueEvent(runId, projectId, ev, { stoppedWithoutArtifacts: false })?.type === 'artifact')
|
|
597
|
-
);
|
|
598
|
-
const hasContinuationAction = (data.events || []).some(ev => ['tool_call', 'image', 'render', 'animation_task', 'video_snapshot', 'music_task'].includes(ev.type));
|
|
599
|
-
const checkpointMode = data.checkpointMode || 'service';
|
|
600
|
-
const context = {
|
|
601
|
-
stoppedWithoutArtifacts: checkpointMode !== 'off'
|
|
602
|
-
&& !isPureQaRun(data)
|
|
603
|
-
&& !hasArtifacts
|
|
604
|
-
&& !hasContinuationAction
|
|
605
|
-
&& !data.incomplete
|
|
606
|
-
&& ['completed', 'failed', 'aborted', 'waiting', 'needs_input'].includes(data.status),
|
|
607
|
-
};
|
|
608
|
-
for (const ev of data.events || []) {
|
|
609
|
-
const normalized = normalizeDialogueEvent(runId, projectId, ev, context);
|
|
610
|
-
if (normalized) events.push(normalized);
|
|
611
|
-
}
|
|
612
|
-
for (const item of data.output || []) {
|
|
613
|
-
const normalized = normalizeDialogueArtifact(runId, projectId, item, item.seq);
|
|
614
|
-
if (normalized && !events.some(ev => ev.type === 'artifact' && ev.id === normalized.id)) events.push(normalized);
|
|
615
|
-
}
|
|
616
|
-
for (const approval of loadApprovals().filter(item => item.runId === runId)) {
|
|
617
|
-
events.push(approval);
|
|
618
|
-
}
|
|
619
|
-
return events;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function compactDialogueEvents(events) {
|
|
623
|
-
const compacted = [];
|
|
624
|
-
for (const ev of events) {
|
|
625
|
-
const prev = compacted[compacted.length - 1];
|
|
626
|
-
const canMerge = prev
|
|
627
|
-
&& ev.type === 'message'
|
|
628
|
-
&& prev.type === 'message'
|
|
629
|
-
&& ev.source_type === 'content'
|
|
630
|
-
&& prev.source_type === 'content'
|
|
631
|
-
&& !ev.proposal
|
|
632
|
-
&& !prev.proposal
|
|
633
|
-
&& Boolean(ev.requires_approval) === Boolean(prev.requires_approval)
|
|
634
|
-
&& (ev.approval_reason || '') === (prev.approval_reason || '');
|
|
635
|
-
if (canMerge) {
|
|
636
|
-
prev.text = `${prev.text}${ev.text}`;
|
|
637
|
-
prev.id = `${prev.id}+${ev.id}`;
|
|
638
|
-
prev.seq_end = ev.seq;
|
|
639
|
-
if (ev.requires_approval) {
|
|
640
|
-
prev.requires_approval = true;
|
|
641
|
-
prev.approval_options = prev.approval_options || ev.approval_options;
|
|
642
|
-
}
|
|
643
|
-
} else {
|
|
644
|
-
compacted.push({ ...ev });
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
return compacted;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function findUnhandledApprovalMessages(events) {
|
|
651
|
-
const approved = new Set(events.filter(ev => ev.type === 'approval').map(ev => ev.messageId || ev.message_id));
|
|
652
|
-
return events.filter(ev => ev.type === 'message' && ev.requires_approval && !approved.has(ev.id));
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
function isCompletedDeliverableArtifact(artifact) {
|
|
656
|
-
return artifact?.type === 'artifact'
|
|
657
|
-
&& ['image', 'video', 'design', 'music', 'audio', 'file'].includes(artifact.kind)
|
|
658
|
-
&& artifact.status === 'completed'
|
|
659
|
-
&& Boolean(artifact.url);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function findUndeliveredArtifacts(runId, events) {
|
|
663
|
-
const delivered = new Set(loadDeliveries().filter(item => item.runId === runId).map(item => item.artifactId || item.artifact_id));
|
|
664
|
-
return events.filter(isCompletedDeliverableArtifact).filter(artifact => !delivered.has(artifact.id));
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function buildCliCommand() {
|
|
668
|
-
return 'npx makaron-cli@latest';
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function buildHandleCommand(messageId, runId, choice) {
|
|
672
|
-
return `${buildCliCommand()} responses handle ${messageId} --run ${runId} --choice ${choice}`;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
function buildNextCommand(runId) {
|
|
676
|
-
return `${buildCliCommand()} responses next ${runId} --json`;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function buildDeliverCommand(artifactId, runId) {
|
|
680
|
-
return `${buildCliCommand()} responses deliver ${artifactId} --run ${runId}`;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function buildRunError(data, events) {
|
|
684
|
-
const errorEvent = events.find(ev => ev.type === 'message' && ev.source_type === 'error') || null;
|
|
685
|
-
const resultError = data.result?.error || data.error || data.message || null;
|
|
686
|
-
if (!errorEvent && !resultError && !['failed', 'aborted'].includes(data.status)) return null;
|
|
687
|
-
const errorType = (typeof resultError === 'object' && (resultError.type || resultError.code)) || data.error_type || data.errorType || data.status || 'failed';
|
|
688
|
-
return {
|
|
689
|
-
type: errorType,
|
|
690
|
-
status: data.status || 'failed',
|
|
691
|
-
message: errorEvent?.text || (typeof resultError === 'string' ? resultError : resultError?.message) || `Makaron run ${data.status || 'failed'}`,
|
|
692
|
-
recoverable: Boolean((typeof resultError === 'object' && resultError.recoverable) || data.recoverable),
|
|
693
|
-
detail: typeof resultError === 'object' ? resultError : undefined,
|
|
694
|
-
};
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function buildNextAction(runId, events, data = {}) {
|
|
698
|
-
const runError = buildRunError(data, events);
|
|
699
|
-
if (runError) {
|
|
700
|
-
return {
|
|
701
|
-
status: 'failed',
|
|
702
|
-
blocking: true,
|
|
703
|
-
runId,
|
|
704
|
-
error: runError,
|
|
705
|
-
next_command: buildNextCommand(runId),
|
|
706
|
-
next_commands: {
|
|
707
|
-
inspect: buildNextCommand(runId),
|
|
708
|
-
},
|
|
709
|
-
events,
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
const checkpoint = findUnhandledApprovalMessages(events)[0] || null;
|
|
713
|
-
const artifacts = events.filter(ev => ev.type === 'artifact');
|
|
714
|
-
const approvals = events.filter(ev => ev.type === 'approval');
|
|
715
|
-
if (data.incomplete || ['queued', 'running', 'rendering', 'processing', 'generating', 'submitted'].includes(data.status)) {
|
|
716
|
-
return {
|
|
717
|
-
status: 'running',
|
|
718
|
-
blocking: false,
|
|
719
|
-
runId,
|
|
720
|
-
artifacts,
|
|
721
|
-
approvals,
|
|
722
|
-
next_command: buildNextCommand(runId),
|
|
723
|
-
next_commands: {
|
|
724
|
-
inspect: buildNextCommand(runId),
|
|
725
|
-
},
|
|
726
|
-
events,
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
if (checkpoint) {
|
|
730
|
-
return {
|
|
731
|
-
status: 'needs_approval',
|
|
732
|
-
blocking: true,
|
|
733
|
-
runId,
|
|
734
|
-
checkpoint,
|
|
735
|
-
next_commands: {
|
|
736
|
-
approve: buildHandleCommand(checkpoint.id, runId, 'approve'),
|
|
737
|
-
revise: `${buildHandleCommand(checkpoint.id, runId, 'revise')} --note "what to change"`,
|
|
738
|
-
ask_user: buildHandleCommand(checkpoint.id, runId, 'ask_user'),
|
|
739
|
-
continue: buildHandleCommand(checkpoint.id, runId, 'continue'),
|
|
740
|
-
inspect: buildNextCommand(runId),
|
|
741
|
-
},
|
|
742
|
-
events,
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
const undeliveredArtifacts = findUndeliveredArtifacts(runId, events);
|
|
746
|
-
if (undeliveredArtifacts.length) {
|
|
747
|
-
const first = undeliveredArtifacts[0];
|
|
748
|
-
return {
|
|
749
|
-
status: 'has_artifacts',
|
|
750
|
-
blocking: true,
|
|
751
|
-
runId,
|
|
752
|
-
artifacts,
|
|
753
|
-
undelivered_artifacts: undeliveredArtifacts,
|
|
754
|
-
approvals,
|
|
755
|
-
next_commands: {
|
|
756
|
-
deliver: buildDeliverCommand(first.id, runId),
|
|
757
|
-
inspect: buildNextCommand(runId),
|
|
758
|
-
},
|
|
759
|
-
events,
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
return {
|
|
763
|
-
status: artifacts.length ? 'delivered' : 'ready',
|
|
764
|
-
blocking: false,
|
|
765
|
-
runId,
|
|
766
|
-
artifacts,
|
|
767
|
-
approvals,
|
|
768
|
-
deliveries: loadDeliveries().filter(item => item.runId === runId),
|
|
769
|
-
next_commands: {
|
|
770
|
-
inspect: buildNextCommand(runId),
|
|
771
|
-
},
|
|
772
|
-
events,
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
async function fetchRun(baseUrl, headers, runId, opts = {}) {
|
|
777
|
-
const params = new URLSearchParams();
|
|
778
|
-
if (opts.events) params.set('events', 'true');
|
|
779
|
-
const suffix = params.toString() ? `?${params}` : '';
|
|
780
|
-
const res = await fetch(`${baseUrl}/api/agent/run/${runId}${suffix}`, { headers });
|
|
781
|
-
if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
|
|
782
|
-
return normalizeRunResponse(await res.json());
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
async function printDialogueEvents(baseUrl, headers, runId, opts = {}) {
|
|
786
|
-
const { jsonl = false, failOnUnapproved = false, follow = false, interval = 5000, compact = false, checkpointMode = 'service' } = opts;
|
|
787
|
-
const printed = new Set();
|
|
788
|
-
|
|
789
|
-
while (true) {
|
|
790
|
-
const data = await fetchRun(baseUrl, headers, runId, { events: true });
|
|
791
|
-
const dialogueEvents = buildDialogueEvents(runId, { ...data, checkpointMode });
|
|
792
|
-
const events = compact ? compactDialogueEvents(dialogueEvents) : dialogueEvents;
|
|
793
|
-
const unhandled = findUnhandledApprovalMessages(events);
|
|
794
|
-
if (failOnUnapproved && unhandled.length) {
|
|
795
|
-
process.stderr.write(`Unhandled Makaron message requires approval: ${unhandled.map(ev => ev.id).join(', ')}\n`);
|
|
796
|
-
process.exit(3);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
const nextEvents = follow ? events.filter(ev => !printed.has(`${ev.type}:${ev.id || ev.seq}`)) : events;
|
|
800
|
-
for (const ev of nextEvents) {
|
|
801
|
-
printed.add(`${ev.type}:${ev.id || ev.seq}`);
|
|
802
|
-
if (jsonl) console.log(JSON.stringify(ev));
|
|
803
|
-
}
|
|
804
|
-
if (!jsonl) console.log(JSON.stringify(nextEvents, null, 2));
|
|
805
|
-
|
|
806
|
-
if (!follow || (!data.incomplete && ['completed', 'failed', 'aborted'].includes(data.status))) {
|
|
807
|
-
if (data.status === 'failed' || data.status === 'aborted') process.exit(1);
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
await new Promise(r => setTimeout(r, data.next_poll_after_ms || interval));
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
async function printAgentNext(baseUrl, headers, runId, opts = {}) {
|
|
815
|
-
const { json = false, checkpointMode = 'service', failOnCheckpoint = true } = opts;
|
|
816
|
-
const data = await fetchRun(baseUrl, headers, runId, { events: true });
|
|
817
|
-
const events = compactDialogueEvents(buildDialogueEvents(runId, { ...data, checkpointMode }));
|
|
818
|
-
const action = buildNextAction(runId, events, data);
|
|
819
|
-
if (json) {
|
|
820
|
-
console.log(JSON.stringify(action, null, 2));
|
|
821
|
-
} else if (action.status === 'failed') {
|
|
822
|
-
console.log(`failed: ${action.error?.message || 'Makaron run failed'}`);
|
|
823
|
-
console.log(`inspect: ${action.next_commands.inspect}`);
|
|
824
|
-
} else if (action.status === 'needs_approval') {
|
|
825
|
-
console.log(`needs_approval: ${action.checkpoint.id}`);
|
|
826
|
-
if (action.checkpoint.text) console.log(action.checkpoint.text);
|
|
827
|
-
console.log(`approve: ${action.next_commands.approve}`);
|
|
828
|
-
console.log(`revise: ${action.next_commands.revise}`);
|
|
829
|
-
console.log(`ask_user: ${action.next_commands.ask_user}`);
|
|
830
|
-
console.log(`continue: ${action.next_commands.continue}`);
|
|
831
|
-
console.log(`inspect: ${action.next_commands.inspect}`);
|
|
832
|
-
} else if (action.status === 'has_artifacts') {
|
|
833
|
-
for (const artifact of action.undelivered_artifacts || action.artifacts) {
|
|
834
|
-
console.log(`${artifact.kind} ${artifact.status}${artifact.url ? ` ${artifact.url}` : ''}`);
|
|
835
|
-
}
|
|
836
|
-
if (action.next_commands.deliver) console.log(`deliver: ${action.next_commands.deliver}`);
|
|
837
|
-
console.log(`inspect: ${action.next_commands.inspect}`);
|
|
838
|
-
} else if (action.status === 'running') {
|
|
839
|
-
console.log('running');
|
|
840
|
-
console.log(`inspect: ${action.next_commands.inspect}`);
|
|
841
|
-
} else if (action.status === 'delivered') {
|
|
842
|
-
console.log('delivered');
|
|
843
|
-
console.log(`inspect: ${action.next_commands.inspect}`);
|
|
844
|
-
} else {
|
|
845
|
-
console.log('ready');
|
|
846
|
-
console.log(`inspect: ${action.next_commands.inspect}`);
|
|
847
|
-
}
|
|
848
|
-
if (action.status === 'needs_approval' && failOnCheckpoint) process.exit(3);
|
|
849
|
-
if (action.status === 'failed') process.exit(1);
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
function recordApproval(runId, messageId, choice, note) {
|
|
853
|
-
const approvals = loadApprovals();
|
|
854
|
-
const approval = {
|
|
855
|
-
type: 'approval',
|
|
856
|
-
id: `approval_${Date.now()}`,
|
|
857
|
-
runId,
|
|
858
|
-
messageId,
|
|
859
|
-
choice,
|
|
860
|
-
status: 'recorded',
|
|
861
|
-
createdAt: new Date().toISOString(),
|
|
862
|
-
};
|
|
863
|
-
if (note) approval.note = note;
|
|
864
|
-
approvals.push(approval);
|
|
865
|
-
saveApprovals(approvals);
|
|
866
|
-
console.log(JSON.stringify(approval));
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
function recordDelivery(runId, artifactId, opts = {}) {
|
|
870
|
-
const deliveries = loadDeliveries();
|
|
871
|
-
const delivery = {
|
|
872
|
-
type: 'delivery',
|
|
873
|
-
id: `delivery_${Date.now()}`,
|
|
874
|
-
runId,
|
|
875
|
-
artifactId,
|
|
876
|
-
status: 'recorded',
|
|
877
|
-
createdAt: new Date().toISOString(),
|
|
878
|
-
};
|
|
879
|
-
if (opts.channel) delivery.channel = opts.channel;
|
|
880
|
-
if (opts.messageId) delivery.messageId = opts.messageId;
|
|
881
|
-
if (opts.note) delivery.note = opts.note;
|
|
882
|
-
deliveries.push(delivery);
|
|
883
|
-
saveDeliveries(deliveries);
|
|
884
|
-
console.log(JSON.stringify(delivery));
|
|
885
|
-
}
|
|
886
|
-
|
|
887
432
|
// ─── Watch (incremental event stream) ───────────────────────────────────────
|
|
888
433
|
|
|
889
434
|
async function watchRun(baseUrl, headers, runId, opts = {}) {
|
|
@@ -1041,6 +586,34 @@ async function listProjects(baseUrl, headers) {
|
|
|
1041
586
|
console.log('');
|
|
1042
587
|
}
|
|
1043
588
|
|
|
589
|
+
async function listProjectMedia(baseUrl, headers, projectId, opts = {}) {
|
|
590
|
+
const res = await fetch(`${baseUrl}/api/projects/${projectId}/media`, { headers });
|
|
591
|
+
if (!res.ok) { console.error('Project media failed:', await res.text()); process.exit(1); }
|
|
592
|
+
const data = await res.json();
|
|
593
|
+
if (opts.json) {
|
|
594
|
+
console.log(JSON.stringify(data, null, 2));
|
|
595
|
+
return data;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
console.log(`🎞️ ${data.title || 'Untitled'}`);
|
|
599
|
+
console.log(` Project: ${data.projectUrl || `${APP_URL}/projects/${projectId}`}`);
|
|
600
|
+
const media = data.media || [];
|
|
601
|
+
if (!media.length) {
|
|
602
|
+
console.log(' No timeline media yet.');
|
|
603
|
+
return data;
|
|
604
|
+
}
|
|
605
|
+
for (const item of media) {
|
|
606
|
+
const ref = item.ref || `<<<media_${item.index}>>>`;
|
|
607
|
+
const status = item.status && item.status !== 'completed' ? ` ${item.status}` : '';
|
|
608
|
+
const duration = typeof item.duration === 'number' ? ` ${item.duration}s` : '';
|
|
609
|
+
const dimensions = item.width && item.height ? ` ${item.width}x${item.height}` : '';
|
|
610
|
+
const description = item.description ? ` — ${item.description}` : '';
|
|
611
|
+
const url = item.url ? `\n ${item.url}` : '';
|
|
612
|
+
console.log(` ${String(item.index).padStart(2)}. ${ref} [${item.type}${status}${duration}${dimensions}]${description}${url}`);
|
|
613
|
+
}
|
|
614
|
+
return data;
|
|
615
|
+
}
|
|
616
|
+
|
|
1044
617
|
function timeSince(date) {
|
|
1045
618
|
const s = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
1046
619
|
if (s < 60) return 'just now';
|
|
@@ -1192,13 +765,15 @@ function probeLocalVideo(videoPath) {
|
|
|
1192
765
|
return probeVideoWithFfprobe(videoPath) || probeVideoWithFfmpeg(videoPath);
|
|
1193
766
|
}
|
|
1194
767
|
|
|
1195
|
-
function validateVideoFile(videoPath) {
|
|
768
|
+
function validateVideoFile(videoPath, options = {}) {
|
|
769
|
+
const maxDuration = options.maxDuration ?? MAX_VIDEO_UPLOAD_DURATION;
|
|
770
|
+
const durationTolerance = options.durationTolerance ?? MAX_VIDEO_UPLOAD_DURATION_TOLERANCE;
|
|
1196
771
|
if (!fs.existsSync(videoPath)) {
|
|
1197
772
|
return { ok: false, error: `Video file not found: ${videoPath}` };
|
|
1198
773
|
}
|
|
1199
774
|
const stat = fs.statSync(videoPath);
|
|
1200
|
-
if (stat.size >
|
|
1201
|
-
return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max
|
|
775
|
+
if (stat.size > MAX_VIDEO_UPLOAD_FILE_SIZE) {
|
|
776
|
+
return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max ${MAX_VIDEO_UPLOAD_FILE_SIZE_MB}MB). The CLI uploads directly to Storage; use the frontend to transcode larger videos first.` };
|
|
1202
777
|
}
|
|
1203
778
|
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
1204
779
|
if (!['mp4', 'mov', 'webm'].includes(ext)) {
|
|
@@ -1208,8 +783,8 @@ function validateVideoFile(videoPath) {
|
|
|
1208
783
|
if (!meta) {
|
|
1209
784
|
return { ok: false, error: 'Cannot read video duration/resolution. Install ffmpeg/ffprobe or use the normal frontend upload flow.' };
|
|
1210
785
|
}
|
|
1211
|
-
if (meta.duration >
|
|
1212
|
-
return { ok: false, error: `Video too long: ${formatSeconds(meta.duration)}s (max ${
|
|
786
|
+
if (meta.duration > maxDuration + durationTolerance) {
|
|
787
|
+
return { ok: false, error: `Video too long: ${formatSeconds(meta.duration)}s (max ${maxDuration}s, with ${durationTolerance}s metadata tolerance)` };
|
|
1213
788
|
}
|
|
1214
789
|
if (meta.width * meta.height > MAX_VIDEO_FRAME_PIXELS) {
|
|
1215
790
|
return { ok: false, error: `Video resolution too high: ${meta.width}x${meta.height} (${meta.width * meta.height} px). Max is <=1080p (${MAX_VIDEO_FRAME_PIXELS} px). Re-upload through the frontend to transcode, or export a smaller video.` };
|
|
@@ -1226,8 +801,8 @@ function validateVideoFileForAnalysis(videoPath) {
|
|
|
1226
801
|
if (stat.size === 0) {
|
|
1227
802
|
return { ok: false, error: `Video file is empty: ${videoPath}` };
|
|
1228
803
|
}
|
|
1229
|
-
if (stat.size >
|
|
1230
|
-
return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max
|
|
804
|
+
if (stat.size > MAX_VIDEO_UPLOAD_FILE_SIZE) {
|
|
805
|
+
return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max ${MAX_VIDEO_UPLOAD_FILE_SIZE_MB}MB). The CLI uploads directly to Storage; use the frontend to transcode larger videos first.` };
|
|
1231
806
|
}
|
|
1232
807
|
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
1233
808
|
if (!['mp4', 'mov', 'webm'].includes(ext)) {
|
|
@@ -1252,48 +827,6 @@ function saveMcpImage(result, outputPath) {
|
|
|
1252
827
|
return null;
|
|
1253
828
|
}
|
|
1254
829
|
|
|
1255
|
-
function wantsHelp(values) {
|
|
1256
|
-
return values.includes('--help') || values.includes('-h');
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
function printResponsesUsage() {
|
|
1260
|
-
console.log(`Responses commands:
|
|
1261
|
-
responses get <runId> Get status and output (JSON)
|
|
1262
|
-
responses get <runId> --wait Poll until completed
|
|
1263
|
-
responses get <runId> --pick <field> Extract: first_image_url, first_video_url, project_url, output
|
|
1264
|
-
responses next <runId> --json New Agent entry: compact timeline + checkpoint guidance
|
|
1265
|
-
responses handle <messageId> --run <runId> --choice approve|revise|ask_user|continue
|
|
1266
|
-
responses deliver <artifactId> --run <runId> Record artifact delivered to the user
|
|
1267
|
-
responses events <runId> --jsonl Emit message/approval/artifact events for external Agents
|
|
1268
|
-
responses timeline <runId> --jsonl Alias for responses events
|
|
1269
|
-
responses timeline <runId> --checkpoint-mode off Disable text-only checkpoints for pure Q&A
|
|
1270
|
-
responses approve <messageId> --run <runId> Record approval for a Makaron message
|
|
1271
|
-
responses revise <messageId> --run <runId> Record revision request for a Makaron message
|
|
1272
|
-
responses ask-user <messageId> --run <runId> Record that the user must decide
|
|
1273
|
-
responses continue <messageId> --run <runId> Record continue decision
|
|
1274
|
-
responses watch <runId> --jsonl Watch until done (incremental events)
|
|
1275
|
-
responses list --project <id> List runs for a project
|
|
1276
|
-
`);
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
function printResponsesSubcommandUsage(sub) {
|
|
1280
|
-
const usages = {
|
|
1281
|
-
get: 'Usage: makaron responses get <runId> [--wait] [--json] [--pick <field>]',
|
|
1282
|
-
watch: 'Usage: makaron responses watch <runId> [--jsonl] [--interval <ms>]',
|
|
1283
|
-
next: 'Usage: makaron responses next <runId> [--json] [--checkpoint-mode service|off] [--no-fail]',
|
|
1284
|
-
events: 'Usage: makaron responses events <runId> [--jsonl] [--compact] [--checkpoint-mode service|off] [--follow] [--interval <ms>] [--fail-on-unapproved]',
|
|
1285
|
-
timeline: 'Usage: makaron responses timeline <runId> [--jsonl] [--compact] [--checkpoint-mode service|off] [--follow] [--interval <ms>] [--fail-on-unapproved]',
|
|
1286
|
-
deliver: 'Usage: makaron responses deliver <artifactId> --run <runId> [--channel <name>] [--message-id <id>] [--note <text>]',
|
|
1287
|
-
handle: 'Usage: makaron responses handle <messageId> --run <runId> --choice approve|revise|ask_user|continue [--note <text>]',
|
|
1288
|
-
approve: 'Usage: makaron responses approve <messageId> --run <runId> [--note <text>]',
|
|
1289
|
-
revise: 'Usage: makaron responses revise <messageId> --run <runId> [--note <text>]',
|
|
1290
|
-
'ask-user': 'Usage: makaron responses ask-user <messageId> --run <runId> [--note <text>]',
|
|
1291
|
-
continue: 'Usage: makaron responses continue <messageId> --run <runId> [--note <text>]',
|
|
1292
|
-
list: 'Usage: makaron responses list --project <id>',
|
|
1293
|
-
};
|
|
1294
|
-
console.log(usages[sub] || 'Usage: makaron responses --help');
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
830
|
async function analyzeVideoCli(baseUrl, headers, rawVideo, questionParts) {
|
|
1298
831
|
if (!rawVideo) {
|
|
1299
832
|
console.error('Usage: makaron analyze --video <file|url> ["question"]');
|
|
@@ -1455,7 +988,7 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1455
988
|
const uploadedVideoUrls = [...prevalidatedVideoUrlList];
|
|
1456
989
|
const uploadedVideoMetas = prevalidatedVideoUrlList.map(() => null);
|
|
1457
990
|
if (prevalidatedVideoUrlList.length) {
|
|
1458
|
-
process.stderr.write(`📹 Assuming public video URL(s) already match Makaron upload limits: ≤${
|
|
991
|
+
process.stderr.write(`📹 Assuming public video URL(s) already match Makaron upload limits: ≤${MAX_VIDEO_UPLOAD_DURATION}s, ≤${MAX_VIDEO_UPLOAD_FILE_SIZE_MB}MB, ≤1080p.\n`);
|
|
1459
992
|
}
|
|
1460
993
|
for (const videoPath of prevalidatedVideoFileList) {
|
|
1461
994
|
process.stderr.write(`📹 Uploading ${path.basename(videoPath)} (${(fs.statSync(videoPath).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
@@ -1471,7 +1004,7 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1471
1004
|
|
|
1472
1005
|
// Add videos to project via projects/create (same as images)
|
|
1473
1006
|
if (uploadedVideoUrls.length === 0) {
|
|
1474
|
-
process.stderr.write(`❌ No valid videos were uploaded. Local videos must be MP4/MOV/WebM, ≤${
|
|
1007
|
+
process.stderr.write(`❌ No valid videos were uploaded. Local videos must be MP4/MOV/WebM, ≤${MAX_VIDEO_UPLOAD_DURATION}s, ≤${MAX_VIDEO_UPLOAD_FILE_SIZE_MB}MB, and ≤1080p.\n`);
|
|
1475
1008
|
process.exit(1);
|
|
1476
1009
|
}
|
|
1477
1010
|
|
|
@@ -1521,16 +1054,8 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1521
1054
|
}
|
|
1522
1055
|
}
|
|
1523
1056
|
} else if (command === 'responses' || command === 'run') {
|
|
1524
|
-
const sub = args[1];
|
|
1525
|
-
if (!sub || sub === '--help' || sub === '-h') {
|
|
1526
|
-
printResponsesUsage();
|
|
1527
|
-
process.exit(0);
|
|
1528
|
-
}
|
|
1529
|
-
if (wantsHelp(args.slice(2))) {
|
|
1530
|
-
printResponsesSubcommandUsage(sub);
|
|
1531
|
-
process.exit(0);
|
|
1532
|
-
}
|
|
1533
1057
|
const { headers, baseUrl } = getAuth();
|
|
1058
|
+
const sub = args[1];
|
|
1534
1059
|
|
|
1535
1060
|
if (sub === 'get') {
|
|
1536
1061
|
const runId = args[2];
|
|
@@ -1568,85 +1093,6 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1568
1093
|
}
|
|
1569
1094
|
await watchRun(baseUrl, headers, runId, { interval, jsonl });
|
|
1570
1095
|
|
|
1571
|
-
} else if (sub === 'next') {
|
|
1572
|
-
const runId = args[2];
|
|
1573
|
-
if (!runId) { console.error('Usage: makaron responses next <runId> [--json] [--checkpoint-mode service|off] [--no-fail]'); process.exit(1); }
|
|
1574
|
-
let jsonOutput = false, checkpointMode = 'service', failOnCheckpoint = true;
|
|
1575
|
-
for (let i = 3; i < args.length; i++) {
|
|
1576
|
-
if (args[i] === '--json') jsonOutput = true;
|
|
1577
|
-
else if (args[i] === '--checkpoint-mode' && args[i + 1]) checkpointMode = args[++i];
|
|
1578
|
-
else if (args[i] === '--no-fail') failOnCheckpoint = false;
|
|
1579
|
-
}
|
|
1580
|
-
if (!['service', 'off'].includes(checkpointMode)) {
|
|
1581
|
-
console.error('--checkpoint-mode must be service or off');
|
|
1582
|
-
process.exit(1);
|
|
1583
|
-
}
|
|
1584
|
-
await printAgentNext(baseUrl, headers, runId, { json: jsonOutput, checkpointMode, failOnCheckpoint });
|
|
1585
|
-
|
|
1586
|
-
} else if (sub === 'events' || sub === 'timeline') {
|
|
1587
|
-
const runId = args[2];
|
|
1588
|
-
if (!runId) { console.error(`Usage: makaron responses ${sub} <runId> [--jsonl] [--compact] [--checkpoint-mode service|off] [--follow] [--interval <ms>] [--fail-on-unapproved]`); process.exit(1); }
|
|
1589
|
-
let interval = 5000, jsonl = false, follow = false, failOnUnapproved = false, compact = false, checkpointMode = 'service';
|
|
1590
|
-
for (let i = 3; i < args.length; i++) {
|
|
1591
|
-
if (args[i] === '--jsonl') jsonl = true;
|
|
1592
|
-
else if (args[i] === '--compact') compact = true;
|
|
1593
|
-
else if (args[i] === '--checkpoint-mode' && args[i + 1]) checkpointMode = args[++i];
|
|
1594
|
-
else if (args[i] === '--follow') follow = true;
|
|
1595
|
-
else if (args[i] === '--fail-on-unapproved') failOnUnapproved = true;
|
|
1596
|
-
else if (args[i] === '--interval' && args[i + 1]) interval = parseInt(args[++i]);
|
|
1597
|
-
}
|
|
1598
|
-
if (!['service', 'off'].includes(checkpointMode)) {
|
|
1599
|
-
console.error('--checkpoint-mode must be service or off');
|
|
1600
|
-
process.exit(1);
|
|
1601
|
-
}
|
|
1602
|
-
await printDialogueEvents(baseUrl, headers, runId, { interval, jsonl, follow, failOnUnapproved, compact, checkpointMode });
|
|
1603
|
-
|
|
1604
|
-
} else if (sub === 'deliver') {
|
|
1605
|
-
const artifactId = args[2];
|
|
1606
|
-
if (!artifactId) { console.error('Usage: makaron responses deliver <artifactId> --run <runId> [--channel <name>] [--message-id <id>] [--note <text>]'); process.exit(1); }
|
|
1607
|
-
let runId = null, note = null, channel = null, messageId = null;
|
|
1608
|
-
const noteParts = [];
|
|
1609
|
-
for (let i = 3; i < args.length; i++) {
|
|
1610
|
-
if (args[i] === '--run' && args[i + 1]) runId = args[++i];
|
|
1611
|
-
else if (args[i] === '--note' && args[i + 1]) note = args[++i];
|
|
1612
|
-
else if (args[i] === '--channel' && args[i + 1]) channel = args[++i];
|
|
1613
|
-
else if (args[i] === '--message-id' && args[i + 1]) messageId = args[++i];
|
|
1614
|
-
else noteParts.push(args[i]);
|
|
1615
|
-
}
|
|
1616
|
-
if (!runId) { console.error('Usage: makaron responses deliver <artifactId> --run <runId> [--channel <name>] [--message-id <id>] [--note <text>]'); process.exit(1); }
|
|
1617
|
-
if (!note && noteParts.length) note = noteParts.join(' ');
|
|
1618
|
-
recordDelivery(runId, artifactId, { note, channel, messageId });
|
|
1619
|
-
|
|
1620
|
-
} else if (sub === 'handle' || ['approve', 'revise', 'ask-user', 'continue'].includes(sub)) {
|
|
1621
|
-
const messageId = args[2];
|
|
1622
|
-
if (!messageId) {
|
|
1623
|
-
console.error(sub === 'handle'
|
|
1624
|
-
? 'Usage: makaron responses handle <messageId> --run <runId> --choice approve|revise|ask_user|continue [--note <text>]'
|
|
1625
|
-
: `Usage: makaron responses ${sub} <messageId> --run <runId> [--note <text>]`);
|
|
1626
|
-
process.exit(1);
|
|
1627
|
-
}
|
|
1628
|
-
let runId = null, note = null, choice = sub === 'handle' ? null : sub;
|
|
1629
|
-
const noteParts = [];
|
|
1630
|
-
for (let i = 3; i < args.length; i++) {
|
|
1631
|
-
if (args[i] === '--run' && args[i + 1]) runId = args[++i];
|
|
1632
|
-
else if (args[i] === '--note' && args[i + 1]) note = args[++i];
|
|
1633
|
-
else if (args[i] === '--choice' && args[i + 1]) choice = args[++i];
|
|
1634
|
-
else noteParts.push(args[i]);
|
|
1635
|
-
}
|
|
1636
|
-
if (!runId || !choice) {
|
|
1637
|
-
console.error(sub === 'handle'
|
|
1638
|
-
? 'Usage: makaron responses handle <messageId> --run <runId> --choice approve|revise|ask_user|continue [--note <text>]'
|
|
1639
|
-
: `Usage: makaron responses ${sub} <messageId> --run <runId> [--note <text>]`);
|
|
1640
|
-
process.exit(1);
|
|
1641
|
-
}
|
|
1642
|
-
choice = choice === 'ask-user' ? 'ask_user' : choice;
|
|
1643
|
-
if (!['approve', 'revise', 'ask_user', 'continue'].includes(choice)) {
|
|
1644
|
-
console.error('--choice must be approve, revise, ask_user, or continue');
|
|
1645
|
-
process.exit(1);
|
|
1646
|
-
}
|
|
1647
|
-
if (!note && noteParts.length) note = noteParts.join(' ');
|
|
1648
|
-
recordApproval(runId, messageId, choice, note);
|
|
1649
|
-
|
|
1650
1096
|
} else if (sub === 'list') {
|
|
1651
1097
|
let projectId = null;
|
|
1652
1098
|
for (let i = 2; i < args.length; i++) {
|
|
@@ -1666,11 +1112,30 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1666
1112
|
}
|
|
1667
1113
|
|
|
1668
1114
|
} else {
|
|
1669
|
-
|
|
1115
|
+
console.log(`Responses commands:
|
|
1116
|
+
responses get <runId> Get status and output (JSON)
|
|
1117
|
+
responses get <runId> --wait Poll until completed
|
|
1118
|
+
responses get <runId> --pick <field> Extract: first_image_url, first_video_url, project_url, output
|
|
1119
|
+
responses watch <runId> --jsonl Watch until done (incremental events)
|
|
1120
|
+
responses list --project <id> List runs for a project
|
|
1121
|
+
`);
|
|
1670
1122
|
}
|
|
1671
1123
|
} else if (command === 'list' || command === 'ls') {
|
|
1672
1124
|
const { headers, baseUrl } = getAuth();
|
|
1673
1125
|
await listProjects(baseUrl, headers);
|
|
1126
|
+
} else if (command === 'project' || command === 'projects') {
|
|
1127
|
+
const { headers, baseUrl } = getAuth();
|
|
1128
|
+
const sub = args[1];
|
|
1129
|
+
if (sub === 'media') {
|
|
1130
|
+
const projectId = args[2];
|
|
1131
|
+
if (!projectId) { console.error('Usage: makaron project media <projectId> [--json]'); process.exit(1); }
|
|
1132
|
+
const jsonOutput = args.includes('--json');
|
|
1133
|
+
await listProjectMedia(baseUrl, headers, projectId, { json: jsonOutput });
|
|
1134
|
+
} else {
|
|
1135
|
+
console.log(`Project commands:
|
|
1136
|
+
project media <projectId> --json List timeline media for a project
|
|
1137
|
+
`);
|
|
1138
|
+
}
|
|
1674
1139
|
} else if (command === 'abort') {
|
|
1675
1140
|
const { headers, baseUrl } = getAuth();
|
|
1676
1141
|
const runId = args[1];
|
|
@@ -1772,10 +1237,13 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1772
1237
|
let videoUrl = isHttpUrl(video) ? video : null;
|
|
1773
1238
|
let inputVideoMeta = null;
|
|
1774
1239
|
if (videoUrl) {
|
|
1775
|
-
process.stderr.write(`📹 Assuming public video URL already matches
|
|
1240
|
+
process.stderr.write(`📹 Assuming public video URL already matches provider reference limits: ≤${MAX_VIDEO_PROVIDER_REFERENCE_DURATION}s, ≤${MAX_VIDEO_UPLOAD_FILE_SIZE_MB}MB, ≤1080p.\n`);
|
|
1776
1241
|
}
|
|
1777
1242
|
if (video && !videoUrl) {
|
|
1778
|
-
const valid = validateVideoFile(video
|
|
1243
|
+
const valid = validateVideoFile(video, {
|
|
1244
|
+
maxDuration: MAX_VIDEO_PROVIDER_REFERENCE_DURATION,
|
|
1245
|
+
durationTolerance: MAX_VIDEO_PROVIDER_REFERENCE_DURATION_TOLERANCE,
|
|
1246
|
+
});
|
|
1779
1247
|
if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
|
|
1780
1248
|
inputVideoMeta = valid.meta;
|
|
1781
1249
|
process.stderr.write(`📹 Uploading ${path.basename(video)} (${(fs.statSync(video).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
@@ -1788,7 +1256,7 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1788
1256
|
const vArgs = videoUrl
|
|
1789
1257
|
? { videoUrl, editPrompt: script, images, videoModel: videoModel || 'kling', referType: (videoModel || 'kling') === 'seedance' ? 'feature' : 'base' }
|
|
1790
1258
|
: { script, images };
|
|
1791
|
-
const effectiveDuration = duration || (inputVideoMeta?.duration ? Math.min(
|
|
1259
|
+
const effectiveDuration = duration || (inputVideoMeta?.duration ? Math.min(MAX_VIDEO_PROVIDER_REFERENCE_DURATION, Math.round(inputVideoMeta.duration)) : undefined);
|
|
1792
1260
|
if (effectiveDuration) vArgs.duration = effectiveDuration;
|
|
1793
1261
|
if (aspectRatio) vArgs.aspectRatio = aspectRatio;
|
|
1794
1262
|
if (videoModel && !videoUrl) vArgs.videoModel = videoModel;
|
|
@@ -2089,6 +1557,7 @@ Commands:
|
|
|
2089
1557
|
claim Get claim URL for human to link account
|
|
2090
1558
|
login Log in to Makaron (human interactive)
|
|
2091
1559
|
list (ls) List all projects
|
|
1560
|
+
project media <projectId> --json List timeline media for a project
|
|
2092
1561
|
create --image <file> Create project from local image
|
|
2093
1562
|
create --image-url <url> Create project from URL
|
|
2094
1563
|
create --title "name" Create empty project (text-to-image)
|
|
@@ -2101,11 +1570,6 @@ Commands:
|
|
|
2101
1570
|
|
|
2102
1571
|
responses get <runId> Get run status and results
|
|
2103
1572
|
responses get <runId> --wait Poll until completed
|
|
2104
|
-
responses next <runId> --json New Agent entry: compact timeline + checkpoint guidance
|
|
2105
|
-
responses handle <messageId> --run <runId> --choice approve|revise|ask_user|continue
|
|
2106
|
-
responses deliver <artifactId> --run <runId> Record artifact delivered to the user
|
|
2107
|
-
responses events <runId> --jsonl Emit message/approval/artifact events
|
|
2108
|
-
responses timeline <runId> --checkpoint-mode off Disable text-only checkpoints for pure Q&A
|
|
2109
1573
|
responses list --project <id> List runs for a project
|
|
2110
1574
|
abort <runId> Abort a running Agent
|
|
2111
1575
|
|
package/package.json
CHANGED
package/skills/makaron/SKILL.md
CHANGED
|
@@ -86,6 +86,16 @@ Returns immediately:
|
|
|
86
86
|
npx makaron-cli chat --project <id> --image ref1.jpg --image ref2.jpg -b "use these as style reference"
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
+
### Inspect existing timeline media
|
|
90
|
+
|
|
91
|
+
Before starting a follow-up run on an existing project, list the current timeline media so you know what assets are available and which `<<<media_N>>>` references to use:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
npx makaron-cli project media <projectId> --json
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
This is project-scoped. `responses get <runId> --pick output` only returns artifacts from one run; `project media` returns the whole project timeline: original uploads, references, generated images, video snapshots, and editable compositions.
|
|
98
|
+
|
|
89
99
|
### With video input (edit, compose, extend)
|
|
90
100
|
|
|
91
101
|
```bash
|
|
@@ -102,7 +112,7 @@ npx makaron-cli chat --project <id> --video clip1.mp4 --video clip2.mp4 -b "comb
|
|
|
102
112
|
npx makaron-cli chat --project auto --video https://example.com/dance.mp4 -b "extend this to 15 seconds"
|
|
103
113
|
```
|
|
104
114
|
|
|
105
|
-
Supported formats: MP4, MOV, WebM. CLI local video uploads
|
|
115
|
+
Supported formats: MP4, MOV, WebM. CLI local video uploads support max 50MB, max 120s with 1s metadata tolerance, and <=1080p / 2,086,876 frame pixels. The frontend can transcode larger videos before upload; the CLI uploads directly to Storage and rejects videos above those limits. Videos are uploaded to the project timeline. The Agent can analyze scenes, edit content, compose multiple clips, extend duration, and add effects — all via natural language. Seedance video-reference editing is still limited to ~15s provider references, so longer uploaded videos should be split/prepared by the agent before model submission; Kling remains the base/direct edit path.
|
|
106
116
|
|
|
107
117
|
Use `chat --project <id|auto> --video ...` for any project/timeline video work. Direct video commands are standalone raw-tool calls.
|
|
108
118
|
|
|
@@ -126,64 +136,6 @@ Outputs one JSON per line as artifacts appear:
|
|
|
126
136
|
{"event":"done","status":"completed"}
|
|
127
137
|
```
|
|
128
138
|
|
|
129
|
-
### New Agent quickstart
|
|
130
|
-
|
|
131
|
-
For a new Agent, keep the default service flow to two response commands:
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
npx makaron-cli responses next <runId> --json
|
|
135
|
-
npx makaron-cli responses handle <messageId> --run <runId> --choice approve
|
|
136
|
-
npx makaron-cli responses deliver <artifactId> --run <runId> --channel feishu --message-id <messageId>
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
`responses next` emits a compact timeline, detects text-only checkpoints for creative/service work, and returns `next_commands` when the Agent must handle a Makaron message before waiting for artifacts. It exits with code `3` on an unhandled checkpoint so wrappers stop instead of silently waiting. After handling a checkpoint, run the returned `next_commands.inspect` command to continue. While the run is still generating, it returns `status: "running"` and `blocking: false`. If completed image/video artifacts exist and have not been delivered, it returns `status: "has_artifacts"` and `blocking: true` with compact `undelivered_artifacts` entries containing `kind`, `status`, `url`, `fileName`, and `contentType` when available, plus `next_commands.deliver`; the Agent must send the artifact URL to the user, then record delivery with `responses deliver`. A non-checkpoint response returns `status: "ready"` or `status: "delivered"`. If a run failed, `responses next --json` prints `status: "failed"` and `blocking: true` plus `error.type`, `error.message`, `error.recoverable`, `error.detail`, and `next_command` to stdout before exiting `1`, so wrappers can parse the failure without scraping stderr. Use `--no-fail` to inspect checkpoint JSON without failing. For pure Q&A runs, use:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
npx makaron-cli responses next <runId> --json --checkpoint-mode off
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
`responses handle` is the single checkpoint action command. Valid choices are `approve`, `revise`, `ask_user`, and `continue`. `responses deliver` records that a completed artifact was actually delivered to the user, preventing repeated delivery prompts.
|
|
146
|
-
|
|
147
|
-
### Dialogue events for external Agents
|
|
148
|
-
|
|
149
|
-
Use this when another Agent needs to read what Makaron said, handle text checkpoints, and relay artifacts without inventing customer-service wording.
|
|
150
|
-
|
|
151
|
-
```bash
|
|
152
|
-
npx makaron-cli responses events <runId> --jsonl
|
|
153
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl
|
|
154
|
-
# compact view: npx makaron-cli responses timeline <runId> --jsonl --compact
|
|
155
|
-
# pure Q&A view: npx makaron-cli responses timeline <runId> --jsonl --checkpoint-mode off
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
`events` and `timeline` are lower-level commands. New Agents should start with `responses next` and use these only when they need raw event streams.
|
|
159
|
-
|
|
160
|
-
Events use only three factual types:
|
|
161
|
-
|
|
162
|
-
```json
|
|
163
|
-
{"type":"message","id":"msg_1","runId":"run_xxx","seq":1,"text":"Makaron original message","requires_approval":true,"approval_options":["approve","revise","ask_user","continue"]}
|
|
164
|
-
{"type":"approval","messageId":"msg_1","choice":"approve","status":"recorded"}
|
|
165
|
-
{"type":"artifact","kind":"image","status":"completed","url":"https://..."}
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Makaron uses a conservative checkpoint rule for creative/service Agents: if a run stops with substantive text, has no image/video/music/design artifact, and has no continuing execution action such as a tool call, the message is marked `requires_approval: true`. Pure status text such as queued, rendering, uploading, or completed is not treated as a checkpoint. Pure Q&A flows should pass `--checkpoint-mode off`. If a message has `requires_approval: true`, the external Agent must record an approval before continuing to wait for artifacts or claiming completion:
|
|
169
|
-
|
|
170
|
-
```bash
|
|
171
|
-
npx makaron-cli responses approve msg_1 --run <runId> --note "Proceed."
|
|
172
|
-
npx makaron-cli responses revise msg_1 --run <runId> "make it softer"
|
|
173
|
-
npx makaron-cli responses ask-user msg_1 --run <runId>
|
|
174
|
-
npx makaron-cli responses continue msg_1 --run <runId>
|
|
175
|
-
npx makaron-cli responses handle msg_1 --run <runId> --choice approve
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
Important: approvals are a local Agent gate in v0. They are recorded by the CLI so wrappers and Skills can fail fast, but they do not pause or resume the remote Makaron runtime yet. Wrappers can enforce the local gate with:
|
|
179
|
-
|
|
180
|
-
```bash
|
|
181
|
-
npx makaron-cli responses events <runId> --jsonl --checkpoint-mode service --fail-on-unapproved
|
|
182
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl --compact --fail-on-unapproved
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Use `--compact` when relaying to another Agent or chat system; it merges consecutive Makaron content chunks into a single readable message.
|
|
186
|
-
|
|
187
139
|
### Extract specific results
|
|
188
140
|
|
|
189
141
|
```bash
|
|
@@ -246,7 +198,7 @@ npx makaron-cli video status <taskId>
|
|
|
246
198
|
npx makaron-cli chat --project <id|auto> --video input.mp4 -b "make it funny"
|
|
247
199
|
```
|
|
248
200
|
|
|
249
|
-
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration
|
|
201
|
+
Options for `video create`: `--script "..."`, `--script-file <path>`, `--image <url>` (repeatable, up to 7), `--video <file|url>`, `--duration <seconds>`, `--aspect 9:16|16:9|1:1`, `--model kling|seedance`. SeeDance accepts integer output duration 4-15s (default 5s); Kling supports 5-10s.
|
|
250
202
|
|
|
251
203
|
Video edit model behavior: `--model kling --video` uses Kling base/direct edit internally; `--model seedance --video` uses the Seedance video-reference path and requires target <=15s, <=1080p input. Tiny metadata padding up to 15.5s is accepted and output duration is clamped to 15s.
|
|
252
204
|
|
|
@@ -310,41 +262,6 @@ type MakaronOutput =
|
|
|
310
262
|
| Motion design | "create an Instagram story with animated text" |
|
|
311
263
|
| Multi-step | "edit the photo then make a video from it" |
|
|
312
264
|
|
|
313
|
-
## Minimal Agent Wrapper
|
|
314
|
-
|
|
315
|
-
A new Agent can use this minimal flow:
|
|
316
|
-
|
|
317
|
-
```bash
|
|
318
|
-
RUN_JSON=$(npx makaron-cli chat --project auto --json -b "$USER_PROMPT")
|
|
319
|
-
RUN_ID=$(echo "$RUN_JSON" | jq -r .runId)
|
|
320
|
-
PROJECT_URL=$(echo "$RUN_JSON" | jq -r .projectUrl)
|
|
321
|
-
send_message "Project created: $PROJECT_URL"
|
|
322
|
-
|
|
323
|
-
if ! NEXT=$(npx makaron-cli responses next "$RUN_ID" --json); then
|
|
324
|
-
STATUS=$(echo "$NEXT" | jq -r .status)
|
|
325
|
-
if [ "$STATUS" = "needs_approval" ]; then
|
|
326
|
-
MSG_ID=$(echo "$NEXT" | jq -r .checkpoint.id)
|
|
327
|
-
TEXT=$(echo "$NEXT" | jq -r .checkpoint.text)
|
|
328
|
-
send_message "$TEXT"
|
|
329
|
-
npx makaron-cli responses handle "$MSG_ID" --run "$RUN_ID" --choice ask_user
|
|
330
|
-
exit 3
|
|
331
|
-
elif [ "$STATUS" = "failed" ]; then
|
|
332
|
-
send_message "$(echo "$NEXT" | jq -r .error.message)"
|
|
333
|
-
exit 1
|
|
334
|
-
fi
|
|
335
|
-
fi
|
|
336
|
-
|
|
337
|
-
NEXT=$(npx makaron-cli responses next "$RUN_ID" --json --no-fail)
|
|
338
|
-
if [ "$(echo "$NEXT" | jq -r .status)" = "has_artifacts" ]; then
|
|
339
|
-
ARTIFACT_ID=$(echo "$NEXT" | jq -r ".undelivered_artifacts[0].id")
|
|
340
|
-
ARTIFACT_URL=$(echo "$NEXT" | jq -r ".undelivered_artifacts[0].url")
|
|
341
|
-
DELIVERY_MESSAGE_ID=$(send_message "$ARTIFACT_URL")
|
|
342
|
-
npx makaron-cli responses deliver "$ARTIFACT_ID" --run "$RUN_ID" --channel feishu --message-id "$DELIVERY_MESSAGE_ID"
|
|
343
|
-
fi
|
|
344
|
-
|
|
345
|
-
RESULT=$(npx makaron-cli responses get "$RUN_ID" --wait --json)
|
|
346
|
-
```
|
|
347
|
-
|
|
348
265
|
## Recommended Pattern: Service Flow (Feishu/OpenClaw/Group Chat)
|
|
349
266
|
|
|
350
267
|
When serving end-users in a chat environment (Feishu, Slack, Discord), use this proactive message pattern:
|