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 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 follow the same compatibility contract as the normal frontend flow: `.mp4`, `.mov`, or `.webm`, max 200MB, target max 15s with 0.5s metadata tolerance, and <=1080p / 2,086,876 frame pixels. The frontend can transcode oversized videos; the CLI rejects them so later Seedance editing does not fail.
114
- The agent understands video content natively — it can analyze scenes, edit, extend, and compose videos. Seedance video-reference editing is supported for ~15s videos that meet the same upload limits; Kling remains the base/direct edit path.
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 follow the same compatibility contract as the normal frontend flow: max 200MB, target max 15s with 0.5s metadata tolerance, and <=1080p / 2,086,876 frame pixels. The frontend can transcode oversized videos; the CLI rejects them so later Seedance editing does not fail. 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 supported for ~15s videos that meet these upload limits; Kling remains the base/direct edit path.
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 MAX_VIDEO_FILE_SIZE = 200 * 1024 * 1024;
32
- const MAX_VIDEO_DURATION = 15;
33
- const MAX_VIDEO_DURATION_TOLERANCE = 0.5;
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 > MAX_VIDEO_FILE_SIZE) {
1000
- return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max 200MB)` };
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 > MAX_VIDEO_DURATION + MAX_VIDEO_DURATION_TOLERANCE) {
1011
- return { ok: false, error: `Video too long: ${formatSeconds(meta.duration)}s (max ${MAX_VIDEO_DURATION}s, with ${MAX_VIDEO_DURATION_TOLERANCE}s metadata tolerance)` };
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 > MAX_VIDEO_FILE_SIZE) {
1029
- return { ok: false, error: `Video too large: ${(stat.size / 1024 / 1024).toFixed(1)}MB (max 200MB)` };
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: ≤${MAX_VIDEO_DURATION}s, ≤200MB, ≤1080p.\n`);
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, ≤${MAX_VIDEO_DURATION}s, ≤200MB, and ≤1080p.\n`);
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 Makaron upload limits: ≤${MAX_VIDEO_DURATION}s, ≤200MB, ≤1080p.\n`);
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(MAX_VIDEO_DURATION, Math.round(inputVideoMeta.duration)) : undefined);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.7.10",
3
+ "version": "0.7.11",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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 follow the same compatibility contract as the normal frontend flow: max 200MB, target max 15s with 0.5s metadata tolerance, and <=1080p / 2,086,876 frame pixels. The frontend can transcode oversized videos; the CLI rejects them so later Seedance editing does not fail. 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 supported for ~15s videos that meet these upload limits; Kling remains the base/direct edit path.
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: