egaki 0.3.0 → 0.4.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,44 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## 0.4.0
6
+
7
+ 1. **ChatGPT login for OpenAI image generation** — use your ChatGPT subscription in `egaki` without a separate OpenAI platform API key:
8
+
9
+ ```bash
10
+ egaki login --provider chatgpt
11
+ egaki image "a dreamy studio ghibli style bakery at sunrise" -m gpt-image-1.5 -o bakery.png
12
+ ```
13
+
14
+ OpenAI image requests authenticated this way now follow the same Codex backend flow used by ChatGPT instead of the normal Image API path.
15
+
16
+ 2. **ChatGPT-backed image editing** — edit existing images with OpenAI image models through the ChatGPT/Codex backend:
17
+
18
+ ```bash
19
+ egaki image "change the red jacket to a blue jacket" -m gpt-image-1.5 --input portrait.png -o portrait-blue.png
20
+ egaki image "turn this product shot into a clay render" -m gpt-image-1.5 --input product.png -o product-clay.png
21
+ ```
22
+
23
+ Multiple input images are supported. For this backend path, `egaki` now explicitly rejects unsupported options like `--seed`, `--mask`, and multi-image output instead of pretending they work.
24
+
25
+ 3. **Aspect ratio support for ChatGPT image generation** — supported ChatGPT/OpenAI aspect ratios now map to the backend's real size controls:
26
+
27
+ ```bash
28
+ egaki image "wide landscape matte painting" -m gpt-image-1.5 --aspect-ratio 3:2 -o wide.png
29
+ egaki image "book cover concept art" -m gpt-image-1.5 --aspect-ratio 2:3 -o cover.png
30
+ ```
31
+
32
+ Supported ratios on this path are `1:1`, `3:2`, and `2:3`.
33
+
34
+ 4. **`egaki models` now shows login availability** — model discovery output includes whether each provider is currently usable from your configured credentials:
35
+
36
+ ```bash
37
+ egaki models
38
+ egaki models --json | jq '.[] | {id, provider, auth}'
39
+ ```
40
+
41
+ The output now includes `auth.available` and `auth.source` (`env`, `stored`, `oauth`, or `none`) so you can see which model families are ready to use.
42
+
5
43
  ## 0.3.0
6
44
 
7
45
  1. **Google Vertex AI provider** — use Google Cloud billing instead of AI Studio by
package/README.md CHANGED
@@ -20,6 +20,14 @@ egaki image "a watercolor fox reading a map" -o fox.png
20
20
  egaki video "a paper boat drifting on a calm lake at sunrise" -o boat.mp4
21
21
  ```
22
22
 
23
+ ### Quick start with ChatGPT auth
24
+
25
+ ```bash
26
+ egaki login --provider chatgpt
27
+ egaki image "a dreamy studio ghibli style bakery at sunrise" -m gpt-image-1.5 -o bakery.png
28
+ egaki image "change the red jacket to a blue jacket" -m gpt-image-1.5 --input portrait.png -o portrait-blue.png
29
+ ```
30
+
23
31
  ## CLI examples
24
32
 
25
33
  ### Generate an image from text
@@ -34,6 +42,7 @@ egaki image "isometric floating city, detailed, soft colors" -m imagen-4.0-gener
34
42
  ```bash
35
43
  egaki image "add a red scarf and make it winter" --input portrait.jpg -o portrait-winter.png
36
44
  egaki image "turn this into a manga panel" --input https://example.com/photo.jpg -o manga.png
45
+ egaki image "change the red square to a blue square" -m gpt-image-1.5 --input input.png -o output.png
37
46
  ```
38
47
 
39
48
  ### Inpainting with a mask
@@ -53,6 +62,7 @@ egaki image "minimal logo concepts for a cat cafe" -n 4 -o logo.png
53
62
  ```bash
54
63
  egaki image "cyberpunk alley at night" --aspect-ratio 16:9
55
64
  egaki image "polaroid-style travel photo" --aspect-ratio 4:5
65
+ egaki image "wide landscape matte painting" -m gpt-image-1.5 --aspect-ratio 3:2 -o wide.png
56
66
  ```
57
67
 
58
68
  ### Use Google Cloud billing via Vertex AI
@@ -209,6 +219,7 @@ egaki image "mascot variations, flat vector look" \
209
219
  egaki supports **both** authentication modes:
210
220
 
211
221
  - **BYOK (bring your own keys):** add provider keys with `egaki login` per provider.
222
+ - **ChatGPT auth:** log in with `egaki login --provider chatgpt` to use the ChatGPT/Codex backend for supported OpenAI image generation and editing flows.
212
223
  - **Egaki subscription:** use one `egaki_...` key to access all supported models without managing keys for each provider.
213
224
  - **Google vs Vertex:** bare model IDs (e.g. `imagen-4.0-generate-001`) use Google AI Studio. Prefix with `vertex/` (e.g. `vertex/imagen-4.0-generate-001`) to route through Vertex AI / Google Cloud billing.
214
225
 
@@ -226,6 +237,10 @@ egaki login --provider egaki --key egaki_...
226
237
  egaki login --provider google --key AIza...
227
238
  egaki login --provider vertex --key AIza...
228
239
 
240
+ # ChatGPT OAuth for Codex-backed image generation/editing
241
+ egaki login --provider chatgpt
242
+ egaki image "turn this product shot into a clay render" -m gpt-image-1.5 --input product.png -o product-clay.png
243
+
229
244
  # Check subscription usage / cancel
230
245
  egaki usage
231
246
  egaki unsubscribe
@@ -0,0 +1,25 @@
1
+ export type ChatGptAuth = {
2
+ email?: string;
3
+ accountId?: string;
4
+ plan?: string;
5
+ refresh: string;
6
+ access: string;
7
+ expires: number;
8
+ };
9
+ /** Extract the ChatGPT plan type (e.g. "plus", "pro") from the access token. */
10
+ export declare function extractPlanType(auth: ChatGptAuth): string | undefined;
11
+ /**
12
+ * Run the full ChatGPT OAuth browser flow with clack UI.
13
+ * Returns the ChatGptAuth object to be stored in credentials.json.
14
+ */
15
+ export declare function chatGptOAuthLogin(): Promise<ChatGptAuth>;
16
+ /**
17
+ * Refresh an expired ChatGPT access token using the refresh token.
18
+ * Returns the updated auth object, or an Error if refresh fails.
19
+ */
20
+ export declare function refreshChatGptToken(auth: ChatGptAuth): Promise<ChatGptAuth | Error>;
21
+ /**
22
+ * Refresh the auth object if needed and return the full updated state.
23
+ */
24
+ export declare function getValidChatGptAuth(auth: ChatGptAuth, onRefresh?: (updated: ChatGptAuth) => void): Promise<ChatGptAuth | Error>;
25
+ //# sourceMappingURL=chatgpt-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chatgpt-auth.d.ts","sourceRoot":"","sources":["../src/chatgpt-auth.ts"],"names":[],"mappings":"AAmBA,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAgGD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,SAAS,CAOrE;AA8JD;;;GAGG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,WAAW,CAAC,CA8C9D;AAED;;;GAGG;AACH,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,CAyBzF;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,WAAW,EACjB,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,IAAI,GACzC,OAAO,CAAC,WAAW,GAAG,KAAK,CAAC,CAS9B"}
@@ -3,8 +3,8 @@
3
3
  // Tokens are stored inside credentials.json under the 'chatgpt' key as a
4
4
  // structured object (not a plain string like other providers).
5
5
  //
6
- // The access token is a standard OpenAI bearer token that works with the
7
- // Image API, so we can inject it as OPENAI_API_KEY for the SDK to pick up.
6
+ // Tokens are stored so egaki can reuse the ChatGPT login for Codex-style
7
+ // backend requests and refresh them when needed.
8
8
  import { createServer } from 'node:http';
9
9
  import { spawn } from 'node:child_process';
10
10
  import { spinner, log, note } from '@clack/prompts';
@@ -55,6 +55,13 @@ function extractEmail(tokens) {
55
55
  return ((typeof idClaims?.email === 'string' ? idClaims.email : undefined) ??
56
56
  (typeof accessClaims?.email === 'string' ? accessClaims.email : undefined));
57
57
  }
58
+ function extractPlanTypeFromTokens(tokens) {
59
+ const idClaims = tokens.id_token ? parseJwtClaims(tokens.id_token) : undefined;
60
+ const accessClaims = parseJwtClaims(tokens.access_token);
61
+ const idAuth = idClaims?.['https://api.openai.com/auth'];
62
+ const accessAuth = accessClaims?.['https://api.openai.com/auth'];
63
+ return idAuth?.chatgpt_plan_type ?? accessAuth?.chatgpt_plan_type;
64
+ }
58
65
  function extractAccountId(tokens) {
59
66
  const idClaims = tokens.id_token ? parseJwtClaims(tokens.id_token) : undefined;
60
67
  const accessClaims = parseJwtClaims(tokens.access_token);
@@ -71,6 +78,8 @@ function extractAccountId(tokens) {
71
78
  }
72
79
  /** Extract the ChatGPT plan type (e.g. "plus", "pro") from the access token. */
73
80
  export function extractPlanType(auth) {
81
+ if (auth.plan)
82
+ return auth.plan;
74
83
  const claims = parseJwtClaims(auth.access);
75
84
  const authClaim = claims?.['https://api.openai.com/auth'];
76
85
  return authClaim?.chatgpt_plan_type;
@@ -81,7 +90,7 @@ function buildAuthorizeUrl(pkce, state) {
81
90
  response_type: 'code',
82
91
  client_id: CLIENT_ID,
83
92
  redirect_uri: REDIRECT_URI,
84
- scope: 'openid profile email offline_access',
93
+ scope: 'openid profile email offline_access api.connectors.read api.connectors.invoke',
85
94
  code_challenge: pkce.challenge,
86
95
  code_challenge_method: 'S256',
87
96
  id_token_add_organizations: 'true',
@@ -236,6 +245,7 @@ export async function chatGptOAuthLogin() {
236
245
  const auth = {
237
246
  email: extractEmail(tokens),
238
247
  accountId: extractAccountId(tokens),
248
+ plan: extractPlanTypeFromTokens(tokens),
239
249
  refresh: tokens.refresh_token,
240
250
  access: tokens.access_token,
241
251
  expires: now + (tokens.expires_in ?? 3600) * 1000,
@@ -266,6 +276,7 @@ export async function refreshChatGptToken(auth) {
266
276
  const json = (await response.json());
267
277
  return {
268
278
  ...auth,
279
+ plan: extractPlanTypeFromTokens(json) ?? auth.plan,
269
280
  access: json.access_token,
270
281
  refresh: json.refresh_token ?? auth.refresh,
271
282
  expires: Date.now() + (json.expires_in ?? 3600) * 1000,
@@ -276,19 +287,15 @@ export async function refreshChatGptToken(auth) {
276
287
  }
277
288
  }
278
289
  /**
279
- * Get a valid access token from a ChatGptAuth object.
280
- * Auto-refreshes if expired. Returns the token string or an Error.
281
- * If the auth was refreshed, the `onRefresh` callback is called with
282
- * the updated auth so the caller can persist it.
290
+ * Refresh the auth object if needed and return the full updated state.
283
291
  */
284
- export async function getValidAccessToken(auth, onRefresh) {
285
- // Token still valid (with 60s buffer)
292
+ export async function getValidChatGptAuth(auth, onRefresh) {
286
293
  if (auth.expires > Date.now() + 60_000) {
287
- return auth.access;
294
+ return auth;
288
295
  }
289
296
  const refreshed = await refreshChatGptToken(auth);
290
297
  if (refreshed instanceof Error)
291
298
  return refreshed;
292
299
  onRefresh?.(refreshed);
293
- return refreshed.access;
300
+ return refreshed;
294
301
  }
package/dist/cli.js CHANGED
@@ -14,12 +14,14 @@ import { generateImage as aiGenerateImage, generateText, experimental_generateVi
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import pc from 'picocolors';
17
+ import { createParser } from 'eventsource-parser';
17
18
  import pkg from '../package.json' with { type: 'json' };
18
- import { injectCredentialsToEnv, PROVIDERS } from './credentials.js';
19
- import { IMAGE_MODELS, VIDEO_MODELS, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, createImageModel, createTextModel, createVideoModel, } from './models.js';
19
+ import { injectCredentialsToEnv, PROVIDERS, getChatGptAuth, saveChatGptAuth, getKeyStatus, } from './credentials.js';
20
+ import { IMAGE_MODELS, VIDEO_MODELS, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, createImageModel, createTextModel, createVideoModel, shouldUseResponsesApi, } from './models.js';
20
21
  import { VIDEO_CATALOG } from './video-model-catalog.js';
21
22
  import { loginInteractive, loginNonInteractive, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
22
23
  import { subscribeInteractive, subscribeNonInteractive, unsubscribe, showUsage, } from './subscription.js';
24
+ import { getValidChatGptAuth } from './chatgpt-auth.js';
23
25
  const cli = goke('egaki');
24
26
  process.title = 'egaki';
25
27
  // Print clean error output for unhandled rejections (e.g. AI SDK APICallError).
@@ -73,8 +75,13 @@ cli
73
75
  }
74
76
  // Non-interactive: --provider + --key or stdin
75
77
  if (options.provider) {
78
+ // ChatGPT uses browser OAuth — skip key reading
79
+ if (options.provider === 'chatgpt') {
80
+ await loginNonInteractive({ provider: options.provider, key: '' });
81
+ return;
82
+ }
76
83
  const key = options.key || (await readKeyFromStdin());
77
- loginNonInteractive({ provider: options.provider, key });
84
+ await loginNonInteractive({ provider: options.provider, key });
78
85
  return;
79
86
  }
80
87
  // Interactive mode
@@ -178,29 +185,52 @@ cli
178
185
  .example('# Pipe to another tool')
179
186
  .example('egaki image "logo design" --stdout | convert - -resize 512x512 logo.png')
180
187
  .action(async (prompt, options) => {
181
- // Inject stored API keys as env vars before calling the AI SDK
188
+ // Inject stored API keys as env vars before calling the AI SDK.
182
189
  injectCredentialsToEnv();
183
- const model = options.model;
184
- const outputPath = options.output;
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;
194
+ const outputPath = options.output ?? 'egaki-output.png';
195
+ const count = options.count ?? 1;
185
196
  const config = getModelConfig(model);
197
+ const parsedAspectRatio = parseAspectRatioOption(options.aspectRatio);
186
198
  if (!options.stdout) {
187
199
  console.error(pc.dim(`Model: ${model}`));
188
200
  console.error(pc.dim(`Prompt: ${prompt}`));
189
- console.error(pc.dim(config.strategy === 'image'
190
- ? 'Mode: Image API (generateImage)'
191
- : 'Mode: Text+Image (generateText)'));
192
201
  }
193
202
  const inputImages = await readInputImages(options.input);
194
203
  const maskImage = options.mask
195
204
  ? await readInputSource(options.mask)
196
205
  : undefined;
197
- if (config.strategy === 'image') {
206
+ // When using ChatGPT OAuth with OpenAI image models, route through the
207
+ // Responses API + imageGeneration tool instead of the Image API.
208
+ // The Codex OAuth client lacks the `api.model.images.request` scope.
209
+ const useResponsesApi = config.strategy === 'image' && shouldUseResponsesApi(model);
210
+ if (useResponsesApi) {
211
+ if (!options.stdout) {
212
+ console.error(pc.dim('Mode: Responses API (ChatGPT OAuth)'));
213
+ }
214
+ await generateWithResponsesApi({
215
+ prompt,
216
+ model,
217
+ outputPath,
218
+ count,
219
+ aspectRatio: parsedAspectRatio,
220
+ seed: options.seed,
221
+ inputImages,
222
+ maskImage,
223
+ json: options.json || false,
224
+ stdout: options.stdout || false,
225
+ });
226
+ }
227
+ else if (config.strategy === 'image') {
198
228
  await generateWithImageModel({
199
229
  prompt,
200
230
  model,
201
231
  outputPath,
202
- count: options.count,
203
- aspectRatio: options.aspectRatio,
232
+ count,
233
+ aspectRatio: parsedAspectRatio,
204
234
  seed: options.seed,
205
235
  inputImages,
206
236
  maskImage,
@@ -261,8 +291,10 @@ cli
261
291
  .example('egaki video "waves crashing on cliffs" -n 2 -o waves.mp4')
262
292
  .action(async (prompt, options) => {
263
293
  injectCredentialsToEnv();
264
- const model = options.model;
265
- const outputPath = options.output;
294
+ // Apply the same .default(...) values goke's input-type inference drops.
295
+ const model = options.model ?? DEFAULT_VIDEO_MODEL;
296
+ const outputPath = options.output ?? 'egaki-output.mp4';
297
+ const count = options.count ?? 1;
266
298
  const config = getModelConfig(model);
267
299
  if (config.strategy !== 'video') {
268
300
  console.error(pc.red(`Model ${model} is not a video model`));
@@ -280,7 +312,7 @@ cli
280
312
  prompt,
281
313
  model,
282
314
  outputPath,
283
- count: options.count,
315
+ count,
284
316
  aspectRatio: options.aspectRatio,
285
317
  resolution: options.resolution,
286
318
  duration: options.duration,
@@ -306,6 +338,7 @@ cli
306
338
  .action(async (options) => {
307
339
  const { CATALOG } = await import('./model-catalog.js');
308
340
  const yaml = await import('js-yaml');
341
+ const providerStatuses = Object.fromEntries(Object.keys(PROVIDERS).map((provider) => [provider, getKeyStatus(provider)]));
309
342
  let models = options.type === 'video'
310
343
  ? VIDEO_CATALOG
311
344
  : options.type === 'image'
@@ -323,6 +356,7 @@ cli
323
356
  name: m.name,
324
357
  ...(m.description ? { description: m.description } : {}),
325
358
  provider: m.provider,
359
+ auth: providerStatuses[m.provider] ?? { available: false, source: 'none' },
326
360
  strategy: m.strategy,
327
361
  released: m.released,
328
362
  cost: formatCatalogCost(m.cost),
@@ -529,6 +563,148 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
529
563
  console.log(JSON.stringify(output, null, 2));
530
564
  }
531
565
  }
566
+ // Generate using the OpenAI Responses API with imageGeneration tool.
567
+ // Used when auth comes from ChatGPT OAuth (no Image API scope).
568
+ async function generateWithResponsesApi({ prompt, model, outputPath, count, aspectRatio, seed, inputImages, maskImage, json, stdout, }) {
569
+ if (count !== 1) {
570
+ console.error(pc.red('ChatGPT image generation currently supports exactly one output image.'));
571
+ process.exit(1);
572
+ }
573
+ if (seed !== undefined) {
574
+ console.error(pc.red('ChatGPT image generation does not support --seed.'));
575
+ process.exit(1);
576
+ }
577
+ if (maskImage) {
578
+ console.error(pc.red('ChatGPT image generation does not support --mask yet.'));
579
+ process.exit(1);
580
+ }
581
+ const size = aspectRatio ? chatGptImageSizeFromAspectRatio(aspectRatio) : undefined;
582
+ if (aspectRatio && !size) {
583
+ console.error(pc.red('ChatGPT image generation only supports --aspect-ratio 1:1, 3:2, or 2:3.'));
584
+ process.exit(1);
585
+ }
586
+ const storedAuth = getChatGptAuth();
587
+ if (!storedAuth?.accountId) {
588
+ console.error(pc.red('Missing ChatGPT account metadata. Please run `egaki login --provider chatgpt` again.'));
589
+ process.exit(1);
590
+ }
591
+ const auth = await getValidChatGptAuth(storedAuth, saveChatGptAuth);
592
+ if (auth instanceof Error) {
593
+ console.error(pc.red(auth.message));
594
+ process.exit(1);
595
+ }
596
+ if (!stdout) {
597
+ console.error(pc.cyan('Generating...'));
598
+ }
599
+ const content = [
600
+ { type: 'input_text', text: prompt },
601
+ ...inputImages.map((image) => ({
602
+ type: 'input_image',
603
+ image_url: imageBytesToDataUrl(image),
604
+ })),
605
+ ];
606
+ const response = await fetch('https://chatgpt.com/backend-api/codex/responses', {
607
+ method: 'POST',
608
+ headers: {
609
+ Authorization: `Bearer ${auth.access}`,
610
+ 'ChatGPT-Account-ID': auth.accountId,
611
+ 'Content-Type': 'application/json',
612
+ },
613
+ body: JSON.stringify({
614
+ model: 'gpt-5.4',
615
+ instructions: 'You are Codex.',
616
+ input: [
617
+ {
618
+ type: 'message',
619
+ role: 'user',
620
+ content,
621
+ },
622
+ ],
623
+ tools: [
624
+ {
625
+ type: 'image_generation',
626
+ model,
627
+ size: 'auto',
628
+ quality: 'auto',
629
+ output_format: 'png',
630
+ output_compression: 100,
631
+ moderation: 'auto',
632
+ ...(size ? { size } : {}),
633
+ },
634
+ ],
635
+ tool_choice: 'auto',
636
+ parallel_tool_calls: true,
637
+ stream: true,
638
+ store: false,
639
+ include: [],
640
+ }),
641
+ });
642
+ if (!response.ok || !response.body) {
643
+ const body = await response.text().catch(() => '');
644
+ console.error(pc.red(`ChatGPT image generation failed: ${response.status}`));
645
+ if (body)
646
+ console.error(pc.dim(body));
647
+ process.exit(1);
648
+ }
649
+ let imageBase64;
650
+ let revisedPrompt = null;
651
+ const processSseMessage = (message) => {
652
+ if (!message.data)
653
+ return;
654
+ try {
655
+ const event = JSON.parse(message.data);
656
+ if (event.type === 'response.image_generation_call.partial_image' &&
657
+ event.partial_image_b64 &&
658
+ !imageBase64) {
659
+ imageBase64 = event.partial_image_b64;
660
+ }
661
+ if (event.type === 'response.output_item.done' &&
662
+ event.item?.type === 'image_generation_call') {
663
+ if (event.item.result)
664
+ imageBase64 = event.item.result;
665
+ revisedPrompt = event.item.revised_prompt ?? revisedPrompt;
666
+ }
667
+ }
668
+ catch {
669
+ // Ignore keepalive and partial parse noise.
670
+ }
671
+ };
672
+ const parser = createParser({
673
+ onEvent: processSseMessage,
674
+ });
675
+ const decoder = new TextDecoder();
676
+ for await (const chunk of response.body) {
677
+ parser.feed(decoder.decode(chunk, { stream: true }));
678
+ }
679
+ parser.feed(decoder.decode());
680
+ if (!imageBase64) {
681
+ console.error(pc.red('No images generated.'));
682
+ process.exit(1);
683
+ }
684
+ const imageBytes = Buffer.from(imageBase64, 'base64');
685
+ const mediaType = 'image/png';
686
+ if (stdout) {
687
+ process.stdout.write(imageBytes);
688
+ return;
689
+ }
690
+ const savedFiles = [];
691
+ const filePath = ensureExtension(outputPath, extensionFromMediaType(mediaType));
692
+ fs.writeFileSync(filePath, imageBytes);
693
+ console.error(pc.green(`Saved: ${filePath}`));
694
+ savedFiles.push(filePath);
695
+ if (revisedPrompt && !json) {
696
+ console.error(pc.dim(`Revised prompt: ${revisedPrompt}`));
697
+ }
698
+ if (json) {
699
+ const output = {
700
+ model,
701
+ files: savedFiles,
702
+ count: 1,
703
+ revisedPrompt,
704
+ };
705
+ console.log(JSON.stringify(output, null, 2));
706
+ }
707
+ }
532
708
  // Generate using experimental_generateVideo (video models)
533
709
  async function generateWithVideoModel({ prompt, model, outputPath, count, aspectRatio, resolution, duration, fps, seed, inputImage, json, stdout, }) {
534
710
  const videoModel = await createVideoModel(model);
@@ -648,6 +824,40 @@ function extensionFromMediaType(mediaType) {
648
824
  };
649
825
  return map[mediaType] || (mediaType.startsWith('video/') ? '.mp4' : '.png');
650
826
  }
827
+ function inferImageMediaType(image) {
828
+ if (image.length >= 8 && image[0] === 0x89 && image[1] === 0x50 && image[2] === 0x4e && image[3] === 0x47) {
829
+ return 'image/png';
830
+ }
831
+ if (image.length >= 3 && image[0] === 0xff && image[1] === 0xd8 && image[2] === 0xff) {
832
+ return 'image/jpeg';
833
+ }
834
+ if (image.length >= 12 &&
835
+ image[0] === 0x52 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x46 &&
836
+ image[8] === 0x57 && image[9] === 0x45 && image[10] === 0x42 && image[11] === 0x50) {
837
+ return 'image/webp';
838
+ }
839
+ if (image.length >= 6 &&
840
+ image[0] === 0x47 && image[1] === 0x49 && image[2] === 0x46 && image[3] === 0x38) {
841
+ return 'image/gif';
842
+ }
843
+ return 'image/png';
844
+ }
845
+ function imageBytesToDataUrl(image) {
846
+ const mediaType = inferImageMediaType(image);
847
+ return `data:${mediaType};base64,${Buffer.from(image).toString('base64')}`;
848
+ }
849
+ function chatGptImageSizeFromAspectRatio(aspectRatio) {
850
+ switch (aspectRatio) {
851
+ case '1:1':
852
+ return '1024x1024';
853
+ case '3:2':
854
+ return '1536x1024';
855
+ case '2:3':
856
+ return '1024x1536';
857
+ default:
858
+ return undefined;
859
+ }
860
+ }
651
861
  function normalizeResolutionKey(input) {
652
862
  if (!input)
653
863
  return undefined;
@@ -660,6 +870,15 @@ function normalizeResolutionKey(input) {
660
870
  return '480p';
661
871
  return normalized;
662
872
  }
873
+ function isAspectRatio(input) {
874
+ return /^\d+:\d+$/.test(input);
875
+ }
876
+ function parseAspectRatioOption(input) {
877
+ if (!input || !isAspectRatio(input)) {
878
+ return undefined;
879
+ }
880
+ return input;
881
+ }
663
882
  function ensureExtension(filePath, ext) {
664
883
  const parsed = path.parse(filePath);
665
884
  if (parsed.ext) {
@@ -1,18 +1,31 @@
1
- export declare const PROVIDERS: Record<string, {
1
+ import type { ChatGptAuth } from './chatgpt-auth.js';
2
+ export type ProviderInfo = {
2
3
  envVar: string;
3
4
  label: string;
4
5
  hint: string;
5
- }>;
6
+ /** If true, this provider uses OAuth instead of a plain API key */
7
+ oauth?: boolean;
8
+ };
9
+ export declare const PROVIDERS: Record<string, ProviderInfo>;
6
10
  export declare const EGAKI_GATEWAY_URL = "https://egaki.org/v3/ai";
7
- export type Credentials = Record<string, string>;
11
+ /** A credential value is either a plain API key string or a ChatGPT OAuth object. */
12
+ export type CredentialValue = string | ChatGptAuth;
13
+ export type Credentials = Record<string, CredentialValue>;
8
14
  export declare function readCredentials(): Credentials;
9
15
  export declare function writeCredentials(creds: Credentials): void;
10
16
  export declare function saveProviderKey(provider: string, key: string): void;
17
+ export declare function saveChatGptAuth(auth: ChatGptAuth): void;
18
+ export declare function getChatGptAuth(): ChatGptAuth | undefined;
11
19
  export declare function removeProviderKey(provider: string): void;
12
20
  export declare function getProviderKey(provider: string): string | undefined;
13
21
  export declare function injectCredentialsToEnv(): void;
22
+ /**
23
+ * Returns true when the current run should use the ChatGPT/Codex backend flow
24
+ * for OpenAI image models instead of a direct OpenAI API key.
25
+ */
26
+ export declare function shouldUseChatGptBackend(): boolean;
14
27
  export declare function getKeyStatus(provider: string): {
15
28
  available: boolean;
16
- source: 'env' | 'stored' | 'none';
29
+ source: 'env' | 'stored' | 'oauth' | 'none';
17
30
  };
18
31
  //# sourceMappingURL=credentials.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,SAAS,EAAE,MAAM,CAC5B,MAAM,EACN;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAgChD,CAAA;AAED,eAAO,MAAM,iBAAiB,4BAA4B,CAAA;AAa1D,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;AAEhD,wBAAgB,eAAe,IAAI,WAAW,CAO7C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAOzD;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAInE;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIxD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGnE;AAKD,wBAAgB,sBAAsB,IAAI,IAAI,CAW7C;AAID,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC9C,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;CAClC,CAaA"}
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAEpD,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,mEAAmE;IACnE,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAKD,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAqClD,CAAA;AAED,eAAO,MAAM,iBAAiB,4BAA4B,CAAA;AAa1D,qFAAqF;AACrF,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,WAAW,CAAA;AAClD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;AAEzD,wBAAgB,eAAe,IAAI,WAAW,CAO7C;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,CAOzD;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAInE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,WAAW,GAAG,IAAI,CAIvD;AAED,wBAAgB,cAAc,IAAI,WAAW,GAAG,SAAS,CAOxD;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIxD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAInE;AAKD,wBAAgB,sBAAsB,IAAI,IAAI,CAa7C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAID,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG;IAC9C,SAAS,EAAE,OAAO,CAAA;IAClB,MAAM,EAAE,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAA;CAC5C,CAuBA"}