makaron-cli 0.7.10 → 0.7.11
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 +12 -66
- package/SKILL.md +11 -65
- package/bin/makaron.mjs +65 -313
- package/package.json +1 -1
- package/skills/makaron/SKILL.md +11 -65
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,43 +144,6 @@ Outputs one JSON per line as artifacts appear:
|
|
|
134
144
|
{"event":"done","status":"completed"}
|
|
135
145
|
```
|
|
136
146
|
|
|
137
|
-
### Dialogue events for external Agents
|
|
138
|
-
|
|
139
|
-
Use this when another Agent needs to read what Makaron said, handle text checkpoints, and relay artifacts without inventing customer-service wording.
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
npx makaron-cli responses events <runId> --jsonl
|
|
143
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl
|
|
144
|
-
# compact view: npx makaron-cli responses timeline <runId> --jsonl --compact
|
|
145
|
-
# pure Q&A view: npx makaron-cli responses timeline <runId> --jsonl --checkpoint-mode off
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
Events use only three factual types:
|
|
149
|
-
|
|
150
|
-
```json
|
|
151
|
-
{"type":"message","id":"msg_1","runId":"run_xxx","seq":1,"text":"Makaron original message","requires_approval":true,"approval_options":["approve","revise","ask_user","continue"]}
|
|
152
|
-
{"type":"approval","messageId":"msg_1","choice":"approve","status":"recorded"}
|
|
153
|
-
{"type":"artifact","kind":"image","status":"completed","url":"https://..."}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
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:
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
npx makaron-cli responses approve msg_1 --run <runId> --note "Proceed."
|
|
160
|
-
npx makaron-cli responses revise msg_1 --run <runId> "make it softer"
|
|
161
|
-
npx makaron-cli responses ask-user msg_1 --run <runId>
|
|
162
|
-
npx makaron-cli responses continue msg_1 --run <runId>
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
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:
|
|
166
|
-
|
|
167
|
-
```bash
|
|
168
|
-
npx makaron-cli responses events <runId> --jsonl --checkpoint-mode service --fail-on-unapproved
|
|
169
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl --compact --fail-on-unapproved
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
Use `--compact` when relaying to another Agent or chat system; it merges consecutive Makaron content chunks into a single readable message.
|
|
173
|
-
|
|
174
147
|
### Extract specific results
|
|
175
148
|
|
|
176
149
|
```bash
|
|
@@ -300,33 +273,6 @@ type MakaronOutput =
|
|
|
300
273
|
| Motion design | "create an Instagram story with animated text" |
|
|
301
274
|
| Multi-step | "edit the photo then make a video from it" |
|
|
302
275
|
|
|
303
|
-
## Minimal Agent Wrapper
|
|
304
|
-
|
|
305
|
-
A new Agent can use this minimal flow:
|
|
306
|
-
|
|
307
|
-
```bash
|
|
308
|
-
RUN_JSON=$(npx makaron-cli chat --project auto --json -b "$USER_PROMPT")
|
|
309
|
-
RUN_ID=$(echo "$RUN_JSON" | jq -r .runId)
|
|
310
|
-
PROJECT_URL=$(echo "$RUN_JSON" | jq -r .projectUrl)
|
|
311
|
-
send_message "Project created: $PROJECT_URL"
|
|
312
|
-
|
|
313
|
-
if ! npx makaron-cli responses timeline "$RUN_ID" --jsonl --compact --checkpoint-mode service --fail-on-unapproved > /tmp/makaron-events.jsonl; then
|
|
314
|
-
npx makaron-cli responses timeline "$RUN_ID" --jsonl --compact | while read -r event; do
|
|
315
|
-
TYPE=$(echo "$event" | jq -r .type)
|
|
316
|
-
REQUIRES=$(echo "$event" | jq -r ".requires_approval // false")
|
|
317
|
-
TEXT=$(echo "$event" | jq -r ".text // empty")
|
|
318
|
-
MSG_ID=$(echo "$event" | jq -r ".id // empty")
|
|
319
|
-
if [ "$TYPE" = "message" ] && [ "$REQUIRES" = "true" ]; then
|
|
320
|
-
send_message "$TEXT"
|
|
321
|
-
npx makaron-cli responses ask-user "$MSG_ID" --run "$RUN_ID"
|
|
322
|
-
exit 3
|
|
323
|
-
fi
|
|
324
|
-
done
|
|
325
|
-
fi
|
|
326
|
-
|
|
327
|
-
RESULT=$(npx makaron-cli responses get "$RUN_ID" --wait --json)
|
|
328
|
-
```
|
|
329
|
-
|
|
330
276
|
## Recommended Pattern: Service Flow (Feishu/OpenClaw/Group Chat)
|
|
331
277
|
|
|
332
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,43 +136,6 @@ Outputs one JSON per line as artifacts appear:
|
|
|
126
136
|
{"event":"done","status":"completed"}
|
|
127
137
|
```
|
|
128
138
|
|
|
129
|
-
### Dialogue events for external Agents
|
|
130
|
-
|
|
131
|
-
Use this when another Agent needs to read what Makaron said, handle text checkpoints, and relay artifacts without inventing customer-service wording.
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
npx makaron-cli responses events <runId> --jsonl
|
|
135
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl
|
|
136
|
-
# compact view: npx makaron-cli responses timeline <runId> --jsonl --compact
|
|
137
|
-
# pure Q&A view: npx makaron-cli responses timeline <runId> --jsonl --checkpoint-mode off
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Events use only three factual types:
|
|
141
|
-
|
|
142
|
-
```json
|
|
143
|
-
{"type":"message","id":"msg_1","runId":"run_xxx","seq":1,"text":"Makaron original message","requires_approval":true,"approval_options":["approve","revise","ask_user","continue"]}
|
|
144
|
-
{"type":"approval","messageId":"msg_1","choice":"approve","status":"recorded"}
|
|
145
|
-
{"type":"artifact","kind":"image","status":"completed","url":"https://..."}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
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:
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
npx makaron-cli responses approve msg_1 --run <runId> --note "Proceed."
|
|
152
|
-
npx makaron-cli responses revise msg_1 --run <runId> "make it softer"
|
|
153
|
-
npx makaron-cli responses ask-user msg_1 --run <runId>
|
|
154
|
-
npx makaron-cli responses continue msg_1 --run <runId>
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
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:
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
npx makaron-cli responses events <runId> --jsonl --checkpoint-mode service --fail-on-unapproved
|
|
161
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl --compact --fail-on-unapproved
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Use `--compact` when relaying to another Agent or chat system; it merges consecutive Makaron content chunks into a single readable message.
|
|
165
|
-
|
|
166
139
|
### Extract specific results
|
|
167
140
|
|
|
168
141
|
```bash
|
|
@@ -289,33 +262,6 @@ type MakaronOutput =
|
|
|
289
262
|
| Motion design | "create an Instagram story with animated text" |
|
|
290
263
|
| Multi-step | "edit the photo then make a video from it" |
|
|
291
264
|
|
|
292
|
-
## Minimal Agent Wrapper
|
|
293
|
-
|
|
294
|
-
A new Agent can use this minimal flow:
|
|
295
|
-
|
|
296
|
-
```bash
|
|
297
|
-
RUN_JSON=$(npx makaron-cli chat --project auto --json -b "$USER_PROMPT")
|
|
298
|
-
RUN_ID=$(echo "$RUN_JSON" | jq -r .runId)
|
|
299
|
-
PROJECT_URL=$(echo "$RUN_JSON" | jq -r .projectUrl)
|
|
300
|
-
send_message "Project created: $PROJECT_URL"
|
|
301
|
-
|
|
302
|
-
if ! npx makaron-cli responses timeline "$RUN_ID" --jsonl --compact --checkpoint-mode service --fail-on-unapproved > /tmp/makaron-events.jsonl; then
|
|
303
|
-
npx makaron-cli responses timeline "$RUN_ID" --jsonl --compact | while read -r event; do
|
|
304
|
-
TYPE=$(echo "$event" | jq -r .type)
|
|
305
|
-
REQUIRES=$(echo "$event" | jq -r ".requires_approval // false")
|
|
306
|
-
TEXT=$(echo "$event" | jq -r ".text // empty")
|
|
307
|
-
MSG_ID=$(echo "$event" | jq -r ".id // empty")
|
|
308
|
-
if [ "$TYPE" = "message" ] && [ "$REQUIRES" = "true" ]; then
|
|
309
|
-
send_message "$TEXT"
|
|
310
|
-
npx makaron-cli responses ask-user "$MSG_ID" --run "$RUN_ID"
|
|
311
|
-
exit 3
|
|
312
|
-
fi
|
|
313
|
-
done
|
|
314
|
-
fi
|
|
315
|
-
|
|
316
|
-
RESULT=$(npx makaron-cli responses get "$RUN_ID" --wait --json)
|
|
317
|
-
```
|
|
318
|
-
|
|
319
265
|
## Recommended Pattern: Service Flow (Feishu/OpenClaw/Group Chat)
|
|
320
266
|
|
|
321
267
|
When serving end-users in a chat environment (Feishu, Slack, Discord), use this proactive message pattern:
|
package/bin/makaron.mjs
CHANGED
|
@@ -19,7 +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
22
|
const DEFAULT_URL = 'https://www.makaron.app';
|
|
24
23
|
const BASE_URL = process.env.MAKARON_URL || DEFAULT_URL;
|
|
25
24
|
const APP_URL = process.env.MAKARON_APP_URL || DEFAULT_URL;
|
|
@@ -28,9 +27,12 @@ const APP_URL = process.env.MAKARON_APP_URL || DEFAULT_URL;
|
|
|
28
27
|
const SUPABASE_URL = 'https://sdyrtztrjgmmpnirswxt.supabase.co';
|
|
29
28
|
const SUPABASE_ANON_KEY = 'sb_publishable_FJFN2YYaWaQjABUKLqxQcA_fhxPLFDY';
|
|
30
29
|
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
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;
|
|
34
36
|
const MAX_VIDEO_FRAME_PIXELS = 2_086_876;
|
|
35
37
|
|
|
36
38
|
function getCliVersion() {
|
|
@@ -63,20 +65,6 @@ function saveAuth(data) {
|
|
|
63
65
|
fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2));
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
function loadApprovals() {
|
|
67
|
-
try {
|
|
68
|
-
return JSON.parse(fs.readFileSync(APPROVALS_FILE, 'utf-8'));
|
|
69
|
-
} catch {
|
|
70
|
-
return [];
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function saveApprovals(approvals) {
|
|
75
|
-
const dir = path.dirname(APPROVALS_FILE);
|
|
76
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
77
|
-
fs.writeFileSync(APPROVALS_FILE, JSON.stringify(approvals, null, 2));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
68
|
function buildCookie(tokenJson) {
|
|
81
69
|
const url = tokenJson._supabaseUrl || SUPABASE_URL;
|
|
82
70
|
const ref = url.match(/\/\/([^.]+)\./)?.[1] || '';
|
|
@@ -441,248 +429,6 @@ function applyPick(data, field) {
|
|
|
441
429
|
}
|
|
442
430
|
}
|
|
443
431
|
|
|
444
|
-
// ─── Dialogue Events (message / approval / artifact) ─────────────────────────
|
|
445
|
-
|
|
446
|
-
function stableEventId(prefix, runId, seq, fallback) {
|
|
447
|
-
return fallback || `${prefix}_${runId}_${seq ?? Date.now()}`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function inferApprovalIntent(text) {
|
|
451
|
-
if (!text) return false;
|
|
452
|
-
const normalized = text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
453
|
-
const hasEnglishApprovalIntent = /\b(shall i|should i|do you want me to|confirm|approve|approval|permission)\b/.test(normalized);
|
|
454
|
-
const hasEnglishAction = /\b(go ahead|proceed|continue|generate|create|submit|start|run|render)\b/.test(normalized);
|
|
455
|
-
const hasChineseApprovalIntent = /(是否|要不要|是否要|需要我|请确认|确认|同意|批准|可以吗|是否可以)/.test(normalized);
|
|
456
|
-
const hasChineseAction = /(继续|开始|生成|创建|提交|执行|渲染|出图|出视频|制作)/.test(normalized);
|
|
457
|
-
return (hasEnglishApprovalIntent && hasEnglishAction) || (hasChineseApprovalIntent && hasChineseAction);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
function isPureStatusMessage(text, sourceType, status) {
|
|
461
|
-
const normalized = String(text || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
462
|
-
if (sourceType === 'status') return true;
|
|
463
|
-
if (status && !normalized) return true;
|
|
464
|
-
if (!normalized) return false;
|
|
465
|
-
return /^(queued|running|rendering|uploading|processing|generating|completed|complete|done|failed|aborted|started|submitted|waiting|polling)(\.|…|\.\.\.)?$/.test(normalized)
|
|
466
|
-
|| /^(排队中|队列中|运行中|渲染中|上传中|处理中|生成中|已完成|完成|失败|已失败|已提交|等待中)$/.test(normalized);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function hasExplicitApprovalRequirement(data, text) {
|
|
470
|
-
return Boolean(
|
|
471
|
-
data.requires_approval
|
|
472
|
-
|| data.requiresApproval
|
|
473
|
-
|| data.action_required
|
|
474
|
-
|| data.actionRequired
|
|
475
|
-
|| data.proposal
|
|
476
|
-
|| inferApprovalIntent(text)
|
|
477
|
-
);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function isPureQaRun(data) {
|
|
481
|
-
const raw = [
|
|
482
|
-
data.intent,
|
|
483
|
-
data.mode,
|
|
484
|
-
data.workflow,
|
|
485
|
-
data.workflow_type,
|
|
486
|
-
data.workflowType,
|
|
487
|
-
data.run_type,
|
|
488
|
-
data.runType,
|
|
489
|
-
data.kind,
|
|
490
|
-
].filter(Boolean).join(' ').toLowerCase();
|
|
491
|
-
return /\b(qa|q&a|question_answer|question-answer|answer|pure_qa|pure-qa)\b/.test(raw);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
function normalizeDialogueMessage(runId, projectId, ev, context = {}) {
|
|
495
|
-
const data = ev.data || ev;
|
|
496
|
-
const text = data.text || data.message || data.content || data.statusText || '';
|
|
497
|
-
if (!text && ev.type !== 'tool_call') return null;
|
|
498
|
-
const explicitApproval = hasExplicitApprovalRequirement(data, text);
|
|
499
|
-
const textOnlyCheckpoint = Boolean(
|
|
500
|
-
context.stoppedWithoutArtifacts
|
|
501
|
-
&& !explicitApproval
|
|
502
|
-
&& !isPureStatusMessage(text, ev.type, data.status)
|
|
503
|
-
);
|
|
504
|
-
const requiresApproval = explicitApproval || textOnlyCheckpoint;
|
|
505
|
-
const message = {
|
|
506
|
-
type: 'message',
|
|
507
|
-
id: stableEventId('msg', runId, ev.seq, data.id || ev.id),
|
|
508
|
-
runId,
|
|
509
|
-
projectId,
|
|
510
|
-
seq: ev.seq,
|
|
511
|
-
status: data.status || undefined,
|
|
512
|
-
text: text || `${data.tool || 'tool_call'}${data.input?.description ? `: ${data.input.description}` : ''}`,
|
|
513
|
-
};
|
|
514
|
-
if (ev.type) message.source_type = ev.type;
|
|
515
|
-
if (requiresApproval) {
|
|
516
|
-
message.requires_approval = true;
|
|
517
|
-
message.approval_options = data.approval_options || data.approvalOptions || ['approve', 'revise', 'ask_user', 'continue'];
|
|
518
|
-
if (textOnlyCheckpoint) message.approval_reason = 'text_only_checkpoint';
|
|
519
|
-
}
|
|
520
|
-
if (data.proposal) message.proposal = data.proposal;
|
|
521
|
-
return message;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
function normalizeDialogueArtifact(runId, projectId, item, seq) {
|
|
525
|
-
if (!item) return null;
|
|
526
|
-
const kind = item.type || item.kind || (item.imageUrl ? 'image' : item.videoUrl ? 'video' : undefined);
|
|
527
|
-
if (!kind) return null;
|
|
528
|
-
if (!['image', 'video', 'design', 'music', 'audio', 'file'].includes(kind)) return null;
|
|
529
|
-
const artifact = {
|
|
530
|
-
type: 'artifact',
|
|
531
|
-
id: stableEventId('artifact', runId, seq, item.id || item.snapshotId || item.taskId),
|
|
532
|
-
runId,
|
|
533
|
-
projectId,
|
|
534
|
-
seq,
|
|
535
|
-
kind,
|
|
536
|
-
status: item.status || (item.url || item.imageUrl || item.videoUrl || item.audioUrl ? 'completed' : 'running'),
|
|
537
|
-
};
|
|
538
|
-
const url = item.url || item.imageUrl || item.videoUrl || item.audioUrl;
|
|
539
|
-
if (url) artifact.url = url;
|
|
540
|
-
if (item.error) artifact.error = item.error;
|
|
541
|
-
if (item.taskId) artifact.taskId = item.taskId;
|
|
542
|
-
if (item.snapshotId) artifact.snapshotId = item.snapshotId;
|
|
543
|
-
return artifact;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function normalizeDialogueEvent(runId, projectId, ev, context = {}) {
|
|
547
|
-
const data = ev.data || {};
|
|
548
|
-
if (ev.type === 'message' || ev.type === 'approval' || ev.type === 'artifact') {
|
|
549
|
-
return { ...data, ...ev, runId: ev.runId || runId, projectId: ev.projectId || projectId };
|
|
550
|
-
}
|
|
551
|
-
switch (ev.type) {
|
|
552
|
-
case 'content':
|
|
553
|
-
case 'status':
|
|
554
|
-
case 'tool_call':
|
|
555
|
-
case 'error':
|
|
556
|
-
return normalizeDialogueMessage(runId, projectId, ev, context);
|
|
557
|
-
case 'image':
|
|
558
|
-
return normalizeDialogueArtifact(runId, projectId, { type: 'image', status: data.imageUrl ? 'completed' : 'running', imageUrl: data.imageUrl, snapshotId: data.snapshotId }, ev.seq);
|
|
559
|
-
case 'render':
|
|
560
|
-
return normalizeDialogueArtifact(runId, projectId, { type: data.animation ? 'video' : 'design', status: data.published ? 'completed' : 'running', url: data.url, snapshotId: data.snapshotId }, ev.seq);
|
|
561
|
-
case 'animation_task':
|
|
562
|
-
case 'video_snapshot':
|
|
563
|
-
return normalizeDialogueArtifact(runId, projectId, { type: 'video', status: 'running', taskId: data.taskId, snapshotId: data.snapshotId }, ev.seq);
|
|
564
|
-
case 'music_task':
|
|
565
|
-
return normalizeDialogueArtifact(runId, projectId, { type: 'music', status: 'running', taskId: data.taskId }, ev.seq);
|
|
566
|
-
default:
|
|
567
|
-
return null;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function buildDialogueEvents(runId, data) {
|
|
572
|
-
const projectId = data.projectId || data.project_id;
|
|
573
|
-
const events = [];
|
|
574
|
-
const hasArtifacts = Boolean(
|
|
575
|
-
(data.output || []).some(item => normalizeDialogueArtifact(runId, projectId, item, item.seq))
|
|
576
|
-
|| (data.events || []).some(ev => normalizeDialogueEvent(runId, projectId, ev, { stoppedWithoutArtifacts: false })?.type === 'artifact')
|
|
577
|
-
);
|
|
578
|
-
const hasContinuationAction = (data.events || []).some(ev => ['tool_call', 'image', 'render', 'animation_task', 'video_snapshot', 'music_task'].includes(ev.type));
|
|
579
|
-
const checkpointMode = data.checkpointMode || 'service';
|
|
580
|
-
const context = {
|
|
581
|
-
stoppedWithoutArtifacts: checkpointMode !== 'off'
|
|
582
|
-
&& !isPureQaRun(data)
|
|
583
|
-
&& !hasArtifacts
|
|
584
|
-
&& !hasContinuationAction
|
|
585
|
-
&& !data.incomplete
|
|
586
|
-
&& ['completed', 'failed', 'aborted', 'waiting', 'needs_input'].includes(data.status),
|
|
587
|
-
};
|
|
588
|
-
for (const ev of data.events || []) {
|
|
589
|
-
const normalized = normalizeDialogueEvent(runId, projectId, ev, context);
|
|
590
|
-
if (normalized) events.push(normalized);
|
|
591
|
-
}
|
|
592
|
-
for (const item of data.output || []) {
|
|
593
|
-
const normalized = normalizeDialogueArtifact(runId, projectId, item, item.seq);
|
|
594
|
-
if (normalized && !events.some(ev => ev.type === 'artifact' && ev.id === normalized.id)) events.push(normalized);
|
|
595
|
-
}
|
|
596
|
-
for (const approval of loadApprovals().filter(item => item.runId === runId)) {
|
|
597
|
-
events.push(approval);
|
|
598
|
-
}
|
|
599
|
-
return events;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function compactDialogueEvents(events) {
|
|
603
|
-
const compacted = [];
|
|
604
|
-
for (const ev of events) {
|
|
605
|
-
const prev = compacted[compacted.length - 1];
|
|
606
|
-
const canMerge = prev
|
|
607
|
-
&& ev.type === 'message'
|
|
608
|
-
&& prev.type === 'message'
|
|
609
|
-
&& ev.source_type === 'content'
|
|
610
|
-
&& prev.source_type === 'content'
|
|
611
|
-
&& !ev.requires_approval
|
|
612
|
-
&& !prev.requires_approval
|
|
613
|
-
&& !ev.proposal
|
|
614
|
-
&& !prev.proposal;
|
|
615
|
-
if (canMerge) {
|
|
616
|
-
prev.text = `${prev.text}${ev.text}`;
|
|
617
|
-
prev.id = `${prev.id}+${ev.id}`;
|
|
618
|
-
prev.seq_end = ev.seq;
|
|
619
|
-
} else {
|
|
620
|
-
compacted.push({ ...ev });
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
return compacted;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
function findUnhandledApprovalMessages(events) {
|
|
627
|
-
const approved = new Set(events.filter(ev => ev.type === 'approval').map(ev => ev.messageId || ev.message_id));
|
|
628
|
-
return events.filter(ev => ev.type === 'message' && ev.requires_approval && !approved.has(ev.id));
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
async function fetchRun(baseUrl, headers, runId, opts = {}) {
|
|
632
|
-
const params = new URLSearchParams();
|
|
633
|
-
if (opts.events) params.set('events', 'true');
|
|
634
|
-
const suffix = params.toString() ? `?${params}` : '';
|
|
635
|
-
const res = await fetch(`${baseUrl}/api/agent/run/${runId}${suffix}`, { headers });
|
|
636
|
-
if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
|
|
637
|
-
return normalizeRunResponse(await res.json());
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
async function printDialogueEvents(baseUrl, headers, runId, opts = {}) {
|
|
641
|
-
const { jsonl = false, failOnUnapproved = false, follow = false, interval = 5000, compact = false, checkpointMode = 'service' } = opts;
|
|
642
|
-
const printed = new Set();
|
|
643
|
-
|
|
644
|
-
while (true) {
|
|
645
|
-
const data = await fetchRun(baseUrl, headers, runId, { events: true });
|
|
646
|
-
const dialogueEvents = buildDialogueEvents(runId, { ...data, checkpointMode });
|
|
647
|
-
const events = compact ? compactDialogueEvents(dialogueEvents) : dialogueEvents;
|
|
648
|
-
const unhandled = findUnhandledApprovalMessages(events);
|
|
649
|
-
if (failOnUnapproved && unhandled.length) {
|
|
650
|
-
process.stderr.write(`Unhandled Makaron message requires approval: ${unhandled.map(ev => ev.id).join(', ')}\n`);
|
|
651
|
-
process.exit(3);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
const nextEvents = follow ? events.filter(ev => !printed.has(`${ev.type}:${ev.id || ev.seq}`)) : events;
|
|
655
|
-
for (const ev of nextEvents) {
|
|
656
|
-
printed.add(`${ev.type}:${ev.id || ev.seq}`);
|
|
657
|
-
if (jsonl) console.log(JSON.stringify(ev));
|
|
658
|
-
}
|
|
659
|
-
if (!jsonl) console.log(JSON.stringify(nextEvents, null, 2));
|
|
660
|
-
|
|
661
|
-
if (!follow || (!data.incomplete && ['completed', 'failed', 'aborted'].includes(data.status))) {
|
|
662
|
-
if (data.status === 'failed' || data.status === 'aborted') process.exit(1);
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
await new Promise(r => setTimeout(r, data.next_poll_after_ms || interval));
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function recordApproval(runId, messageId, choice, note) {
|
|
670
|
-
const approvals = loadApprovals();
|
|
671
|
-
const approval = {
|
|
672
|
-
type: 'approval',
|
|
673
|
-
id: `approval_${Date.now()}`,
|
|
674
|
-
runId,
|
|
675
|
-
messageId,
|
|
676
|
-
choice,
|
|
677
|
-
status: 'recorded',
|
|
678
|
-
createdAt: new Date().toISOString(),
|
|
679
|
-
};
|
|
680
|
-
if (note) approval.note = note;
|
|
681
|
-
approvals.push(approval);
|
|
682
|
-
saveApprovals(approvals);
|
|
683
|
-
console.log(JSON.stringify(approval));
|
|
684
|
-
}
|
|
685
|
-
|
|
686
432
|
// ─── Watch (incremental event stream) ───────────────────────────────────────
|
|
687
433
|
|
|
688
434
|
async function watchRun(baseUrl, headers, runId, opts = {}) {
|
|
@@ -840,6 +586,34 @@ async function listProjects(baseUrl, headers) {
|
|
|
840
586
|
console.log('');
|
|
841
587
|
}
|
|
842
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
|
+
|
|
843
617
|
function timeSince(date) {
|
|
844
618
|
const s = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
845
619
|
if (s < 60) return 'just now';
|
|
@@ -991,13 +765,15 @@ function probeLocalVideo(videoPath) {
|
|
|
991
765
|
return probeVideoWithFfprobe(videoPath) || probeVideoWithFfmpeg(videoPath);
|
|
992
766
|
}
|
|
993
767
|
|
|
994
|
-
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;
|
|
995
771
|
if (!fs.existsSync(videoPath)) {
|
|
996
772
|
return { ok: false, error: `Video file not found: ${videoPath}` };
|
|
997
773
|
}
|
|
998
774
|
const stat = fs.statSync(videoPath);
|
|
999
|
-
if (stat.size >
|
|
1000
|
-
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.` };
|
|
1001
777
|
}
|
|
1002
778
|
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
1003
779
|
if (!['mp4', 'mov', 'webm'].includes(ext)) {
|
|
@@ -1007,8 +783,8 @@ function validateVideoFile(videoPath) {
|
|
|
1007
783
|
if (!meta) {
|
|
1008
784
|
return { ok: false, error: 'Cannot read video duration/resolution. Install ffmpeg/ffprobe or use the normal frontend upload flow.' };
|
|
1009
785
|
}
|
|
1010
|
-
if (meta.duration >
|
|
1011
|
-
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)` };
|
|
1012
788
|
}
|
|
1013
789
|
if (meta.width * meta.height > MAX_VIDEO_FRAME_PIXELS) {
|
|
1014
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.` };
|
|
@@ -1025,8 +801,8 @@ function validateVideoFileForAnalysis(videoPath) {
|
|
|
1025
801
|
if (stat.size === 0) {
|
|
1026
802
|
return { ok: false, error: `Video file is empty: ${videoPath}` };
|
|
1027
803
|
}
|
|
1028
|
-
if (stat.size >
|
|
1029
|
-
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.` };
|
|
1030
806
|
}
|
|
1031
807
|
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
1032
808
|
if (!['mp4', 'mov', 'webm'].includes(ext)) {
|
|
@@ -1212,7 +988,7 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1212
988
|
const uploadedVideoUrls = [...prevalidatedVideoUrlList];
|
|
1213
989
|
const uploadedVideoMetas = prevalidatedVideoUrlList.map(() => null);
|
|
1214
990
|
if (prevalidatedVideoUrlList.length) {
|
|
1215
|
-
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`);
|
|
1216
992
|
}
|
|
1217
993
|
for (const videoPath of prevalidatedVideoFileList) {
|
|
1218
994
|
process.stderr.write(`📹 Uploading ${path.basename(videoPath)} (${(fs.statSync(videoPath).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
@@ -1228,7 +1004,7 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1228
1004
|
|
|
1229
1005
|
// Add videos to project via projects/create (same as images)
|
|
1230
1006
|
if (uploadedVideoUrls.length === 0) {
|
|
1231
|
-
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`);
|
|
1232
1008
|
process.exit(1);
|
|
1233
1009
|
}
|
|
1234
1010
|
|
|
@@ -1317,38 +1093,6 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1317
1093
|
}
|
|
1318
1094
|
await watchRun(baseUrl, headers, runId, { interval, jsonl });
|
|
1319
1095
|
|
|
1320
|
-
} else if (sub === 'events' || sub === 'timeline') {
|
|
1321
|
-
const runId = args[2];
|
|
1322
|
-
if (!runId) { console.error(`Usage: makaron responses ${sub} <runId> [--jsonl] [--compact] [--checkpoint-mode service|off] [--follow] [--interval <ms>] [--fail-on-unapproved]`); process.exit(1); }
|
|
1323
|
-
let interval = 5000, jsonl = false, follow = false, failOnUnapproved = false, compact = false, checkpointMode = 'service';
|
|
1324
|
-
for (let i = 3; i < args.length; i++) {
|
|
1325
|
-
if (args[i] === '--jsonl') jsonl = true;
|
|
1326
|
-
else if (args[i] === '--compact') compact = true;
|
|
1327
|
-
else if (args[i] === '--checkpoint-mode' && args[i + 1]) checkpointMode = args[++i];
|
|
1328
|
-
else if (args[i] === '--follow') follow = true;
|
|
1329
|
-
else if (args[i] === '--fail-on-unapproved') failOnUnapproved = true;
|
|
1330
|
-
else if (args[i] === '--interval' && args[i + 1]) interval = parseInt(args[++i]);
|
|
1331
|
-
}
|
|
1332
|
-
if (!['service', 'off'].includes(checkpointMode)) {
|
|
1333
|
-
console.error('--checkpoint-mode must be service or off');
|
|
1334
|
-
process.exit(1);
|
|
1335
|
-
}
|
|
1336
|
-
await printDialogueEvents(baseUrl, headers, runId, { interval, jsonl, follow, failOnUnapproved, compact, checkpointMode });
|
|
1337
|
-
|
|
1338
|
-
} else if (['approve', 'revise', 'ask-user', 'continue'].includes(sub)) {
|
|
1339
|
-
const messageId = args[2];
|
|
1340
|
-
if (!messageId) { console.error(`Usage: makaron responses ${sub} <messageId> --run <runId> [--note <text>]`); process.exit(1); }
|
|
1341
|
-
let runId = null, note = null;
|
|
1342
|
-
const noteParts = [];
|
|
1343
|
-
for (let i = 3; i < args.length; i++) {
|
|
1344
|
-
if (args[i] === '--run' && args[i + 1]) runId = args[++i];
|
|
1345
|
-
else if (args[i] === '--note' && args[i + 1]) note = args[++i];
|
|
1346
|
-
else noteParts.push(args[i]);
|
|
1347
|
-
}
|
|
1348
|
-
if (!runId) { console.error(`Usage: makaron responses ${sub} <messageId> --run <runId> [--note <text>]`); process.exit(1); }
|
|
1349
|
-
if (!note && noteParts.length) note = noteParts.join(' ');
|
|
1350
|
-
recordApproval(runId, messageId, sub === 'ask-user' ? 'ask_user' : sub, note);
|
|
1351
|
-
|
|
1352
1096
|
} else if (sub === 'list') {
|
|
1353
1097
|
let projectId = null;
|
|
1354
1098
|
for (let i = 2; i < args.length; i++) {
|
|
@@ -1372,13 +1116,6 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1372
1116
|
responses get <runId> Get status and output (JSON)
|
|
1373
1117
|
responses get <runId> --wait Poll until completed
|
|
1374
1118
|
responses get <runId> --pick <field> Extract: first_image_url, first_video_url, project_url, output
|
|
1375
|
-
responses events <runId> --jsonl Emit message/approval/artifact events for external Agents
|
|
1376
|
-
responses timeline <runId> --jsonl Alias for responses events
|
|
1377
|
-
responses timeline <runId> --checkpoint-mode off Disable text-only checkpoints for pure Q&A
|
|
1378
|
-
responses approve <messageId> --run <runId> Record approval for a Makaron message
|
|
1379
|
-
responses revise <messageId> --run <runId> Record revision request for a Makaron message
|
|
1380
|
-
responses ask-user <messageId> --run <runId> Record that the user must decide
|
|
1381
|
-
responses continue <messageId> --run <runId> Record continue decision
|
|
1382
1119
|
responses watch <runId> --jsonl Watch until done (incremental events)
|
|
1383
1120
|
responses list --project <id> List runs for a project
|
|
1384
1121
|
`);
|
|
@@ -1386,6 +1123,19 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1386
1123
|
} else if (command === 'list' || command === 'ls') {
|
|
1387
1124
|
const { headers, baseUrl } = getAuth();
|
|
1388
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
|
+
}
|
|
1389
1139
|
} else if (command === 'abort') {
|
|
1390
1140
|
const { headers, baseUrl } = getAuth();
|
|
1391
1141
|
const runId = args[1];
|
|
@@ -1487,10 +1237,13 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1487
1237
|
let videoUrl = isHttpUrl(video) ? video : null;
|
|
1488
1238
|
let inputVideoMeta = null;
|
|
1489
1239
|
if (videoUrl) {
|
|
1490
|
-
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`);
|
|
1491
1241
|
}
|
|
1492
1242
|
if (video && !videoUrl) {
|
|
1493
|
-
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
|
+
});
|
|
1494
1247
|
if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
|
|
1495
1248
|
inputVideoMeta = valid.meta;
|
|
1496
1249
|
process.stderr.write(`📹 Uploading ${path.basename(video)} (${(fs.statSync(video).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
@@ -1503,7 +1256,7 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1503
1256
|
const vArgs = videoUrl
|
|
1504
1257
|
? { videoUrl, editPrompt: script, images, videoModel: videoModel || 'kling', referType: (videoModel || 'kling') === 'seedance' ? 'feature' : 'base' }
|
|
1505
1258
|
: { script, images };
|
|
1506
|
-
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);
|
|
1507
1260
|
if (effectiveDuration) vArgs.duration = effectiveDuration;
|
|
1508
1261
|
if (aspectRatio) vArgs.aspectRatio = aspectRatio;
|
|
1509
1262
|
if (videoModel && !videoUrl) vArgs.videoModel = videoModel;
|
|
@@ -1804,6 +1557,7 @@ Commands:
|
|
|
1804
1557
|
claim Get claim URL for human to link account
|
|
1805
1558
|
login Log in to Makaron (human interactive)
|
|
1806
1559
|
list (ls) List all projects
|
|
1560
|
+
project media <projectId> --json List timeline media for a project
|
|
1807
1561
|
create --image <file> Create project from local image
|
|
1808
1562
|
create --image-url <url> Create project from URL
|
|
1809
1563
|
create --title "name" Create empty project (text-to-image)
|
|
@@ -1816,8 +1570,6 @@ Commands:
|
|
|
1816
1570
|
|
|
1817
1571
|
responses get <runId> Get run status and results
|
|
1818
1572
|
responses get <runId> --wait Poll until completed
|
|
1819
|
-
responses events <runId> --jsonl Emit message/approval/artifact events
|
|
1820
|
-
responses timeline <runId> --checkpoint-mode off Disable text-only checkpoints for pure Q&A
|
|
1821
1573
|
responses list --project <id> List runs for a project
|
|
1822
1574
|
abort <runId> Abort a running Agent
|
|
1823
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,43 +136,6 @@ Outputs one JSON per line as artifacts appear:
|
|
|
126
136
|
{"event":"done","status":"completed"}
|
|
127
137
|
```
|
|
128
138
|
|
|
129
|
-
### Dialogue events for external Agents
|
|
130
|
-
|
|
131
|
-
Use this when another Agent needs to read what Makaron said, handle text checkpoints, and relay artifacts without inventing customer-service wording.
|
|
132
|
-
|
|
133
|
-
```bash
|
|
134
|
-
npx makaron-cli responses events <runId> --jsonl
|
|
135
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl
|
|
136
|
-
# compact view: npx makaron-cli responses timeline <runId> --jsonl --compact
|
|
137
|
-
# pure Q&A view: npx makaron-cli responses timeline <runId> --jsonl --checkpoint-mode off
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Events use only three factual types:
|
|
141
|
-
|
|
142
|
-
```json
|
|
143
|
-
{"type":"message","id":"msg_1","runId":"run_xxx","seq":1,"text":"Makaron original message","requires_approval":true,"approval_options":["approve","revise","ask_user","continue"]}
|
|
144
|
-
{"type":"approval","messageId":"msg_1","choice":"approve","status":"recorded"}
|
|
145
|
-
{"type":"artifact","kind":"image","status":"completed","url":"https://..."}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
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:
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
npx makaron-cli responses approve msg_1 --run <runId> --note "Proceed."
|
|
152
|
-
npx makaron-cli responses revise msg_1 --run <runId> "make it softer"
|
|
153
|
-
npx makaron-cli responses ask-user msg_1 --run <runId>
|
|
154
|
-
npx makaron-cli responses continue msg_1 --run <runId>
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
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:
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
npx makaron-cli responses events <runId> --jsonl --checkpoint-mode service --fail-on-unapproved
|
|
161
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl --compact --fail-on-unapproved
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
Use `--compact` when relaying to another Agent or chat system; it merges consecutive Makaron content chunks into a single readable message.
|
|
165
|
-
|
|
166
139
|
### Extract specific results
|
|
167
140
|
|
|
168
141
|
```bash
|
|
@@ -289,33 +262,6 @@ type MakaronOutput =
|
|
|
289
262
|
| Motion design | "create an Instagram story with animated text" |
|
|
290
263
|
| Multi-step | "edit the photo then make a video from it" |
|
|
291
264
|
|
|
292
|
-
## Minimal Agent Wrapper
|
|
293
|
-
|
|
294
|
-
A new Agent can use this minimal flow:
|
|
295
|
-
|
|
296
|
-
```bash
|
|
297
|
-
RUN_JSON=$(npx makaron-cli chat --project auto --json -b "$USER_PROMPT")
|
|
298
|
-
RUN_ID=$(echo "$RUN_JSON" | jq -r .runId)
|
|
299
|
-
PROJECT_URL=$(echo "$RUN_JSON" | jq -r .projectUrl)
|
|
300
|
-
send_message "Project created: $PROJECT_URL"
|
|
301
|
-
|
|
302
|
-
if ! npx makaron-cli responses timeline "$RUN_ID" --jsonl --compact --checkpoint-mode service --fail-on-unapproved > /tmp/makaron-events.jsonl; then
|
|
303
|
-
npx makaron-cli responses timeline "$RUN_ID" --jsonl --compact | while read -r event; do
|
|
304
|
-
TYPE=$(echo "$event" | jq -r .type)
|
|
305
|
-
REQUIRES=$(echo "$event" | jq -r ".requires_approval // false")
|
|
306
|
-
TEXT=$(echo "$event" | jq -r ".text // empty")
|
|
307
|
-
MSG_ID=$(echo "$event" | jq -r ".id // empty")
|
|
308
|
-
if [ "$TYPE" = "message" ] && [ "$REQUIRES" = "true" ]; then
|
|
309
|
-
send_message "$TEXT"
|
|
310
|
-
npx makaron-cli responses ask-user "$MSG_ID" --run "$RUN_ID"
|
|
311
|
-
exit 3
|
|
312
|
-
fi
|
|
313
|
-
done
|
|
314
|
-
fi
|
|
315
|
-
|
|
316
|
-
RESULT=$(npx makaron-cli responses get "$RUN_ID" --wait --json)
|
|
317
|
-
```
|
|
318
|
-
|
|
319
265
|
## Recommended Pattern: Service Flow (Feishu/OpenClaw/Group Chat)
|
|
320
266
|
|
|
321
267
|
When serving end-users in a chat environment (Feishu, Slack, Discord), use this proactive message pattern:
|