egaki 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/cli/cache-utils.d.ts +33 -0
  3. package/dist/cli/cache-utils.d.ts.map +1 -0
  4. package/dist/cli/cache-utils.js +157 -0
  5. package/dist/cli/cached-generate.d.ts +105 -0
  6. package/dist/cli/cached-generate.d.ts.map +1 -0
  7. package/dist/cli/cached-generate.js +208 -0
  8. package/dist/cli/cached-generate.test.d.ts +11 -0
  9. package/dist/cli/cached-generate.test.d.ts.map +1 -0
  10. package/dist/cli/cached-generate.test.js +431 -0
  11. package/dist/cli/cartesia-provider.d.ts +16 -0
  12. package/dist/cli/cartesia-provider.d.ts.map +1 -0
  13. package/dist/cli/cartesia-provider.js +184 -0
  14. package/dist/cli/chatgpt-auth.d.ts.map +1 -0
  15. package/dist/cli/cli.d.ts +4 -0
  16. package/dist/cli/cli.d.ts.map +1 -0
  17. package/dist/cli/cli.js +1329 -0
  18. package/dist/cli/clone-generate.d.ts +26 -0
  19. package/dist/cli/clone-generate.d.ts.map +1 -0
  20. package/dist/cli/clone-generate.js +148 -0
  21. package/dist/{credentials.d.ts → cli/credentials.d.ts} +11 -2
  22. package/dist/cli/credentials.d.ts.map +1 -0
  23. package/dist/{credentials.js → cli/credentials.js} +88 -5
  24. package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
  25. package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
  26. package/dist/cli/elevenlabs-speech-provider.js +199 -0
  27. package/dist/cli/generate.d.ts +140 -0
  28. package/dist/cli/generate.d.ts.map +1 -0
  29. package/dist/cli/generate.js +604 -0
  30. package/dist/cli/generate.test.d.ts +2 -0
  31. package/dist/cli/generate.test.d.ts.map +1 -0
  32. package/dist/cli/generate.test.js +131 -0
  33. package/dist/cli/login.d.ts.map +1 -0
  34. package/dist/{login.js → cli/login.js} +45 -4
  35. package/dist/cli/main.d.ts +3 -0
  36. package/dist/cli/main.d.ts.map +1 -0
  37. package/dist/cli/main.js +7 -0
  38. package/dist/cli/model-catalog.d.ts +141 -0
  39. package/dist/cli/model-catalog.d.ts.map +1 -0
  40. package/dist/cli/model-catalog.js +2135 -0
  41. package/dist/{models.d.ts → cli/models.d.ts} +16 -9
  42. package/dist/cli/models.d.ts.map +1 -0
  43. package/dist/cli/models.js +313 -0
  44. package/dist/cli/open-browser.d.ts.map +1 -0
  45. package/dist/cli/openai-speech-provider.d.ts +3 -0
  46. package/dist/cli/openai-speech-provider.d.ts.map +1 -0
  47. package/dist/cli/openai-speech-provider.js +28 -0
  48. package/dist/cli/separation-generate.d.ts +34 -0
  49. package/dist/cli/separation-generate.d.ts.map +1 -0
  50. package/dist/cli/separation-generate.js +117 -0
  51. package/dist/cli/speech-catalog.d.ts +35 -0
  52. package/dist/cli/speech-catalog.d.ts.map +1 -0
  53. package/dist/cli/speech-catalog.js +195 -0
  54. package/dist/cli/speech-generate.d.ts +86 -0
  55. package/dist/cli/speech-generate.d.ts.map +1 -0
  56. package/dist/cli/speech-generate.js +156 -0
  57. package/dist/cli/speech-models.d.ts +8 -0
  58. package/dist/cli/speech-models.d.ts.map +1 -0
  59. package/dist/cli/speech-models.js +109 -0
  60. package/dist/cli/speech-provider.test.d.ts +2 -0
  61. package/dist/cli/speech-provider.test.d.ts.map +1 -0
  62. package/dist/cli/speech-provider.test.js +197 -0
  63. package/dist/cli/subscription.d.ts.map +1 -0
  64. package/dist/cli/transcription-catalog.d.ts +35 -0
  65. package/dist/cli/transcription-catalog.d.ts.map +1 -0
  66. package/dist/cli/transcription-catalog.js +192 -0
  67. package/dist/cli/transcription-generate.d.ts +62 -0
  68. package/dist/cli/transcription-generate.d.ts.map +1 -0
  69. package/dist/cli/transcription-generate.js +144 -0
  70. package/dist/cli/transcription-models.d.ts +9 -0
  71. package/dist/cli/transcription-models.d.ts.map +1 -0
  72. package/dist/cli/transcription-models.js +100 -0
  73. package/dist/cli/transcription-providers.d.ts +22 -0
  74. package/dist/cli/transcription-providers.d.ts.map +1 -0
  75. package/dist/cli/transcription-providers.js +290 -0
  76. package/dist/cli/video-model-catalog.d.ts +2 -0
  77. package/dist/cli/video-model-catalog.d.ts.map +1 -0
  78. package/dist/cli/video-model-catalog.js +5 -0
  79. package/dist/cli/xai-auth.d.ts +29 -0
  80. package/dist/cli/xai-auth.d.ts.map +1 -0
  81. package/dist/cli/xai-auth.js +325 -0
  82. package/dist/vite/angled-screen.d.ts +57 -0
  83. package/dist/vite/angled-screen.d.ts.map +1 -0
  84. package/dist/vite/angled-screen.js +115 -0
  85. package/dist/vite/app.d.ts +51 -0
  86. package/dist/vite/app.d.ts.map +1 -0
  87. package/dist/vite/app.js +150 -0
  88. package/dist/vite/bands-shader.d.ts +47 -0
  89. package/dist/vite/bands-shader.d.ts.map +1 -0
  90. package/dist/vite/bands-shader.js +267 -0
  91. package/dist/vite/code-block.d.ts +107 -0
  92. package/dist/vite/code-block.d.ts.map +1 -0
  93. package/dist/vite/code-block.js +641 -0
  94. package/dist/vite/components.d.ts +36 -0
  95. package/dist/vite/components.d.ts.map +1 -0
  96. package/dist/vite/components.js +113 -0
  97. package/dist/vite/dispersion-rings-shader.d.ts +44 -0
  98. package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
  99. package/dist/vite/dispersion-rings-shader.js +282 -0
  100. package/dist/vite/easing-curves.d.ts +129 -0
  101. package/dist/vite/easing-curves.d.ts.map +1 -0
  102. package/dist/vite/easing-curves.js +584 -0
  103. package/dist/vite/easing-curves.test.d.ts +2 -0
  104. package/dist/vite/easing-curves.test.d.ts.map +1 -0
  105. package/dist/vite/easing-curves.test.js +200 -0
  106. package/dist/vite/generated-media-client.d.ts +35 -0
  107. package/dist/vite/generated-media-client.d.ts.map +1 -0
  108. package/dist/vite/generated-media-client.js +76 -0
  109. package/dist/vite/keyframes.d.ts +129 -0
  110. package/dist/vite/keyframes.d.ts.map +1 -0
  111. package/dist/vite/keyframes.js +194 -0
  112. package/dist/vite/layout-editor.d.ts +45 -0
  113. package/dist/vite/layout-editor.d.ts.map +1 -0
  114. package/dist/vite/layout-editor.js +587 -0
  115. package/dist/vite/layout-transition.d.ts +136 -0
  116. package/dist/vite/layout-transition.d.ts.map +1 -0
  117. package/dist/vite/layout-transition.js +581 -0
  118. package/dist/vite/liquid-gradient-shader.d.ts +62 -0
  119. package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
  120. package/dist/vite/liquid-gradient-shader.js +413 -0
  121. package/dist/vite/mdx-client.d.ts +15 -0
  122. package/dist/vite/mdx-client.d.ts.map +1 -0
  123. package/dist/vite/mdx-client.js +265 -0
  124. package/dist/vite/mdx-parse.d.ts +102 -0
  125. package/dist/vite/mdx-parse.d.ts.map +1 -0
  126. package/dist/vite/mdx-parse.js +251 -0
  127. package/dist/vite/mdx-provided-components.d.ts +18 -0
  128. package/dist/vite/mdx-provided-components.d.ts.map +1 -0
  129. package/dist/vite/mdx-provided-components.js +14 -0
  130. package/dist/vite/mdx-video.d.ts +327 -0
  131. package/dist/vite/mdx-video.d.ts.map +1 -0
  132. package/dist/vite/mdx-video.js +596 -0
  133. package/dist/vite/mdx-video.test.d.ts +9 -0
  134. package/dist/vite/mdx-video.test.d.ts.map +1 -0
  135. package/dist/vite/mdx-video.test.js +1945 -0
  136. package/dist/vite/media-components.d.ts +45 -0
  137. package/dist/vite/media-components.d.ts.map +1 -0
  138. package/dist/vite/media-components.js +355 -0
  139. package/dist/vite/media-duration-store.d.ts +66 -0
  140. package/dist/vite/media-duration-store.d.ts.map +1 -0
  141. package/dist/vite/media-duration-store.js +204 -0
  142. package/dist/vite/player-page.d.ts +35 -0
  143. package/dist/vite/player-page.d.ts.map +1 -0
  144. package/dist/vite/player-page.js +720 -0
  145. package/dist/vite/render-client.d.ts +18 -0
  146. package/dist/vite/render-client.d.ts.map +1 -0
  147. package/dist/vite/render-client.js +67 -0
  148. package/dist/vite/sdk.d.ts +221 -0
  149. package/dist/vite/sdk.d.ts.map +1 -0
  150. package/dist/vite/sdk.js +355 -0
  151. package/dist/vite/server-components.d.ts +55 -0
  152. package/dist/vite/server-components.d.ts.map +1 -0
  153. package/dist/vite/server-components.js +156 -0
  154. package/dist/vite/server-mdx.d.ts +74 -0
  155. package/dist/vite/server-mdx.d.ts.map +1 -0
  156. package/dist/vite/server-mdx.js +259 -0
  157. package/dist/vite/shader-renderer.d.ts +48 -0
  158. package/dist/vite/shader-renderer.d.ts.map +1 -0
  159. package/dist/vite/shader-renderer.js +411 -0
  160. package/dist/vite/store-hooks.d.ts +12 -0
  161. package/dist/vite/store-hooks.d.ts.map +1 -0
  162. package/dist/vite/store-hooks.js +93 -0
  163. package/dist/vite/store.d.ts +75 -0
  164. package/dist/vite/store.d.ts.map +1 -0
  165. package/dist/vite/store.js +52 -0
  166. package/dist/vite/store.test.d.ts +6 -0
  167. package/dist/vite/store.test.d.ts.map +1 -0
  168. package/dist/vite/store.test.js +172 -0
  169. package/dist/vite/tweakpane-hook.d.ts +94 -0
  170. package/dist/vite/tweakpane-hook.d.ts.map +1 -0
  171. package/dist/vite/tweakpane-hook.js +450 -0
  172. package/dist/vite/vite-plugin.d.ts +28 -0
  173. package/dist/vite/vite-plugin.d.ts.map +1 -0
  174. package/dist/vite/vite-plugin.js +538 -0
  175. package/dist/vite/wave-gradient-shader.d.ts +41 -0
  176. package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
  177. package/dist/vite/wave-gradient-shader.js +207 -0
  178. package/package.json +103 -25
  179. package/skills/egaki/SKILL.md +162 -1
  180. package/src/cli/cache-utils.ts +169 -0
  181. package/src/cli/cached-generate.test.ts +531 -0
  182. package/src/cli/cached-generate.ts +348 -0
  183. package/src/cli/cartesia-provider.ts +249 -0
  184. package/src/cli/cli.ts +1818 -0
  185. package/src/cli/clone-generate.ts +202 -0
  186. package/src/{credentials.ts → cli/credentials.ts} +90 -7
  187. package/src/cli/elevenlabs-speech-provider.ts +243 -0
  188. package/src/cli/generate.test.ts +166 -0
  189. package/src/cli/generate.ts +912 -0
  190. package/src/{login.ts → cli/login.ts} +48 -3
  191. package/src/cli/main.ts +8 -0
  192. package/src/cli/model-catalog.ts +2336 -0
  193. package/src/cli/models.ts +408 -0
  194. package/src/cli/openai-speech-provider.ts +34 -0
  195. package/src/cli/separation-generate.ts +187 -0
  196. package/src/cli/speech-catalog.ts +253 -0
  197. package/src/cli/speech-generate.ts +275 -0
  198. package/src/cli/speech-models.ts +146 -0
  199. package/src/cli/speech-provider.test.ts +226 -0
  200. package/src/cli/transcription-catalog.ts +250 -0
  201. package/src/cli/transcription-generate.ts +214 -0
  202. package/src/cli/transcription-models.ts +136 -0
  203. package/src/cli/transcription-providers.ts +427 -0
  204. package/src/cli/video-model-catalog.ts +14 -0
  205. package/src/cli/xai-auth.ts +396 -0
  206. package/src/vite/angled-screen.tsx +214 -0
  207. package/src/vite/app.tsx +197 -0
  208. package/src/vite/bands-shader.tsx +319 -0
  209. package/src/vite/code-block.tsx +939 -0
  210. package/src/vite/components.tsx +278 -0
  211. package/src/vite/dispersion-rings-shader.tsx +331 -0
  212. package/src/vite/easing-curves.test.ts +232 -0
  213. package/src/vite/easing-curves.ts +743 -0
  214. package/src/vite/generated-media-client.tsx +121 -0
  215. package/src/vite/keyframes.tsx +347 -0
  216. package/src/vite/layout-editor.tsx +709 -0
  217. package/src/vite/layout-transition.tsx +706 -0
  218. package/src/vite/liquid-gradient-shader.tsx +474 -0
  219. package/src/vite/mdx-client.tsx +420 -0
  220. package/src/vite/mdx-parse.ts +343 -0
  221. package/src/vite/mdx-provided-components.ts +19 -0
  222. package/src/vite/mdx-video.test.tsx +2219 -0
  223. package/src/vite/mdx-video.tsx +976 -0
  224. package/src/vite/media-components.tsx +473 -0
  225. package/src/vite/media-duration-store.ts +225 -0
  226. package/src/vite/motion-timing.ts +194 -0
  227. package/src/vite/player-page.tsx +1107 -0
  228. package/src/vite/render-client.ts +84 -0
  229. package/src/vite/sdk.ts +598 -0
  230. package/src/vite/server-components.tsx +241 -0
  231. package/src/vite/server-mdx.ts +285 -0
  232. package/src/vite/shader-renderer.tsx +535 -0
  233. package/src/vite/store-hooks.ts +117 -0
  234. package/src/vite/store.test.ts +202 -0
  235. package/src/vite/store.ts +115 -0
  236. package/src/vite/styles.css +21 -0
  237. package/src/vite/tweakpane-hook.tsx +593 -0
  238. package/src/vite/virtual-modules.d.ts +30 -0
  239. package/src/vite/vite-plugin.ts +584 -0
  240. package/src/vite/wave-gradient-shader.tsx +253 -0
  241. package/README.md +0 -266
  242. package/dist/antigravity-auth.d.ts +0 -15
  243. package/dist/antigravity-auth.d.ts.map +0 -1
  244. package/dist/antigravity-auth.js +0 -544
  245. package/dist/antigravity-image.d.ts +0 -11
  246. package/dist/antigravity-image.d.ts.map +0 -1
  247. package/dist/antigravity-image.js +0 -285
  248. package/dist/chatgpt-auth.d.ts.map +0 -1
  249. package/dist/cli.d.ts +0 -3
  250. package/dist/cli.d.ts.map +0 -1
  251. package/dist/cli.js +0 -968
  252. package/dist/credentials.d.ts.map +0 -1
  253. package/dist/gateway-model-candidates.generated.d.ts +0 -11
  254. package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
  255. package/dist/gateway-model-candidates.generated.js +0 -64
  256. package/dist/login.d.ts.map +0 -1
  257. package/dist/model-catalog.d.ts +0 -42
  258. package/dist/model-catalog.d.ts.map +0 -1
  259. package/dist/model-catalog.js +0 -1001
  260. package/dist/models.d.ts.map +0 -1
  261. package/dist/models.js +0 -246
  262. package/dist/open-browser.d.ts.map +0 -1
  263. package/dist/subscription.d.ts.map +0 -1
  264. package/dist/video-model-catalog.d.ts +0 -52
  265. package/dist/video-model-catalog.d.ts.map +0 -1
  266. package/dist/video-model-catalog.js +0 -306
  267. package/src/cli.ts +0 -1378
  268. package/src/model-catalog.ts +0 -1077
  269. package/src/models.ts +0 -309
  270. package/src/video-model-catalog.ts +0 -363
  271. /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
  272. /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
  273. /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
  274. /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
  275. /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
  276. /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
  277. /package/dist/{subscription.js → cli/subscription.js} +0 -0
  278. /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
  279. /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
  280. /package/src/{subscription.ts → cli/subscription.ts} +0 -0
package/src/cli.ts DELETED
@@ -1,1378 +0,0 @@
1
- #!/usr/bin/env node
2
- // Main CLI entrypoint for egaki - AI image and video generation.
3
- // Uses the Vercel AI SDK for image generation across multiple providers.
4
- // Designed to be called by agents and humans alike.
5
- //
6
- // Two generation paths depending on model:
7
- // - imagen-* models → generateImage() with google.image()
8
- // - all other models → generateText() with responseModalities: ['IMAGE']
9
- // The CLI auto-detects which path to use based on model ID prefix.
10
- import { goke } from 'goke'
11
- import { z } from 'zod'
12
- import dedent from 'string-dedent'
13
- import {
14
- generateImage as aiGenerateImage,
15
- generateText,
16
- experimental_generateVideo as aiGenerateVideo,
17
- } from 'ai'
18
- import { select, isCancel, cancel } from '@clack/prompts'
19
- import fs from 'node:fs'
20
- import path from 'node:path'
21
- import pc from 'picocolors'
22
- import { createParser, type EventSourceMessage } from 'eventsource-parser'
23
- import pkg from '../package.json' with { type: 'json' }
24
- import {
25
- injectCredentialsToEnv,
26
- PROVIDERS,
27
- getChatGptAuth,
28
- saveChatGptAuth,
29
- getKeyStatus,
30
- } from './credentials.js'
31
- import {
32
- IMAGE_MODELS,
33
- VIDEO_MODELS,
34
- DEFAULT_MODEL,
35
- DEFAULT_VIDEO_MODEL,
36
- getModelConfig,
37
- createImageModel,
38
- createTextModel,
39
- createVideoModel,
40
- shouldUseResponsesApi,
41
- } from './models.js'
42
- import { CATALOG } from './model-catalog.js'
43
- import { VIDEO_CATALOG } from './video-model-catalog.js'
44
- import {
45
- loginInteractive,
46
- loginNonInteractive,
47
- showLoginStatus,
48
- removeLogin,
49
- readKeyFromStdin,
50
- } from './login.js'
51
- import {
52
- subscribeInteractive,
53
- subscribeNonInteractive,
54
- unsubscribe,
55
- showUsage,
56
- } from './subscription.js'
57
- import { getValidChatGptAuth } from './chatgpt-auth.js'
58
-
59
- const cli = goke('egaki')
60
-
61
- process.title = 'egaki'
62
-
63
- // Print clean error output for unhandled rejections (e.g. AI SDK APICallError).
64
- // The AI SDK errors include request bodies, response headers, and other noisy
65
- // properties that get dumped by Node's default handler. We strip those and just
66
- // print the message + stack trace.
67
- process.on('uncaughtException', (err) => {
68
- console.error(pc.red(err.message))
69
- if (err.stack) {
70
- const lines = err.stack.split('\n')
71
- const stackOnly = lines.filter((l) => l.trimStart().startsWith('at '))
72
- if (stackOnly.length > 0) {
73
- console.error(pc.dim(stackOnly.join('\n')))
74
- }
75
- }
76
- process.exit(1)
77
- })
78
-
79
- // ─── login command ───────────────────────────────────────────────────────────
80
-
81
- cli
82
- .command(
83
- 'login',
84
- dedent`
85
- Configure API keys for image generation providers.
86
- Interactive mode: shows a provider picker and secure key input.
87
- Non-interactive mode: pass --provider and --key flags, or pipe key via stdin.
88
- Keys are saved to ~/.config/egaki/credentials.json (mode 0600).
89
- `,
90
- )
91
- .option(
92
- '-p, --provider [name]',
93
- z
94
- .string()
95
- .describe(
96
- `Provider name for non-interactive login (${Object.keys(PROVIDERS).join(', ')})`,
97
- ),
98
- )
99
- .option(
100
- '-k, --key [key]',
101
- z.string().describe('API key value for non-interactive login'),
102
- )
103
- .option('--show', 'Show which providers are configured and their status')
104
- .option(
105
- '--remove [provider]',
106
- z.string().describe('Remove the stored key for a provider'),
107
- )
108
- .example('# Interactive login (pick provider, paste key)')
109
- .example('egaki login')
110
- .example('# Non-interactive login with flags')
111
- .example('egaki login --provider google --key AIza...')
112
- .example('egaki login --provider vertex --key AIza...')
113
- .example('# Pipe key from stdin (useful in CI/scripts)')
114
- .example('echo "AIza..." | egaki login --provider google')
115
- .example('# Show configured providers')
116
- .example('egaki login --show')
117
- .example('# Remove a stored key')
118
- .example('egaki login --remove google')
119
- .action(async (options) => {
120
- if (options.show) {
121
- showLoginStatus()
122
- return
123
- }
124
-
125
- if (options.remove) {
126
- removeLogin(options.remove)
127
- return
128
- }
129
-
130
- // Non-interactive: --provider + --key or stdin
131
- if (options.provider) {
132
- // ChatGPT uses browser OAuth — skip key reading
133
- if (options.provider === 'chatgpt') {
134
- await loginNonInteractive({ provider: options.provider, key: '' })
135
- return
136
- }
137
- const key = options.key || (await readKeyFromStdin())
138
- await loginNonInteractive({ provider: options.provider, key })
139
- return
140
- }
141
-
142
- // Interactive mode
143
- await loginInteractive()
144
- })
145
-
146
- // ─── subscribe command ───────────────────────────────────────────────────────
147
-
148
- cli
149
- .command(
150
- 'subscribe',
151
- dedent`
152
- Subscribe to Egaki for access to all image models with a single API key.
153
- You can also use your own provider keys (Google/OpenAI/Replicate/Fal)
154
- via 'egaki login --provider <name> --key <key>' if you prefer BYOK.
155
- Egaki subscription avoids managing one key per provider.
156
- Three plans: Starter ($9/mo, 100 credits), Pro ($29/mo, 500 credits),
157
- Unlimited ($99/mo, 2000 credits). One credit ≈ one standard image.
158
- Interactive mode: pick a plan and get a checkout URL (email prefill optional).
159
- Non-interactive: --email is optional and only pre-fills checkout.
160
- `,
161
- )
162
- .option(
163
- '-e, --email [email]',
164
- z.string().describe('Optional email prefill for checkout (skips interactive prompt)'),
165
- )
166
- .option(
167
- '--plan [plan]',
168
- z.string().describe('Plan ID: starter, pro, or unlimited (default: pro)'),
169
- )
170
- .example('# Interactive subscribe')
171
- .example('egaki subscribe')
172
- .example('# Non-interactive (for agents)')
173
- .example('egaki subscribe --email user@example.com --plan pro')
174
- .example('# Non-interactive without email prefill')
175
- .example('egaki subscribe --plan pro')
176
- .action(async (options) => {
177
- const isTTY = process.stdout.isTTY && process.stdin.isTTY
178
- if (!isTTY || options.email || options.plan) {
179
- subscribeNonInteractive(options.email, options.plan)
180
- return
181
- }
182
- await subscribeInteractive()
183
- })
184
-
185
- // ─── unsubscribe command ─────────────────────────────────────────────────────
186
-
187
- cli
188
- .command(
189
- 'unsubscribe',
190
- dedent`
191
- Cancel your Egaki subscription. Uses the stored API key to identify
192
- the subscription. You can resubscribe anytime with 'egaki subscribe'.
193
- `,
194
- )
195
- .example('egaki unsubscribe')
196
- .action(async () => {
197
- await unsubscribe()
198
- })
199
-
200
- // ─── usage command ───────────────────────────────────────────────────────────
201
-
202
- cli
203
- .command(
204
- 'usage',
205
- dedent`
206
- Show your current Egaki credit usage for this billing period.
207
- Displays plan, credits used, credits remaining, and period info.
208
- `,
209
- )
210
- .example('egaki usage')
211
- .action(async () => {
212
- await showUsage()
213
- })
214
-
215
- // ─── image command ───────────────────────────────────────────────────────────
216
-
217
- cli
218
- .command(
219
- 'image <prompt>',
220
- dedent`
221
- Generate images from a text prompt using AI models.
222
- Supports Imagen models (dedicated image generation) and Gemini
223
- multimodal models (text+image output). The model type is auto-detected
224
- from the model ID: imagen-* uses the image API, everything else uses
225
- the text API with image output enabled.
226
- `,
227
- )
228
- .option(
229
- '-m, --model [model]',
230
- z.enum(IMAGE_MODELS).describe('Model ID for generation. If omitted, shows an interactive picker (or uses default in non-TTY mode)'),
231
- )
232
- .option(
233
- '-o, --output [path]',
234
- z
235
- .string()
236
- .default('egaki-output.png')
237
- .describe('Output file path (index suffix added when generating multiple)'),
238
- )
239
- .option(
240
- '-n, --count [n]',
241
- z.number().default(1).describe('Number of images to generate'),
242
- )
243
- .option(
244
- '--aspect-ratio [ratio]',
245
- z
246
- .string()
247
- .describe(
248
- 'Aspect ratio for the generated image. Imagen supports: 1:1, 3:4, 4:3, 9:16, 16:9. Gemini supports additional ratios: 2:3, 3:2, 4:5, 5:4, 21:9',
249
- ),
250
- )
251
- .option(
252
- '--seed [seed]',
253
- z.number().describe('Seed for reproducible generation. Same seed + same prompt = same image'),
254
- )
255
- .option(
256
- '--image-size [size]',
257
- z
258
- .enum(['1K', '2K', '4K'])
259
- .describe(
260
- 'Output resolution for Gemini text-model image generation. Only applies to gemini-*-image* models',
261
- ),
262
- )
263
- .option(
264
- '-i, --input [file]',
265
- z
266
- .array(z.string())
267
- .describe(
268
- 'Reference image for editing or variations (repeatable). Accepts local file paths or URLs (http/https). Pass one or more images along with a text prompt to edit them',
269
- ),
270
- )
271
- .option(
272
- '--mask [file]',
273
- z
274
- .string()
275
- .describe(
276
- 'Mask image for inpainting. Accepts a local file path or URL (http/https). White areas in the mask are replaced with generated content. Used together with --input',
277
- ),
278
- )
279
- .option(
280
- '--allow-people',
281
- 'Allow generating images of people (Imagen blocks people by default)',
282
- )
283
- .option(
284
- '--json',
285
- 'Output result metadata as JSON to stdout (model, usage, warnings, file paths)',
286
- )
287
- .option(
288
- '--stdout',
289
- 'Write raw image bytes to stdout instead of saving to a file. Useful for piping to other tools',
290
- )
291
- .example('# Generate a simple image')
292
- .example('egaki image "a sunset over mars"')
293
- .example('# Use a specific model with aspect ratio')
294
- .example('egaki image "cyberpunk city at night" -m imagen-4.0-ultra-generate-001 --aspect-ratio 16:9')
295
- .example('# Edit an existing image')
296
- .example('egaki image "add a wizard hat to the cat" --input cat.jpg -o cat-wizard.png')
297
- .example('# Edit an image from a URL')
298
- .example('egaki image "make it pop art" --input https://example.com/photo.jpg')
299
- .example('# Inpainting with a mask')
300
- .example('egaki image "fill with flowers" --input photo.jpg --mask mask.png')
301
- .example('# Generate with Gemini multimodal at 4K')
302
- .example('egaki image "dreamy landscape" -m gemini-2.5-flash-image --image-size 4K')
303
- .example('# Route through Vertex AI (Google Cloud billing)')
304
- .example('egaki image "product photo on marble" -m vertex/imagen-4.0-generate-001')
305
- .example('# Generate multiple images')
306
- .example('egaki image "abstract art" -n 4 -o art.png')
307
- .example('# Pipe to another tool')
308
- .example('egaki image "logo design" --stdout | convert - -resize 512x512 logo.png')
309
- .action(async (prompt, options) => {
310
- // Inject stored API keys as env vars before calling the AI SDK.
311
- injectCredentialsToEnv()
312
-
313
- const model = options.model ?? await resolveImageModel()
314
- const outputPath = options.output ?? 'egaki-output.png'
315
- const count = options.count ?? 1
316
- const config = getModelConfig(model)
317
- const parsedAspectRatio = parseAspectRatioOption(options.aspectRatio)
318
-
319
- if (!options.stdout) {
320
- console.error(pc.dim(`Model: ${model}`))
321
- console.error(pc.dim(`Prompt: ${prompt}`))
322
- }
323
-
324
- const inputImages = await readInputImages(options.input)
325
- const maskImage = options.mask
326
- ? await readInputSource(options.mask)
327
- : undefined
328
-
329
- // When using ChatGPT OAuth with OpenAI image models, route through the
330
- // Responses API + imageGeneration tool instead of the Image API.
331
- // The Codex OAuth client lacks the `api.model.images.request` scope.
332
- const useResponsesApi = config.strategy === 'image' && shouldUseResponsesApi(model)
333
-
334
- if (useResponsesApi) {
335
- if (!options.stdout) {
336
- console.error(pc.dim('Mode: Responses API (ChatGPT OAuth)'))
337
- }
338
- await generateWithResponsesApi({
339
- prompt,
340
- model,
341
- outputPath,
342
- count,
343
- aspectRatio: parsedAspectRatio,
344
- seed: options.seed,
345
- inputImages,
346
- maskImage,
347
- json: options.json || false,
348
- stdout: options.stdout || false,
349
- })
350
- } else if (config.strategy === 'image') {
351
- await generateWithImageModel({
352
- prompt,
353
- model,
354
- outputPath,
355
- count,
356
- aspectRatio: parsedAspectRatio,
357
- seed: options.seed,
358
- inputImages,
359
- maskImage,
360
- allowPeople: options.allowPeople || false,
361
- json: options.json || false,
362
- stdout: options.stdout || false,
363
- })
364
- } else {
365
- await generateWithTextModel({
366
- prompt,
367
- model,
368
- outputPath,
369
- inputImages,
370
- imageSize: options.imageSize,
371
- aspectRatio: options.aspectRatio,
372
- json: options.json || false,
373
- stdout: options.stdout || false,
374
- })
375
- }
376
- })
377
-
378
- // ─── video command ───────────────────────────────────────────────────────────
379
-
380
- cli
381
- .command(
382
- 'video <prompt>',
383
- dedent`
384
- Generate videos from a text prompt (or image+text prompt for models that
385
- support image-to-video). Uses AI SDK experimental_generateVideo under the hood.
386
-
387
- Agent note: video generation can be slow. When invoking this command from
388
- automation, use a command timeout of at least 5 minutes.
389
- `,
390
- )
391
- .option(
392
- '-m, --model [model]',
393
- z.enum(VIDEO_MODELS).describe('Video model ID for generation. If omitted, shows an interactive picker (or uses default in non-TTY mode)'),
394
- )
395
- .option(
396
- '-o, --output [path]',
397
- z
398
- .string()
399
- .default('egaki-output.mp4')
400
- .describe('Output file path (index suffix added when generating multiple)'),
401
- )
402
- .option(
403
- '-n, --count [n]',
404
- z.number().default(1).describe('Number of videos to generate'),
405
- )
406
- .option(
407
- '--aspect-ratio [ratio]',
408
- z.string().describe('Video aspect ratio in WIDTH:HEIGHT format (e.g. 16:9, 9:16)'),
409
- )
410
- .option(
411
- '--resolution [resolution]',
412
- z
413
- .string()
414
- .describe('Video resolution in WIDTHxHEIGHT or provider format (e.g. 1280x720, 720p)'),
415
- )
416
- .option(
417
- '--duration [seconds]',
418
- z.number().describe('Video duration in seconds (provider/model-specific limits apply)'),
419
- )
420
- .option(
421
- '--fps [fps]',
422
- z.number().describe('Frames per second for video models that support fps override'),
423
- )
424
- .option(
425
- '--seed [seed]',
426
- z.number().describe('Seed for reproducible video generation (model support varies)'),
427
- )
428
- .option(
429
- '-i, --input [file]',
430
- z
431
- .string()
432
- .describe(
433
- 'Optional reference image for image-to-video. Accepts local file path or URL (http/https)',
434
- ),
435
- )
436
- .option(
437
- '--json',
438
- 'Output result metadata as JSON to stdout (model, usage, warnings, file paths)',
439
- )
440
- .option(
441
- '--stdout',
442
- 'Write raw video bytes to stdout instead of saving to a file. Useful for piping to other tools',
443
- )
444
- .example('# Generate a video')
445
- .example('egaki video "A paper airplane gliding through clouds" -o airplane.mp4')
446
- .example('# Generate with Veo model + duration')
447
- .example('egaki video "cinematic rainy street at night" -m veo-3.1-fast-generate-001 --duration 6')
448
- .example('# Route through Vertex AI (Google Cloud billing)')
449
- .example('egaki video "storm over mountains" -m vertex/veo-3.1-fast-generate-001 --duration 6')
450
- .example('# Image-to-video (model support required)')
451
- .example('egaki video "animate subtle camera pan" --model luma-ray-2 --input frame.png -o animated.mp4')
452
- .example('# Generate multiple videos')
453
- .example('egaki video "waves crashing on cliffs" -n 2 -o waves.mp4')
454
- .action(async (prompt, options) => {
455
- injectCredentialsToEnv()
456
-
457
- const model = options.model ?? await resolveVideoModel()
458
- const outputPath = options.output ?? 'egaki-output.mp4'
459
- const count = options.count ?? 1
460
- const config = getModelConfig(model)
461
-
462
- if (config.strategy !== 'video') {
463
- console.error(pc.red(`Model ${model} is not a video model`))
464
- process.exit(1)
465
- }
466
-
467
- if (!options.stdout) {
468
- console.error(pc.dim(`Model: ${model}`))
469
- console.error(pc.dim(`Prompt: ${prompt}`))
470
- console.error(pc.dim('Mode: Video API (experimental_generateVideo)'))
471
- }
472
-
473
- const inputImage = options.input
474
- ? await readInputSource(options.input)
475
- : undefined
476
-
477
- await generateWithVideoModel({
478
- prompt,
479
- model,
480
- outputPath,
481
- count,
482
- aspectRatio: options.aspectRatio,
483
- resolution: options.resolution,
484
- duration: options.duration,
485
- fps: options.fps,
486
- seed: options.seed,
487
- inputImage,
488
- json: options.json || false,
489
- stdout: options.stdout || false,
490
- })
491
- })
492
-
493
- // ─── models command ──────────────────────────────────────────────────────────
494
-
495
- cli
496
- .command(
497
- 'models',
498
- dedent`
499
- List all supported image generation models with pricing, features,
500
- and provider info. Output is YAML for easy reading and piping.
501
- `,
502
- )
503
- .option(
504
- '-p, --provider [provider]',
505
- z.string().describe('Filter models by provider name (e.g. google, openai, replicate, fal)'),
506
- )
507
- .option(
508
- '--type [type]',
509
- z
510
- .enum(['all', 'image', 'video'])
511
- .default('all')
512
- .describe('Filter by model type: image (image+text-image), video, or all'),
513
- )
514
- .option('--json', 'Output as JSON instead of YAML')
515
- .action(async (options) => {
516
- const yaml = await import('js-yaml')
517
- const providerStatuses = Object.fromEntries(
518
- Object.keys(PROVIDERS).map((provider) => [provider, getKeyStatus(provider)]),
519
- )
520
-
521
- let models = options.type === 'video'
522
- ? VIDEO_CATALOG
523
- : options.type === 'image'
524
- ? CATALOG
525
- : [...CATALOG, ...VIDEO_CATALOG]
526
-
527
- if (options.provider) {
528
- models = models.filter((m) => m.provider === options.provider)
529
- if (models.length === 0) {
530
- console.error(pc.red(`No models found for provider: ${options.provider}`))
531
- process.exit(1)
532
- }
533
- }
534
-
535
- const output = models.map((m) => ({
536
- id: m.id,
537
- name: m.name,
538
- ...(m.description ? { description: m.description } : {}),
539
- provider: m.provider,
540
- auth: providerStatuses[m.provider] ?? { available: false, source: 'none' },
541
- strategy: m.strategy,
542
- released: m.released,
543
- cost: formatCatalogCost(m.cost),
544
- features:
545
- m.strategy === 'video'
546
- ? {
547
- textToVideo: m.features.textToVideo,
548
- imageToVideo: m.features.imageToVideo,
549
- capabilities: m.features.capabilities.join(', ') || 'none',
550
- seed: m.features.seed,
551
- multipleVideos: m.features.multipleVideos,
552
- aspectRatios: m.features.aspectRatios?.join(', ') || 'none',
553
- resolutions: m.features.resolutions?.join(', ') || 'unknown',
554
- durationRangeSec: m.features.durationRangeSec
555
- ? `${m.features.durationRangeSec.min}-${m.features.durationRangeSec.max}`
556
- : 'unknown',
557
- }
558
- : {
559
- editing: m.features.editing,
560
- inpainting: m.features.inpainting,
561
- seed: m.features.seed,
562
- multipleImages: m.features.multipleImages,
563
- aspectRatios: m.features.aspectRatios.join(', ') || 'none',
564
- ...(m.features.sizes ? { sizes: m.features.sizes.join(', ') } : {}),
565
- },
566
- }))
567
-
568
- if (options.json) {
569
- console.log(JSON.stringify(output, null, 2))
570
- } else {
571
- console.log(yaml.dump(output, { lineWidth: 120, noRefs: true }))
572
- }
573
- })
574
-
575
- cli.help()
576
- cli.version(pkg.version)
577
- cli.parse()
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
-
667
- function isUrl(input: string): boolean {
668
- return /^https?:\/\//i.test(input)
669
- }
670
-
671
- async function readInputSource(input: string): Promise<Uint8Array> {
672
- if (isUrl(input)) {
673
- console.error(pc.dim(`Fetching ${input}...`))
674
- const res = await fetch(input)
675
- if (!res.ok) {
676
- console.error(pc.red(`Failed to fetch ${input}: ${res.status} ${res.statusText}`))
677
- process.exit(1)
678
- }
679
- return new Uint8Array(await res.arrayBuffer())
680
- }
681
- const resolved = path.resolve(input)
682
- if (!fs.existsSync(resolved)) {
683
- console.error(pc.red(`File not found: ${resolved}`))
684
- process.exit(1)
685
- }
686
- return new Uint8Array(fs.readFileSync(resolved))
687
- }
688
-
689
- async function readInputImages(
690
- inputs: string[] | undefined,
691
- ): Promise<Uint8Array[]> {
692
- if (!inputs || inputs.length === 0) {
693
- return []
694
- }
695
- return Promise.all(inputs.map((f) => readInputSource(f)))
696
- }
697
-
698
- // Generate using the dedicated generateImage API (Imagen models)
699
- async function generateWithImageModel({
700
- prompt,
701
- model,
702
- outputPath,
703
- count,
704
- aspectRatio,
705
- seed,
706
- inputImages,
707
- maskImage,
708
- allowPeople,
709
- json,
710
- stdout,
711
- }: {
712
- prompt: string
713
- model: string
714
- outputPath: string
715
- count: number
716
- aspectRatio?: `${number}:${number}`
717
- seed?: number
718
- inputImages: Uint8Array[]
719
- maskImage?: Uint8Array
720
- allowPeople: boolean
721
- json: boolean
722
- stdout: boolean
723
- }) {
724
- // Build the prompt: plain string or multimodal with reference images
725
- const imagePrompt = inputImages.length > 0
726
- ? { text: prompt, images: inputImages, ...(maskImage ? { mask: maskImage } : {}) }
727
- : prompt
728
-
729
- const config = getModelConfig(model)
730
- const imageModel = await createImageModel(model)
731
-
732
- // Use the correct providerOptions key based on provider (google vs vertex)
733
- const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google'
734
-
735
- if (!stdout) {
736
- console.error(pc.cyan('Generating...'))
737
- }
738
-
739
- const result = await aiGenerateImage({
740
- model: imageModel,
741
- prompt: imagePrompt,
742
- n: count,
743
- ...(aspectRatio ? { aspectRatio } : {}),
744
- ...(seed !== undefined ? { seed } : {}),
745
- providerOptions: {
746
- [providerOptionsKey]: {
747
- ...(allowPeople ? { personGeneration: 'allow_all' } : {}),
748
- ...(aspectRatio ? { aspectRatio } : {}),
749
- },
750
- },
751
- })
752
-
753
- if (stdout) {
754
- // Write first image to stdout for piping
755
- const image = result.images[0]
756
- if (image) {
757
- process.stdout.write(Buffer.from(image.uint8Array))
758
- }
759
- return
760
- }
761
-
762
- const savedFiles: string[] = []
763
- for (let i = 0; i < result.images.length; i++) {
764
- const image = result.images[i]!
765
- const ext = extensionFromMediaType(image.mediaType)
766
- const filePath =
767
- result.images.length === 1
768
- ? ensureExtension(outputPath, ext)
769
- : insertIndex(outputPath, i, ext)
770
-
771
- fs.writeFileSync(filePath, image.uint8Array)
772
- console.error(pc.green(`Saved: ${filePath}`))
773
- savedFiles.push(filePath)
774
- }
775
-
776
- const cost = calculateCost(config.cost, result.usage, result.images.length)
777
- if (cost != null) {
778
- console.error(pc.dim(`Cost: ${formatCost(cost)}`))
779
- }
780
-
781
- if (json) {
782
- const output = {
783
- model,
784
- files: savedFiles,
785
- count: result.images.length,
786
- cost,
787
- usage: result.usage,
788
- warnings: result.warnings,
789
- }
790
- console.log(JSON.stringify(output, null, 2))
791
- }
792
- }
793
-
794
- // Generate using generateText with responseModalities (Gemini multimodal models)
795
- async function generateWithTextModel({
796
- prompt,
797
- model,
798
- outputPath,
799
- inputImages,
800
- imageSize,
801
- aspectRatio,
802
- json,
803
- stdout,
804
- }: {
805
- prompt: string
806
- model: string
807
- outputPath: string
808
- inputImages: Uint8Array[]
809
- imageSize?: '1K' | '2K' | '4K'
810
- aspectRatio?: string
811
- json: boolean
812
- stdout: boolean
813
- }) {
814
- const textModel = await createTextModel(model)
815
- const config = getModelConfig(model)
816
- const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google'
817
-
818
- if (!stdout) {
819
- console.error(pc.cyan('Generating...'))
820
- }
821
-
822
- // Build messages: if we have input images, create a multimodal prompt
823
- const messages = inputImages.length > 0
824
- ? [
825
- {
826
- role: 'user' as const,
827
- content: [
828
- { type: 'text' as const, text: prompt },
829
- ...inputImages.map((img) => ({
830
- type: 'image' as const,
831
- image: img,
832
- })),
833
- ],
834
- },
835
- ]
836
- : undefined
837
-
838
- const result = await generateText({
839
- model: textModel,
840
- ...(messages ? { messages } : { prompt }),
841
- providerOptions: {
842
- [providerOptionsKey]: {
843
- responseModalities: ['TEXT', 'IMAGE'],
844
- ...(imageSize || aspectRatio
845
- ? {
846
- imageConfig: {
847
- ...(imageSize ? { imageSize } : {}),
848
- ...(aspectRatio ? { aspectRatio } : {}),
849
- },
850
- }
851
- : {}),
852
- },
853
- },
854
- })
855
-
856
- const imageFiles = result.files.filter((f) => {
857
- return f.mediaType.startsWith('image/')
858
- })
859
-
860
- if (imageFiles.length === 0) {
861
- console.error(pc.red('No images generated.'))
862
- process.exit(1)
863
- }
864
-
865
- if (stdout) {
866
- const file = imageFiles[0]
867
- if (file) {
868
- process.stdout.write(Buffer.from(file.uint8Array))
869
- }
870
- return
871
- }
872
-
873
- const savedFiles: string[] = []
874
- for (let i = 0; i < imageFiles.length; i++) {
875
- const file = imageFiles[i]!
876
- const ext = extensionFromMediaType(file.mediaType)
877
- const filePath =
878
- imageFiles.length === 1
879
- ? ensureExtension(outputPath, ext)
880
- : insertIndex(outputPath, i, ext)
881
-
882
- fs.writeFileSync(filePath, file.uint8Array)
883
- console.error(pc.green(`Saved: ${filePath}`))
884
- savedFiles.push(filePath)
885
- }
886
-
887
- const cost = calculateCost(config.cost, result.usage, imageFiles.length)
888
- if (cost != null) {
889
- console.error(pc.dim(`Cost: ${formatCost(cost)}`))
890
- }
891
-
892
- if (result.text && !json) {
893
- console.error(pc.dim(result.text))
894
- }
895
-
896
- if (json) {
897
- const output = {
898
- model,
899
- files: savedFiles,
900
- count: imageFiles.length,
901
- text: result.text || null,
902
- cost,
903
- usage: result.usage,
904
- }
905
- console.log(JSON.stringify(output, null, 2))
906
- }
907
- }
908
-
909
- // Generate using the OpenAI Responses API with imageGeneration tool.
910
- // Used when auth comes from ChatGPT OAuth (no Image API scope).
911
- async function generateWithResponsesApi({
912
- prompt,
913
- model,
914
- outputPath,
915
- count,
916
- aspectRatio,
917
- seed,
918
- inputImages,
919
- maskImage,
920
- json,
921
- stdout,
922
- }: {
923
- prompt: string
924
- model: string
925
- outputPath: string
926
- count: number
927
- aspectRatio?: `${number}:${number}`
928
- seed?: number
929
- inputImages: Uint8Array[]
930
- maskImage?: Uint8Array
931
- json: boolean
932
- stdout: boolean
933
- }) {
934
- if (count !== 1) {
935
- console.error(pc.red('ChatGPT image generation currently supports exactly one output image.'))
936
- process.exit(1)
937
- }
938
-
939
- if (seed !== undefined) {
940
- console.error(pc.red('ChatGPT image generation does not support --seed.'))
941
- process.exit(1)
942
- }
943
-
944
- if (maskImage) {
945
- console.error(pc.red('ChatGPT image generation does not support --mask yet.'))
946
- process.exit(1)
947
- }
948
-
949
- const size = aspectRatio ? chatGptImageSizeFromAspectRatio(aspectRatio) : undefined
950
- if (aspectRatio && !size) {
951
- console.error(
952
- pc.red(
953
- 'ChatGPT image generation only supports --aspect-ratio 1:1, 3:2, or 2:3.',
954
- ),
955
- )
956
- process.exit(1)
957
- }
958
-
959
- const storedAuth = getChatGptAuth()
960
- if (!storedAuth?.accountId) {
961
- console.error(pc.red('Missing ChatGPT account metadata. Please run `egaki login --provider chatgpt` again.'))
962
- process.exit(1)
963
- }
964
-
965
- const auth = await getValidChatGptAuth(storedAuth, saveChatGptAuth)
966
- if (auth instanceof Error) {
967
- console.error(pc.red(auth.message))
968
- process.exit(1)
969
- }
970
-
971
- if (!stdout) {
972
- console.error(pc.cyan('Generating...'))
973
- }
974
-
975
- const content: Array<{ type: 'input_text'; text: string } | { type: 'input_image'; image_url: string }> = [
976
- { type: 'input_text', text: prompt },
977
- ...inputImages.map((image) => ({
978
- type: 'input_image' as const,
979
- image_url: imageBytesToDataUrl(image),
980
- })),
981
- ]
982
-
983
- const response = await fetch('https://chatgpt.com/backend-api/codex/responses', {
984
- method: 'POST',
985
- headers: {
986
- Authorization: `Bearer ${auth.access}`,
987
- 'ChatGPT-Account-ID': auth.accountId,
988
- 'Content-Type': 'application/json',
989
- },
990
- body: JSON.stringify({
991
- model: 'gpt-5.4',
992
- instructions: 'You are Codex.',
993
- input: [
994
- {
995
- type: 'message',
996
- role: 'user',
997
- content,
998
- },
999
- ],
1000
- tools: [
1001
- {
1002
- type: 'image_generation',
1003
- model,
1004
- size: 'auto',
1005
- quality: 'auto',
1006
- output_format: 'png',
1007
- output_compression: 100,
1008
- moderation: 'auto',
1009
- ...(size ? { size } : {}),
1010
- },
1011
- ],
1012
- tool_choice: 'auto',
1013
- parallel_tool_calls: true,
1014
- stream: true,
1015
- store: false,
1016
- include: [],
1017
- }),
1018
- })
1019
-
1020
- if (!response.ok || !response.body) {
1021
- const body = await response.text().catch(() => '')
1022
- console.error(pc.red(`ChatGPT image generation failed: ${response.status}`))
1023
- if (body) console.error(pc.dim(body))
1024
- process.exit(1)
1025
- }
1026
-
1027
- let imageBase64: string | undefined
1028
- let revisedPrompt: string | null = null
1029
-
1030
- const processSseMessage = (message: EventSourceMessage) => {
1031
- if (!message.data) return
1032
- try {
1033
- const event = JSON.parse(message.data) as {
1034
- type?: string
1035
- partial_image_b64?: string
1036
- item?: { type?: string; result?: string; revised_prompt?: string | null }
1037
- }
1038
- if (
1039
- event.type === 'response.image_generation_call.partial_image' &&
1040
- event.partial_image_b64 &&
1041
- !imageBase64
1042
- ) {
1043
- imageBase64 = event.partial_image_b64
1044
- }
1045
- if (
1046
- event.type === 'response.output_item.done' &&
1047
- event.item?.type === 'image_generation_call'
1048
- ) {
1049
- if (event.item.result) imageBase64 = event.item.result
1050
- revisedPrompt = event.item.revised_prompt ?? revisedPrompt
1051
- }
1052
- } catch {
1053
- // Ignore keepalive and partial parse noise.
1054
- }
1055
- }
1056
-
1057
- const parser = createParser({
1058
- onEvent: processSseMessage,
1059
- })
1060
-
1061
- const decoder = new TextDecoder()
1062
-
1063
- for await (const chunk of response.body) {
1064
- parser.feed(decoder.decode(chunk, { stream: true }))
1065
- }
1066
- parser.feed(decoder.decode())
1067
-
1068
- if (!imageBase64) {
1069
- console.error(pc.red('No images generated.'))
1070
- process.exit(1)
1071
- }
1072
-
1073
- const imageBytes = Buffer.from(imageBase64, 'base64')
1074
- const mediaType = 'image/png'
1075
-
1076
- if (stdout) {
1077
- process.stdout.write(imageBytes)
1078
- return
1079
- }
1080
-
1081
- const savedFiles: string[] = []
1082
- const filePath = ensureExtension(outputPath, extensionFromMediaType(mediaType))
1083
- fs.writeFileSync(filePath, imageBytes)
1084
- console.error(pc.green(`Saved: ${filePath}`))
1085
- savedFiles.push(filePath)
1086
-
1087
- if (revisedPrompt && !json) {
1088
- console.error(pc.dim(`Revised prompt: ${revisedPrompt}`))
1089
- }
1090
-
1091
- if (json) {
1092
- const output = {
1093
- model,
1094
- files: savedFiles,
1095
- count: 1,
1096
- revisedPrompt,
1097
- }
1098
- console.log(JSON.stringify(output, null, 2))
1099
- }
1100
- }
1101
-
1102
- // Generate using experimental_generateVideo (video models)
1103
- async function generateWithVideoModel({
1104
- prompt,
1105
- model,
1106
- outputPath,
1107
- count,
1108
- aspectRatio,
1109
- resolution,
1110
- duration,
1111
- fps,
1112
- seed,
1113
- inputImage,
1114
- json,
1115
- stdout,
1116
- }: {
1117
- prompt: string
1118
- model: string
1119
- outputPath: string
1120
- count: number
1121
- aspectRatio?: string
1122
- resolution?: string
1123
- duration?: number
1124
- fps?: number
1125
- seed?: number
1126
- inputImage?: Uint8Array
1127
- json: boolean
1128
- stdout: boolean
1129
- }) {
1130
- const videoModel = await createVideoModel(model)
1131
-
1132
- if (!stdout) {
1133
- console.error(pc.cyan('Generating...'))
1134
- }
1135
-
1136
- const result = await aiGenerateVideo({
1137
- model: videoModel,
1138
- prompt: inputImage
1139
- ? { image: inputImage, text: prompt }
1140
- : prompt,
1141
- n: count,
1142
- ...(aspectRatio ? { aspectRatio: aspectRatio as `${number}:${number}` } : {}),
1143
- ...(resolution ? { resolution: resolution as `${number}x${number}` } : {}),
1144
- ...(duration != null ? { duration } : {}),
1145
- ...(fps != null ? { fps } : {}),
1146
- ...(seed != null ? { seed } : {}),
1147
- })
1148
-
1149
- if (stdout) {
1150
- const video = result.videos[0]
1151
- if (video) {
1152
- process.stdout.write(Buffer.from(video.uint8Array))
1153
- }
1154
- return
1155
- }
1156
-
1157
- const savedFiles: string[] = []
1158
- for (let i = 0; i < result.videos.length; i++) {
1159
- const video = result.videos[i]!
1160
- const ext = extensionFromMediaType(video.mediaType)
1161
- const filePath =
1162
- result.videos.length === 1
1163
- ? ensureExtension(outputPath, ext)
1164
- : insertIndex(outputPath, i, ext)
1165
-
1166
- fs.writeFileSync(filePath, video.uint8Array)
1167
- console.error(pc.green(`Saved: ${filePath}`))
1168
- savedFiles.push(filePath)
1169
- }
1170
-
1171
- const config = getModelConfig(model)
1172
- const cost = calculateCost(config.cost, {
1173
- videosGenerated: result.videos.length,
1174
- durationSeconds: duration,
1175
- resolution,
1176
- }, result.videos.length)
1177
- if (cost != null) {
1178
- console.error(pc.dim(`Cost: ${formatCost(cost)}`))
1179
- }
1180
-
1181
- if (json) {
1182
- const output = {
1183
- model,
1184
- files: savedFiles,
1185
- count: result.videos.length,
1186
- cost,
1187
- warnings: result.warnings,
1188
- responses: result.responses,
1189
- }
1190
- console.log(JSON.stringify(output, null, 2))
1191
- }
1192
- }
1193
-
1194
- // ─── cost helpers ────────────────────────────────────────────────────────────
1195
-
1196
- function calculateCost(
1197
- cost: {
1198
- type: 'per-image'
1199
- perImage: number
1200
- } | {
1201
- type: 'per-token'
1202
- inputPerM: number
1203
- outputPerM: number
1204
- } | {
1205
- type: 'per-video-second'
1206
- defaultDurationSec: number
1207
- tiers: Array<{ resolution?: string; costPerSecond: number }>
1208
- } | {
1209
- type: 'unknown'
1210
- },
1211
- usage: {
1212
- inputTokens?: number
1213
- outputTokens?: number
1214
- imagesGenerated?: number
1215
- videosGenerated?: number
1216
- durationSeconds?: number
1217
- resolution?: string
1218
- },
1219
- count: number = 1,
1220
- ): number | null {
1221
- if (cost.type === 'per-image') {
1222
- return cost.perImage * count
1223
- }
1224
- if (cost.type === 'per-token' && usage.inputTokens != null && usage.outputTokens != null) {
1225
- return (
1226
- (usage.inputTokens * cost.inputPerM + usage.outputTokens * cost.outputPerM) / 1_000_000
1227
- )
1228
- }
1229
- if (cost.type === 'per-video-second') {
1230
- const durationSec = usage.durationSeconds ?? cost.defaultDurationSec
1231
- const resolution = normalizeResolutionKey(usage.resolution)
1232
- const tier =
1233
- cost.tiers.find((t) => normalizeResolutionKey(t.resolution) === resolution) ??
1234
- cost.tiers[0]
1235
- if (!tier) {
1236
- return null
1237
- }
1238
- return tier.costPerSecond * durationSec * count
1239
- }
1240
- return null
1241
- }
1242
-
1243
- function formatCatalogCost(
1244
- cost: {
1245
- type: 'per-image'
1246
- perImage: number
1247
- } | {
1248
- type: 'per-token'
1249
- inputPerM: number
1250
- outputPerM: number
1251
- } | {
1252
- type: 'per-video-second'
1253
- defaultDurationSec: number
1254
- tiers: Array<{ resolution?: string; mode?: string; audio?: boolean; costPerSecond: number }>
1255
- } | {
1256
- type: 'unknown'
1257
- },
1258
- ): string {
1259
- if (cost.type === 'per-image') {
1260
- return `$${cost.perImage}/image`
1261
- }
1262
- if (cost.type === 'per-token') {
1263
- return `$${cost.inputPerM}/M input, $${cost.outputPerM}/M output`
1264
- }
1265
- if (cost.type === 'per-video-second') {
1266
- const tierText = cost.tiers
1267
- .map((t) => {
1268
- const parts = [
1269
- t.resolution,
1270
- t.mode ? `mode=${t.mode}` : undefined,
1271
- t.audio != null ? `audio=${t.audio}` : undefined,
1272
- ].filter(Boolean)
1273
- return parts.length > 0
1274
- ? `$${t.costPerSecond}/s (${parts.join(', ')})`
1275
- : `$${t.costPerSecond}/s`
1276
- })
1277
- .join('; ')
1278
- return `${tierText} (default duration ${cost.defaultDurationSec}s)`
1279
- }
1280
- return 'unknown'
1281
- }
1282
-
1283
- function formatCost(dollars: number): string {
1284
- if (dollars < 0.01) {
1285
- return `$${dollars.toFixed(4)}`
1286
- }
1287
- return `$${dollars.toFixed(2)}`
1288
- }
1289
-
1290
- function extensionFromMediaType(mediaType: string): string {
1291
- const map: Record<string, string> = {
1292
- 'image/png': '.png',
1293
- 'image/jpeg': '.jpg',
1294
- 'image/webp': '.webp',
1295
- 'image/gif': '.gif',
1296
- 'video/mp4': '.mp4',
1297
- 'video/webm': '.webm',
1298
- 'video/quicktime': '.mov',
1299
- }
1300
- return map[mediaType] || (mediaType.startsWith('video/') ? '.mp4' : '.png')
1301
- }
1302
-
1303
- function inferImageMediaType(image: Uint8Array): string {
1304
- if (image.length >= 8 && image[0] === 0x89 && image[1] === 0x50 && image[2] === 0x4e && image[3] === 0x47) {
1305
- return 'image/png'
1306
- }
1307
- if (image.length >= 3 && image[0] === 0xff && image[1] === 0xd8 && image[2] === 0xff) {
1308
- return 'image/jpeg'
1309
- }
1310
- if (
1311
- image.length >= 12 &&
1312
- image[0] === 0x52 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x46 &&
1313
- image[8] === 0x57 && image[9] === 0x45 && image[10] === 0x42 && image[11] === 0x50
1314
- ) {
1315
- return 'image/webp'
1316
- }
1317
- if (
1318
- image.length >= 6 &&
1319
- image[0] === 0x47 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x38
1320
- ) {
1321
- return 'image/gif'
1322
- }
1323
- return 'image/png'
1324
- }
1325
-
1326
- function imageBytesToDataUrl(image: Uint8Array): string {
1327
- const mediaType = inferImageMediaType(image)
1328
- return `data:${mediaType};base64,${Buffer.from(image).toString('base64')}`
1329
- }
1330
-
1331
- function chatGptImageSizeFromAspectRatio(
1332
- aspectRatio: `${number}:${number}`,
1333
- ): '1024x1024' | '1536x1024' | '1024x1536' | undefined {
1334
- switch (aspectRatio) {
1335
- case '1:1':
1336
- return '1024x1024'
1337
- case '3:2':
1338
- return '1536x1024'
1339
- case '2:3':
1340
- return '1024x1536'
1341
- default:
1342
- return undefined
1343
- }
1344
- }
1345
-
1346
- function normalizeResolutionKey(input?: string): string | undefined {
1347
- if (!input) return undefined
1348
- const normalized = input.trim().toLowerCase()
1349
- if (normalized === '1920x1080') return '1080p'
1350
- if (normalized === '1280x720') return '720p'
1351
- if (normalized === '854x480' || normalized === '848x480') return '480p'
1352
- return normalized
1353
- }
1354
-
1355
- function isAspectRatio(input: string): input is `${number}:${number}` {
1356
- return /^\d+:\d+$/.test(input)
1357
- }
1358
-
1359
- function parseAspectRatioOption(input?: string): `${number}:${number}` | undefined {
1360
- if (!input || !isAspectRatio(input)) {
1361
- return undefined
1362
- }
1363
- return input
1364
- }
1365
-
1366
- function ensureExtension(filePath: string, ext: string): string {
1367
- const parsed = path.parse(filePath)
1368
- if (parsed.ext) {
1369
- return filePath
1370
- }
1371
- return filePath + ext
1372
- }
1373
-
1374
- function insertIndex(filePath: string, index: number, ext: string): string {
1375
- const parsed = path.parse(filePath)
1376
- const finalExt = parsed.ext || ext
1377
- return path.join(parsed.dir, `${parsed.name}-${index}${finalExt}`)
1378
- }