makaron-cli 0.13.4 → 0.13.6

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.13.3",
4
- "description": "AI image editing, video generation, music creation, and marketplace skill workflows via CLI. Agents can self-register, install skills, create projects, and produce creative media.",
3
+ "version": "0.13.6",
4
+ "description": "Give Claude Code a creative agent. Pass complete creative requests and source media to Makaron Chat.",
5
5
  "author": {
6
- "name": "Makaron AI",
6
+ "name": "Versa AI",
7
+ "email": "tianyi@versa-ai.com",
7
8
  "url": "https://www.makaron.app"
8
9
  },
9
10
  "homepage": "https://www.makaron.app/agent",
10
- "repository": "https://github.com/vegekyd/ai-image-editor",
11
11
  "skills": "./skills/"
12
12
  }
@@ -1,21 +1,30 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.13.3",
4
- "description": "AI image editing, video generation, music creation, and marketplace skill workflows via CLI. Agents can self-register, install skills, create projects, and produce creative media.",
5
- "displayName": "Makaron",
6
- "shortDescription": "AI image/video/music creation from the terminal",
7
- "longDescription": "makaron.app is for humans. makaron-cli is for AI agents. Talk to Makaron Agent from the terminal — list, search, show, and install marketplace skills; create projects; edit images; generate videos; and compose music. Zero dependencies, single file, works with npx. Agents can self-register to get an API key without human intervention.",
3
+ "version": "0.13.6",
4
+ "description": "Give Codex a creative agent. Pass complete creative requests and source media to Makaron Chat.",
8
5
  "author": {
9
- "name": "Makaron AI",
6
+ "name": "Versa AI",
7
+ "email": "tianyi@versa-ai.com",
10
8
  "url": "https://www.makaron.app"
11
9
  },
12
10
  "homepage": "https://www.makaron.app/agent",
13
- "repository": "https://github.com/vegekyd/ai-image-editor",
14
11
  "license": "MIT",
15
- "keywords": ["ai", "image-editing", "video", "music", "agent", "creative", "skills"],
16
- "category": "Creative Tools",
17
- "capabilities": ["image-editing", "video-generation", "music-creation", "text-to-image", "marketplace-skills"],
12
+ "keywords": ["creative-production", "image-editing", "video-generation", "music", "agent", "cli"],
18
13
  "skills": "./skills/",
19
- "websiteURL": "https://www.makaron.app",
20
- "brandColor": "#d946ef"
14
+ "interface": {
15
+ "displayName": "Makaron Creative Studio",
16
+ "shortDescription": "Give Codex a creative agent",
17
+ "longDescription": "Pass any complete creative request and source media to Makaron Chat. Makaron plans and produces images, video, music, and motion design in a persistent project that a human can continue editing in makaron.app.",
18
+ "developerName": "Versa AI",
19
+ "category": "Creative Tools",
20
+ "capabilities": ["Read", "Write"],
21
+ "websiteURL": "https://www.makaron.app/agent",
22
+ "privacyPolicyURL": "https://www.makaron.app/privacy",
23
+ "defaultPrompt": [
24
+ "Pass this complete creative brief and its source files to Makaron Chat.",
25
+ "Ask Makaron Chat to turn this image into the finished media described in my request.",
26
+ "Continue this Makaron project with my complete follow-up request."
27
+ ],
28
+ "brandColor": "#d946ef"
29
+ }
21
30
  }
package/README.md CHANGED
@@ -151,6 +151,42 @@ npx makaron-cli project media <projectId> --json
151
151
 
152
152
  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.
153
153
 
154
+ External source ranges can be added without uploading either the original video or a derivative clip:
155
+
156
+ ```bash
157
+ npx makaron-cli project media add <projectId> \
158
+ --source-url "https://cdn.example.com/source.mp4" \
159
+ --start 12.5 --end 19 \
160
+ --description "Racket frame molding"
161
+
162
+ # Batch form: a JSON array or {"clips": [...]}
163
+ npx makaron-cli project media add <projectId> --input ranges.json --json
164
+ ```
165
+
166
+ The returned `<<<media_N>>>` is immediately usable by a later Agent run. Its range remains non-destructive metadata; Remotion must trim the original URL to these exact source bounds.
167
+ Use `description` as the provider-neutral media-understanding field. Put any
168
+ already-known summary, editorial purpose, concrete scene evidence, confidence,
169
+ and limitations there. Makaron exposes the full description in Media List
170
+ context so the Agent can edit from it without repeating image/video analysis
171
+ unless a required detail is missing or uncertain.
172
+
173
+ For an agent-to-agent handoff, create the project, import the external ranges,
174
+ and start the Agent in one command:
175
+
176
+ ```bash
177
+ npx makaron-cli chat --project auto \
178
+ --media-manifest set-01.json \
179
+ --json -b \
180
+ "Make a 30-second 9:16 TikTok with English VO and burned-in captions"
181
+ ```
182
+
183
+ The manifest is a JSON array or `{ "title": "...", "clips": [...] }`. Every
184
+ clip contains exactly `source_url`, `start`, `end`, and `description`; the time
185
+ values are seconds and array order is edit order. It is validated before project
186
+ creation and supports up to 20 clips for one Makaron task. Batch planning remains
187
+ the upstream orchestrator's responsibility: convert each plan into one manifest
188
+ and start one independent Makaron task.
189
+
154
190
  ### Export editable Remotion compositions
155
191
 
156
192
  Animated Remotion compositions are saved as editable timeline/code artifacts first. To materialize one into an MP4 that CLI, V, or another service can read, call the backend export worker:
package/bin/makaron.mjs CHANGED
@@ -156,6 +156,70 @@ function readJsonInput(filePath) {
156
156
  return JSON.parse(raw);
157
157
  }
158
158
 
159
+ const MAX_MEDIA_MANIFEST_RANGES = 20;
160
+
161
+ function normalizeMediaManifest(input) {
162
+ const manifest = Array.isArray(input) ? { clips: input } : input;
163
+ if (!manifest || typeof manifest !== 'object') {
164
+ throw new Error('Media manifest must be a JSON object or an array of source ranges.');
165
+ }
166
+ const rawRanges = Array.isArray(manifest.clips)
167
+ ? manifest.clips
168
+ : Array.isArray(manifest.source_ranges)
169
+ ? manifest.source_ranges
170
+ : Array.isArray(manifest.sourceRanges)
171
+ ? manifest.sourceRanges
172
+ : null;
173
+ if (!rawRanges?.length) {
174
+ throw new Error('Media manifest must contain a non-empty clips array.');
175
+ }
176
+ if (rawRanges.length > MAX_MEDIA_MANIFEST_RANGES) {
177
+ throw new Error(`Media manifest supports at most ${MAX_MEDIA_MANIFEST_RANGES} source ranges per Makaron task.`);
178
+ }
179
+
180
+ const sourceRanges = rawRanges.map((raw, index) => {
181
+ if (!raw || typeof raw !== 'object') throw new Error(`clips[${index}] must be an object.`);
182
+ const sourceUrl = typeof raw.source_url === 'string' ? raw.source_url.trim() : '';
183
+ let parsed;
184
+ try {
185
+ parsed = new URL(sourceUrl);
186
+ } catch {
187
+ throw new Error(`clips[${index}].source_url must be a valid HTTP(S) URL.`);
188
+ }
189
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
190
+ throw new Error(`clips[${index}].source_url must use HTTP or HTTPS.`);
191
+ }
192
+ const start = Number(raw.start ?? raw.start_sec);
193
+ const end = Number(raw.end ?? raw.end_sec);
194
+ if (!Number.isFinite(start) || start < 0) {
195
+ throw new Error(`clips[${index}].start must be a finite number >= 0.`);
196
+ }
197
+ if (!Number.isFinite(end) || end <= start) {
198
+ throw new Error(`clips[${index}].end must be greater than start.`);
199
+ }
200
+ return {
201
+ source_url: sourceUrl,
202
+ start,
203
+ end,
204
+ description: typeof raw.description === 'string' ? raw.description.trim() : '',
205
+ };
206
+ });
207
+
208
+ return {
209
+ sourceRanges,
210
+ title: typeof manifest.title === 'string' && manifest.title.trim() ? manifest.title.trim() : undefined,
211
+ };
212
+ }
213
+
214
+ function readMediaManifest(filePath) {
215
+ try {
216
+ return normalizeMediaManifest(readJsonInput(filePath));
217
+ } catch (error) {
218
+ const message = error instanceof Error ? error.message : String(error);
219
+ throw new Error(`Invalid media manifest: ${message}`);
220
+ }
221
+ }
222
+
159
223
  // ─── Auth ────────────────────────────────────────────────────────────────────
160
224
 
161
225
  function loadAuth() {
@@ -285,6 +349,7 @@ Options:
285
349
  --image <file|url> Attach a reference image or screenshot. Repeatable.
286
350
  --video <file|url> Attach a video to the project timeline. Repeatable.
287
351
  --audio <file|url> Attach a song, beat, or voice reference. MP3/WAV, repeatable.
352
+ --media-manifest <file|-> Import source_url + start + end + description clips before this run.
288
353
  --skill <id|label|name> Use an installed skill or auto-install a matched marketplace skill.
289
354
  --background, -b Submit and print a runId.
290
355
  --json Output structured JSON.
@@ -313,6 +378,9 @@ What you can ask:
313
378
  Video cuts and assembly
314
379
  makaron chat --project <id> --video clip.mp4 "cut out the dead air and keep the best 20 seconds"
315
380
 
381
+ Agent-to-agent source-range handoff
382
+ makaron chat --project auto --media-manifest set-01.json -b --json "make a 30s vertical video"
383
+
316
384
  Music
317
385
  makaron chat --project <id> "add calm piano background music"
318
386
 
@@ -826,9 +894,33 @@ async function listProjectMedia(baseUrl, headers, projectId, opts = {}) {
826
894
  const status = item.status && item.status !== 'completed' ? ` ${item.status}` : '';
827
895
  const duration = typeof item.duration === 'number' ? ` ${item.duration}s` : '';
828
896
  const dimensions = item.width && item.height ? ` ${item.width}x${item.height}` : '';
829
- const description = item.description ? ` — ${item.description}` : '';
897
+ const description = item.description ? ` — ${String(item.description).replace(/\s*\n\s*/g, ' | ')}` : '';
898
+ const sourceRange = item.source_range || item.sourceRange;
899
+ const range = sourceRange
900
+ ? ` source ${formatSeconds(sourceRange.start_sec)}-${formatSeconds(sourceRange.end_sec)}s`
901
+ : '';
830
902
  const url = item.url ? `\n ${item.url}` : '';
831
- console.log(` ${String(item.index).padStart(2)}. ${ref} [${item.type}${status}${duration}${dimensions}]${description}${url}`);
903
+ console.log(` ${String(item.index).padStart(2)}. ${ref} [${item.type}${status}${duration}${dimensions}${range}]${description}${url}`);
904
+ }
905
+ return data;
906
+ }
907
+
908
+ async function addProjectMediaSourceRanges(baseUrl, headers, projectId, ranges, opts = {}) {
909
+ const res = await fetch(`${baseUrl}/api/projects/${projectId}/media`, {
910
+ method: 'POST',
911
+ headers: { 'Content-Type': 'application/json', ...headers },
912
+ body: JSON.stringify({ clips: ranges }),
913
+ });
914
+ if (!res.ok) { console.error('Project media add failed:', await res.text()); process.exit(1); }
915
+ const data = await res.json();
916
+ if (opts.json && !opts.silent) {
917
+ console.log(JSON.stringify(data, null, 2));
918
+ return data;
919
+ }
920
+ if (!opts.silent) {
921
+ for (const item of data.media || []) {
922
+ console.log(`${item.ref} ${item.source_url} ${formatSeconds(item.start_sec)}-${formatSeconds(item.end_sec)}s${item.created ? '' : ' (existing)'}`);
923
+ }
832
924
  }
833
925
  return data;
834
926
  }
@@ -1593,6 +1685,8 @@ Commands:
1593
1685
  credits Show current credit balance
1594
1686
  list (ls) List all projects
1595
1687
  project media <projectId> --json List timeline media for a project
1688
+ project media add <projectId> --source-url <url> --start <n> --end <n>
1689
+ Add an external source range without uploading video
1596
1690
  create --image <file> Create project from local image
1597
1691
  create --image-url <url> Create project from URL
1598
1692
  create --title "name" Create empty project (text-to-image)
@@ -1601,6 +1695,8 @@ Commands:
1601
1695
  chat --project <id> --skill <id> Use a built-in or marketplace skill
1602
1696
  chat --project <id> --video <file> Attach video to conversation
1603
1697
  chat --project <id> --audio <file> Attach song/beat/voice reference
1698
+ chat --project auto --media-manifest <file> "message"
1699
+ Create, import external ranges, and run Agent
1604
1700
  chat --project <id> -b "message" Background: submit and print runId
1605
1701
  chat --project <id> --stream "msg" Legacy: stream SSE in real-time
1606
1702
  chat --project <id> --json "msg" Output structured JSON result
@@ -1708,9 +1804,12 @@ function printHelp(topic, subtopic) {
1708
1804
  } else if (topic === 'credits' || topic === 'credit' || topic === 'balance') {
1709
1805
  console.log('Usage: makaron credits [--json]');
1710
1806
  } else if (topic === 'project' || topic === 'projects') {
1711
- if (subtopic === 'media') console.log('Usage: makaron project media <projectId> [--json]');
1807
+ if (subtopic === 'media') console.log(`Usage: makaron project media <projectId> [--json]
1808
+ makaron project media add <projectId> --source-url <url> --start <n> --end <n> [--description <text>] [--json]
1809
+ makaron project media add <projectId> --input <ranges.json> [--json]`);
1712
1810
  else console.log(`Project commands:
1713
1811
  project media <projectId> --json List timeline media for a project
1812
+ project media add <projectId> ... Add external source_url + start + end media
1714
1813
  `);
1715
1814
  } else if (topic === 'abort') {
1716
1815
  console.log('Usage: makaron abort <runId>');
@@ -1868,11 +1967,20 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1868
1967
  let background = false;
1869
1968
  let jsonOutput = false;
1870
1969
  let activeSkill = undefined;
1970
+ let mediaManifestPath = undefined;
1871
1971
  for (let i = 1; i < args.length; i++) {
1872
1972
  if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
1873
1973
  else if (args[i] === '--image' && args[i + 1]) chatImages.push(args[++i]);
1874
1974
  else if (args[i] === '--video' && args[i + 1]) chatVideos.push(args[++i]);
1875
1975
  else if (args[i] === '--audio' && args[i + 1]) chatAudios.push(args[++i]);
1976
+ else if (args[i] === '--media-manifest') {
1977
+ if (!args[i + 1] || args[i + 1].startsWith('--')) {
1978
+ process.stderr.write('❌ --media-manifest requires a JSON file path or -.\n');
1979
+ process.exit(1);
1980
+ }
1981
+ mediaManifestPath = args[++i];
1982
+ }
1983
+ else if (args[i].startsWith('--media-manifest=')) mediaManifestPath = args[i].slice('--media-manifest='.length);
1876
1984
  else if (args[i] === '--skill' && args[i + 1]) activeSkill = args[++i];
1877
1985
  else if (args[i].startsWith('--skill=')) activeSkill = args[i].slice('--skill='.length);
1878
1986
  else if (args[i] === '--stream') useStream = true;
@@ -1898,6 +2006,15 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1898
2006
  console.error('Run: makaron chat --help');
1899
2007
  process.exit(1);
1900
2008
  }
2009
+ let mediaManifest;
2010
+ if (mediaManifestPath) {
2011
+ try {
2012
+ mediaManifest = readMediaManifest(mediaManifestPath);
2013
+ } catch (error) {
2014
+ process.stderr.write(`❌ ${error instanceof Error ? error.message : String(error)}\n`);
2015
+ process.exit(1);
2016
+ }
2017
+ }
1901
2018
  const { headers, baseUrl } = getAuth();
1902
2019
  // Split images into URLs vs local files
1903
2020
  const imageUrlList = chatImages.filter(p => p.startsWith('http://') || p.startsWith('https://'));
@@ -1954,13 +2071,31 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
1954
2071
  const res = await fetch(`${baseUrl}/api/projects/create`, {
1955
2072
  method: 'POST',
1956
2073
  headers: { 'Content-Type': 'application/json', ...headers },
1957
- body: JSON.stringify({ title: prompt.slice(0, 50) }),
2074
+ body: JSON.stringify({ title: mediaManifest?.title || prompt.slice(0, 50) }),
1958
2075
  });
1959
2076
  if (!res.ok) { process.stderr.write(`❌ Failed to create project: ${await res.text()}\n`); process.exit(1); }
1960
2077
  const data = await res.json();
1961
2078
  projectId = data.projectId;
1962
2079
  process.stderr.write(`📦 Project created: ${projectId}\n`);
1963
2080
  }
2081
+ let importedManifestMedia = [];
2082
+ if (mediaManifest) {
2083
+ const imported = await addProjectMediaSourceRanges(
2084
+ baseUrl,
2085
+ headers,
2086
+ projectId,
2087
+ mediaManifest.sourceRanges,
2088
+ { silent: true },
2089
+ );
2090
+ importedManifestMedia = imported.media || [];
2091
+ if (importedManifestMedia.length !== mediaManifest.sourceRanges.length) {
2092
+ process.stderr.write(`❌ Imported ${importedManifestMedia.length}/${mediaManifest.sourceRanges.length} source range(s); aborting run.\n`);
2093
+ process.exit(1);
2094
+ }
2095
+ uploadedTurnMediaCount += importedManifestMedia.length;
2096
+ uploadedTurnVideoCount += importedManifestMedia.length;
2097
+ process.stderr.write(`📎 Imported ${importedManifestMedia.length} external source range(s) from media manifest\n`);
2098
+ }
1964
2099
  // Upload additional images to existing project
1965
2100
  if (imageFileList.length > 0 || imageUrlList.length > 0) {
1966
2101
  const uploadedImageUrls = imageFileList.length
@@ -2106,7 +2241,20 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2106
2241
  if (background) {
2107
2242
  // Just print runId and exit
2108
2243
  if (jsonOutput) {
2109
- console.log(JSON.stringify({ runId, projectId, projectUrl: `${APP_URL}/projects/${projectId}`, status: 'running' }));
2244
+ console.log(JSON.stringify({
2245
+ runId,
2246
+ projectId,
2247
+ projectUrl: `${APP_URL}/projects/${projectId}`,
2248
+ status: 'running',
2249
+ ...(importedManifestMedia.length ? {
2250
+ importedMedia: importedManifestMedia.map(item => ({
2251
+ ref: item.ref,
2252
+ source_url: item.source_url,
2253
+ start_sec: item.start_sec,
2254
+ end_sec: item.end_sec,
2255
+ })),
2256
+ } : {}),
2257
+ }));
2110
2258
  } else {
2111
2259
  console.log(runId);
2112
2260
  }
@@ -2344,13 +2492,48 @@ if (!command || command === '--help' || command === '-h' || command === 'help')
2344
2492
  const { headers, baseUrl } = getAuth();
2345
2493
  const sub = args[1];
2346
2494
  if (sub === 'media') {
2347
- const projectId = args[2];
2348
- if (!projectId) { console.error('Usage: makaron project media <projectId> [--json]'); process.exit(1); }
2349
2495
  const jsonOutput = args.includes('--json');
2350
- await listProjectMedia(baseUrl, headers, projectId, { json: jsonOutput });
2496
+ if (args[2] === 'add') {
2497
+ const projectId = args[3];
2498
+ if (!projectId) { console.error('Usage: makaron project media add <projectId> --source-url <url> --start <n> --end <n>'); process.exit(1); }
2499
+ const readOption = (name) => {
2500
+ const index = args.indexOf(name);
2501
+ return index >= 0 ? args[index + 1] : undefined;
2502
+ };
2503
+ const inputPath = readOption('--input');
2504
+ let ranges;
2505
+ if (inputPath) {
2506
+ const input = readJsonInput(inputPath);
2507
+ ranges = Array.isArray(input) ? input : input.clips || input.source_ranges || input.sourceRanges;
2508
+ } else {
2509
+ const sourceUrl = readOption('--source-url');
2510
+ const start = Number(readOption('--start') ?? readOption('--start-sec'));
2511
+ const end = Number(readOption('--end') ?? readOption('--end-sec'));
2512
+ if (!sourceUrl || !Number.isFinite(start) || !Number.isFinite(end)) {
2513
+ console.error('Provide --source-url, --start, and --end, or --input <manifest.json>.');
2514
+ process.exit(1);
2515
+ }
2516
+ ranges = [{
2517
+ source_url: sourceUrl,
2518
+ start,
2519
+ end,
2520
+ ...(readOption('--description') ? { description: readOption('--description') } : {}),
2521
+ }];
2522
+ }
2523
+ if (!Array.isArray(ranges) || !ranges.length) {
2524
+ console.error('Input must contain a non-empty clips array.');
2525
+ process.exit(1);
2526
+ }
2527
+ await addProjectMediaSourceRanges(baseUrl, headers, projectId, ranges, { json: jsonOutput });
2528
+ } else {
2529
+ const projectId = args[2];
2530
+ if (!projectId) { console.error('Usage: makaron project media <projectId> [--json]'); process.exit(1); }
2531
+ await listProjectMedia(baseUrl, headers, projectId, { json: jsonOutput });
2532
+ }
2351
2533
  } else {
2352
2534
  console.log(`Project commands:
2353
2535
  project media <projectId> --json List timeline media for a project
2536
+ project media add <projectId> ... Add external source_url + start + end media
2354
2537
  `);
2355
2538
  }
2356
2539
  } else if (command === 'abort') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "makaron-cli",
3
- "version": "0.13.4",
3
+ "version": "0.13.6",
4
4
  "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -149,6 +149,21 @@ npx makaron-cli project media <projectId> --json
149
149
 
150
150
  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.
151
151
 
152
+ Publish an external video interval directly into that Media List without uploading the original or a derivative MP4:
153
+
154
+ ```bash
155
+ npx makaron-cli project media add <projectId> --source-url "https://cdn.example.com/source.mp4" --start 12.5 --end 19 --description "Racket frame molding"
156
+ npx makaron-cli project media add <projectId> --input ranges.json --json
157
+ ```
158
+
159
+ The JSON input may be an array or `{ "clips": [...] }`. Each clip has exactly `source_url + start + end + description`; `start` and `end` are seconds, array order is edit order, and `source_url` is opaque. Do not add or request provider-specific identity fields. Put existing media understanding (summary, editorial purpose, scene evidence, confidence, and limitations) in `description`. Makaron reads that provider-neutral Media List field before deciding whether any additional image/video analysis is needed.
160
+
161
+ For one-call orchestration, use `chat --project auto --media-manifest plan.json`. Makaron validates the manifest, creates the project, imports its ranges, and starts the Agent. If an upstream service returns multiple plans, the caller should start one independent Makaron task per plan instead of passing the provider-specific batch response into Makaron.
162
+
163
+ ```bash
164
+ npx makaron-cli chat --project auto --media-manifest set-01.json --json -b "Make a 30-second 9:16 TikTok with English VO and captions"
165
+ ```
166
+
152
167
  ### With video input (edit, compose, extend)
153
168
 
154
169
  ```bash