makaron-cli 0.1.0

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.
Files changed (2) hide show
  1. package/bin/makaron.mjs +377 -0
  2. package/package.json +18 -0
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Makaron CLI — Talk to Makaron Agent from the terminal.
4
+ *
5
+ * Usage:
6
+ * npx makaron-cli login
7
+ * npx makaron-cli create --image photo.jpg
8
+ * npx makaron-cli chat --project <id> "make it look cinematic"
9
+ * npx makaron-cli list
10
+ */
11
+
12
+ import fs from 'fs';
13
+ import path from 'path';
14
+ import { createInterface } from 'readline';
15
+
16
+ // ─── Config ──────────────────────────────────────────────────────────────────
17
+
18
+ const AUTH_FILE = path.join(process.env.HOME || '~', '.makaron', 'auth.json');
19
+ const DEFAULT_URL = 'https://www.makaron.app';
20
+ const BASE_URL = process.env.MAKARON_URL || DEFAULT_URL;
21
+ const APP_URL = process.env.MAKARON_APP_URL || DEFAULT_URL;
22
+
23
+ // Public anon key (safe to embed — only enables auth, not data access)
24
+ const SUPABASE_URL = 'https://sdyrtztrjgmmpnirswxt.supabase.co';
25
+ const SUPABASE_ANON_KEY = 'sb_publishable_FJFN2YYaWaQjABUKLqxQcA_fhxPLFDY';
26
+
27
+ // ─── Auth ────────────────────────────────────────────────────────────────────
28
+
29
+ function loadAuth() {
30
+ try {
31
+ return JSON.parse(fs.readFileSync(AUTH_FILE, 'utf-8'));
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+
37
+ function saveAuth(data) {
38
+ const dir = path.dirname(AUTH_FILE);
39
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
40
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2));
41
+ }
42
+
43
+ function buildCookie(tokenJson) {
44
+ const url = tokenJson._supabaseUrl || SUPABASE_URL;
45
+ const ref = url.match(/\/\/([^.]+)\./)?.[1] || '';
46
+ const encoded = encodeURIComponent(JSON.stringify(tokenJson));
47
+ return `sb-${ref}-auth-token=${encoded}`;
48
+ }
49
+
50
+ async function login() {
51
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
52
+ const ask = (q) => new Promise(r => rl.question(q, r));
53
+
54
+ const email = await ask('Email: ');
55
+ const password = await ask('Password: ');
56
+ rl.close();
57
+
58
+ const supabaseUrl = process.env.SUPABASE_URL || SUPABASE_URL;
59
+ const anonKey = process.env.SUPABASE_ANON_KEY || SUPABASE_ANON_KEY;
60
+
61
+ const res = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=password`, {
62
+ method: 'POST',
63
+ headers: { 'apikey': anonKey, 'Content-Type': 'application/json' },
64
+ body: JSON.stringify({ email, password }),
65
+ });
66
+
67
+ if (!res.ok) {
68
+ console.error('Login failed:', await res.text());
69
+ process.exit(1);
70
+ }
71
+
72
+ const tokenJson = await res.json();
73
+ tokenJson._supabaseUrl = supabaseUrl;
74
+ tokenJson._baseUrl = BASE_URL;
75
+ saveAuth(tokenJson);
76
+ console.error(`✅ Logged in as ${email}`);
77
+ console.error(` Token saved to ${AUTH_FILE}`);
78
+ }
79
+
80
+ function getAuthCookie() {
81
+ const auth = loadAuth();
82
+ if (!auth) {
83
+ console.error('Not logged in. Run: npx makaron-cli login');
84
+ process.exit(1);
85
+ }
86
+ return { cookie: buildCookie(auth), baseUrl: auth._baseUrl || BASE_URL };
87
+ }
88
+
89
+ // ─── SSE Consumer ────────────────────────────────────────────────────────────
90
+
91
+ async function streamAgent(baseUrl, cookie, projectId, prompt) {
92
+ const res = await fetch(`${baseUrl}/api/agent`, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ 'Cookie': cookie,
97
+ },
98
+ body: JSON.stringify({
99
+ projectId,
100
+ prompt,
101
+ headless: true,
102
+ }),
103
+ });
104
+
105
+ if (!res.ok) {
106
+ console.error(`Error ${res.status}:`, await res.text());
107
+ process.exit(1);
108
+ }
109
+
110
+ const runId = res.headers.get('X-Agent-Run-Id');
111
+ const reader = res.body.getReader();
112
+ const decoder = new TextDecoder();
113
+ let buffer = '';
114
+
115
+ const results = { images: [], designs: [], animationTasks: [], musicTasks: [], text: '' };
116
+
117
+ while (true) {
118
+ const { done, value } = await reader.read();
119
+ if (done) break;
120
+
121
+ buffer += decoder.decode(value, { stream: true });
122
+ const lines = buffer.split('\n');
123
+ buffer = lines.pop() || '';
124
+
125
+ for (const line of lines) {
126
+ if (!line.startsWith('data: ')) continue;
127
+ let event;
128
+ try { event = JSON.parse(line.slice(6)); } catch { continue; }
129
+
130
+ switch (event.type) {
131
+ case 'content':
132
+ process.stdout.write(event.text);
133
+ results.text += event.text;
134
+ break;
135
+
136
+ case 'status':
137
+ process.stderr.write(`\r⏳ ${event.text}`);
138
+ break;
139
+
140
+ case 'tool_call':
141
+ process.stderr.write(`\n🔧 ${event.tool}`);
142
+ if (event.input?.editPrompt) process.stderr.write(`: ${event.input.editPrompt.substring(0, 80)}`);
143
+ if (event.input?.description) process.stderr.write(`: ${event.input.description.substring(0, 80)}`);
144
+ process.stderr.write('\n');
145
+ break;
146
+
147
+ case 'image':
148
+ results.images.push({ snapshotId: event.snapshotId, imageUrl: event.imageUrl });
149
+ process.stderr.write(`\n🖼️ Image: ${event.imageUrl || '(uploading...)'}\n`);
150
+ break;
151
+
152
+ case 'render':
153
+ if (event.published) {
154
+ const desc = event.animation
155
+ ? `${event.animation.durationInSeconds}s video (${event.width}x${event.height})`
156
+ : `still design (${event.width}x${event.height})`;
157
+ results.designs.push({ snapshotId: event.snapshotId, desc });
158
+ process.stderr.write(`\n🎨 Design published: ${desc}\n`);
159
+ }
160
+ break;
161
+
162
+ case 'animation_task':
163
+ results.animationTasks.push({ taskId: event.taskId, prompt: event.prompt });
164
+ process.stderr.write(`\n🎬 Video submitted: ${event.taskId}\n`);
165
+ break;
166
+
167
+ case 'music_task':
168
+ results.musicTasks.push({ taskId: event.taskId });
169
+ process.stderr.write(`\n🎵 Music submitted: ${event.taskId}\n`);
170
+ break;
171
+
172
+ case 'error':
173
+ process.stderr.write(`\n❌ Error: ${event.message}\n`);
174
+ break;
175
+
176
+ case 'done':
177
+ break;
178
+ }
179
+ }
180
+ }
181
+
182
+ if (results.text) process.stdout.write('\n');
183
+ return { runId, results };
184
+ }
185
+
186
+ // ─── Async Task Polling ──────────────────────────────────────────────────────
187
+
188
+ async function pollVideo(baseUrl, cookie, taskId) {
189
+ process.stderr.write(`🎬 Waiting for video ${taskId}...\n`);
190
+ const start = Date.now();
191
+ while (true) {
192
+ await new Promise(r => setTimeout(r, 10_000));
193
+ const elapsed = Math.round((Date.now() - start) / 1000);
194
+ try {
195
+ const res = await fetch(`${baseUrl}/api/animate/${taskId}`, { headers: { 'Cookie': cookie } });
196
+ if (!res.ok) continue;
197
+ const data = await res.json();
198
+ if (data.videoUrl) { process.stderr.write(`\r🎬 Video done (${elapsed}s): ${data.videoUrl}\n`); return data.videoUrl; }
199
+ if (data.status === 'failed') { process.stderr.write(`\r🎬 Video failed (${elapsed}s)\n`); return null; }
200
+ process.stderr.write(`\r🎬 Video rendering... ${elapsed}s`);
201
+ } catch { /* retry */ }
202
+ if (elapsed > 600) { process.stderr.write(`\r🎬 Video timeout (${elapsed}s)\n`); return null; }
203
+ }
204
+ }
205
+
206
+ async function pollMusic(baseUrl, cookie, taskId) {
207
+ process.stderr.write(`🎵 Waiting for music ${taskId}...\n`);
208
+ const start = Date.now();
209
+ while (true) {
210
+ await new Promise(r => setTimeout(r, 5_000));
211
+ const elapsed = Math.round((Date.now() - start) / 1000);
212
+ try {
213
+ const res = await fetch(`${baseUrl}/api/music/${taskId}`, { headers: { 'Cookie': cookie } });
214
+ if (!res.ok) continue;
215
+ const data = await res.json();
216
+ const trackUrl = data.audioUrl || data.tracks?.[0]?.audioUrl;
217
+ const streamUrl = data.streamAudioUrl || data.tracks?.[0]?.streamAudioUrl;
218
+ if (data.status === 'completed' || trackUrl) {
219
+ const tracks = data.tracks || [];
220
+ if (tracks.length > 1) {
221
+ process.stderr.write(`\r🎵 Music done (${elapsed}s): ${tracks.length} tracks\n`);
222
+ tracks.forEach((t, i) => process.stderr.write(` ${i + 1}. ${t.title} (${Math.round(t.duration)}s) — ${t.audioUrl}\n`));
223
+ } else {
224
+ process.stderr.write(`\r🎵 Music done (${elapsed}s): ${trackUrl}\n`);
225
+ }
226
+ return trackUrl;
227
+ }
228
+ if (streamUrl && elapsed > 20) process.stderr.write(`\r🎵 Music streaming: ${streamUrl}\n`);
229
+ if (data.status === 'failed') { process.stderr.write(`\r🎵 Music failed (${elapsed}s)\n`); return null; }
230
+ process.stderr.write(`\r🎵 Music generating... ${elapsed}s`);
231
+ } catch { /* retry */ }
232
+ if (elapsed > 300) { process.stderr.write(`\r🎵 Music timeout (${elapsed}s)\n`); return null; }
233
+ }
234
+ }
235
+
236
+ // ─── Create Project ──────────────────────────────────────────────────────────
237
+
238
+ async function createProject(baseUrl, cookie, opts) {
239
+ const body = {};
240
+ if (opts.imageUrls?.length) {
241
+ body.imageUrls = opts.imageUrls;
242
+ } else if (opts.images?.length) {
243
+ body.imageBase64s = opts.images.map(f => {
244
+ const buf = fs.readFileSync(f);
245
+ return `data:image/jpeg;base64,${buf.toString('base64')}`;
246
+ });
247
+ } else if (opts.imageUrl) {
248
+ body.imageUrl = opts.imageUrl;
249
+ } else if (opts.image) {
250
+ const buf = fs.readFileSync(opts.image);
251
+ body.imageBase64 = `data:image/jpeg;base64,${buf.toString('base64')}`;
252
+ }
253
+ if (opts.title) body.title = opts.title;
254
+
255
+ const res = await fetch(`${baseUrl}/api/projects/create`, {
256
+ method: 'POST',
257
+ headers: { 'Content-Type': 'application/json', 'Cookie': cookie },
258
+ body: JSON.stringify(body),
259
+ });
260
+
261
+ if (!res.ok) { console.error('Create failed:', await res.text()); process.exit(1); }
262
+
263
+ const data = await res.json();
264
+ console.log(`✅ Project created`);
265
+ console.log(` ID: ${data.projectId}`);
266
+ if (data.snapshots?.length) {
267
+ console.log(` Images: ${data.snapshots.length}`);
268
+ data.snapshots.forEach((s, i) => console.log(` [${i + 1}] ${s.imageUrl}`));
269
+ }
270
+ console.log(` URL: ${data.projectUrl}`);
271
+ return data;
272
+ }
273
+
274
+ // ─── List Projects ───────────────────────────────────────────────────────────
275
+
276
+ async function listProjects(baseUrl, cookie) {
277
+ const res = await fetch(`${baseUrl}/api/projects/list`, { headers: { 'Cookie': cookie } });
278
+ if (!res.ok) { console.error('List failed:', await res.text()); process.exit(1); }
279
+ const { projects } = await res.json();
280
+ if (!projects.length) { console.log('No projects yet. Create one with: makaron create --image <file>'); return; }
281
+ console.log(`📁 ${projects.length} projects\n`);
282
+ for (const p of projects) {
283
+ const age = timeSince(new Date(p.updatedAt));
284
+ console.log(` ${p.id} ${p.title.padEnd(30)} ${String(p.snapshotCount).padStart(2)} snaps ${age}`);
285
+ }
286
+ console.log('');
287
+ }
288
+
289
+ function timeSince(date) {
290
+ const s = Math.floor((Date.now() - date.getTime()) / 1000);
291
+ if (s < 60) return 'just now';
292
+ if (s < 3600) return `${Math.floor(s / 60)}m ago`;
293
+ if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
294
+ return `${Math.floor(s / 86400)}d ago`;
295
+ }
296
+
297
+ // ─── Main ────────────────────────────────────────────────────────────────────
298
+
299
+ const args = process.argv.slice(2);
300
+ const command = args[0];
301
+
302
+ if (command === 'login') {
303
+ await login();
304
+ } else if (command === 'create') {
305
+ const { cookie, baseUrl } = getAuthCookie();
306
+ const opts = { images: [], imageUrls: [] };
307
+ for (let i = 1; i < args.length; i++) {
308
+ if (args[i] === '--image' && args[i + 1]) opts.images.push(args[++i]);
309
+ else if (args[i] === '--image-url' && args[i + 1]) opts.imageUrls.push(args[++i]);
310
+ else if (args[i] === '--title' && args[i + 1]) opts.title = args[++i];
311
+ }
312
+ if (opts.images.length === 1) { opts.image = opts.images[0]; opts.images = []; }
313
+ if (opts.imageUrls.length === 1) { opts.imageUrl = opts.imageUrls[0]; opts.imageUrls = []; }
314
+ if (!opts.image && !opts.imageUrl && !opts.images.length && !opts.imageUrls.length && !opts.title) {
315
+ console.error('Usage: makaron create --image <file> [--image <file2>] or --title "name"');
316
+ process.exit(1);
317
+ }
318
+ await createProject(baseUrl, cookie, opts);
319
+ } else if (command === 'chat') {
320
+ const { cookie, baseUrl } = getAuthCookie();
321
+ let projectId = null;
322
+ const chatImages = [];
323
+ const promptParts = [];
324
+ for (let i = 1; i < args.length; i++) {
325
+ if (args[i] === '--project' && args[i + 1]) projectId = args[++i];
326
+ else if (args[i] === '--image' && args[i + 1]) chatImages.push(args[++i]);
327
+ else promptParts.push(args[i]);
328
+ }
329
+ const prompt = promptParts.join(' ');
330
+ if (!projectId || !prompt) {
331
+ console.error('Usage: makaron chat --project <id> [--image <file>] "your message"');
332
+ process.exit(1);
333
+ }
334
+ if (chatImages.length > 0) {
335
+ const base64s = chatImages.map(imgPath => {
336
+ process.stderr.write(`📤 Uploading ${path.basename(imgPath)}...\n`);
337
+ const buf = fs.readFileSync(imgPath);
338
+ return `data:image/jpeg;base64,${buf.toString('base64')}`;
339
+ });
340
+ const res = await fetch(`${baseUrl}/api/projects/create`, {
341
+ method: 'POST',
342
+ headers: { 'Content-Type': 'application/json', 'Cookie': cookie },
343
+ body: JSON.stringify({ imageBase64s: base64s, _addToProject: projectId }),
344
+ });
345
+ if (res.ok) {
346
+ const data = await res.json();
347
+ process.stderr.write(`📤 Added ${data.snapshots?.length || 0} image(s) to project\n`);
348
+ } else {
349
+ process.stderr.write(`⚠️ Failed to upload images: ${await res.text()}\n`);
350
+ }
351
+ }
352
+ const { results } = await streamAgent(baseUrl, cookie, projectId, prompt);
353
+ process.stderr.write('\n━━━ Results ━━━\n');
354
+ for (const img of results.images) process.stderr.write(`🖼️ Image: ${img.imageUrl}\n`);
355
+ for (const d of results.designs) process.stderr.write(`🎨 ${d.desc}\n`);
356
+ process.stderr.write(`🔗 ${APP_URL}/projects/${projectId}\n`);
357
+ for (const task of results.animationTasks) await pollVideo(baseUrl, cookie, task.taskId);
358
+ for (const task of results.musicTasks) await pollMusic(baseUrl, cookie, task.taskId);
359
+ } else if (command === 'list' || command === 'ls') {
360
+ const { cookie, baseUrl } = getAuthCookie();
361
+ await listProjects(baseUrl, cookie);
362
+ } else {
363
+ console.log(`Makaron CLI — Talk to Makaron Agent from the terminal
364
+
365
+ Commands:
366
+ login Log in to Makaron
367
+ list (ls) List all projects
368
+ create --image <file> Create project from local image
369
+ create --image-url <url> Create project from URL
370
+ create --title "name" Create empty project (text-to-image)
371
+ chat --project <id> "message" Chat with Makaron Agent
372
+ chat --project <id> --image <file> "message" Add image + chat
373
+
374
+ Environment:
375
+ MAKARON_URL API base (default: ${DEFAULT_URL})
376
+ `);
377
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "makaron-cli",
3
+ "version": "0.1.0",
4
+ "description": "Talk to Makaron Agent from the terminal — create projects, edit images, generate videos",
5
+ "type": "module",
6
+ "bin": {
7
+ "makaron": "./bin/makaron.mjs"
8
+ },
9
+ "files": [
10
+ "bin/"
11
+ ],
12
+ "keywords": ["makaron", "ai", "image-editing", "video", "cli", "agent"],
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/vegekyd/ai-image-editor"
17
+ }
18
+ }