gunni 0.3.2 → 0.3.3

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/dist/index.js +509 -1232
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -9,276 +9,6 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/core/model-registry.ts
13
- var model_registry_exports = {};
14
- __export(model_registry_exports, {
15
- DEFAULTS: () => DEFAULTS,
16
- getAllModels: () => getAllModels,
17
- getCategories: () => getCategories,
18
- getDefaultModel: () => getDefaultModel,
19
- getModel: () => getModel,
20
- getModelsByCategory: () => getModelsByCategory
21
- });
22
- function getAllModels() {
23
- return MODELS;
24
- }
25
- function getModelsByCategory(category) {
26
- return MODELS.filter((m) => m.category === category);
27
- }
28
- function getModel(id) {
29
- return MODELS.find((m) => m.id === id);
30
- }
31
- function getDefaultModel(category) {
32
- return MODELS.find((m) => m.category === category && m.isDefault);
33
- }
34
- function getCategories() {
35
- return [...new Set(MODELS.map((m) => m.category))];
36
- }
37
- var DEFAULTS, MODELS;
38
- var init_model_registry = __esm({
39
- "src/core/model-registry.ts"() {
40
- "use strict";
41
- DEFAULTS = {
42
- image: "nano-banana",
43
- edit: "nano-banana-edit",
44
- describe: "florence-2",
45
- upscale: "topaz-upscale",
46
- removeBg: "bria-bg-remove",
47
- video: "kling-v3-pro",
48
- audio: "minimax-speech",
49
- lipsync: "kling-lipsync",
50
- design: "recraft-v4"
51
- };
52
- MODELS = [
53
- // --- Image generation (hero: nano-banana) ---
54
- {
55
- id: "nano-banana",
56
- name: "Nano Banana Pro",
57
- provider: "google",
58
- endpoint: "gemini-3-pro-image-preview",
59
- category: "image",
60
- description: "Google's state-of-the-art. Best overall quality and speed. Up to 4K resolution.",
61
- isDefault: true
62
- },
63
- {
64
- id: "recraft-v4",
65
- name: "Recraft V4",
66
- provider: "fal",
67
- endpoint: "fal-ai/recraft/v4/text-to-image",
68
- category: "image",
69
- description: "Design and marketing. Only model that reliably renders text in images."
70
- },
71
- {
72
- id: "flux-2-pro",
73
- name: "Flux 2 Pro",
74
- provider: "fal",
75
- endpoint: "fal-ai/flux-2-pro",
76
- category: "image",
77
- description: "High-quality photorealism from Black Forest Labs."
78
- },
79
- // --- Image editing (hero: nano-banana-edit) ---
80
- {
81
- id: "nano-banana-edit",
82
- name: "Nano Banana Pro Edit",
83
- provider: "google",
84
- endpoint: "gemini-3-pro-image-preview",
85
- category: "edit",
86
- description: "Google's model for precise image editing. Consistent with generation pipeline.",
87
- isDefault: true,
88
- multiImage: true
89
- },
90
- {
91
- id: "flux-kontext",
92
- name: "Flux Kontext Pro",
93
- provider: "fal",
94
- endpoint: "fal-ai/flux-pro/kontext/max/multi",
95
- category: "edit",
96
- description: "Targeted local edits with text and reference images. Supports multiple input images.",
97
- multiImage: true
98
- },
99
- {
100
- id: "flux-2-pro-edit",
101
- name: "Flux 2 Pro Edit",
102
- provider: "fal",
103
- endpoint: "fal-ai/flux-2-pro/edit",
104
- category: "edit",
105
- description: "Multi-image editing. Combine up to 9 reference images with natural language. Use @image1, @image2 notation in prompts.",
106
- multiImage: true
107
- },
108
- // --- Upscale (hero: topaz) ---
109
- {
110
- id: "topaz-upscale",
111
- name: "Topaz Upscale",
112
- provider: "fal",
113
- endpoint: "fal-ai/topaz/upscale/image",
114
- category: "upscale",
115
- description: "Industry-standard image upscaling and enhancement.",
116
- isDefault: true
117
- },
118
- // --- Video (hero: kling-v3-pro) ---
119
- {
120
- id: "kling-v3-pro",
121
- name: "Kling V3 Pro",
122
- provider: "fal",
123
- endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
124
- category: "video",
125
- description: "Cinematic video with fluid motion and native audio.",
126
- isDefault: true
127
- },
128
- {
129
- id: "kling-v3-pro-t2v",
130
- name: "Kling V3 Pro (Text)",
131
- provider: "fal",
132
- endpoint: "fal-ai/kling-video/v3/pro/text-to-video",
133
- category: "video",
134
- description: "Kling V3 Pro text-to-video. No source image needed."
135
- },
136
- {
137
- id: "veo-3.1",
138
- name: "Veo 3.1",
139
- provider: "fal",
140
- endpoint: "fal-ai/veo3.1/image-to-video",
141
- category: "video",
142
- description: "Google's video generation with sound, via Fal."
143
- },
144
- {
145
- id: "veo-3.1-t2v",
146
- name: "Veo 3.1 (Text)",
147
- provider: "fal",
148
- endpoint: "fal-ai/veo3.1",
149
- category: "video",
150
- description: "Veo 3.1 text-to-video with native audio."
151
- },
152
- {
153
- id: "veo-3.1-fast",
154
- name: "Veo 3.1 Fast",
155
- provider: "fal",
156
- endpoint: "fal-ai/veo3.1/fast/image-to-video",
157
- category: "video",
158
- description: "Veo 3.1 Fast \u2014 62% cheaper, slightly lower quality. Image-to-video with sound."
159
- },
160
- {
161
- id: "veo-3.1-fast-t2v",
162
- name: "Veo 3.1 Fast (Text)",
163
- provider: "fal",
164
- endpoint: "fal-ai/veo3.1/fast",
165
- category: "video",
166
- description: "Veo 3.1 Fast text-to-video with native audio. Budget-friendly option."
167
- },
168
- {
169
- id: "minimax-i2v",
170
- name: "MiniMax Hailuo 2.3",
171
- provider: "fal",
172
- endpoint: "fal-ai/minimax/hailuo-2.3/pro/image-to-video",
173
- category: "video",
174
- description: "MiniMax Hailuo 2.3 Pro image-to-video. 1080p output."
175
- },
176
- {
177
- id: "wan-2.6",
178
- name: "Wan 2.6",
179
- provider: "fal",
180
- endpoint: "wan/v2.6/image-to-video",
181
- category: "video",
182
- description: "Alibaba Wan 2.6 image-to-video. Up to 1080p with native audio."
183
- },
184
- // --- Audio (hero: minimax-speech) ---
185
- {
186
- id: "minimax-speech",
187
- name: "MiniMax Speech 2.8 HD",
188
- provider: "fal",
189
- endpoint: "fal-ai/minimax/speech-2.8-hd",
190
- category: "audio",
191
- description: "High-quality text-to-speech with multiple voices.",
192
- isDefault: true
193
- },
194
- {
195
- id: "elevenlabs-tts",
196
- name: "ElevenLabs TTS v3",
197
- provider: "fal",
198
- endpoint: "fal-ai/elevenlabs/tts/turbo-v2.5",
199
- category: "audio",
200
- description: "Natural, conversational text-to-speech. Best for UGC voiceovers and narration."
201
- },
202
- // --- Lip sync (hero: kling-lipsync) ---
203
- {
204
- id: "kling-lipsync",
205
- name: "Kling LipSync",
206
- provider: "fal",
207
- endpoint: "fal-ai/kling-video/lipsync/audio-to-video",
208
- category: "lipsync",
209
- description: "Lip-sync audio onto a video. Input: source video + audio track. Best quality lip sync.",
210
- isDefault: true
211
- },
212
- {
213
- id: "kling-avatar",
214
- name: "Kling AI Avatar v2",
215
- provider: "fal",
216
- endpoint: "fal-ai/kling-video/ai-avatar/v2/standard",
217
- category: "lipsync",
218
- description: "Generate talking head video from a still image + audio. One-step avatar animation."
219
- },
220
- {
221
- id: "sync-lipsync",
222
- name: "Sync Labs Lipsync 2.0",
223
- provider: "fal",
224
- endpoint: "fal-ai/sync-lipsync",
225
- category: "lipsync",
226
- description: "Advanced lip sync with natural motion. Alternative to Kling LipSync."
227
- },
228
- // --- Describe / vision (hero: florence-2) ---
229
- {
230
- id: "florence-2",
231
- name: "Florence 2 Large",
232
- provider: "fal",
233
- endpoint: "fal-ai/florence-2-large/detailed-caption",
234
- category: "describe",
235
- description: "Detailed image captioning and description.",
236
- isDefault: true
237
- },
238
- // --- OpenAI image generation ---
239
- {
240
- id: "gpt-image",
241
- name: "GPT Image 1.5",
242
- provider: "openai",
243
- endpoint: "gpt-image-1.5",
244
- category: "image",
245
- description: "OpenAI's latest image model. Best-in-class text rendering and editing.",
246
- isDefault: false
247
- },
248
- {
249
- id: "gpt-image-mini",
250
- name: "GPT Image Mini",
251
- provider: "openai",
252
- endpoint: "gpt-image-1-mini",
253
- category: "image",
254
- description: "Cost-optimized OpenAI image model. Great for high-volume generation.",
255
- isDefault: false
256
- },
257
- // --- OpenAI image editing ---
258
- {
259
- id: "gpt-image-edit",
260
- name: "GPT Image 1.5 Edit",
261
- provider: "openai",
262
- endpoint: "gpt-image-1.5",
263
- category: "edit",
264
- description: "OpenAI image editing. Surgical precision for inpainting and modifications.",
265
- isDefault: false,
266
- multiImage: true
267
- },
268
- // --- Utility ---
269
- {
270
- id: "bria-bg-remove",
271
- name: "Bria Background Removal",
272
- provider: "fal",
273
- endpoint: "fal-ai/bria/background/remove",
274
- category: "utility",
275
- description: "Remove backgrounds from images.",
276
- isDefault: true
277
- }
278
- ];
279
- }
280
- });
281
-
282
12
  // src/core/supabase.ts
283
13
  import { createClient } from "@supabase/supabase-js";
284
14
  function getSupabase() {
@@ -422,7 +152,7 @@ var init_refs = __esm({
422
152
 
423
153
  // src/index.ts
424
154
  import { Command } from "commander";
425
- import pc25 from "picocolors";
155
+ import pc21 from "picocolors";
426
156
 
427
157
  // src/commands/image.ts
428
158
  import { existsSync } from "fs";
@@ -573,8 +303,253 @@ async function detectImageFormat(filePath) {
573
303
  return void 0;
574
304
  }
575
305
 
576
- // src/core/model-router.ts
577
- init_model_registry();
306
+ // data/models.json
307
+ var models_default = {
308
+ defaults: {
309
+ image: "nano-banana",
310
+ edit: "nano-banana-edit",
311
+ describe: "florence-2",
312
+ upscale: "topaz-upscale",
313
+ removeBg: "bria-bg-remove",
314
+ video: "kling-v3-pro",
315
+ audio: "minimax-speech",
316
+ lipsync: "kling-lipsync",
317
+ design: "recraft-v4"
318
+ },
319
+ models: [
320
+ {
321
+ id: "nano-banana",
322
+ name: "Nano Banana Pro",
323
+ provider: "google",
324
+ endpoint: "gemini-3-pro-image-preview",
325
+ category: "image",
326
+ description: "Google's state-of-the-art. Best overall quality and speed. Up to 4K resolution.",
327
+ isDefault: true
328
+ },
329
+ {
330
+ id: "recraft-v4",
331
+ name: "Recraft V4",
332
+ provider: "fal",
333
+ endpoint: "fal-ai/recraft/v4/text-to-image",
334
+ category: "image",
335
+ description: "Design and marketing. Only model that reliably renders text in images."
336
+ },
337
+ {
338
+ id: "flux-2-pro",
339
+ name: "Flux 2 Pro",
340
+ provider: "fal",
341
+ endpoint: "fal-ai/flux-2-pro",
342
+ category: "image",
343
+ description: "High-quality photorealism from Black Forest Labs."
344
+ },
345
+ {
346
+ id: "nano-banana-edit",
347
+ name: "Nano Banana Pro Edit",
348
+ provider: "google",
349
+ endpoint: "gemini-3-pro-image-preview",
350
+ category: "edit",
351
+ description: "Google's model for precise image editing. Consistent with generation pipeline.",
352
+ isDefault: true,
353
+ multiImage: true
354
+ },
355
+ {
356
+ id: "flux-kontext",
357
+ name: "Flux Kontext Pro",
358
+ provider: "fal",
359
+ endpoint: "fal-ai/flux-pro/kontext/max/multi",
360
+ category: "edit",
361
+ description: "Targeted local edits with text and reference images. Supports multiple input images.",
362
+ multiImage: true
363
+ },
364
+ {
365
+ id: "flux-2-pro-edit",
366
+ name: "Flux 2 Pro Edit",
367
+ provider: "fal",
368
+ endpoint: "fal-ai/flux-2-pro/edit",
369
+ category: "edit",
370
+ description: "Multi-image editing. Combine up to 9 reference images with natural language. Use @image1, @image2 notation in prompts.",
371
+ multiImage: true
372
+ },
373
+ {
374
+ id: "topaz-upscale",
375
+ name: "Topaz Upscale",
376
+ provider: "fal",
377
+ endpoint: "fal-ai/topaz/upscale/image",
378
+ category: "upscale",
379
+ description: "Industry-standard image upscaling and enhancement.",
380
+ isDefault: true
381
+ },
382
+ {
383
+ id: "kling-v3-pro",
384
+ name: "Kling V3 Pro",
385
+ provider: "fal",
386
+ endpoint: "fal-ai/kling-video/v3/pro/image-to-video",
387
+ category: "video",
388
+ description: "Cinematic video with fluid motion and native audio.",
389
+ isDefault: true
390
+ },
391
+ {
392
+ id: "kling-v3-pro-t2v",
393
+ name: "Kling V3 Pro (Text)",
394
+ provider: "fal",
395
+ endpoint: "fal-ai/kling-video/v3/pro/text-to-video",
396
+ category: "video",
397
+ description: "Kling V3 Pro text-to-video. No source image needed."
398
+ },
399
+ {
400
+ id: "veo-3.1",
401
+ name: "Veo 3.1",
402
+ provider: "fal",
403
+ endpoint: "fal-ai/veo3.1/image-to-video",
404
+ category: "video",
405
+ description: "Google's video generation with sound, via Fal."
406
+ },
407
+ {
408
+ id: "veo-3.1-t2v",
409
+ name: "Veo 3.1 (Text)",
410
+ provider: "fal",
411
+ endpoint: "fal-ai/veo3.1",
412
+ category: "video",
413
+ description: "Veo 3.1 text-to-video with native audio."
414
+ },
415
+ {
416
+ id: "veo-3.1-fast",
417
+ name: "Veo 3.1 Fast",
418
+ provider: "fal",
419
+ endpoint: "fal-ai/veo3.1/fast/image-to-video",
420
+ category: "video",
421
+ description: "Veo 3.1 Fast \u2014 62% cheaper, slightly lower quality. Image-to-video with sound."
422
+ },
423
+ {
424
+ id: "veo-3.1-fast-t2v",
425
+ name: "Veo 3.1 Fast (Text)",
426
+ provider: "fal",
427
+ endpoint: "fal-ai/veo3.1/fast",
428
+ category: "video",
429
+ description: "Veo 3.1 Fast text-to-video with native audio. Budget-friendly option."
430
+ },
431
+ {
432
+ id: "minimax-i2v",
433
+ name: "MiniMax Hailuo 2.3",
434
+ provider: "fal",
435
+ endpoint: "fal-ai/minimax/hailuo-2.3/pro/image-to-video",
436
+ category: "video",
437
+ description: "MiniMax Hailuo 2.3 Pro image-to-video. 1080p output."
438
+ },
439
+ {
440
+ id: "wan-2.6",
441
+ name: "Wan 2.6",
442
+ provider: "fal",
443
+ endpoint: "wan/v2.6/image-to-video",
444
+ category: "video",
445
+ description: "Alibaba Wan 2.6 image-to-video. Up to 1080p with native audio."
446
+ },
447
+ {
448
+ id: "minimax-speech",
449
+ name: "MiniMax Speech 2.8 HD",
450
+ provider: "fal",
451
+ endpoint: "fal-ai/minimax/speech-2.8-hd",
452
+ category: "audio",
453
+ description: "High-quality text-to-speech with multiple voices.",
454
+ isDefault: true
455
+ },
456
+ {
457
+ id: "elevenlabs-tts",
458
+ name: "ElevenLabs TTS v3",
459
+ provider: "fal",
460
+ endpoint: "fal-ai/elevenlabs/tts/turbo-v2.5",
461
+ category: "audio",
462
+ description: "Natural, conversational text-to-speech. Best for UGC voiceovers and narration."
463
+ },
464
+ {
465
+ id: "kling-lipsync",
466
+ name: "Kling LipSync",
467
+ provider: "fal",
468
+ endpoint: "fal-ai/kling-video/lipsync/audio-to-video",
469
+ category: "lipsync",
470
+ description: "Lip-sync audio onto a video. Input: source video + audio track. Best quality lip sync.",
471
+ isDefault: true
472
+ },
473
+ {
474
+ id: "kling-avatar",
475
+ name: "Kling AI Avatar v2",
476
+ provider: "fal",
477
+ endpoint: "fal-ai/kling-video/ai-avatar/v2/standard",
478
+ category: "lipsync",
479
+ description: "Generate talking head video from a still image + audio. One-step avatar animation."
480
+ },
481
+ {
482
+ id: "sync-lipsync",
483
+ name: "Sync Labs Lipsync 2.0",
484
+ provider: "fal",
485
+ endpoint: "fal-ai/sync-lipsync",
486
+ category: "lipsync",
487
+ description: "Advanced lip sync with natural motion. Alternative to Kling LipSync."
488
+ },
489
+ {
490
+ id: "florence-2",
491
+ name: "Florence 2 Large",
492
+ provider: "fal",
493
+ endpoint: "fal-ai/florence-2-large/detailed-caption",
494
+ category: "describe",
495
+ description: "Detailed image captioning and description.",
496
+ isDefault: true
497
+ },
498
+ {
499
+ id: "gpt-image",
500
+ name: "GPT Image 1.5",
501
+ provider: "openai",
502
+ endpoint: "gpt-image-1.5",
503
+ category: "image",
504
+ description: "OpenAI's latest image model. Best-in-class text rendering and editing.",
505
+ isDefault: false
506
+ },
507
+ {
508
+ id: "gpt-image-mini",
509
+ name: "GPT Image Mini",
510
+ provider: "openai",
511
+ endpoint: "gpt-image-1-mini",
512
+ category: "image",
513
+ description: "Cost-optimized OpenAI image model. Great for high-volume generation.",
514
+ isDefault: false
515
+ },
516
+ {
517
+ id: "gpt-image-edit",
518
+ name: "GPT Image 1.5 Edit",
519
+ provider: "openai",
520
+ endpoint: "gpt-image-1.5",
521
+ category: "edit",
522
+ description: "OpenAI image editing. Surgical precision for inpainting and modifications.",
523
+ isDefault: false,
524
+ multiImage: true
525
+ },
526
+ {
527
+ id: "bria-bg-remove",
528
+ name: "Bria Background Removal",
529
+ provider: "fal",
530
+ endpoint: "fal-ai/bria/background/remove",
531
+ category: "utility",
532
+ description: "Remove backgrounds from images.",
533
+ isDefault: true
534
+ }
535
+ ]
536
+ };
537
+
538
+ // src/core/model-registry.ts
539
+ var DEFAULTS = models_default.defaults;
540
+ var MODELS = models_default.models;
541
+ function getAllModels() {
542
+ return MODELS;
543
+ }
544
+ function getModelsByCategory(category) {
545
+ return MODELS.filter((m) => m.category === category);
546
+ }
547
+ function getModel(id) {
548
+ return MODELS.find((m) => m.id === id);
549
+ }
550
+ function getCategories() {
551
+ return [...new Set(MODELS.map((m) => m.category))];
552
+ }
578
553
 
579
554
  // src/providers/base.ts
580
555
  function resolveImagePaths(options) {
@@ -665,9 +640,6 @@ function isNodeError(err) {
665
640
  return err instanceof Error && "code" in err;
666
641
  }
667
642
 
668
- // src/providers/fal.ts
669
- init_model_registry();
670
-
671
643
  // src/core/uploader.ts
672
644
  import { readFile as readFile3 } from "fs/promises";
673
645
  import { basename } from "path";
@@ -758,9 +730,9 @@ var FalProvider = class extends BaseProvider {
758
730
  const message = err instanceof Error ? err.message : String(err);
759
731
  if (message.includes("Forbidden") || message.includes("401") || message.includes("403")) {
760
732
  throw new GunniError(
761
- "API key rejected by fal.ai",
733
+ "Authentication failed",
762
734
  "AUTH_ERROR",
763
- "Check your key at https://fal.ai/dashboard/keys and re-run `gunni config`."
735
+ "Check your API key and re-run `gunni config`."
764
736
  );
765
737
  }
766
738
  throw new GunniError("Request failed", "PROVIDER_ERROR", message);
@@ -886,6 +858,28 @@ var FalProvider = class extends BaseProvider {
886
858
  requestId: result.requestId
887
859
  };
888
860
  }
861
+ async submitToQueue(endpoint, input) {
862
+ const apiKey = this.overrideApiKey ?? await this.configManager.getApiKey("fal");
863
+ if (!apiKey) {
864
+ throw new GunniError("Not configured yet", "NO_API_KEY", "Run `gunni config` to get started.");
865
+ }
866
+ const { createFalClient } = await import("@fal-ai/client");
867
+ const client2 = createFalClient({ credentials: apiKey });
868
+ const result = await client2.queue.submit(endpoint, { input });
869
+ return result.request_id;
870
+ }
871
+ async checkQueueStatus(endpoint, requestId) {
872
+ const apiKey = this.overrideApiKey ?? await this.configManager.getApiKey("fal");
873
+ if (!apiKey) throw new GunniError("Not configured yet", "NO_API_KEY");
874
+ const { createFalClient } = await import("@fal-ai/client");
875
+ const client2 = createFalClient({ credentials: apiKey });
876
+ const status = await client2.queue.status(endpoint, { requestId });
877
+ if (status.status === "COMPLETED") {
878
+ const result = await client2.queue.result(endpoint, { requestId });
879
+ return { status: "COMPLETED", data: result.data };
880
+ }
881
+ return { status: status.status };
882
+ }
889
883
  async prepareImage(imagePath) {
890
884
  return uploadToFal(imagePath);
891
885
  }
@@ -894,7 +888,6 @@ var FalProvider = class extends BaseProvider {
894
888
  // src/providers/vertex.ts
895
889
  import { readFile as readFile4 } from "fs/promises";
896
890
  import { extname as extname2 } from "path";
897
- init_model_registry();
898
891
  var VERTEX_PROJECT = "gunni-488216";
899
892
  var VERTEX_LOCATION = "us-central1";
900
893
  function resolveAspectRatio(width, height) {
@@ -1136,7 +1129,6 @@ async function downloadFromGcs(gcsUri) {
1136
1129
 
1137
1130
  // src/providers/openai.ts
1138
1131
  import { readFile as readFile5 } from "fs/promises";
1139
- init_model_registry();
1140
1132
  var OPENAI_API_BASE = "https://api.openai.com/v1";
1141
1133
  function resolveSize(width, height) {
1142
1134
  if (!width || !height) return "1024x1024";
@@ -1369,16 +1361,9 @@ var ModelRouter = class {
1369
1361
  }
1370
1362
  };
1371
1363
 
1372
- // src/commands/image.ts
1373
- init_model_registry();
1374
-
1375
- // src/core/provider-resolver.ts
1376
- init_model_registry();
1377
-
1378
1364
  // src/providers/google.ts
1379
1365
  import { readFile as readFile6 } from "fs/promises";
1380
1366
  import { extname as extname3 } from "path";
1381
- init_model_registry();
1382
1367
  function imageExtToMimeType2(imagePath) {
1383
1368
  const ext = extname3(imagePath).toLowerCase();
1384
1369
  const map = {
@@ -1630,6 +1615,8 @@ function openFile(filePath) {
1630
1615
  detached: true,
1631
1616
  stdio: "ignore"
1632
1617
  });
1618
+ child.on("error", () => {
1619
+ });
1633
1620
  child.unref();
1634
1621
  } catch {
1635
1622
  }
@@ -1642,6 +1629,8 @@ function openUrl(url) {
1642
1629
  detached: true,
1643
1630
  stdio: "ignore"
1644
1631
  });
1632
+ child.on("error", () => {
1633
+ });
1645
1634
  child.unref();
1646
1635
  } catch {
1647
1636
  }
@@ -1681,6 +1670,7 @@ async function searchHistory(query) {
1681
1670
  // src/core/remote-client.ts
1682
1671
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1683
1672
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1673
+ var DEFAULT_TIMEOUT_MS = 6e4;
1684
1674
  var RemoteClient = class {
1685
1675
  client = null;
1686
1676
  async connect(apiKey, serverUrl) {
@@ -1692,14 +1682,18 @@ var RemoteClient = class {
1692
1682
  }
1693
1683
  }
1694
1684
  );
1695
- this.client = new Client({ name: "gunni-cli", version: "0.2.0" });
1685
+ this.client = new Client({ name: "gunni-cli", version: "0.3.3" });
1696
1686
  await this.client.connect(transport);
1697
1687
  }
1698
1688
  async callTool(name, args) {
1699
1689
  if (!this.client) {
1700
1690
  throw new GunniError("Remote client not connected", "NOT_CONNECTED");
1701
1691
  }
1702
- const result = await this.client.callTool({ name, arguments: args });
1692
+ const result = await this.client.callTool(
1693
+ { name, arguments: args },
1694
+ void 0,
1695
+ { timeout: DEFAULT_TIMEOUT_MS }
1696
+ );
1703
1697
  const firstContent = result.content?.[0];
1704
1698
  if (!firstContent || firstContent.type !== "text") {
1705
1699
  throw new GunniError(
@@ -1707,7 +1701,20 @@ var RemoteClient = class {
1707
1701
  "INVALID_RESPONSE"
1708
1702
  );
1709
1703
  }
1710
- return JSON.parse(firstContent.text);
1704
+ const text3 = firstContent.text;
1705
+ if (result.isError) {
1706
+ try {
1707
+ const parsed = JSON.parse(text3);
1708
+ throw new GunniError(
1709
+ parsed.error?.message ?? parsed.error ?? text3,
1710
+ parsed.error?.code ?? "REMOTE_ERROR"
1711
+ );
1712
+ } catch (e) {
1713
+ if (e instanceof GunniError) throw e;
1714
+ throw new GunniError(text3, "REMOTE_ERROR");
1715
+ }
1716
+ }
1717
+ return JSON.parse(text3);
1711
1718
  }
1712
1719
  async disconnect() {
1713
1720
  if (this.client) {
@@ -2421,7 +2428,6 @@ async function handleRemoveBg(imagePath, modelId, opts, json) {
2421
2428
  // src/commands/video.ts
2422
2429
  import { existsSync as existsSync2 } from "fs";
2423
2430
  import ora3 from "ora";
2424
- init_model_registry();
2425
2431
  import pc4 from "picocolors";
2426
2432
  var DEFAULT_MODEL = "kling-v3-pro";
2427
2433
  function registerVideoCommand(program2) {
@@ -2598,13 +2604,20 @@ async function handleRemote2(input, opts, json, apiKey, serverUrl) {
2598
2604
  "MISSING_INPUT"
2599
2605
  );
2600
2606
  }
2601
- if (existsSync2(input)) {
2607
+ const looksLikeUrl = /^https?:\/\//.test(input);
2608
+ if (!existsSync2(input) && !looksLikeUrl) {
2609
+ prompt = opts.prompt ? `${input} ${opts.prompt}` : input;
2610
+ if (!userExplicitModel && modelId === DEFAULT_MODEL) {
2611
+ modelId = "kling-v3-pro-t2v";
2612
+ }
2613
+ } else if (existsSync2(input)) {
2602
2614
  update("Uploading image\u2026");
2603
2615
  imageUrl = await uploadLocalFile(input, apiKey, serverUrl);
2616
+ prompt = opts.prompt ?? "";
2604
2617
  } else {
2605
2618
  imageUrl = input;
2619
+ prompt = opts.prompt ?? "";
2606
2620
  }
2607
- prompt = opts.prompt ?? "";
2608
2621
  }
2609
2622
  if (opts.style) {
2610
2623
  const customerId = process.env.GUNNI_CUSTOMER_ID;
@@ -2636,7 +2649,22 @@ async function handleRemote2(input, opts, json, apiKey, serverUrl) {
2636
2649
  update("Generating video\u2026");
2637
2650
  const client2 = new RemoteClient();
2638
2651
  await client2.connect(apiKey, serverUrl);
2639
- const result = await client2.callTool("video", toolArgs);
2652
+ let result = await client2.callTool("video", toolArgs);
2653
+ if (result.jobId) {
2654
+ const jobId = result.jobId;
2655
+ let jobStatus;
2656
+ while (true) {
2657
+ await new Promise((r) => setTimeout(r, 5e3));
2658
+ jobStatus = await client2.callTool("job_status", { job_id: jobId });
2659
+ if (jobStatus.status === "completed") break;
2660
+ if (jobStatus.status === "failed") {
2661
+ await client2.disconnect();
2662
+ throw new GunniError(jobStatus.error ?? "Job failed", "JOB_FAILED");
2663
+ }
2664
+ update(`Generating video\u2026 (${jobStatus.status})`);
2665
+ }
2666
+ result = jobStatus?.result ?? {};
2667
+ }
2640
2668
  await client2.disconnect();
2641
2669
  const requestedPath = opts.output ?? `gunni-video-${Date.now()}.mp4`;
2642
2670
  const videoAssetUrl = result.video?.assetUrl;
@@ -2678,7 +2706,6 @@ async function handleRemote2(input, opts, json, apiKey, serverUrl) {
2678
2706
 
2679
2707
  // src/commands/audio.ts
2680
2708
  import ora4 from "ora";
2681
- init_model_registry();
2682
2709
  import pc5 from "picocolors";
2683
2710
  var DEFAULT_MODEL2 = "minimax-speech";
2684
2711
  function registerAudioCommand(program2) {
@@ -2707,14 +2734,14 @@ Examples:
2707
2734
  Available audio models: minimax-speech (default), elevenlabs-tts
2708
2735
  Run 'gunni list models --type audio' for details.
2709
2736
  `
2710
- ).action(async (text4, opts) => {
2737
+ ).action(async (text3, opts) => {
2711
2738
  const json = program2.opts().json ?? false;
2712
2739
  try {
2713
2740
  const config = new ConfigManager();
2714
2741
  const gunniKey = await config.getGunniApiKey();
2715
2742
  if (gunniKey) {
2716
2743
  const serverUrl = await config.getServerUrl();
2717
- return handleRemote3(text4, opts, json, gunniKey, serverUrl);
2744
+ return handleRemote3(text3, opts, json, gunniKey, serverUrl);
2718
2745
  }
2719
2746
  throw new GunniError(
2720
2747
  "No API key configured",
@@ -2729,7 +2756,7 @@ Run 'gunni list models --type audio' for details.
2729
2756
  const endpoint = getEndpoint(modelId);
2730
2757
  const falInput = {};
2731
2758
  if (modelId === "minimax-speech") {
2732
- falInput.prompt = text4;
2759
+ falInput.prompt = text3;
2733
2760
  falInput.voice_setting = {
2734
2761
  voice_id: opts.voice ?? "Wise_Woman",
2735
2762
  speed: 1,
@@ -2737,10 +2764,10 @@ Run 'gunni list models --type audio' for details.
2737
2764
  pitch: 0
2738
2765
  };
2739
2766
  } else if (modelId === "elevenlabs-tts") {
2740
- falInput.text = text4;
2767
+ falInput.text = text3;
2741
2768
  if (opts.voice) falInput.voice_id = opts.voice;
2742
2769
  } else {
2743
- falInput.text = text4;
2770
+ falInput.text = text3;
2744
2771
  if (opts.voice) {
2745
2772
  falInput.audio_url = opts.voice;
2746
2773
  }
@@ -2761,7 +2788,7 @@ Run 'gunni list models --type audio' for details.
2761
2788
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2762
2789
  command: "audio",
2763
2790
  model: modelId,
2764
- prompt: text4,
2791
+ prompt: text3,
2765
2792
  outputPath: finalPath,
2766
2793
  outputUrl: audioUrl
2767
2794
  }).catch(() => {
@@ -2769,7 +2796,7 @@ Run 'gunni list models --type audio' for details.
2769
2796
  const output = {
2770
2797
  audio: { path: finalPath, url: audioUrl },
2771
2798
  model: modelId,
2772
- text: text4,
2799
+ text: text3,
2773
2800
  voice: opts.voice
2774
2801
  };
2775
2802
  if (json) {
@@ -2796,14 +2823,14 @@ function getEndpoint(modelId) {
2796
2823
  }
2797
2824
  return model.endpoint;
2798
2825
  }
2799
- async function handleRemote3(text4, opts, json, apiKey, serverUrl) {
2826
+ async function handleRemote3(text3, opts, json, apiKey, serverUrl) {
2800
2827
  const spinner = json ? void 0 : ora4("Generating audio\u2026").start();
2801
2828
  const update = (s) => {
2802
2829
  if (spinner) spinner.text = s;
2803
2830
  };
2804
2831
  try {
2805
2832
  const modelId = opts.model ?? DEFAULT_MODEL2;
2806
- const toolArgs = { text: text4, model: modelId };
2833
+ const toolArgs = { text: text3, model: modelId };
2807
2834
  if (opts.voice) toolArgs.voice = opts.voice;
2808
2835
  const client2 = new RemoteClient();
2809
2836
  await client2.connect(apiKey, serverUrl);
@@ -2821,7 +2848,7 @@ async function handleRemote3(text4, opts, json, apiKey, serverUrl) {
2821
2848
  const output = {
2822
2849
  audio: { path: finalPath, url: audioAssetUrl },
2823
2850
  model: modelId,
2824
- text: text4,
2851
+ text: text3,
2825
2852
  voice: opts.voice
2826
2853
  };
2827
2854
  if (json) {
@@ -2841,7 +2868,6 @@ async function handleRemote3(text4, opts, json, apiKey, serverUrl) {
2841
2868
  // src/commands/lipsync.ts
2842
2869
  import { existsSync as existsSync3 } from "fs";
2843
2870
  import ora5 from "ora";
2844
- init_model_registry();
2845
2871
  import pc6 from "picocolors";
2846
2872
  var DEFAULT_MODEL3 = "kling-lipsync";
2847
2873
  function registerLipsyncCommand(program2) {
@@ -2992,7 +3018,22 @@ async function handleRemote4(audio, opts, json, apiKey, serverUrl) {
2992
3018
  update("Generating lip sync\u2026");
2993
3019
  const client2 = new RemoteClient();
2994
3020
  await client2.connect(apiKey, serverUrl);
2995
- const result = await client2.callTool("lipsync", toolArgs);
3021
+ let result = await client2.callTool("lipsync", toolArgs);
3022
+ if (result.jobId) {
3023
+ const jobId = result.jobId;
3024
+ let jobStatus;
3025
+ while (true) {
3026
+ await new Promise((r) => setTimeout(r, 5e3));
3027
+ jobStatus = await client2.callTool("job_status", { job_id: jobId });
3028
+ if (jobStatus.status === "completed") break;
3029
+ if (jobStatus.status === "failed") {
3030
+ await client2.disconnect();
3031
+ throw new GunniError(jobStatus.error ?? "Job failed", "JOB_FAILED");
3032
+ }
3033
+ update(`Generating lip sync\u2026 (${jobStatus.status})`);
3034
+ }
3035
+ result = jobStatus?.result ?? {};
3036
+ }
2996
3037
  await client2.disconnect();
2997
3038
  const videoAssetUrl = result.video?.assetUrl;
2998
3039
  if (!videoAssetUrl) {
@@ -3092,34 +3133,18 @@ function formatEntry(e) {
3092
3133
  // src/commands/learn.ts
3093
3134
  import pc8 from "picocolors";
3094
3135
 
3095
- // src/core/knowledge.ts
3096
- var TOPICS = {
3097
- overview: {
3098
- topic: "overview",
3099
- title: "Gunni Overview",
3100
- content: `Gunni tools for professional media creation:
3101
-
3102
- MEDIA: image (generate/edit/describe/upscale/remove-bg, routes by input), video, audio
3103
- REFS: ref_save (save image as ref), refs (list or get refs by name), upload_ref (user uploads from their device)
3104
- UTILITY: models (list models + updates), history (search past work), learn (you're here)
3105
-
3106
- ROUTING (image): prompt only \u2192 generate, image+prompt \u2192 edit, image only \u2192 describe, +upscale/remove_bg flags
3107
- MULTI-IMAGE: pass an array of URLs to image for multi-ref editing
3108
-
3109
- USER IMAGE UPLOADS: When a user provides or pastes an image and you need to use it as input (for editing, video, describe, etc.), you MUST call upload_ref first. The image/video tools require a URL (https://...), not a local file path. Workflow:
3110
- 1. Call upload_ref with a name for the reference
3111
- 2. The user uploads their file via the Gunni Studio UI
3112
- 3. After upload completes, use the resulting URL in image/video tools
3113
- Never pass a local file path to image or video. It will fail.
3114
-
3115
- PROJECT SYSTEM: BRIEF.md (creative direction), .gunni/assets.json (lineage), .gunni/seeds.json (proven seeds)
3116
-
3117
- IMPORTANT: Every generation response includes a galleryUrl. Always share it with the user so they can view results in full resolution.`
3118
- },
3119
- exploration: {
3120
- topic: "exploration",
3121
- title: "Creative Exploration",
3122
- content: `Creative work is a tree, not a pipeline. Here's how to explore effectively:
3136
+ // data/knowledge.json
3137
+ var knowledge_default = {
3138
+ topics: {
3139
+ overview: {
3140
+ topic: "overview",
3141
+ title: "Gunni Overview",
3142
+ content: "Gunni tools for professional media creation:\n\nMEDIA: image (generate/edit/describe/upscale/remove-bg, routes by input), video, audio\nREFS: ref_save (save image as ref), refs (list or get refs by name), upload_ref (user uploads from their device)\nUTILITY: models (list models + updates), history (search past work), learn (you're here)\n\nROUTING (image): prompt only \u2192 generate, image+prompt \u2192 edit, image only \u2192 describe, +upscale/remove_bg flags\nMULTI-IMAGE: pass an array of URLs to image for multi-ref editing\n\nUSER IMAGE UPLOADS: When a user provides or pastes an image and you need to use it as input (for editing, video, describe, etc.), you MUST call upload_ref first. The image/video tools require a URL (https://...), not a local file path. Workflow:\n1. Call upload_ref with a name for the reference\n2. The user uploads their file via the Gunni Studio UI\n3. After upload completes, use the resulting URL in image/video tools\nNever pass a local file path to image or video. It will fail.\n\nPROJECT SYSTEM: BRIEF.md (creative direction), .gunni/assets.json (lineage), .gunni/seeds.json (proven seeds)\n\nIMPORTANT: Every generation response includes a galleryUrl. Always share it with the user so they can view results in full resolution."
3143
+ },
3144
+ exploration: {
3145
+ topic: "exploration",
3146
+ title: "Creative Exploration",
3147
+ content: `Creative work is a tree, not a pipeline. Here's how to explore effectively:
3123
3148
 
3124
3149
  ROUND 1: START FOCUSED
3125
3150
  - Generate 1 image first. Show it to the user and get feedback before making more.
@@ -3145,91 +3170,21 @@ POLISH
3145
3170
  - Don't upscale until the concept is locked. Upscaling is the final step.
3146
3171
  - Use image --upscale for production quality.
3147
3172
  - Describe the final result to verify it meets the brief.`
3148
- },
3149
- prompting: {
3150
- topic: "prompting",
3151
- title: "Prompt Craft",
3152
- content: `How to write effective prompts for image generation:
3153
-
3154
- STRUCTURE
3155
- - Lead with the subject, then add style, lighting, and mood.
3156
- - Be specific: subject + action + setting + style + technical elements.
3157
- - Good: "ceramic mug on wooden table, morning light, editorial photography, shallow depth of field"
3158
- - Weak: "a nice mug photo"
3159
-
3160
- STYLE KEYWORDS THAT WORK
3161
- - Photography: "editorial", "product photography", "studio lighting", "natural light", "shallow depth of field"
3162
- - Design: "minimalist", "clean lines", "modern", "professional", "flat design"
3163
- - Mood: "warm", "cinematic", "moody", "bright and airy", "dramatic lighting"
3164
- - Quality: "high detail", "photorealistic", "4K", "professional grade"
3165
- - Camera: "wide-angle", "f/2.8", "85mm portrait lens", "macro", "low angle shot", "dolly zoom"
3166
-
3167
- NEGATIVE PROMPTS
3168
- - Explicitly exclude unwanted elements: "no blur, no artifacts, no text"
3169
- - Effective for cleaning up outputs, especially with Recraft v4 and Flux.
3170
- - Example: "professional headshot, studio lighting, no watermark, no distortion"
3171
-
3172
- CONSISTENCY
3173
- - Use seeds for reproducible results. Same seed + same prompt = same image.
3174
- - When creating a series, keep the style keywords consistent across prompts.
3175
- - Reference BRIEF.md constraints to stay on-brand.
3176
-
3177
- FOR EDITS
3178
- - Be specific about what to change: "make the lighting warmer" not "improve it".
3179
- - Reference specific elements: "change the background to marble" not "change the background".
3180
- - Additive edits work better than subtractive ones.
3181
- - For faces/people: use "preserve facial features" or "maintain likeness" to keep identity.
3182
- - One edit at a time yields better results than compound changes.
3183
-
3184
- REFERENCES
3185
- - High-quality references (2K/4K) prevent artifacts like blurriness.
3186
- - Tag inputs clearly for multi-image edits: @image1 for character, @image2 for style.
3187
-
3188
- MODEL STRENGTHS
3189
- - Nano Banana: Best overall quality. Photorealism, complex scenes, multi-turn edits. Use "preserve facial features" for likeness consistency.
3190
- - Recraft v4: Text rendering, design, vectors, marketing materials. Use negative prompts for text-heavy designs. Only model that reliably puts text in images.
3191
- - Flux 2 Pro / Flux Kontext: Technical descriptions over vague adjectives. Specify camera settings (lens, aperture, angle) for best results. Kontext excels at targeted local edits.
3192
- - GPT Image: Strong text rendering and surgical inpainting. Good for precise modifications.
3193
- - Use default models unless you have a specific reason to override.`
3194
- },
3195
- brand: {
3196
- topic: "brand",
3197
- title: "Brand Identity Design",
3198
- content: `How to create a cohesive brand identity with Gunni:
3199
-
3200
- START WITH THE BRIEF
3201
- - Read BRIEF.md for colors, style, constraints, and references.
3202
- - If no BRIEF.md exists, ask what the brand represents before generating.
3203
-
3204
- LOGO EXPLORATION
3205
- - Start with 1 logo concept. Show it, get feedback, then iterate or try a different approach.
3206
- - Only generate multiple variants when the user asks for options.
3207
- - ALWAYS include the exact brand/company name in the prompt when the logo should contain text.
3208
- - Use Recraft v4 for logos that include text (only model that nails typography).
3209
- - Use Nano Banana for abstract marks and iconic logos.
3210
- - Always remove background after selecting a logo direction.
3211
-
3212
- COLOR PALETTE
3213
- - Extract colors from the best generation and codify them in BRIEF.md.
3214
- - Use consistent color keywords in all subsequent prompts.
3215
- - Test colors across light and dark backgrounds.
3216
-
3217
- BUILDING THE KIT
3218
- 1. Logo (with and without background)
3219
- 2. Hero image (wide format, brand aesthetic)
3220
- 3. Social media templates (square, story, header sizes)
3221
- 4. Pattern or texture (for backgrounds)
3222
- 5. Product mockups (logo placed in context)
3223
-
3224
- CONSISTENCY
3225
- - Save good seeds to seeds.json after each successful generation.
3226
- - Reference the logo in subsequent prompts: "in the style of [brand], using [colors]"
3227
- - Keep style keywords identical across all generations.`
3228
- },
3229
- "ui-design": {
3230
- topic: "ui-design",
3231
- title: "UI/UX Design",
3232
- content: `How to generate UI designs and interface assets with Gunni:
3173
+ },
3174
+ prompting: {
3175
+ topic: "prompting",
3176
+ title: "Prompt Craft",
3177
+ content: 'How to write effective prompts for image generation:\n\nSTRUCTURE\n- Lead with the subject, then add style, lighting, and mood.\n- Be specific: subject + action + setting + style + technical elements.\n- Good: "ceramic mug on wooden table, morning light, editorial photography, shallow depth of field"\n- Weak: "a nice mug photo"\n\nSTYLE KEYWORDS THAT WORK\n- Photography: "editorial", "product photography", "studio lighting", "natural light", "shallow depth of field"\n- Design: "minimalist", "clean lines", "modern", "professional", "flat design"\n- Mood: "warm", "cinematic", "moody", "bright and airy", "dramatic lighting"\n- Quality: "high detail", "photorealistic", "4K", "professional grade"\n- Camera: "wide-angle", "f/2.8", "85mm portrait lens", "macro", "low angle shot", "dolly zoom"\n\nNEGATIVE PROMPTS\n- Explicitly exclude unwanted elements: "no blur, no artifacts, no text"\n- Effective for cleaning up outputs, especially with Recraft v4 and Flux.\n- Example: "professional headshot, studio lighting, no watermark, no distortion"\n\nCONSISTENCY\n- Use seeds for reproducible results. Same seed + same prompt = same image.\n- When creating a series, keep the style keywords consistent across prompts.\n- Reference BRIEF.md constraints to stay on-brand.\n\nFOR EDITS\n- Be specific about what to change: "make the lighting warmer" not "improve it".\n- Reference specific elements: "change the background to marble" not "change the background".\n- Additive edits work better than subtractive ones.\n- For faces/people: use "preserve facial features" or "maintain likeness" to keep identity.\n- One edit at a time yields better results than compound changes.\n\nREFERENCES\n- High-quality references (2K/4K) prevent artifacts like blurriness.\n- Tag inputs clearly for multi-image edits: @image1 for character, @image2 for style.\n\nMODEL STRENGTHS\n- Nano Banana: Best overall quality. Photorealism, complex scenes, multi-turn edits. Use "preserve facial features" for likeness consistency.\n- Recraft v4: Text rendering, design, vectors, marketing materials. Use negative prompts for text-heavy designs. Only model that reliably puts text in images.\n- Flux 2 Pro / Flux Kontext: Technical descriptions over vague adjectives. Specify camera settings (lens, aperture, angle) for best results. Kontext excels at targeted local edits.\n- GPT Image: Strong text rendering and surgical inpainting. Good for precise modifications.\n- Use default models unless you have a specific reason to override.'
3178
+ },
3179
+ brand: {
3180
+ topic: "brand",
3181
+ title: "Brand Identity Design",
3182
+ content: 'How to create a cohesive brand identity with Gunni:\n\nSTART WITH THE BRIEF\n- Read BRIEF.md for colors, style, constraints, and references.\n- If no BRIEF.md exists, ask what the brand represents before generating.\n\nLOGO EXPLORATION\n- Start with 1 logo concept. Show it, get feedback, then iterate or try a different approach.\n- Only generate multiple variants when the user asks for options.\n- ALWAYS include the exact brand/company name in the prompt when the logo should contain text.\n- Use Recraft v4 for logos that include text (only model that nails typography).\n- Use Nano Banana for abstract marks and iconic logos.\n- Always remove background after selecting a logo direction.\n\nCOLOR PALETTE\n- Extract colors from the best generation and codify them in BRIEF.md.\n- Use consistent color keywords in all subsequent prompts.\n- Test colors across light and dark backgrounds.\n\nBUILDING THE KIT\n1. Logo (with and without background)\n2. Hero image (wide format, brand aesthetic)\n3. Social media templates (square, story, header sizes)\n4. Pattern or texture (for backgrounds)\n5. Product mockups (logo placed in context)\n\nCONSISTENCY\n- Save good seeds to seeds.json after each successful generation.\n- Reference the logo in subsequent prompts: "in the style of [brand], using [colors]"\n- Keep style keywords identical across all generations.'
3183
+ },
3184
+ "ui-design": {
3185
+ topic: "ui-design",
3186
+ title: "UI/UX Design",
3187
+ content: `How to generate UI designs and interface assets with Gunni:
3233
3188
 
3234
3189
  SCREEN GENERATION
3235
3190
  - Nano Banana excels at generating realistic UI screenshots and mockups.
@@ -3255,143 +3210,31 @@ TIPS
3255
3210
  - Reference specific apps in prompts: "in the style of Linear" or "inspired by Stripe's dashboard"
3256
3211
  - For responsive design, generate the same screen at different widths.
3257
3212
  - Use describe to document what each screen shows for handoff.`
3258
- },
3259
- advertising: {
3260
- topic: "advertising",
3261
- title: "Ad Creative",
3262
- content: `How to create advertising assets with Gunni:
3263
-
3264
- CAMPAIGN STRUCTURE
3265
- - Start with the hero image: the single strongest visual.
3266
- - Generate platform-specific sizes from the same concept:
3267
- - Instagram: 1080x1080 (feed), 1080x1920 (story)
3268
- - Twitter/X: 1200x675 (post), 1500x500 (header)
3269
- - YouTube: 1280x720 (thumbnail)
3270
- - LinkedIn: 1200x627 (post)
3271
- - OG: 1200x630
3272
-
3273
- A/B TESTING
3274
- - Start with 1 hero image. Only generate additional variants when the user wants to compare options.
3275
- - Keep copy consistent, vary imagery. Or keep imagery consistent, vary composition.
3276
- - Use describe on each to document the differences for the team.
3277
-
3278
- PRODUCT SHOTS
3279
- - Start with a clean product image (generate or use existing).
3280
- - Remove background for a clean cutout.
3281
- - Use edit to place in different lifestyle scenes.
3282
- - Upscale the winners for production use.
3283
-
3284
- VIDEO ADS
3285
- - Generate a compelling still, then create a 5-10 second video.
3286
- - Add voiceover with gunni audio for complete ad packages.
3287
- - Different video styles: zoom in, pan, parallax, subtle motion.
3288
-
3289
- TIPS
3290
- - Ad images should have clear focal points and work at small sizes.
3291
- - Test thumbnails at actual display size (they're often tiny).
3292
- - Keep text minimal in generated images. Add text in post-production or use Recraft v4.`
3293
- },
3294
- "product-photo": {
3295
- topic: "product-photo",
3296
- title: "Product Photography",
3297
- content: `How to create product photography with Gunni:
3298
-
3299
- FROM SCRATCH
3300
- 1. Generate the product: "ceramic mug, white background, studio lighting, product photography"
3301
- 2. Remove background: image --remove-bg
3302
- 3. Place in scenes: "place on marble countertop, morning light, coffee beans scattered nearby"
3303
- 4. Upscale the final: image --upscale
3304
-
3305
- FROM EXISTING PRODUCT PHOTO
3306
- 1. Remove background from the original photo.
3307
- 2. Use edit to place in new contexts and lighting.
3308
- 3. Generate multiple scene variants with --variants.
3309
- 4. Upscale winners.
3310
-
3311
- LIGHTING STYLES
3312
- - "Studio lighting" \u2014 clean, professional, white background
3313
- - "Natural light" \u2014 warm, lifestyle feel
3314
- - "Dramatic lighting" \u2014 moody, high contrast, dark background
3315
- - "Flat lay" \u2014 top-down arrangement with props
3316
- - "Lifestyle" \u2014 product in use, real-world context
3317
-
3318
- SCENE TYPES
3319
- - Hero shot (product as star, minimal background)
3320
- - Lifestyle (product in use context)
3321
- - Detail (close-up of texture, material, craftsmanship)
3322
- - Group (multiple products arranged together)
3323
- - Scale (product next to familiar objects for size reference)
3324
-
3325
- E-COMMERCE
3326
- - Main image: white background, centered product, high resolution.
3327
- - Use describe to generate alt text and product descriptions.
3328
- - Generate 4-6 angles per product for a complete listing.`
3329
- },
3330
- video: {
3331
- topic: "video",
3332
- title: "Video Generation",
3333
- content: `How to create effective video content with Gunni:
3334
-
3335
- PROMPT STRUCTURE
3336
- - Include: subject + action + motion style + setting + camera movement.
3337
- - Good: "golden retriever running through autumn leaves, slow motion, tracking shot, warm afternoon light, cinematic"
3338
- - Weak: "dog running"
3339
-
3340
- KLING V3 (default video model)
3341
- - Structure prompts with subject, action, motion, and negative elements.
3342
- - Use negative phrases to prevent common issues: "no flicker, no distortion, no blur".
3343
- - Best for: dynamic scenes with fluid motion, product reveals, character animation.
3344
- - Keep clips under 10 seconds for best quality.
3345
- - Start from a still image for more control (image-to-video).
3346
-
3347
- VEO 3.1
3348
- - Five-part prompt formula: narrative + characters + visuals + audio + camera controls.
3349
- - Avoid quotes in prompts (they can trigger unwanted text rendering).
3350
- - Stick to single scenes in short clips. Use jump-cuts for multi-scene narratives.
3351
- - Specify camera techniques: "dolly zoom", "tracking shot", "aerial pullback".
3352
-
3353
- GENERAL VIDEO TIPS
3354
- - Start with a strong still image first, then animate. More control than text-to-video.
3355
- - Test with 5-second clips before committing to longer generations.
3356
- - Camera motion keywords: "static", "slow pan", "tracking", "dolly", "crane", "handheld".
3357
- - For product videos: start with hero shot, add subtle motion (rotation, parallax, zoom).
3358
- - For social: keep under 6 seconds, strong first frame, works on mute.`
3359
- },
3360
- "concept-art": {
3361
- topic: "concept-art",
3362
- title: "Concept Art & Illustration",
3363
- content: `How to create concept art and illustrations with Gunni:
3364
-
3365
- ENVIRONMENT DESIGN
3366
- - Start broad: "Pacific Northwest forest, morning mist, golden hour, cinematic"
3367
- - Refine with edits: add structures, characters, atmospheric effects.
3368
- - Use --variants to explore different moods and lighting for the same scene.
3369
-
3370
- CHARACTER CONCEPTS
3371
- - Generate full-body character designs with clear descriptions.
3372
- - Create expression sheets with edit: same character, different expressions.
3373
- - Use consistent style keywords and seeds across a character series.
3374
-
3375
- ARCHITECTURAL CONCEPTS
3376
- - Specify materials, era, and style: "modern lakehouse, cedar and glass, Olson Kundig inspired"
3377
- - Generate multiple views: exterior, interior, aerial, detail.
3378
- - Use edit to iterate on specific elements without regenerating the whole scene.
3379
-
3380
- ITERATION WORKFLOW
3381
- - Round 1: Generate 1 concept. Show it and get feedback.
3382
- - Round 2: Refine via edit, or try a different direction if needed.
3383
- - Round 3: Use 2-3 variants only when exploring mood/lighting options the user requested.
3384
- - Round 4: Upscale and describe for documentation.
3385
-
3386
- STYLE CONSISTENCY
3387
- - Lock in style keywords early and reuse them.
3388
- - Save the seed from your best generations.
3389
- - Reference previous work in edit prompts: "maintaining the same art style"`
3390
- },
3391
- pipelines: {
3392
- topic: "pipelines",
3393
- title: "Pipeline Orchestration",
3394
- content: `How to execute Gunni pipelines. Pipelines are multi-step workflows that chain templates, presets, and tools into complete deliverables.
3213
+ },
3214
+ advertising: {
3215
+ topic: "advertising",
3216
+ title: "Ad Creative",
3217
+ content: "How to create advertising assets with Gunni:\n\nCAMPAIGN STRUCTURE\n- Start with the hero image: the single strongest visual.\n- Generate platform-specific sizes from the same concept:\n - Instagram: 1080x1080 (feed), 1080x1920 (story)\n - Twitter/X: 1200x675 (post), 1500x500 (header)\n - YouTube: 1280x720 (thumbnail)\n - LinkedIn: 1200x627 (post)\n - OG: 1200x630\n\nA/B TESTING\n- Start with 1 hero image. Only generate additional variants when the user wants to compare options.\n- Keep copy consistent, vary imagery. Or keep imagery consistent, vary composition.\n- Use describe on each to document the differences for the team.\n\nPRODUCT SHOTS\n- Start with a clean product image (generate or use existing).\n- Remove background for a clean cutout.\n- Use edit to place in different lifestyle scenes.\n- Upscale the winners for production use.\n\nVIDEO ADS\n- Generate a compelling still, then create a 5-10 second video.\n- Add voiceover with gunni audio for complete ad packages.\n- Different video styles: zoom in, pan, parallax, subtle motion.\n\nTIPS\n- Ad images should have clear focal points and work at small sizes.\n- Test thumbnails at actual display size (they're often tiny).\n- Keep text minimal in generated images. Add text in post-production or use Recraft v4."
3218
+ },
3219
+ "product-photo": {
3220
+ topic: "product-photo",
3221
+ title: "Product Photography",
3222
+ content: 'How to create product photography with Gunni:\n\nFROM SCRATCH\n1. Generate the product: "ceramic mug, white background, studio lighting, product photography"\n2. Remove background: image --remove-bg\n3. Place in scenes: "place on marble countertop, morning light, coffee beans scattered nearby"\n4. Upscale the final: image --upscale\n\nFROM EXISTING PRODUCT PHOTO\n1. Remove background from the original photo.\n2. Use edit to place in new contexts and lighting.\n3. Generate multiple scene variants with --variants.\n4. Upscale winners.\n\nLIGHTING STYLES\n- "Studio lighting" \u2014 clean, professional, white background\n- "Natural light" \u2014 warm, lifestyle feel\n- "Dramatic lighting" \u2014 moody, high contrast, dark background\n- "Flat lay" \u2014 top-down arrangement with props\n- "Lifestyle" \u2014 product in use, real-world context\n\nSCENE TYPES\n- Hero shot (product as star, minimal background)\n- Lifestyle (product in use context)\n- Detail (close-up of texture, material, craftsmanship)\n- Group (multiple products arranged together)\n- Scale (product next to familiar objects for size reference)\n\nE-COMMERCE\n- Main image: white background, centered product, high resolution.\n- Use describe to generate alt text and product descriptions.\n- Generate 4-6 angles per product for a complete listing.'
3223
+ },
3224
+ video: {
3225
+ topic: "video",
3226
+ title: "Video Generation",
3227
+ content: 'How to create effective video content with Gunni:\n\nPROMPT STRUCTURE\n- Include: subject + action + motion style + setting + camera movement.\n- Good: "golden retriever running through autumn leaves, slow motion, tracking shot, warm afternoon light, cinematic"\n- Weak: "dog running"\n\nKLING V3 (default video model)\n- Structure prompts with subject, action, motion, and negative elements.\n- Use negative phrases to prevent common issues: "no flicker, no distortion, no blur".\n- Best for: dynamic scenes with fluid motion, product reveals, character animation.\n- Keep clips under 10 seconds for best quality.\n- Start from a still image for more control (image-to-video).\n\nVEO 3.1\n- Five-part prompt formula: narrative + characters + visuals + audio + camera controls.\n- Avoid quotes in prompts (they can trigger unwanted text rendering).\n- Stick to single scenes in short clips. Use jump-cuts for multi-scene narratives.\n- Specify camera techniques: "dolly zoom", "tracking shot", "aerial pullback".\n\nGENERAL VIDEO TIPS\n- Start with a strong still image first, then animate. More control than text-to-video.\n- Test with 5-second clips before committing to longer generations.\n- Camera motion keywords: "static", "slow pan", "tracking", "dolly", "crane", "handheld".\n- For product videos: start with hero shot, add subtle motion (rotation, parallax, zoom).\n- For social: keep under 6 seconds, strong first frame, works on mute.'
3228
+ },
3229
+ "concept-art": {
3230
+ topic: "concept-art",
3231
+ title: "Concept Art & Illustration",
3232
+ content: 'How to create concept art and illustrations with Gunni:\n\nENVIRONMENT DESIGN\n- Start broad: "Pacific Northwest forest, morning mist, golden hour, cinematic"\n- Refine with edits: add structures, characters, atmospheric effects.\n- Use --variants to explore different moods and lighting for the same scene.\n\nCHARACTER CONCEPTS\n- Generate full-body character designs with clear descriptions.\n- Create expression sheets with edit: same character, different expressions.\n- Use consistent style keywords and seeds across a character series.\n\nARCHITECTURAL CONCEPTS\n- Specify materials, era, and style: "modern lakehouse, cedar and glass, Olson Kundig inspired"\n- Generate multiple views: exterior, interior, aerial, detail.\n- Use edit to iterate on specific elements without regenerating the whole scene.\n\nITERATION WORKFLOW\n- Round 1: Generate 1 concept. Show it and get feedback.\n- Round 2: Refine via edit, or try a different direction if needed.\n- Round 3: Use 2-3 variants only when exploring mood/lighting options the user requested.\n- Round 4: Upscale and describe for documentation.\n\nSTYLE CONSISTENCY\n- Lock in style keywords early and reuse them.\n- Save the seed from your best generations.\n- Reference previous work in edit prompts: "maintaining the same art style"'
3233
+ },
3234
+ pipelines: {
3235
+ topic: "pipelines",
3236
+ title: "Pipeline Orchestration",
3237
+ content: `How to execute Gunni pipelines. Pipelines are multi-step workflows that chain templates, presets, and tools into complete deliverables.
3395
3238
 
3396
3239
  DISCOVERY
3397
3240
  - Call pipelines() to list available pipelines with names and descriptions
@@ -3437,8 +3280,23 @@ KEY PRINCIPLES
3437
3280
  - Use refs to chain outputs: step 1 output becomes step 2 input via input_refs.
3438
3281
  - The preset auto-applies to every generation. Don't manually add framing/suffix.
3439
3282
  - Assembly notes (the text step) should include: edit sequence, scene order, timing, text overlay copy, CTAs.`
3283
+ }
3284
+ },
3285
+ latest: {
3286
+ version: "0.2.0",
3287
+ date: "2026-02-22",
3288
+ updates: [
3289
+ "Unified image tool: generate, edit, describe, upscale, remove-bg. Routes by input.",
3290
+ "Multi-image editing: pass array of URLs for multi-ref edits (flux-kontext, flux-2-pro-edit, nano-banana-edit, gpt-image-edit).",
3291
+ "Ref system: ref_save + refs for saving/retrieving reference images.",
3292
+ "Variants flag for creative exploration.",
3293
+ "Latest updates merged into models (pass updates=true). refs now handles both list and get."
3294
+ ]
3440
3295
  }
3441
3296
  };
3297
+
3298
+ // src/core/knowledge.ts
3299
+ var TOPICS = knowledge_default.topics;
3442
3300
  function getLearnContent(topic) {
3443
3301
  if (!topic || topic === "overview") {
3444
3302
  return TOPICS.overview;
@@ -3451,17 +3309,7 @@ function getAvailableTopics() {
3451
3309
  return Object.keys(TOPICS);
3452
3310
  }
3453
3311
  function getLatestUpdates() {
3454
- return {
3455
- version: "0.2.0",
3456
- date: "2026-02-22",
3457
- updates: [
3458
- "Unified image tool: generate, edit, describe, upscale, remove-bg. Routes by input.",
3459
- "Multi-image editing: pass array of URLs for multi-ref edits (flux-kontext, flux-2-pro-edit, nano-banana-edit, gpt-image-edit).",
3460
- "Ref system: ref_save + refs for saving/retrieving reference images.",
3461
- "Variants flag for creative exploration.",
3462
- "Latest updates merged into models (pass updates=true). refs now handles both list and get."
3463
- ]
3464
- };
3312
+ return knowledge_default.latest;
3465
3313
  }
3466
3314
 
3467
3315
  // src/commands/learn.ts
@@ -3852,12 +3700,10 @@ async function interactiveConfig(configManager, json) {
3852
3700
  }
3853
3701
 
3854
3702
  // src/commands/list.ts
3855
- init_model_registry();
3856
3703
  import pc12 from "picocolors";
3857
3704
 
3858
- // src/core/guide-registry.ts
3859
- var GUIDES = [
3860
- // --- Branding ---
3705
+ // data/guides.json
3706
+ var guides_default = [
3861
3707
  {
3862
3708
  id: "brand-moodboard",
3863
3709
  title: "Brand Mood Board",
@@ -3897,15 +3743,7 @@ var GUIDES = [
3897
3743
  description: "Upscale the final mood board to presentation quality."
3898
3744
  }
3899
3745
  ],
3900
- tags: [
3901
- "brand",
3902
- "moodboard",
3903
- "mood-board",
3904
- "identity",
3905
- "grid",
3906
- "palette",
3907
- "branding"
3908
- ]
3746
+ tags: ["brand", "moodboard", "mood-board", "identity", "grid", "palette", "branding"]
3909
3747
  },
3910
3748
  {
3911
3749
  id: "logo-concepts",
@@ -3948,15 +3786,7 @@ var GUIDES = [
3948
3786
  description: "Upscale final logo to high resolution."
3949
3787
  }
3950
3788
  ],
3951
- tags: [
3952
- "logo",
3953
- "brand",
3954
- "identity",
3955
- "mark",
3956
- "wordmark",
3957
- "vector",
3958
- "design"
3959
- ]
3789
+ tags: ["logo", "brand", "identity", "mark", "wordmark", "vector", "design"]
3960
3790
  },
3961
3791
  {
3962
3792
  id: "color-palette-exploration",
@@ -3990,28 +3820,14 @@ var GUIDES = [
3990
3820
  description: "Generate a pattern using the palette to test it in a graphic design context."
3991
3821
  }
3992
3822
  ],
3993
- tags: [
3994
- "color",
3995
- "palette",
3996
- "colors",
3997
- "swatches",
3998
- "scheme",
3999
- "brand",
4000
- "design"
4001
- ]
3823
+ tags: ["color", "palette", "colors", "swatches", "scheme", "brand", "design"]
4002
3824
  },
4003
- // --- Product ---
4004
3825
  {
4005
3826
  id: "product-photography",
4006
3827
  title: "Product Photography Pipeline",
4007
3828
  description: "Generate a product shot, remove its background, composite onto a scene, and upscale for production use.",
4008
3829
  category: "product",
4009
- models: [
4010
- "flux-2-pro",
4011
- "bria-bg-remove",
4012
- "flux-kontext",
4013
- "topaz-upscale"
4014
- ],
3830
+ models: ["flux-2-pro", "bria-bg-remove", "flux-kontext", "topaz-upscale"],
4015
3831
  promptTemplate: "Professional product photography of {product_description}. {lighting_style} lighting, {surface_description} surface, {angle} angle. Sharp focus, commercial quality, 8K detail.",
4016
3832
  tips: [
4017
3833
  "Flux 2 Pro produces the most photorealistic product shots out of the box.",
@@ -4049,16 +3865,8 @@ var GUIDES = [
4049
3865
  description: "Verify the final image quality and composition before delivery."
4050
3866
  }
4051
3867
  ],
4052
- tags: [
4053
- "product",
4054
- "photography",
4055
- "ecommerce",
4056
- "packshot",
4057
- "studio",
4058
- "commercial"
4059
- ]
3868
+ tags: ["product", "photography", "ecommerce", "packshot", "studio", "commercial"]
4060
3869
  },
4061
- // --- Social Media ---
4062
3870
  {
4063
3871
  id: "social-media-assets",
4064
3872
  title: "Social Media Asset Creation",
@@ -4094,29 +3902,14 @@ var GUIDES = [
4094
3902
  description: "Analyze the generated asset to verify brand alignment and visual quality."
4095
3903
  }
4096
3904
  ],
4097
- tags: [
4098
- "social",
4099
- "instagram",
4100
- "facebook",
4101
- "twitter",
4102
- "story",
4103
- "post",
4104
- "banner",
4105
- "marketing"
4106
- ]
3905
+ tags: ["social", "instagram", "facebook", "twitter", "story", "post", "banner", "marketing"]
4107
3906
  },
4108
- // --- Iteration ---
4109
3907
  {
4110
3908
  id: "image-refinement",
4111
3909
  title: "Image Refinement and Iteration",
4112
3910
  description: "Systematically improve an image through describe-edit-verify cycles.",
4113
3911
  category: "iteration",
4114
- models: [
4115
- "florence-2",
4116
- "flux-kontext",
4117
- "nano-banana-edit",
4118
- "topaz-upscale"
4119
- ],
3912
+ models: ["florence-2", "flux-kontext", "nano-banana-edit", "topaz-upscale"],
4120
3913
  promptTemplate: "This is a refinement workflow, not a single prompt. Start with gunni describe, then edit based on the analysis.",
4121
3914
  tips: [
4122
3915
  "Always describe before editing \u2014 the description reveals issues you might not notice visually.",
@@ -4156,17 +3949,8 @@ var GUIDES = [
4156
3949
  description: "Upscale the refined image to final production quality."
4157
3950
  }
4158
3951
  ],
4159
- tags: [
4160
- "refine",
4161
- "iterate",
4162
- "improve",
4163
- "fix",
4164
- "edit",
4165
- "quality",
4166
- "polish"
4167
- ]
3952
+ tags: ["refine", "iterate", "improve", "fix", "edit", "quality", "polish"]
4168
3953
  },
4169
- // --- Workflow ---
4170
3954
  {
4171
3955
  id: "hero-image-pipeline",
4172
3956
  title: "Hero Image Pipeline",
@@ -4203,17 +3987,12 @@ var GUIDES = [
4203
3987
  description: "Upscale to retina/4K resolution for high-DPI screens."
4204
3988
  }
4205
3989
  ],
4206
- tags: [
4207
- "hero",
4208
- "banner",
4209
- "website",
4210
- "marketing",
4211
- "campaign",
4212
- "landing-page",
4213
- "header"
4214
- ]
3990
+ tags: ["hero", "banner", "website", "marketing", "campaign", "landing-page", "header"]
4215
3991
  }
4216
3992
  ];
3993
+
3994
+ // src/core/guide-registry.ts
3995
+ var GUIDES = guides_default;
4217
3996
  function getAllGuides() {
4218
3997
  return GUIDES;
4219
3998
  }
@@ -5726,532 +5505,9 @@ ${pc19.dim("Save as ref: gunni ref add <url> --name <name>")}
5726
5505
  });
5727
5506
  }
5728
5507
 
5729
- // src/commands/generate.ts
5508
+ // src/ui/onboarding.ts
5730
5509
  import * as clack3 from "@clack/prompts";
5731
5510
  import pc20 from "picocolors";
5732
- var DEFAULT_MODEL4 = "flux-2-pro";
5733
- function registerGenerateCommand(program2) {
5734
- program2.command("generate").description("Generate an image from a text prompt").argument("[prompt]", "Text prompt for image generation").option("-m, --model <model>", "Model to use (see: gunni list models --type image)", DEFAULT_MODEL4).option("-o, --output <path>", "Output file path (default: auto-generated)").option("--width <pixels>", "Image width in pixels", parseInt).option("--height <pixels>", "Image height in pixels", parseInt).option("--seed <number>", "Random seed for reproducibility", parseInt).addHelpText(
5735
- "after",
5736
- `
5737
- Examples:
5738
-
5739
- Interactive mode (guided walkthrough):
5740
- $ gunni generate
5741
-
5742
- Direct mode:
5743
- $ gunni generate "a ceramic mug on a wooden table, morning light"
5744
- $ gunni generate "a cat" --model flux-dev -o cat.png
5745
- $ gunni generate "hero banner" --width 1920 --height 1080 -o hero.png
5746
- $ gunni generate "abstract art" --seed 42 -o art.png
5747
-
5748
- Agent usage (structured JSON output):
5749
- $ gunni --json generate "product photo" --model flux-2-pro -o product.png
5750
-
5751
- Returns: { images: [{ path, url, width, height }], seed, requestId, model, prompt }
5752
-
5753
- Reproduce a previous generation:
5754
- $ gunni generate "same prompt" --seed 12345 --model flux-2-pro -o v2.png
5755
-
5756
- Available image models: flux-2-pro (default), flux-dev, flux-2-flex, grok-imagine, recraft-v4
5757
- Run 'gunni list models --type image' for details.
5758
- `
5759
- ).action(async (prompt, opts) => {
5760
- const json = program2.opts().json ?? false;
5761
- try {
5762
- let finalPrompt;
5763
- let finalModel;
5764
- let finalOutput;
5765
- let width = opts.width;
5766
- let height = opts.height;
5767
- let seed = opts.seed;
5768
- if (prompt) {
5769
- finalPrompt = prompt;
5770
- finalModel = opts.model;
5771
- finalOutput = opts.output ?? generateDefaultFilename(finalModel);
5772
- } else {
5773
- if (json) {
5774
- throw new GunniError(
5775
- "Prompt is required with --json",
5776
- "MISSING_PROMPT"
5777
- );
5778
- }
5779
- const interactive = await interactiveFlow(opts.model);
5780
- finalPrompt = interactive.prompt;
5781
- finalModel = interactive.model;
5782
- finalOutput = interactive.output;
5783
- width = interactive.width ?? width;
5784
- height = interactive.height ?? height;
5785
- seed = interactive.seed ?? seed;
5786
- }
5787
- const router = new ModelRouter();
5788
- const result = await router.generate({
5789
- prompt: finalPrompt,
5790
- model: finalModel,
5791
- outputPath: finalOutput,
5792
- width,
5793
- height,
5794
- seed,
5795
- showSpinner: !json
5796
- });
5797
- const img = result.images[0];
5798
- logGeneration({
5799
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5800
- command: "generate",
5801
- model: result.model,
5802
- prompt: result.prompt,
5803
- outputPath: img.path,
5804
- outputUrl: img.url,
5805
- seed: result.seed,
5806
- width: img.width,
5807
- height: img.height,
5808
- requestId: result.requestId
5809
- }).catch(() => {
5810
- });
5811
- console.log(formatGenerateResult(result, json));
5812
- } catch (err) {
5813
- if (clack3.isCancel(err)) {
5814
- clack3.cancel("Generation cancelled.");
5815
- process.exit(0);
5816
- }
5817
- console.error(formatError(err, json));
5818
- process.exit(1);
5819
- }
5820
- });
5821
- }
5822
- async function interactiveFlow(defaultModel) {
5823
- clack3.intro(pc20.bold("gunni generate"));
5824
- const prompt = await clack3.text({
5825
- message: "What do you want to generate?",
5826
- placeholder: "A ceramic mug on a wooden table, morning light",
5827
- validate: (val) => {
5828
- if (!val.trim()) return "Prompt cannot be empty";
5829
- }
5830
- });
5831
- if (clack3.isCancel(prompt)) throw prompt;
5832
- const { getModelsByCategory: getModelsByCategory2 } = await Promise.resolve().then(() => (init_model_registry(), model_registry_exports));
5833
- const imageModels = getModelsByCategory2("image");
5834
- const model = await clack3.select({
5835
- message: "Which model?",
5836
- options: imageModels.map((m) => ({
5837
- value: m.id,
5838
- label: m.name,
5839
- hint: m.id === defaultModel ? "default" : m.description
5840
- })),
5841
- initialValue: defaultModel
5842
- });
5843
- if (clack3.isCancel(model)) throw model;
5844
- let width;
5845
- let height;
5846
- let seed;
5847
- const advanced = await clack3.confirm({
5848
- message: "Configure advanced options?",
5849
- initialValue: false
5850
- });
5851
- if (clack3.isCancel(advanced)) throw advanced;
5852
- if (advanced) {
5853
- const seedInput = await clack3.text({
5854
- message: "Seed (leave blank for random):",
5855
- placeholder: "42"
5856
- });
5857
- if (clack3.isCancel(seedInput)) throw seedInput;
5858
- if (seedInput) seed = parseInt(seedInput, 10);
5859
- const widthInput = await clack3.text({
5860
- message: "Width in pixels (leave blank for default):",
5861
- placeholder: "1024"
5862
- });
5863
- if (clack3.isCancel(widthInput)) throw widthInput;
5864
- if (widthInput) width = parseInt(widthInput, 10);
5865
- const heightInput = await clack3.text({
5866
- message: "Height in pixels (leave blank for default):",
5867
- placeholder: "1024"
5868
- });
5869
- if (clack3.isCancel(heightInput)) throw heightInput;
5870
- if (heightInput) height = parseInt(heightInput, 10);
5871
- }
5872
- const output = await clack3.text({
5873
- message: "Output file path:",
5874
- defaultValue: generateDefaultFilename(model),
5875
- placeholder: generateDefaultFilename(model)
5876
- });
5877
- if (clack3.isCancel(output)) throw output;
5878
- clack3.log.step("Starting generation\u2026");
5879
- return {
5880
- prompt,
5881
- model,
5882
- output: output || generateDefaultFilename(model),
5883
- width,
5884
- height,
5885
- seed
5886
- };
5887
- }
5888
- function generateDefaultFilename(model) {
5889
- const timestamp = Date.now();
5890
- return `gunni-${model}-${timestamp}.png`;
5891
- }
5892
-
5893
- // src/commands/edit.ts
5894
- import ora6 from "ora";
5895
- init_model_registry();
5896
- import pc21 from "picocolors";
5897
- var DEFAULT_MODEL5 = "flux-kontext";
5898
- function registerEditCommand(program2) {
5899
- program2.command("edit").description("Edit an image with text instructions").argument("<image>", "Path to input image").argument("<prompt>", "Edit instructions").option("-m, --model <model>", "Model to use (see: gunni list models --type edit)", DEFAULT_MODEL5).option("-o, --output <path>", "Output file path (default: auto-generated)").addHelpText(
5900
- "after",
5901
- `
5902
- Examples:
5903
-
5904
- Basic editing:
5905
- $ gunni edit photo.png "make the lighting warmer"
5906
- $ gunni edit logo.png "place on a coffee cup mockup, studio lighting" -o mockup.png
5907
- $ gunni edit hero.png "add subtle film grain and warmer tones" -o hero-v2.png
5908
-
5909
- Chaining with generate:
5910
- $ gunni generate "a ceramic mug on a table" -o mug.png
5911
- $ gunni edit mug.png "add steam rising from the mug, morning light" -o mug-final.png
5912
-
5913
- Agent usage (structured JSON output):
5914
- $ gunni --json edit input.png "make it more vibrant" -o output.png
5915
-
5916
- Returns: { image: { path, url, width, height }, model, prompt, inputImage }
5917
-
5918
- Available edit models: flux-kontext (default), grok-edit
5919
- Run 'gunni list models --type edit' for details.
5920
- `
5921
- ).action(async (image, prompt, opts) => {
5922
- const json = program2.opts().json ?? false;
5923
- try {
5924
- const spinner = json ? void 0 : ora6("Starting\u2026").start();
5925
- const update = (s) => {
5926
- if (spinner) spinner.text = s;
5927
- };
5928
- const imageUrl = await uploadToFal(image, update);
5929
- update("Editing\u2026");
5930
- const provider = new FalProvider();
5931
- const result = await provider.call(
5932
- getEndpoint2(opts.model ?? DEFAULT_MODEL5),
5933
- { image_url: imageUrl, prompt },
5934
- update
5935
- );
5936
- const data = result.data;
5937
- const img = data.images?.[0];
5938
- if (!img) {
5939
- spinner?.fail("No image returned");
5940
- throw new GunniError("No image in response", "NO_IMAGE");
5941
- }
5942
- const requestedPath = opts.output ?? `gunni-edit-${Date.now()}.png`;
5943
- const finalPath = await downloadImage(img.url, requestedPath, update);
5944
- spinner?.succeed("Done");
5945
- const output = {
5946
- image: {
5947
- path: finalPath,
5948
- url: img.url,
5949
- width: img.width ?? 0,
5950
- height: img.height ?? 0
5951
- },
5952
- model: opts.model ?? DEFAULT_MODEL5,
5953
- prompt,
5954
- inputImage: image
5955
- };
5956
- logGeneration({
5957
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5958
- command: "edit",
5959
- model: output.model,
5960
- prompt,
5961
- inputImage: image,
5962
- outputPath: finalPath,
5963
- outputUrl: img.url,
5964
- width: output.image.width,
5965
- height: output.image.height
5966
- }).catch(() => {
5967
- });
5968
- if (json) {
5969
- console.log(JSON.stringify(output, null, 2));
5970
- } else {
5971
- console.log(`${pc21.green("\u2714")} Edited image saved to ${pc21.bold(finalPath)}`);
5972
- console.log(` ${pc21.dim(`${output.image.width}\xD7${output.image.height} \xB7 ${output.model}`)}`);
5973
- }
5974
- } catch (err) {
5975
- console.error(formatError(err, json));
5976
- process.exit(1);
5977
- }
5978
- });
5979
- }
5980
- function getEndpoint2(modelId) {
5981
- const model = getModel(modelId);
5982
- if (!model) {
5983
- throw new GunniError(
5984
- `Unknown model: ${modelId}`,
5985
- "UNKNOWN_MODEL",
5986
- "Run `gunni list models --type edit` to see available models."
5987
- );
5988
- }
5989
- return model.endpoint;
5990
- }
5991
-
5992
- // src/commands/describe.ts
5993
- import ora7 from "ora";
5994
- init_model_registry();
5995
- var DEFAULT_MODEL6 = "florence-2";
5996
- function registerDescribeCommand(program2) {
5997
- program2.command("describe").description("Describe an image (image-to-text)").argument("<image>", "Path to image file").option("-m, --model <model>", "Model to use", DEFAULT_MODEL6).addHelpText(
5998
- "after",
5999
- `
6000
- Examples:
6001
-
6002
- Describe an image:
6003
- $ gunni describe photo.png
6004
- $ gunni describe logo.png --model florence-2
6005
-
6006
- Agent usage \u2014 understand what was generated, then iterate:
6007
- $ gunni generate "a logo for a coffee brand" -o logo.png
6008
- $ gunni --json describe logo.png
6009
- $ gunni edit logo.png "make the colors warmer" -o logo-v2.png
6010
-
6011
- Structured JSON output:
6012
- $ gunni --json describe image.png
6013
-
6014
- Returns: { description, model, inputImage }
6015
- `
6016
- ).action(async (image, opts) => {
6017
- const json = program2.opts().json ?? false;
6018
- try {
6019
- const spinner = json ? void 0 : ora7("Starting\u2026").start();
6020
- const update = (s) => {
6021
- if (spinner) spinner.text = s;
6022
- };
6023
- const modelId = opts.model ?? DEFAULT_MODEL6;
6024
- const modelDef = getModel(modelId);
6025
- if (!modelDef) {
6026
- spinner?.fail();
6027
- throw new GunniError(
6028
- `Unknown model: ${modelId}`,
6029
- "UNKNOWN_MODEL",
6030
- "Run `gunni list models --type describe` to see available models."
6031
- );
6032
- }
6033
- const imageUrl = await uploadToFal(image, update);
6034
- update("Analyzing\u2026");
6035
- const provider = new FalProvider();
6036
- const result = await provider.call(
6037
- modelDef.endpoint,
6038
- { image_url: imageUrl },
6039
- update
6040
- );
6041
- const data = result.data;
6042
- const description = data.results ?? "";
6043
- spinner?.succeed("Done");
6044
- const output = {
6045
- description,
6046
- model: modelId,
6047
- inputImage: image
6048
- };
6049
- logGeneration({
6050
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6051
- command: "describe",
6052
- model: modelId,
6053
- inputImage: image,
6054
- outputPath: image,
6055
- metadata: { description }
6056
- }).catch(() => {
6057
- });
6058
- if (json) {
6059
- console.log(JSON.stringify(output, null, 2));
6060
- } else {
6061
- console.log(description);
6062
- }
6063
- } catch (err) {
6064
- console.error(formatError(err, json));
6065
- process.exit(1);
6066
- }
6067
- });
6068
- }
6069
-
6070
- // src/commands/upscale.ts
6071
- import ora8 from "ora";
6072
- init_model_registry();
6073
- import pc22 from "picocolors";
6074
- var DEFAULT_MODEL7 = "topaz-upscale";
6075
- function registerUpscaleCommand(program2) {
6076
- program2.command("upscale").description("Upscale an image to higher resolution").argument("<image>", "Path to input image").option("-o, --output <path>", "Output file path (default: auto-generated)").option("-s, --scale <factor>", "Scale factor (2 or 4)", parseInt, 2).option("-m, --model <model>", "Model to use", DEFAULT_MODEL7).addHelpText(
6077
- "after",
6078
- `
6079
- Examples:
6080
-
6081
- Upscale an image:
6082
- $ gunni upscale photo.png -o photo-hires.png
6083
- $ gunni upscale draft.png --scale 4 -o final.png
6084
-
6085
- Chain with generate for production quality:
6086
- $ gunni generate "product photo" -o draft.png
6087
- $ gunni upscale draft.png -o production.png
6088
-
6089
- Agent usage:
6090
- $ gunni --json upscale image.png -o hires.png
6091
-
6092
- Returns: { image: { path, url, width, height }, scale, model, inputImage }
6093
- `
6094
- ).action(async (image, opts) => {
6095
- const json = program2.opts().json ?? false;
6096
- try {
6097
- const spinner = json ? void 0 : ora8("Starting\u2026").start();
6098
- const update = (s) => {
6099
- if (spinner) spinner.text = s;
6100
- };
6101
- const modelId = opts.model ?? DEFAULT_MODEL7;
6102
- const modelDef = getModel(modelId);
6103
- if (!modelDef) {
6104
- spinner?.fail();
6105
- throw new GunniError(`Unknown model: ${modelId}`, "UNKNOWN_MODEL");
6106
- }
6107
- const imageUrl = await uploadToFal(image, update);
6108
- update("Upscaling\u2026");
6109
- const provider = new FalProvider();
6110
- const result = await provider.call(
6111
- modelDef.endpoint,
6112
- { image_url: imageUrl, scale: opts.scale },
6113
- update
6114
- );
6115
- const data = result.data;
6116
- const img = data.image;
6117
- if (!img) {
6118
- spinner?.fail("No image returned");
6119
- throw new GunniError("No image in response", "NO_IMAGE");
6120
- }
6121
- const requestedPath = opts.output ?? `gunni-upscale-${Date.now()}.png`;
6122
- const finalPath = await downloadImage(img.url, requestedPath, update);
6123
- spinner?.succeed("Done");
6124
- const output = {
6125
- image: {
6126
- path: finalPath,
6127
- url: img.url,
6128
- width: img.width ?? 0,
6129
- height: img.height ?? 0
6130
- },
6131
- scale: opts.scale,
6132
- model: modelId,
6133
- inputImage: image
6134
- };
6135
- logGeneration({
6136
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6137
- command: "upscale",
6138
- model: modelId,
6139
- inputImage: image,
6140
- outputPath: finalPath,
6141
- outputUrl: img.url,
6142
- width: output.image.width,
6143
- height: output.image.height,
6144
- metadata: { scale: opts.scale }
6145
- }).catch(() => {
6146
- });
6147
- if (json) {
6148
- console.log(JSON.stringify(output, null, 2));
6149
- } else {
6150
- console.log(`${pc22.green("\u2714")} Upscaled image saved to ${pc22.bold(finalPath)}`);
6151
- console.log(` ${pc22.dim(`${output.image.width}\xD7${output.image.height} \xB7 ${opts.scale}x \xB7 ${modelId}`)}`);
6152
- }
6153
- } catch (err) {
6154
- console.error(formatError(err, json));
6155
- process.exit(1);
6156
- }
6157
- });
6158
- }
6159
-
6160
- // src/commands/remove-bg.ts
6161
- import ora9 from "ora";
6162
- init_model_registry();
6163
- import pc23 from "picocolors";
6164
- var DEFAULT_MODEL8 = "bria-bg-remove";
6165
- function registerRemoveBgCommand(program2) {
6166
- program2.command("remove-bg").description("Remove background from an image").argument("<image>", "Path to input image").option("-o, --output <path>", "Output file path (default: auto-generated)").option("-m, --model <model>", "Model to use", DEFAULT_MODEL8).addHelpText(
6167
- "after",
6168
- `
6169
- Examples:
6170
-
6171
- Remove background:
6172
- $ gunni remove-bg product.png -o product-clean.png
6173
- $ gunni remove-bg photo.jpg -o cutout.png
6174
-
6175
- Chain with other commands:
6176
- $ gunni generate "a coffee bag product shot" -o bag.png
6177
- $ gunni remove-bg bag.png -o bag-clean.png
6178
- $ gunni edit bag-clean.png "place on a marble countertop" -o final.png
6179
-
6180
- Agent usage:
6181
- $ gunni --json remove-bg input.png -o output.png
6182
-
6183
- Returns: { image: { path, url, width, height }, model, inputImage }
6184
- `
6185
- ).action(async (image, opts) => {
6186
- const json = program2.opts().json ?? false;
6187
- try {
6188
- const spinner = json ? void 0 : ora9("Starting\u2026").start();
6189
- const update = (s) => {
6190
- if (spinner) spinner.text = s;
6191
- };
6192
- const modelId = opts.model ?? DEFAULT_MODEL8;
6193
- const modelDef = getModel(modelId);
6194
- if (!modelDef) {
6195
- spinner?.fail();
6196
- throw new GunniError(`Unknown model: ${modelId}`, "UNKNOWN_MODEL");
6197
- }
6198
- const imageUrl = await uploadToFal(image, update);
6199
- update("Removing background\u2026");
6200
- const provider = new FalProvider();
6201
- const result = await provider.call(
6202
- modelDef.endpoint,
6203
- { image_url: imageUrl },
6204
- update
6205
- );
6206
- const data = result.data;
6207
- const img = data.image;
6208
- if (!img) {
6209
- spinner?.fail("No image returned");
6210
- throw new GunniError("No image in response", "NO_IMAGE");
6211
- }
6212
- const requestedPath = opts.output ?? `gunni-nobg-${Date.now()}.png`;
6213
- const finalPath = await downloadImage(img.url, requestedPath, update);
6214
- spinner?.succeed("Done");
6215
- const output = {
6216
- image: {
6217
- path: finalPath,
6218
- url: img.url,
6219
- width: img.width ?? 0,
6220
- height: img.height ?? 0
6221
- },
6222
- model: modelId,
6223
- inputImage: image
6224
- };
6225
- logGeneration({
6226
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6227
- command: "remove-bg",
6228
- model: modelId,
6229
- inputImage: image,
6230
- outputPath: finalPath,
6231
- outputUrl: img.url,
6232
- width: output.image.width,
6233
- height: output.image.height
6234
- }).catch(() => {
6235
- });
6236
- if (json) {
6237
- console.log(JSON.stringify(output, null, 2));
6238
- } else {
6239
- console.log(`${pc23.green("\u2714")} Background removed, saved to ${pc23.bold(finalPath)}`);
6240
- console.log(` ${pc23.dim(`${output.image.width}\xD7${output.image.height} \xB7 ${modelId}`)}`);
6241
- }
6242
- } catch (err) {
6243
- console.error(formatError(err, json));
6244
- process.exit(1);
6245
- }
6246
- });
6247
- }
6248
-
6249
- // src/index.ts
6250
- init_model_registry();
6251
-
6252
- // src/ui/onboarding.ts
6253
- import * as clack4 from "@clack/prompts";
6254
- import pc24 from "picocolors";
6255
5511
  async function hasGunniKey() {
6256
5512
  const config = new ConfigManager();
6257
5513
  const key = await config.getGunniApiKey();
@@ -6259,47 +5515,47 @@ async function hasGunniKey() {
6259
5515
  }
6260
5516
  async function runOnboarding() {
6261
5517
  console.log("");
6262
- console.log(pc24.bold("Welcome to Gunni") + pc24.dim(" \u2014 AI media toolkit"));
5518
+ console.log(pc20.bold("Welcome to Gunni") + pc20.dim(" \u2014 AI media toolkit"));
6263
5519
  console.log("");
6264
5520
  console.log(" Generate images, video, and audio from your terminal.");
6265
5521
  console.log(" One API key. 17 curated models. Zero config.");
6266
5522
  console.log("");
6267
- console.log(` Opening ${pc24.bold("gunni.ai/keys")} in your browser...`);
5523
+ console.log(` Opening ${pc20.bold("gunni.ai/keys")} in your browser...`);
6268
5524
  console.log(` Sign up (or log in), create an API key, and paste it below.`);
6269
5525
  console.log("");
6270
5526
  openUrl("https://gunni.ai/keys");
6271
- const apiKey = await clack4.text({
5527
+ const apiKey = await clack3.text({
6272
5528
  message: "Paste your Gunni API key:",
6273
5529
  placeholder: "gn_live_...",
6274
5530
  validate: (val) => {
6275
5531
  if (!val.trim()) return "API key cannot be empty";
6276
5532
  }
6277
5533
  });
6278
- if (clack4.isCancel(apiKey)) return false;
5534
+ if (clack3.isCancel(apiKey)) return false;
6279
5535
  const config = new ConfigManager();
6280
5536
  await config.setGunniApiKey(apiKey);
6281
5537
  console.log("");
6282
- console.log(` ${pc24.green("\u2714")} API key saved. You're ready to go.`);
5538
+ console.log(` ${pc20.green("\u2714")} API key saved. You're ready to go.`);
6283
5539
  console.log("");
6284
- console.log(` ${pc24.bold("Commands:")}`);
6285
- console.log(` ${pc24.cyan("image")} Generate, edit, describe, upscale, remove-bg`);
6286
- console.log(` ${pc24.cyan("video")} Image or text to video`);
6287
- console.log(` ${pc24.cyan("audio")} Text to speech`);
6288
- console.log(` ${pc24.cyan("learn")} Creative expertise and best practices`);
6289
- console.log(` ${pc24.cyan("config")} Manage API keys`);
5540
+ console.log(` ${pc20.bold("Commands:")}`);
5541
+ console.log(` ${pc20.cyan("image")} Generate, edit, describe, upscale, remove-bg`);
5542
+ console.log(` ${pc20.cyan("video")} Image or text to video`);
5543
+ console.log(` ${pc20.cyan("audio")} Text to speech`);
5544
+ console.log(` ${pc20.cyan("learn")} Creative expertise and best practices`);
5545
+ console.log(` ${pc20.cyan("config")} Manage API keys`);
6290
5546
  console.log("");
6291
- console.log(` ${pc24.bold("Quick start:")}`);
6292
- console.log(` ${pc24.dim("$")} gunni image "coffee bag product shot" -o bag.png`);
6293
- console.log(` ${pc24.dim("$")} gunni image bag.png --remove-bg -o bag-clean.png`);
6294
- console.log(` ${pc24.dim("$")} gunni image bag-clean.png "on marble counter" -o hero.png`);
6295
- console.log(` ${pc24.dim("$")} gunni video hero.png -p "slow cinematic zoom" -o hero.mp4`);
5547
+ console.log(` ${pc20.bold("Quick start:")}`);
5548
+ console.log(` ${pc20.dim("$")} gunni image "coffee bag product shot" -o bag.png`);
5549
+ console.log(` ${pc20.dim("$")} gunni image bag.png --remove-bg -o bag-clean.png`);
5550
+ console.log(` ${pc20.dim("$")} gunni image bag-clean.png "on marble counter" -o hero.png`);
5551
+ console.log(` ${pc20.dim("$")} gunni video hero.png -p "slow cinematic zoom" -o hero.mp4`);
6296
5552
  console.log("");
6297
5553
  return true;
6298
5554
  }
6299
5555
 
6300
5556
  // src/index.ts
6301
5557
  var program = new Command();
6302
- program.name("gunni").description("AI media toolkit \u2014 give any agent the ability to create professional media").version("0.2.0").option("--json", "Output results as JSON").addHelpText(
5558
+ program.name("gunni").description("AI media toolkit \u2014 give any agent the ability to create professional media").version("0.3.3").option("--json", "Output results as JSON").addHelpText(
6303
5559
  "after",
6304
5560
  `
6305
5561
  Examples:
@@ -6343,6 +5599,32 @@ registerInitCommand(program);
6343
5599
  registerHistoryCommand(program);
6344
5600
  registerConfigCommand(program);
6345
5601
  registerListCommand(program);
5602
+ program.command("models").description("List available models (alias for: gunni list models)").option("-t, --type <category>", "Filter by category").action((opts) => {
5603
+ const json = program.opts().json ?? false;
5604
+ const models = opts.type ? getModelsByCategory(opts.type) : getAllModels();
5605
+ if (json) {
5606
+ console.log(JSON.stringify(models, null, 2));
5607
+ return;
5608
+ }
5609
+ if (models.length === 0) {
5610
+ console.log(`No models found${opts.type ? ` for category "${opts.type}"` : ""}.`);
5611
+ return;
5612
+ }
5613
+ const grouped = /* @__PURE__ */ new Map();
5614
+ for (const model of models) {
5615
+ const list = grouped.get(model.category) ?? [];
5616
+ list.push(model);
5617
+ grouped.set(model.category, list);
5618
+ }
5619
+ for (const [category, catModels] of grouped) {
5620
+ console.log(`
5621
+ ${category.toUpperCase()}`);
5622
+ for (const m of catModels) {
5623
+ console.log(` ${m.id} ${m.description}`);
5624
+ }
5625
+ }
5626
+ console.log();
5627
+ });
6346
5628
  registerGuideCommand(program);
6347
5629
  registerRefCommand(program);
6348
5630
  registerStyleCommand(program);
@@ -6350,11 +5632,6 @@ registerTemplateCommand(program);
6350
5632
  registerPresetCommand(program);
6351
5633
  registerPipelineCommand(program);
6352
5634
  registerResearchCommand(program);
6353
- registerGenerateCommand(program);
6354
- registerEditCommand(program);
6355
- registerDescribeCommand(program);
6356
- registerUpscaleCommand(program);
6357
- registerRemoveBgCommand(program);
6358
5635
  program.action(async () => {
6359
5636
  const json = program.opts().json ?? false;
6360
5637
  if (json) {
@@ -6371,7 +5648,7 @@ program.action(async () => {
6371
5648
  { name: "list models", usage: "gunni list models [--type <category>]", description: "List available models" },
6372
5649
  { name: "config", usage: "gunni config", description: "Manage API keys" }
6373
5650
  ];
6374
- console.log(JSON.stringify({ version: "0.2.0", commands, categories, models }, null, 2));
5651
+ console.log(JSON.stringify({ version: "0.3.3", commands, categories, models }, null, 2));
6375
5652
  return;
6376
5653
  }
6377
5654
  const configured = await hasGunniKey();
@@ -6381,49 +5658,49 @@ program.action(async () => {
6381
5658
  return;
6382
5659
  }
6383
5660
  console.log(`
6384
- ${pc25.bold("GUNNI")} ${pc25.dim("v0.2.0")} \u2014 AI media toolkit
6385
-
6386
- ${pc25.bold("Media:")}
6387
- ${pc25.cyan("image")} Generate, edit, describe, upscale, remove-bg ${pc25.dim('gunni image "a cat" -o cat.png')}
6388
- ${pc25.cyan("video")} Image/text \u2192 video ${pc25.dim('gunni video hero.png -p "zoom in"')}
6389
- ${pc25.cyan("audio")} Text \u2192 speech ${pc25.dim('gunni audio "Hello" -o hello.mp3')}
6390
-
6391
- ${pc25.bold("Production:")}
6392
- ${pc25.cyan("template")} Browse & manage prompt templates ${pc25.dim("gunni template list --category ugc")}
6393
- ${pc25.cyan("preset")} Platform presets (framing, aspect ratio) ${pc25.dim("gunni preset get tiktok-ugc")}
6394
- ${pc25.cyan("pipeline")} Multi-step production workflows ${pc25.dim("gunni pipeline get ugc-product-reel")}
6395
- ${pc25.cyan("style")} Visual styles (brand identity) ${pc25.dim("gunni style list")}
6396
-
6397
- ${pc25.bold("Context:")}
6398
- ${pc25.cyan("learn")} Creative expertise & best practices ${pc25.dim("gunni learn exploration")}
6399
- ${pc25.cyan("latest")} New models, features, updates ${pc25.dim("gunni latest")}
6400
-
6401
- ${pc25.bold("Utility:")}
6402
- ${pc25.cyan("init")} Start a new project ${pc25.dim("gunni init --template brand")}
6403
- ${pc25.cyan("history")} Search past work ${pc25.dim('gunni history "coffee"')}
6404
- ${pc25.cyan("list")} Discover models ${pc25.dim("gunni list models")}
6405
- ${pc25.cyan("config")} API key setup ${pc25.dim("gunni config")}
6406
-
6407
- ${pc25.bold("Models:")} ${pc25.dim(`${getAllModels().length} curated models`)}`);
5661
+ ${pc21.bold("GUNNI")} ${pc21.dim("v0.3.0")} \u2014 AI media toolkit
5662
+
5663
+ ${pc21.bold("Media:")}
5664
+ ${pc21.cyan("image")} Generate, edit, describe, upscale, remove-bg ${pc21.dim('gunni image "a cat" -o cat.png')}
5665
+ ${pc21.cyan("video")} Image/text \u2192 video ${pc21.dim('gunni video hero.png -p "zoom in"')}
5666
+ ${pc21.cyan("audio")} Text \u2192 speech ${pc21.dim('gunni audio "Hello" -o hello.mp3')}
5667
+
5668
+ ${pc21.bold("Production:")}
5669
+ ${pc21.cyan("template")} Browse & manage prompt templates ${pc21.dim("gunni template list --category ugc")}
5670
+ ${pc21.cyan("preset")} Platform presets (framing, aspect ratio) ${pc21.dim("gunni preset get tiktok-ugc")}
5671
+ ${pc21.cyan("pipeline")} Multi-step production workflows ${pc21.dim("gunni pipeline get ugc-product-reel")}
5672
+ ${pc21.cyan("style")} Visual styles (brand identity) ${pc21.dim("gunni style list")}
5673
+
5674
+ ${pc21.bold("Context:")}
5675
+ ${pc21.cyan("learn")} Creative expertise & best practices ${pc21.dim("gunni learn exploration")}
5676
+ ${pc21.cyan("latest")} New models, features, updates ${pc21.dim("gunni latest")}
5677
+
5678
+ ${pc21.bold("Utility:")}
5679
+ ${pc21.cyan("init")} Start a new project ${pc21.dim("gunni init --template brand")}
5680
+ ${pc21.cyan("history")} Search past work ${pc21.dim('gunni history "coffee"')}
5681
+ ${pc21.cyan("list")} Discover models ${pc21.dim("gunni list models")}
5682
+ ${pc21.cyan("config")} API key setup ${pc21.dim("gunni config")}
5683
+
5684
+ ${pc21.bold("Models:")} ${pc21.dim(`${getAllModels().length} curated models`)}`);
6408
5685
  for (const cat of getCategories()) {
6409
5686
  const models = getModelsByCategory(cat);
6410
5687
  const defaultModel = models.find((m) => m.isDefault);
6411
5688
  const others = models.filter((m) => m !== defaultModel);
6412
5689
  if (defaultModel) {
6413
- console.log(` ${pc25.bold(cat.toUpperCase())} ${pc25.dim("\u2014")} ${pc25.cyan(defaultModel.id)}${others.length ? pc25.dim(`, ${others.map((m) => m.id).join(", ")}`) : ""}`);
5690
+ console.log(` ${pc21.bold(cat.toUpperCase())} ${pc21.dim("\u2014")} ${pc21.cyan(defaultModel.id)}${others.length ? pc21.dim(`, ${others.map((m) => m.id).join(", ")}`) : ""}`);
6414
5691
  } else {
6415
- console.log(` ${pc25.bold(cat.toUpperCase())} ${pc25.dim("\u2014")} ${models.map((m) => m.id).join(", ")}`);
5692
+ console.log(` ${pc21.bold(cat.toUpperCase())} ${pc21.dim("\u2014")} ${models.map((m) => m.id).join(", ")}`);
6416
5693
  }
6417
5694
  }
6418
5695
  console.log(`
6419
- ${pc25.bold("Quick start:")}
6420
- ${pc25.dim("$")} gunni image "coffee bag product shot" -o bag.png
6421
- ${pc25.dim("$")} gunni image bag.png --remove-bg -o bag-clean.png
6422
- ${pc25.dim("$")} gunni image bag-clean.png "on marble counter" -o hero.png
6423
- ${pc25.dim("$")} gunni image hero.png --upscale -o hero-final.png
6424
-
6425
- ${pc25.dim("Add --json to any command for structured output.")}
6426
- ${pc25.dim("Run gunni learn to load creative expertise.")}
5696
+ ${pc21.bold("Quick start:")}
5697
+ ${pc21.dim("$")} gunni image "coffee bag product shot" -o bag.png
5698
+ ${pc21.dim("$")} gunni image bag.png --remove-bg -o bag-clean.png
5699
+ ${pc21.dim("$")} gunni image bag-clean.png "on marble counter" -o hero.png
5700
+ ${pc21.dim("$")} gunni image hero.png --upscale -o hero-final.png
5701
+
5702
+ ${pc21.dim("Add --json to any command for structured output.")}
5703
+ ${pc21.dim("Run gunni learn to load creative expertise.")}
6427
5704
  `);
6428
5705
  });
6429
5706
  process.on("uncaughtException", (err) => {