egaki 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.5.0
6
+
7
+ 1. **Interactive model picker** — omitting `--model` now shows a select prompt with popular models instead of silently using a default:
8
+
9
+ ```bash
10
+ # Shows a model picker in the terminal
11
+ egaki image "a dreamy watercolor landscape" -o landscape.png
12
+
13
+ # Explicit model skips the picker
14
+ egaki image "a dreamy watercolor landscape" -m imagen-4.0-generate-001 -o landscape.png
15
+ ```
16
+
17
+ Works for both `egaki image` and `egaki video`. In non-interactive (piped/scripted) mode, the default model is used automatically so existing scripts are unaffected.
18
+
5
19
  ## 0.4.1
6
20
 
7
21
  1. **Added `gpt-image-2` model** — OpenAI's new flagship image generation model (released 2026-04-21) is now available in the model catalog:
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { goke } from 'goke';
11
11
  import { z } from 'zod';
12
12
  import dedent from 'string-dedent';
13
13
  import { generateImage as aiGenerateImage, generateText, experimental_generateVideo as aiGenerateVideo, } from 'ai';
14
+ import { select, isCancel, cancel } from '@clack/prompts';
14
15
  import fs from 'node:fs';
15
16
  import path from 'node:path';
16
17
  import pc from 'picocolors';
@@ -18,6 +19,7 @@ import { createParser } from 'eventsource-parser';
18
19
  import pkg from '../package.json' with { type: 'json' };
19
20
  import { injectCredentialsToEnv, PROVIDERS, getChatGptAuth, saveChatGptAuth, getKeyStatus, } from './credentials.js';
20
21
  import { IMAGE_MODELS, VIDEO_MODELS, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, createImageModel, createTextModel, createVideoModel, shouldUseResponsesApi, } from './models.js';
22
+ import { CATALOG } from './model-catalog.js';
21
23
  import { VIDEO_CATALOG } from './video-model-catalog.js';
22
24
  import { loginInteractive, loginNonInteractive, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
23
25
  import { subscribeInteractive, subscribeNonInteractive, unsubscribe, showUsage, } from './subscription.js';
@@ -144,7 +146,7 @@ cli
144
146
  from the model ID: imagen-* uses the image API, everything else uses
145
147
  the text API with image output enabled.
146
148
  `)
147
- .option('-m, --model [model]', z.enum(IMAGE_MODELS).default(DEFAULT_MODEL).describe('Model ID for generation'))
149
+ .option('-m, --model [model]', z.enum(IMAGE_MODELS).describe('Model ID for generation. If omitted, shows an interactive picker (or uses default in non-TTY mode)'))
148
150
  .option('-o, --output [path]', z
149
151
  .string()
150
152
  .default('egaki-output.png')
@@ -187,10 +189,7 @@ cli
187
189
  .action(async (prompt, options) => {
188
190
  // Inject stored API keys as env vars before calling the AI SDK.
189
191
  injectCredentialsToEnv();
190
- // goke infers schema .default() values via Zod's input type, which leaves
191
- // options.model / options.output / options.count as `T | undefined` even
192
- // though the runtime always resolves them. Apply the same defaults here.
193
- const model = options.model ?? DEFAULT_MODEL;
192
+ const model = options.model ?? await resolveImageModel();
194
193
  const outputPath = options.output ?? 'egaki-output.png';
195
194
  const count = options.count ?? 1;
196
195
  const config = getModelConfig(model);
@@ -261,7 +260,7 @@ cli
261
260
  Agent note: video generation can be slow. When invoking this command from
262
261
  automation, use a command timeout of at least 5 minutes.
263
262
  `)
264
- .option('-m, --model [model]', z.enum(VIDEO_MODELS).default(DEFAULT_VIDEO_MODEL).describe('Video model ID for generation'))
263
+ .option('-m, --model [model]', z.enum(VIDEO_MODELS).describe('Video model ID for generation. If omitted, shows an interactive picker (or uses default in non-TTY mode)'))
265
264
  .option('-o, --output [path]', z
266
265
  .string()
267
266
  .default('egaki-output.mp4')
@@ -291,8 +290,7 @@ cli
291
290
  .example('egaki video "waves crashing on cliffs" -n 2 -o waves.mp4')
292
291
  .action(async (prompt, options) => {
293
292
  injectCredentialsToEnv();
294
- // Apply the same .default(...) values goke's input-type inference drops.
295
- const model = options.model ?? DEFAULT_VIDEO_MODEL;
293
+ const model = options.model ?? await resolveVideoModel();
296
294
  const outputPath = options.output ?? 'egaki-output.mp4';
297
295
  const count = options.count ?? 1;
298
296
  const config = getModelConfig(model);
@@ -336,7 +334,6 @@ cli
336
334
  .describe('Filter by model type: image (image+text-image), video, or all'))
337
335
  .option('--json', 'Output as JSON instead of YAML')
338
336
  .action(async (options) => {
339
- const { CATALOG } = await import('./model-catalog.js');
340
337
  const yaml = await import('js-yaml');
341
338
  const providerStatuses = Object.fromEntries(Object.keys(PROVIDERS).map((provider) => [provider, getKeyStatus(provider)]));
342
339
  let models = options.type === 'video'
@@ -392,6 +389,84 @@ cli
392
389
  cli.help();
393
390
  cli.version(pkg.version);
394
391
  cli.parse();
392
+ // ─── interactive model pickers ──────────────────────────────────────────────
393
+ /** Curated image models shown in the interactive picker, ordered by recommendation. */
394
+ const FEATURED_IMAGE_MODELS = [
395
+ 'imagen-4.0-generate-001',
396
+ 'imagen-4.0-ultra-generate-001',
397
+ 'imagen-4.0-fast-generate-001',
398
+ 'gemini-2.5-flash-image',
399
+ 'gemini-3.1-flash-image-preview',
400
+ 'nano-banana-pro-preview',
401
+ 'gpt-image-2',
402
+ 'gpt-image-1.5',
403
+ 'chatgpt-image-latest',
404
+ 'flux-kontext-pro',
405
+ 'flux-kontext-max',
406
+ 'flux-pro-1.1-ultra',
407
+ 'recraft-v4',
408
+ 'dall-e-3',
409
+ 'grok-imagine-image-pro',
410
+ ];
411
+ /**
412
+ * Resolve the image model: show interactive picker in TTY, use default otherwise.
413
+ */
414
+ async function resolveImageModel() {
415
+ const isTTY = process.stdin.isTTY && process.stderr.isTTY;
416
+ if (!isTTY)
417
+ return DEFAULT_MODEL;
418
+ const options = FEATURED_IMAGE_MODELS
419
+ .map((id) => CATALOG.find((m) => m.id === id))
420
+ .filter(Boolean)
421
+ .map((m) => {
422
+ const cost = m.cost.type === 'per-image'
423
+ ? pc.dim(`$${m.cost.perImage}/img`)
424
+ : m.cost.type === 'per-token'
425
+ ? pc.dim(`$${m.cost.outputPerM}/M out`)
426
+ : '';
427
+ return {
428
+ value: m.id,
429
+ label: `${m.name} ${cost}`,
430
+ hint: m.id,
431
+ };
432
+ });
433
+ const selected = await select({
434
+ message: 'Select an image model',
435
+ options,
436
+ });
437
+ if (isCancel(selected)) {
438
+ cancel('Cancelled.');
439
+ process.exit(0);
440
+ }
441
+ return selected;
442
+ }
443
+ /**
444
+ * Resolve the video model: show interactive picker in TTY, use default otherwise.
445
+ */
446
+ async function resolveVideoModel() {
447
+ const isTTY = process.stdin.isTTY && process.stderr.isTTY;
448
+ if (!isTTY)
449
+ return DEFAULT_VIDEO_MODEL;
450
+ const options = VIDEO_CATALOG.map((m) => {
451
+ const dur = m.features.durationRangeSec
452
+ ? pc.dim(`${m.features.durationRangeSec.min}-${m.features.durationRangeSec.max}s`)
453
+ : '';
454
+ return {
455
+ value: m.id,
456
+ label: `${m.name} ${dur}`,
457
+ hint: m.id,
458
+ };
459
+ });
460
+ const selected = await select({
461
+ message: 'Select a video model',
462
+ options,
463
+ });
464
+ if (isCancel(selected)) {
465
+ cancel('Cancelled.');
466
+ process.exit(0);
467
+ }
468
+ return selected;
469
+ }
395
470
  function isUrl(input) {
396
471
  return /^https?:\/\//i.test(input);
397
472
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "egaki",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "main": "./dist/cli.js",
6
6
  "types": "./dist/cli.d.ts",
@@ -45,6 +45,15 @@ the key with `egaki login --provider egaki --key egaki_...`.
45
45
  When login requires a URL flow, run login in the background and send the login URL
46
46
  to the user so they can complete auth interactively.
47
47
 
48
+ ## Model selection
49
+
50
+ The `--model` / `-m` flag is **optional** on both `egaki image` and `egaki video`.
51
+
52
+ - **Interactive (TTY):** omitting `--model` shows a picker with popular models
53
+ - **Non-interactive (piped/scripted):** omitting `--model` uses a sensible default
54
+
55
+ Agents should always pass `-m` explicitly to avoid the interactive picker.
56
+
48
57
  ## Example commands
49
58
 
50
59
  ```bash
@@ -60,7 +69,7 @@ egaki subscribe
60
69
  # check subscription usage
61
70
  egaki usage
62
71
 
63
- # generate an image
72
+ # generate an image (interactive model picker if TTY)
64
73
  egaki image "a watercolor fox reading a map" -o fox.png
65
74
 
66
75
  # select a model explicitly
package/src/cli.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  generateText,
16
16
  experimental_generateVideo as aiGenerateVideo,
17
17
  } from 'ai'
18
+ import { select, isCancel, cancel } from '@clack/prompts'
18
19
  import fs from 'node:fs'
19
20
  import path from 'node:path'
20
21
  import pc from 'picocolors'
@@ -38,6 +39,7 @@ import {
38
39
  createVideoModel,
39
40
  shouldUseResponsesApi,
40
41
  } from './models.js'
42
+ import { CATALOG } from './model-catalog.js'
41
43
  import { VIDEO_CATALOG } from './video-model-catalog.js'
42
44
  import {
43
45
  loginInteractive,
@@ -225,7 +227,7 @@ cli
225
227
  )
226
228
  .option(
227
229
  '-m, --model [model]',
228
- z.enum(IMAGE_MODELS).default(DEFAULT_MODEL).describe('Model ID for generation'),
230
+ z.enum(IMAGE_MODELS).describe('Model ID for generation. If omitted, shows an interactive picker (or uses default in non-TTY mode)'),
229
231
  )
230
232
  .option(
231
233
  '-o, --output [path]',
@@ -308,10 +310,7 @@ cli
308
310
  // Inject stored API keys as env vars before calling the AI SDK.
309
311
  injectCredentialsToEnv()
310
312
 
311
- // goke infers schema .default() values via Zod's input type, which leaves
312
- // options.model / options.output / options.count as `T | undefined` even
313
- // though the runtime always resolves them. Apply the same defaults here.
314
- const model = options.model ?? DEFAULT_MODEL
313
+ const model = options.model ?? await resolveImageModel()
315
314
  const outputPath = options.output ?? 'egaki-output.png'
316
315
  const count = options.count ?? 1
317
316
  const config = getModelConfig(model)
@@ -391,7 +390,7 @@ cli
391
390
  )
392
391
  .option(
393
392
  '-m, --model [model]',
394
- z.enum(VIDEO_MODELS).default(DEFAULT_VIDEO_MODEL).describe('Video model ID for generation'),
393
+ z.enum(VIDEO_MODELS).describe('Video model ID for generation. If omitted, shows an interactive picker (or uses default in non-TTY mode)'),
395
394
  )
396
395
  .option(
397
396
  '-o, --output [path]',
@@ -455,8 +454,7 @@ cli
455
454
  .action(async (prompt, options) => {
456
455
  injectCredentialsToEnv()
457
456
 
458
- // Apply the same .default(...) values goke's input-type inference drops.
459
- const model = options.model ?? DEFAULT_VIDEO_MODEL
457
+ const model = options.model ?? await resolveVideoModel()
460
458
  const outputPath = options.output ?? 'egaki-output.mp4'
461
459
  const count = options.count ?? 1
462
460
  const config = getModelConfig(model)
@@ -515,7 +513,6 @@ cli
515
513
  )
516
514
  .option('--json', 'Output as JSON instead of YAML')
517
515
  .action(async (options) => {
518
- const { CATALOG } = await import('./model-catalog.js')
519
516
  const yaml = await import('js-yaml')
520
517
  const providerStatuses = Object.fromEntries(
521
518
  Object.keys(PROVIDERS).map((provider) => [provider, getKeyStatus(provider)]),
@@ -579,6 +576,94 @@ cli.help()
579
576
  cli.version(pkg.version)
580
577
  cli.parse()
581
578
 
579
+ // ─── interactive model pickers ──────────────────────────────────────────────
580
+
581
+ /** Curated image models shown in the interactive picker, ordered by recommendation. */
582
+ const FEATURED_IMAGE_MODELS = [
583
+ 'imagen-4.0-generate-001',
584
+ 'imagen-4.0-ultra-generate-001',
585
+ 'imagen-4.0-fast-generate-001',
586
+ 'gemini-2.5-flash-image',
587
+ 'gemini-3.1-flash-image-preview',
588
+ 'nano-banana-pro-preview',
589
+ 'gpt-image-2',
590
+ 'gpt-image-1.5',
591
+ 'chatgpt-image-latest',
592
+ 'flux-kontext-pro',
593
+ 'flux-kontext-max',
594
+ 'flux-pro-1.1-ultra',
595
+ 'recraft-v4',
596
+ 'dall-e-3',
597
+ 'grok-imagine-image-pro',
598
+ ]
599
+
600
+ /**
601
+ * Resolve the image model: show interactive picker in TTY, use default otherwise.
602
+ */
603
+ async function resolveImageModel(): Promise<string> {
604
+ const isTTY = process.stdin.isTTY && process.stderr.isTTY
605
+ if (!isTTY) return DEFAULT_MODEL
606
+
607
+ const options = FEATURED_IMAGE_MODELS
608
+ .map((id) => CATALOG.find((m) => m.id === id))
609
+ .filter(Boolean)
610
+ .map((m) => {
611
+ const cost = m!.cost.type === 'per-image'
612
+ ? pc.dim(`$${m!.cost.perImage}/img`)
613
+ : m!.cost.type === 'per-token'
614
+ ? pc.dim(`$${m!.cost.outputPerM}/M out`)
615
+ : ''
616
+ return {
617
+ value: m!.id,
618
+ label: `${m!.name} ${cost}`,
619
+ hint: m!.id,
620
+ }
621
+ })
622
+
623
+ const selected = await select({
624
+ message: 'Select an image model',
625
+ options,
626
+ })
627
+
628
+ if (isCancel(selected)) {
629
+ cancel('Cancelled.')
630
+ process.exit(0)
631
+ }
632
+
633
+ return selected
634
+ }
635
+
636
+ /**
637
+ * Resolve the video model: show interactive picker in TTY, use default otherwise.
638
+ */
639
+ async function resolveVideoModel(): Promise<string> {
640
+ const isTTY = process.stdin.isTTY && process.stderr.isTTY
641
+ if (!isTTY) return DEFAULT_VIDEO_MODEL
642
+
643
+ const options = VIDEO_CATALOG.map((m) => {
644
+ const dur = m.features.durationRangeSec
645
+ ? pc.dim(`${m.features.durationRangeSec.min}-${m.features.durationRangeSec.max}s`)
646
+ : ''
647
+ return {
648
+ value: m.id,
649
+ label: `${m.name} ${dur}`,
650
+ hint: m.id,
651
+ }
652
+ })
653
+
654
+ const selected = await select({
655
+ message: 'Select a video model',
656
+ options,
657
+ })
658
+
659
+ if (isCancel(selected)) {
660
+ cancel('Cancelled.')
661
+ process.exit(0)
662
+ }
663
+
664
+ return selected
665
+ }
666
+
582
667
  function isUrl(input: string): boolean {
583
668
  return /^https?:\/\//i.test(input)
584
669
  }