makaron-cli 0.7.9 → 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 -35
- package/SKILL.md +11 -34
- package/bin/makaron.mjs +65 -228
- package/package.json +1 -1
- package/skills/makaron/SKILL.md +11 -34
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,39 +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, detect approval requirements, 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
|
-
```
|
|
145
|
-
|
|
146
|
-
Events use only three factual types:
|
|
147
|
-
|
|
148
|
-
```json
|
|
149
|
-
{"type":"message","id":"msg_1","runId":"run_xxx","seq":1,"text":"Makaron original message","requires_approval":true,"approval_options":["approve","revise","ask_user","continue"]}
|
|
150
|
-
{"type":"approval","messageId":"msg_1","choice":"approve","status":"recorded"}
|
|
151
|
-
{"type":"artifact","kind":"image","status":"completed","url":"https://..."}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
If a message has `requires_approval: true`, the external Agent must record an approval before continuing to wait for artifacts or claiming completion:
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
npx makaron-cli responses approve msg_1 --run <runId> --note "Proceed."
|
|
158
|
-
npx makaron-cli responses revise msg_1 --run <runId> "make it softer"
|
|
159
|
-
npx makaron-cli responses ask-user msg_1 --run <runId>
|
|
160
|
-
npx makaron-cli responses continue msg_1 --run <runId>
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
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:
|
|
164
|
-
|
|
165
|
-
```bash
|
|
166
|
-
npx makaron-cli responses events <runId> --jsonl
|
|
167
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl --fail-on-unapproved
|
|
168
|
-
```
|
|
169
|
-
|
|
170
147
|
### Extract specific results
|
|
171
148
|
|
|
172
149
|
```bash
|
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,39 +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, detect approval requirements, 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
|
-
```
|
|
137
|
-
|
|
138
|
-
Events use only three factual types:
|
|
139
|
-
|
|
140
|
-
```json
|
|
141
|
-
{"type":"message","id":"msg_1","runId":"run_xxx","seq":1,"text":"Makaron original message","requires_approval":true,"approval_options":["approve","revise","ask_user","continue"]}
|
|
142
|
-
{"type":"approval","messageId":"msg_1","choice":"approve","status":"recorded"}
|
|
143
|
-
{"type":"artifact","kind":"image","status":"completed","url":"https://..."}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
If a message has `requires_approval: true`, the external Agent must record an approval before continuing to wait for artifacts or claiming completion:
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
npx makaron-cli responses approve msg_1 --run <runId> --note "Proceed."
|
|
150
|
-
npx makaron-cli responses revise msg_1 --run <runId> "make it softer"
|
|
151
|
-
npx makaron-cli responses ask-user msg_1 --run <runId>
|
|
152
|
-
npx makaron-cli responses continue msg_1 --run <runId>
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
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:
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
npx makaron-cli responses events <runId> --jsonl
|
|
159
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl --fail-on-unapproved
|
|
160
|
-
```
|
|
161
|
-
|
|
162
139
|
### Extract specific results
|
|
163
140
|
|
|
164
141
|
```bash
|
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,171 +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 inferRequiresApproval(text) {
|
|
451
|
-
if (!text) return false;
|
|
452
|
-
const normalized = text.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
453
|
-
const approvalPhrases = [
|
|
454
|
-
/\bshall i\b.*\b(go ahead|proceed|continue|generate|create|submit|start)\b/,
|
|
455
|
-
/\bshould i\b.*\b(go ahead|proceed|continue|generate|create|submit|start)\b/,
|
|
456
|
-
/\bdo you want me to\b.*\b(go ahead|proceed|continue|generate|create|submit|start)\b/,
|
|
457
|
-
/\bconfirm\b.*\b(before|to)\b.*\b(generate|create|submit|proceed|continue)\b/,
|
|
458
|
-
/\bapprove\b.*\b(before|to)\b.*\b(generate|create|submit|proceed|continue)\b/,
|
|
459
|
-
/\bplease\b.*\b(confirm|approve)\b.*\b(generate|create|submit|proceed|continue)\b/,
|
|
460
|
-
];
|
|
461
|
-
return approvalPhrases.some(pattern => pattern.test(normalized));
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function normalizeDialogueMessage(runId, projectId, ev) {
|
|
465
|
-
const data = ev.data || ev;
|
|
466
|
-
const text = data.text || data.message || data.content || data.statusText || '';
|
|
467
|
-
if (!text && ev.type !== 'tool_call') return null;
|
|
468
|
-
const requiresApproval = Boolean(data.requires_approval || data.requiresApproval || data.action_required || data.actionRequired || inferRequiresApproval(text));
|
|
469
|
-
const message = {
|
|
470
|
-
type: 'message',
|
|
471
|
-
id: stableEventId('msg', runId, ev.seq, data.id || ev.id),
|
|
472
|
-
runId,
|
|
473
|
-
projectId,
|
|
474
|
-
seq: ev.seq,
|
|
475
|
-
status: data.status || undefined,
|
|
476
|
-
text: text || `${data.tool || 'tool_call'}${data.input?.description ? `: ${data.input.description}` : ''}`,
|
|
477
|
-
};
|
|
478
|
-
if (ev.type) message.source_type = ev.type;
|
|
479
|
-
if (requiresApproval) {
|
|
480
|
-
message.requires_approval = true;
|
|
481
|
-
message.approval_options = data.approval_options || data.approvalOptions || ['approve', 'revise', 'ask_user', 'continue'];
|
|
482
|
-
}
|
|
483
|
-
if (data.proposal) message.proposal = data.proposal;
|
|
484
|
-
return message;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function normalizeDialogueArtifact(runId, projectId, item, seq) {
|
|
488
|
-
if (!item) return null;
|
|
489
|
-
const kind = item.type || item.kind || (item.imageUrl ? 'image' : item.videoUrl ? 'video' : undefined);
|
|
490
|
-
if (!kind) return null;
|
|
491
|
-
const artifact = {
|
|
492
|
-
type: 'artifact',
|
|
493
|
-
id: stableEventId('artifact', runId, seq, item.id || item.snapshotId || item.taskId),
|
|
494
|
-
runId,
|
|
495
|
-
projectId,
|
|
496
|
-
seq,
|
|
497
|
-
kind,
|
|
498
|
-
status: item.status || (item.url || item.imageUrl || item.videoUrl || item.audioUrl ? 'completed' : 'running'),
|
|
499
|
-
};
|
|
500
|
-
const url = item.url || item.imageUrl || item.videoUrl || item.audioUrl;
|
|
501
|
-
if (url) artifact.url = url;
|
|
502
|
-
if (item.error) artifact.error = item.error;
|
|
503
|
-
if (item.taskId) artifact.taskId = item.taskId;
|
|
504
|
-
if (item.snapshotId) artifact.snapshotId = item.snapshotId;
|
|
505
|
-
return artifact;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function normalizeDialogueEvent(runId, projectId, ev) {
|
|
509
|
-
const data = ev.data || {};
|
|
510
|
-
if (ev.type === 'message' || ev.type === 'approval' || ev.type === 'artifact') {
|
|
511
|
-
return { ...data, ...ev, runId: ev.runId || runId, projectId: ev.projectId || projectId };
|
|
512
|
-
}
|
|
513
|
-
switch (ev.type) {
|
|
514
|
-
case 'content':
|
|
515
|
-
case 'status':
|
|
516
|
-
case 'tool_call':
|
|
517
|
-
case 'error':
|
|
518
|
-
return normalizeDialogueMessage(runId, projectId, ev);
|
|
519
|
-
case 'image':
|
|
520
|
-
return normalizeDialogueArtifact(runId, projectId, { type: 'image', status: data.imageUrl ? 'completed' : 'running', imageUrl: data.imageUrl, snapshotId: data.snapshotId }, ev.seq);
|
|
521
|
-
case 'render':
|
|
522
|
-
return normalizeDialogueArtifact(runId, projectId, { type: data.animation ? 'video' : 'design', status: data.published ? 'completed' : 'running', url: data.url, snapshotId: data.snapshotId }, ev.seq);
|
|
523
|
-
case 'animation_task':
|
|
524
|
-
case 'video_snapshot':
|
|
525
|
-
return normalizeDialogueArtifact(runId, projectId, { type: 'video', status: 'running', taskId: data.taskId, snapshotId: data.snapshotId }, ev.seq);
|
|
526
|
-
case 'music_task':
|
|
527
|
-
return normalizeDialogueArtifact(runId, projectId, { type: 'music', status: 'running', taskId: data.taskId }, ev.seq);
|
|
528
|
-
default:
|
|
529
|
-
return null;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function buildDialogueEvents(runId, data) {
|
|
534
|
-
const projectId = data.projectId || data.project_id;
|
|
535
|
-
const events = [];
|
|
536
|
-
for (const ev of data.events || []) {
|
|
537
|
-
const normalized = normalizeDialogueEvent(runId, projectId, ev);
|
|
538
|
-
if (normalized) events.push(normalized);
|
|
539
|
-
}
|
|
540
|
-
for (const item of data.output || []) {
|
|
541
|
-
const normalized = normalizeDialogueArtifact(runId, projectId, item, item.seq);
|
|
542
|
-
if (normalized && !events.some(ev => ev.type === 'artifact' && ev.id === normalized.id)) events.push(normalized);
|
|
543
|
-
}
|
|
544
|
-
for (const approval of loadApprovals().filter(item => item.runId === runId)) {
|
|
545
|
-
events.push(approval);
|
|
546
|
-
}
|
|
547
|
-
return events;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
function findUnhandledApprovalMessages(events) {
|
|
551
|
-
const approved = new Set(events.filter(ev => ev.type === 'approval').map(ev => ev.messageId || ev.message_id));
|
|
552
|
-
return events.filter(ev => ev.type === 'message' && ev.requires_approval && !approved.has(ev.id));
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async function fetchRun(baseUrl, headers, runId, opts = {}) {
|
|
556
|
-
const params = new URLSearchParams();
|
|
557
|
-
if (opts.events) params.set('events', 'true');
|
|
558
|
-
const suffix = params.toString() ? `?${params}` : '';
|
|
559
|
-
const res = await fetch(`${baseUrl}/api/agent/run/${runId}${suffix}`, { headers });
|
|
560
|
-
if (!res.ok) { process.stderr.write(`Error ${res.status}: ${await res.text()}\n`); process.exit(1); }
|
|
561
|
-
return normalizeRunResponse(await res.json());
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
async function printDialogueEvents(baseUrl, headers, runId, opts = {}) {
|
|
565
|
-
const { jsonl = false, failOnUnapproved = false, follow = false, interval = 5000 } = opts;
|
|
566
|
-
const printed = new Set();
|
|
567
|
-
|
|
568
|
-
while (true) {
|
|
569
|
-
const data = await fetchRun(baseUrl, headers, runId, { events: true });
|
|
570
|
-
const events = buildDialogueEvents(runId, data);
|
|
571
|
-
const unhandled = findUnhandledApprovalMessages(events);
|
|
572
|
-
if (failOnUnapproved && unhandled.length) {
|
|
573
|
-
process.stderr.write(`Unhandled Makaron message requires approval: ${unhandled.map(ev => ev.id).join(', ')}\n`);
|
|
574
|
-
process.exit(3);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const nextEvents = follow ? events.filter(ev => !printed.has(`${ev.type}:${ev.id || ev.seq}`)) : events;
|
|
578
|
-
for (const ev of nextEvents) {
|
|
579
|
-
printed.add(`${ev.type}:${ev.id || ev.seq}`);
|
|
580
|
-
if (jsonl) console.log(JSON.stringify(ev));
|
|
581
|
-
}
|
|
582
|
-
if (!jsonl) console.log(JSON.stringify(nextEvents, null, 2));
|
|
583
|
-
|
|
584
|
-
if (!follow || (!data.incomplete && ['completed', 'failed', 'aborted'].includes(data.status))) {
|
|
585
|
-
if (data.status === 'failed' || data.status === 'aborted') process.exit(1);
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
await new Promise(r => setTimeout(r, data.next_poll_after_ms || interval));
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function recordApproval(runId, messageId, choice, note) {
|
|
593
|
-
const approvals = loadApprovals();
|
|
594
|
-
const approval = {
|
|
595
|
-
type: 'approval',
|
|
596
|
-
id: `approval_${Date.now()}`,
|
|
597
|
-
runId,
|
|
598
|
-
messageId,
|
|
599
|
-
choice,
|
|
600
|
-
status: 'recorded',
|
|
601
|
-
createdAt: new Date().toISOString(),
|
|
602
|
-
};
|
|
603
|
-
if (note) approval.note = note;
|
|
604
|
-
approvals.push(approval);
|
|
605
|
-
saveApprovals(approvals);
|
|
606
|
-
console.log(JSON.stringify(approval));
|
|
607
|
-
}
|
|
608
|
-
|
|
609
432
|
// ─── Watch (incremental event stream) ───────────────────────────────────────
|
|
610
433
|
|
|
611
434
|
async function watchRun(baseUrl, headers, runId, opts = {}) {
|
|
@@ -763,6 +586,34 @@ async function listProjects(baseUrl, headers) {
|
|
|
763
586
|
console.log('');
|
|
764
587
|
}
|
|
765
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
|
+
|
|
766
617
|
function timeSince(date) {
|
|
767
618
|
const s = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
768
619
|
if (s < 60) return 'just now';
|
|
@@ -914,13 +765,15 @@ function probeLocalVideo(videoPath) {
|
|
|
914
765
|
return probeVideoWithFfprobe(videoPath) || probeVideoWithFfmpeg(videoPath);
|
|
915
766
|
}
|
|
916
767
|
|
|
917
|
-
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;
|
|
918
771
|
if (!fs.existsSync(videoPath)) {
|
|
919
772
|
return { ok: false, error: `Video file not found: ${videoPath}` };
|
|
920
773
|
}
|
|
921
774
|
const stat = fs.statSync(videoPath);
|
|
922
|
-
if (stat.size >
|
|
923
|
-
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.` };
|
|
924
777
|
}
|
|
925
778
|
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
926
779
|
if (!['mp4', 'mov', 'webm'].includes(ext)) {
|
|
@@ -930,8 +783,8 @@ function validateVideoFile(videoPath) {
|
|
|
930
783
|
if (!meta) {
|
|
931
784
|
return { ok: false, error: 'Cannot read video duration/resolution. Install ffmpeg/ffprobe or use the normal frontend upload flow.' };
|
|
932
785
|
}
|
|
933
|
-
if (meta.duration >
|
|
934
|
-
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)` };
|
|
935
788
|
}
|
|
936
789
|
if (meta.width * meta.height > MAX_VIDEO_FRAME_PIXELS) {
|
|
937
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.` };
|
|
@@ -948,8 +801,8 @@ function validateVideoFileForAnalysis(videoPath) {
|
|
|
948
801
|
if (stat.size === 0) {
|
|
949
802
|
return { ok: false, error: `Video file is empty: ${videoPath}` };
|
|
950
803
|
}
|
|
951
|
-
if (stat.size >
|
|
952
|
-
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.` };
|
|
953
806
|
}
|
|
954
807
|
const ext = path.extname(videoPath).slice(1).toLowerCase();
|
|
955
808
|
if (!['mp4', 'mov', 'webm'].includes(ext)) {
|
|
@@ -1135,7 +988,7 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1135
988
|
const uploadedVideoUrls = [...prevalidatedVideoUrlList];
|
|
1136
989
|
const uploadedVideoMetas = prevalidatedVideoUrlList.map(() => null);
|
|
1137
990
|
if (prevalidatedVideoUrlList.length) {
|
|
1138
|
-
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`);
|
|
1139
992
|
}
|
|
1140
993
|
for (const videoPath of prevalidatedVideoFileList) {
|
|
1141
994
|
process.stderr.write(`📹 Uploading ${path.basename(videoPath)} (${(fs.statSync(videoPath).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
@@ -1151,7 +1004,7 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1151
1004
|
|
|
1152
1005
|
// Add videos to project via projects/create (same as images)
|
|
1153
1006
|
if (uploadedVideoUrls.length === 0) {
|
|
1154
|
-
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`);
|
|
1155
1008
|
process.exit(1);
|
|
1156
1009
|
}
|
|
1157
1010
|
|
|
@@ -1240,32 +1093,6 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1240
1093
|
}
|
|
1241
1094
|
await watchRun(baseUrl, headers, runId, { interval, jsonl });
|
|
1242
1095
|
|
|
1243
|
-
} else if (sub === 'events' || sub === 'timeline') {
|
|
1244
|
-
const runId = args[2];
|
|
1245
|
-
if (!runId) { console.error(`Usage: makaron responses ${sub} <runId> [--jsonl] [--follow] [--interval <ms>] [--fail-on-unapproved]`); process.exit(1); }
|
|
1246
|
-
let interval = 5000, jsonl = false, follow = false, failOnUnapproved = false;
|
|
1247
|
-
for (let i = 3; i < args.length; i++) {
|
|
1248
|
-
if (args[i] === '--jsonl') jsonl = true;
|
|
1249
|
-
else if (args[i] === '--follow') follow = true;
|
|
1250
|
-
else if (args[i] === '--fail-on-unapproved') failOnUnapproved = true;
|
|
1251
|
-
else if (args[i] === '--interval' && args[i + 1]) interval = parseInt(args[++i]);
|
|
1252
|
-
}
|
|
1253
|
-
await printDialogueEvents(baseUrl, headers, runId, { interval, jsonl, follow, failOnUnapproved });
|
|
1254
|
-
|
|
1255
|
-
} else if (['approve', 'revise', 'ask-user', 'continue'].includes(sub)) {
|
|
1256
|
-
const messageId = args[2];
|
|
1257
|
-
if (!messageId) { console.error(`Usage: makaron responses ${sub} <messageId> --run <runId> [--note <text>]`); process.exit(1); }
|
|
1258
|
-
let runId = null, note = null;
|
|
1259
|
-
const noteParts = [];
|
|
1260
|
-
for (let i = 3; i < args.length; i++) {
|
|
1261
|
-
if (args[i] === '--run' && args[i + 1]) runId = args[++i];
|
|
1262
|
-
else if (args[i] === '--note' && args[i + 1]) note = args[++i];
|
|
1263
|
-
else noteParts.push(args[i]);
|
|
1264
|
-
}
|
|
1265
|
-
if (!runId) { console.error(`Usage: makaron responses ${sub} <messageId> --run <runId> [--note <text>]`); process.exit(1); }
|
|
1266
|
-
if (!note && noteParts.length) note = noteParts.join(' ');
|
|
1267
|
-
recordApproval(runId, messageId, sub === 'ask-user' ? 'ask_user' : sub, note);
|
|
1268
|
-
|
|
1269
1096
|
} else if (sub === 'list') {
|
|
1270
1097
|
let projectId = null;
|
|
1271
1098
|
for (let i = 2; i < args.length; i++) {
|
|
@@ -1289,12 +1116,6 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1289
1116
|
responses get <runId> Get status and output (JSON)
|
|
1290
1117
|
responses get <runId> --wait Poll until completed
|
|
1291
1118
|
responses get <runId> --pick <field> Extract: first_image_url, first_video_url, project_url, output
|
|
1292
|
-
responses events <runId> --jsonl Emit message/approval/artifact events for external Agents
|
|
1293
|
-
responses timeline <runId> --jsonl Alias for responses events
|
|
1294
|
-
responses approve <messageId> --run <runId> Record approval for a Makaron message
|
|
1295
|
-
responses revise <messageId> --run <runId> Record revision request for a Makaron message
|
|
1296
|
-
responses ask-user <messageId> --run <runId> Record that the user must decide
|
|
1297
|
-
responses continue <messageId> --run <runId> Record continue decision
|
|
1298
1119
|
responses watch <runId> --jsonl Watch until done (incremental events)
|
|
1299
1120
|
responses list --project <id> List runs for a project
|
|
1300
1121
|
`);
|
|
@@ -1302,6 +1123,19 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1302
1123
|
} else if (command === 'list' || command === 'ls') {
|
|
1303
1124
|
const { headers, baseUrl } = getAuth();
|
|
1304
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
|
+
}
|
|
1305
1139
|
} else if (command === 'abort') {
|
|
1306
1140
|
const { headers, baseUrl } = getAuth();
|
|
1307
1141
|
const runId = args[1];
|
|
@@ -1403,10 +1237,13 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1403
1237
|
let videoUrl = isHttpUrl(video) ? video : null;
|
|
1404
1238
|
let inputVideoMeta = null;
|
|
1405
1239
|
if (videoUrl) {
|
|
1406
|
-
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`);
|
|
1407
1241
|
}
|
|
1408
1242
|
if (video && !videoUrl) {
|
|
1409
|
-
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
|
+
});
|
|
1410
1247
|
if (!valid.ok) { console.error(`❌ ${valid.error}`); process.exit(1); }
|
|
1411
1248
|
inputVideoMeta = valid.meta;
|
|
1412
1249
|
process.stderr.write(`📹 Uploading ${path.basename(video)} (${(fs.statSync(video).size/1024/1024).toFixed(1)}MB)...\n`);
|
|
@@ -1419,7 +1256,7 @@ if (command === '--version' || command === '-v' || command === 'version') {
|
|
|
1419
1256
|
const vArgs = videoUrl
|
|
1420
1257
|
? { videoUrl, editPrompt: script, images, videoModel: videoModel || 'kling', referType: (videoModel || 'kling') === 'seedance' ? 'feature' : 'base' }
|
|
1421
1258
|
: { script, images };
|
|
1422
|
-
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);
|
|
1423
1260
|
if (effectiveDuration) vArgs.duration = effectiveDuration;
|
|
1424
1261
|
if (aspectRatio) vArgs.aspectRatio = aspectRatio;
|
|
1425
1262
|
if (videoModel && !videoUrl) vArgs.videoModel = videoModel;
|
|
@@ -1720,6 +1557,7 @@ Commands:
|
|
|
1720
1557
|
claim Get claim URL for human to link account
|
|
1721
1558
|
login Log in to Makaron (human interactive)
|
|
1722
1559
|
list (ls) List all projects
|
|
1560
|
+
project media <projectId> --json List timeline media for a project
|
|
1723
1561
|
create --image <file> Create project from local image
|
|
1724
1562
|
create --image-url <url> Create project from URL
|
|
1725
1563
|
create --title "name" Create empty project (text-to-image)
|
|
@@ -1732,7 +1570,6 @@ Commands:
|
|
|
1732
1570
|
|
|
1733
1571
|
responses get <runId> Get run status and results
|
|
1734
1572
|
responses get <runId> --wait Poll until completed
|
|
1735
|
-
responses events <runId> --jsonl Emit message/approval/artifact events
|
|
1736
1573
|
responses list --project <id> List runs for a project
|
|
1737
1574
|
abort <runId> Abort a running Agent
|
|
1738
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,39 +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, detect approval requirements, 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
|
-
```
|
|
137
|
-
|
|
138
|
-
Events use only three factual types:
|
|
139
|
-
|
|
140
|
-
```json
|
|
141
|
-
{"type":"message","id":"msg_1","runId":"run_xxx","seq":1,"text":"Makaron original message","requires_approval":true,"approval_options":["approve","revise","ask_user","continue"]}
|
|
142
|
-
{"type":"approval","messageId":"msg_1","choice":"approve","status":"recorded"}
|
|
143
|
-
{"type":"artifact","kind":"image","status":"completed","url":"https://..."}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
If a message has `requires_approval: true`, the external Agent must record an approval before continuing to wait for artifacts or claiming completion:
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
npx makaron-cli responses approve msg_1 --run <runId> --note "Proceed."
|
|
150
|
-
npx makaron-cli responses revise msg_1 --run <runId> "make it softer"
|
|
151
|
-
npx makaron-cli responses ask-user msg_1 --run <runId>
|
|
152
|
-
npx makaron-cli responses continue msg_1 --run <runId>
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
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:
|
|
156
|
-
|
|
157
|
-
```bash
|
|
158
|
-
npx makaron-cli responses events <runId> --jsonl
|
|
159
|
-
# alias: npx makaron-cli responses timeline <runId> --jsonl --fail-on-unapproved
|
|
160
|
-
```
|
|
161
|
-
|
|
162
139
|
### Extract specific results
|
|
163
140
|
|
|
164
141
|
```bash
|