@dorely13/ai-imagegen-cli 0.1.3 → 0.3.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/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
5
- import { basename, dirname as dirname2, extname, join as join3, resolve as resolve2 } from "node:path";
4
+ import { mkdir as mkdir5, readFile as readFile3, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
5
+ import { basename, dirname as dirname5, extname, join as join5, resolve as resolve3 } from "node:path";
6
6
 
7
7
  // ../../packages/core/dist/types.js
8
8
  var ProviderError = class extends Error {
@@ -10,6 +10,8 @@ var ProviderError = class extends Error {
10
10
  statusCode;
11
11
  requestId;
12
12
  responseId;
13
+ upstreamProvider;
14
+ retryAfterSeconds;
13
15
  constructor(details) {
14
16
  super(details.message);
15
17
  this.name = "ProviderError";
@@ -17,6 +19,8 @@ var ProviderError = class extends Error {
17
19
  this.statusCode = details.statusCode;
18
20
  this.requestId = details.requestId;
19
21
  this.responseId = details.responseId;
22
+ this.upstreamProvider = details.upstreamProvider;
23
+ this.retryAfterSeconds = details.retryAfterSeconds;
20
24
  }
21
25
  };
22
26
  var MAX_REFERENCE_IMAGES = 4;
@@ -222,20 +226,24 @@ var AUTH_ENDPOINT = "https://auth.openai.com/oauth/authorize";
222
226
  var TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
223
227
  var CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
224
228
  var SCOPE = "openid profile email offline_access";
225
- var RESPONSES_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
229
+ var IMAGES_ENDPOINT = "https://chatgpt.com/backend-api/codex/images";
226
230
  var DEFAULT_REDIRECT_URI = "http://localhost:1455/auth/callback";
227
231
  var LOOPBACK_PORT = 1455;
228
232
  var CALLBACK_PATH = "/auth/callback";
229
233
  var DEFAULT_IMAGE_MODEL = "gpt-image-2.5-flare";
230
- var DEFAULT_ORCHESTRATION_MODEL = "gpt-5.6-luna";
231
234
  var DEFAULT_QUALITY = "auto";
232
235
  var REQUEST_TIMEOUT_SECONDS = 600;
233
- var PARTIAL_IMAGES = 2;
234
236
  var MAX_REFERENCE_IMAGES2 = 4;
235
237
  var MAX_IMAGE_COUNT2 = 4;
236
238
  var MIN_IMAGE_COUNT2 = 1;
237
239
  var FLOW_TIMEOUT_MINUTES = 10;
238
240
  var USER_AGENT = "ImageGenCli";
241
+ var TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1e3;
242
+ var TOKEN_REQUEST_TIMEOUT_MS = 30 * 1e3;
243
+ var REFRESH_LOCK_FILE = "codex-refresh.lock";
244
+ var REFRESH_LOCK_STALE_MS = 60 * 1e3;
245
+ var REFRESH_LOCK_WAIT_MS = 90 * 1e3;
246
+ var REFRESH_LOCK_POLL_MS = 100;
239
247
 
240
248
  // ../../packages/providers-codex/dist/pkce.js
241
249
  import { createHash, randomBytes } from "node:crypto";
@@ -278,7 +286,7 @@ function callbackPage(title, message) {
278
286
  return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family:sans-serif;padding:2rem"><h1>${title}</h1><p>${message}</p></body></html>`;
279
287
  }
280
288
  async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3, port = LOOPBACK_PORT) {
281
- return new Promise((resolve3, reject) => {
289
+ return new Promise((resolve4, reject) => {
282
290
  let server;
283
291
  const timer = setTimeout(() => {
284
292
  closeServer();
@@ -288,9 +296,9 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
288
296
  clearTimeout(timer);
289
297
  server?.close();
290
298
  };
291
- const fail2 = (error) => {
299
+ const fail2 = (error2) => {
292
300
  closeServer();
293
- reject(error);
301
+ reject(error2);
294
302
  };
295
303
  server = createServer((req, res) => {
296
304
  try {
@@ -318,17 +326,17 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
318
326
  res.writeHead(200, { "content-type": "text/html" });
319
327
  res.end(callbackPage("Connected", "ImageGenCli is connected. You can close this tab."));
320
328
  closeServer();
321
- resolve3({ code, state });
322
- } catch (error) {
323
- fail2(error instanceof Error ? error : new LoopbackError("OAuth callback failed.", "callback_error"));
329
+ resolve4({ code, state });
330
+ } catch (error2) {
331
+ fail2(error2 instanceof Error ? error2 : new LoopbackError("OAuth callback failed.", "callback_error"));
324
332
  }
325
333
  });
326
- server.on("error", (error) => {
327
- if (error.code === "EADDRINUSE") {
334
+ server.on("error", (error2) => {
335
+ if (error2.code === "EADDRINUSE") {
328
336
  fail2(new LoopbackError(`Port ${port} is busy. Close the conflicting process and retry.`, "port_busy"));
329
337
  return;
330
338
  }
331
- fail2(error);
339
+ fail2(error2);
332
340
  });
333
341
  server.listen(port, "127.0.0.1");
334
342
  });
@@ -336,7 +344,7 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
336
344
 
337
345
  // ../../packages/providers-codex/dist/codex-auth.js
338
346
  var ReauthRequiredError = class extends Error {
339
- constructor(message = "Codex credentials are missing or expired. Run `imagegen auth login` to reconnect.") {
347
+ constructor(message = "Codex credentials are missing or expired. Run `imagegen auth codex login` to reconnect.") {
340
348
  super(message);
341
349
  this.name = "ReauthRequiredError";
342
350
  }
@@ -349,10 +357,24 @@ function toTokenSet(body, fallbackRefreshToken) {
349
357
  scope: body.scope
350
358
  };
351
359
  }
360
+ var REJECTED_GRANT_CODES = /* @__PURE__ */ new Set([
361
+ "invalid_grant",
362
+ "invalid_token",
363
+ "refresh_token_expired",
364
+ "refresh_token_reused",
365
+ "refresh_token_invalidated"
366
+ ]);
352
367
  async function readOAuthErrorCode(response) {
353
368
  try {
354
369
  const body = await response.json();
355
- return typeof body.error === "string" ? body.error : null;
370
+ if (typeof body.error === "string")
371
+ return body.error;
372
+ if (typeof body.error === "object" && body.error !== null) {
373
+ const code = body.error["code"];
374
+ if (typeof code === "string")
375
+ return code;
376
+ }
377
+ return null;
356
378
  } catch {
357
379
  return null;
358
380
  }
@@ -361,30 +383,31 @@ async function postTokenRequest(params) {
361
383
  const response = await fetch(TOKEN_ENDPOINT, {
362
384
  method: "POST",
363
385
  headers: { "content-type": "application/x-www-form-urlencoded" },
364
- body: new URLSearchParams(params)
386
+ body: new URLSearchParams(params),
387
+ signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)
365
388
  });
366
389
  if (!response.ok) {
367
390
  const errorCode = await readOAuthErrorCode(response.clone());
368
- if (response.status === 401 || response.status === 400 && (errorCode === "invalid_grant" || errorCode === "invalid_token")) {
369
- return null;
391
+ if (response.status === 401 || errorCode !== null && REJECTED_GRANT_CODES.has(errorCode)) {
392
+ return { ok: false, reason: errorCode ?? `status ${response.status}` };
370
393
  }
371
- throw new Error(`Token request failed with status ${response.status}.`);
394
+ throw new Error(`Token request failed with status ${response.status}${errorCode ? ` (${errorCode})` : ""}.`);
372
395
  }
373
396
  const body = await response.json();
374
- return toTokenSet(body, params["refresh_token"]);
397
+ return { ok: true, tokens: toTokenSet(body, params["refresh_token"]) };
375
398
  }
376
399
  async function exchangeCodeForTokens(args) {
377
- const tokens = await postTokenRequest({
400
+ const outcome = await postTokenRequest({
378
401
  grant_type: "authorization_code",
379
402
  client_id: CLIENT_ID,
380
403
  code: args.code,
381
404
  redirect_uri: args.redirectUri,
382
405
  code_verifier: args.codeVerifier
383
406
  });
384
- if (!tokens) {
385
- throw new ReauthRequiredError("The authorization code was rejected. Run `imagegen auth login` again.");
407
+ if (!outcome.ok) {
408
+ throw new ReauthRequiredError(`The authorization code was rejected (${outcome.reason}). Run \`imagegen auth codex login\` again.`);
386
409
  }
387
- return tokens;
410
+ return outcome.tokens;
388
411
  }
389
412
  async function refreshTokens(refreshToken) {
390
413
  return postTokenRequest({
@@ -393,8 +416,8 @@ async function refreshTokens(refreshToken) {
393
416
  refresh_token: refreshToken
394
417
  });
395
418
  }
396
- function isTokenExpired(tokens, now = Date.now()) {
397
- return tokens.expiresAt <= now;
419
+ function needsRefresh(tokens, now = Date.now()) {
420
+ return tokens.expiresAt - TOKEN_REFRESH_MARGIN_MS <= now;
398
421
  }
399
422
  var pendingVerifiers = /* @__PURE__ */ new Map();
400
423
  function startPkceFlow(redirectUri2) {
@@ -420,92 +443,36 @@ function prunePendingVerifiers() {
420
443
  }
421
444
  }
422
445
 
423
- // ../../packages/providers-codex/dist/codex-token-manager.js
424
- var CodexTokenManager = class {
425
- store;
426
- pendingRefresh = null;
427
- constructor(store) {
428
- this.store = store;
429
- }
430
- async exchangeAndSave(args) {
431
- const tokens = await exchangeCodeForTokens(args);
432
- await this.store.save(PROVIDER_NAME, tokens);
433
- return tokens;
434
- }
435
- async status() {
436
- const tokens = await this.store.load(PROVIDER_NAME);
437
- if (!tokens)
438
- return { connected: false };
439
- if (!isTokenExpired(tokens))
440
- return { connected: true, expiresAt: tokens.expiresAt };
441
- if (!tokens.refreshToken)
442
- return { connected: false };
443
- try {
444
- const refreshed = await this.getValidAccessToken();
445
- void refreshed;
446
- const current = await this.store.load(PROVIDER_NAME);
447
- return { connected: true, expiresAt: current?.expiresAt };
448
- } catch {
449
- return { connected: false };
450
- }
451
- }
452
- async logout() {
453
- await this.store.clear(PROVIDER_NAME);
454
- }
455
- async getValidAccessToken() {
456
- const tokens = await this.store.load(PROVIDER_NAME);
457
- if (!tokens)
458
- throw new ReauthRequiredError();
459
- if (!isTokenExpired(tokens))
460
- return tokens.accessToken;
461
- if (!tokens.refreshToken) {
462
- await this.store.clear(PROVIDER_NAME);
463
- throw new ReauthRequiredError();
464
- }
465
- if (!this.pendingRefresh) {
466
- this.pendingRefresh = this.performRefresh(tokens.refreshToken).finally(() => {
467
- this.pendingRefresh = null;
468
- });
469
- }
470
- return this.pendingRefresh;
471
- }
472
- async performRefresh(refreshToken) {
473
- const refreshed = await refreshTokens(refreshToken);
474
- if (!refreshed) {
475
- await this.store.clear(PROVIDER_NAME);
476
- throw new ReauthRequiredError();
477
- }
478
- await this.store.save(PROVIDER_NAME, refreshed);
479
- return refreshed.accessToken;
480
- }
481
- };
446
+ // ../../packages/providers-codex/dist/refresh-lock.js
447
+ import { mkdir, open, rm, stat } from "node:fs/promises";
448
+ import { dirname, join as join2 } from "node:path";
482
449
 
483
- // ../../packages/providers-codex/dist/token-store.js
450
+ // ../../packages/credentials/dist/index.js
484
451
  import { homedir } from "node:os";
485
452
  import { join } from "node:path";
486
453
  function asKeytar(module) {
487
454
  if (typeof module !== "object" || module === null)
488
455
  return null;
489
- const record = module;
490
- const candidate = record["default"] ?? module;
456
+ const record3 = module;
457
+ const candidate = record3["default"] ?? module;
491
458
  if (typeof candidate.getPassword === "function" && typeof candidate.setPassword === "function" && typeof candidate.deletePassword === "function") {
492
459
  return candidate;
493
460
  }
494
461
  return null;
495
462
  }
496
- async function resolveTokenStore() {
463
+ async function resolveCredentialStore() {
497
464
  try {
498
465
  const keytar = asKeytar(await import("keytar"));
499
466
  if (!keytar)
500
- return new FileTokenStore();
501
- const store = new KeytarTokenStore(keytar);
467
+ return new FileCredentialStore();
468
+ const store = new KeytarCredentialStore(keytar);
502
469
  await store.probe();
503
470
  return store;
504
471
  } catch {
505
- return new FileTokenStore();
472
+ return new FileCredentialStore();
506
473
  }
507
474
  }
508
- var KeytarTokenStore = class {
475
+ var KeytarCredentialStore = class {
509
476
  keytar;
510
477
  constructor(keytar) {
511
478
  this.keytar = keytar;
@@ -517,7 +484,11 @@ var KeytarTokenStore = class {
517
484
  const raw = await this.keytar.getPassword("ImageGenCli", provider);
518
485
  if (!raw)
519
486
  return null;
520
- return JSON.parse(raw);
487
+ try {
488
+ return JSON.parse(raw);
489
+ } catch {
490
+ throw new Error("Stored credential record is invalid. Run the provider login command.");
491
+ }
521
492
  }
522
493
  async save(provider, tokens) {
523
494
  await this.keytar.setPassword("ImageGenCli", provider, JSON.stringify(tokens));
@@ -526,13 +497,16 @@ var KeytarTokenStore = class {
526
497
  await this.keytar.deletePassword("ImageGenCli", provider);
527
498
  }
528
499
  describe() {
529
- return { backend: "vault", location: "OS credential vault (service ImageGenCli)" };
500
+ return {
501
+ backend: "vault",
502
+ location: "OS credential vault (service ImageGenCli)"
503
+ };
530
504
  }
531
505
  };
532
- var FileTokenStore = class {
506
+ var FileCredentialStore = class {
533
507
  filePath;
534
508
  constructor(filePath) {
535
- this.filePath = filePath ?? defaultTokenFilePath();
509
+ this.filePath = filePath ?? defaultCredentialFilePath();
536
510
  }
537
511
  async load(provider) {
538
512
  const all = await this.readAll();
@@ -552,27 +526,37 @@ var FileTokenStore = class {
552
526
  return { backend: "file", location: this.filePath };
553
527
  }
554
528
  async readAll() {
555
- const { readFile: readFile3 } = await import("node:fs/promises");
529
+ const { readFile: readFile4 } = await import("node:fs/promises");
556
530
  try {
557
- const raw = await readFile3(this.filePath, "utf8");
558
- return JSON.parse(raw);
559
- } catch (error) {
560
- if (error.code === "ENOENT")
531
+ const raw = await readFile4(this.filePath, "utf8");
532
+ let value;
533
+ try {
534
+ value = JSON.parse(raw);
535
+ } catch {
536
+ throw new Error("Credential file is invalid. Restore a valid credential file before retrying.");
537
+ }
538
+ if (typeof value !== "object" || value === null || Array.isArray(value))
539
+ throw new Error("Credential file must contain provider records.");
540
+ return value;
541
+ } catch (error2) {
542
+ if (error2.code === "ENOENT")
561
543
  return {};
562
- throw error;
544
+ throw error2;
563
545
  }
564
546
  }
565
547
  async writeAll(all) {
566
- const { mkdir: mkdir3, writeFile: writeFile3, chmod } = await import("node:fs/promises");
567
- const { dirname: dirname3 } = await import("node:path");
568
- await mkdir3(dirname3(this.filePath), { recursive: true });
569
- await writeFile3(this.filePath, JSON.stringify(all, null, 2), { mode: 384 });
548
+ const { mkdir: mkdir6, writeFile: writeFile4, chmod } = await import("node:fs/promises");
549
+ const { dirname: dirname6 } = await import("node:path");
550
+ await mkdir6(dirname6(this.filePath), { recursive: true });
551
+ await writeFile4(this.filePath, JSON.stringify(all, null, 2), {
552
+ mode: 384
553
+ });
570
554
  if (process.platform !== "win32") {
571
555
  await chmod(this.filePath, 384);
572
556
  }
573
557
  }
574
558
  };
575
- function defaultTokenFilePath() {
559
+ function defaultCredentialFilePath() {
576
560
  if (process.platform === "win32") {
577
561
  const base2 = process.env["LOCALAPPDATA"] ?? join(process.env["USERPROFILE"] ?? ".", "AppData", "Local");
578
562
  return join(base2, "ImageGenCli", "tokens.json");
@@ -581,6 +565,153 @@ function defaultTokenFilePath() {
581
565
  return join(base, "ImageGenCli", "tokens.json");
582
566
  }
583
567
 
568
+ // ../../packages/providers-codex/dist/refresh-lock.js
569
+ function defaultRefreshLockPath() {
570
+ return join2(dirname(defaultCredentialFilePath()), REFRESH_LOCK_FILE);
571
+ }
572
+ function createRefreshLock(options = {}) {
573
+ const path = options.path ?? defaultRefreshLockPath();
574
+ const staleMs = options.staleMs ?? REFRESH_LOCK_STALE_MS;
575
+ const waitMs = options.waitMs ?? REFRESH_LOCK_WAIT_MS;
576
+ const pollMs = options.pollMs ?? REFRESH_LOCK_POLL_MS;
577
+ return async (work) => {
578
+ await acquire(path, staleMs, waitMs, pollMs);
579
+ try {
580
+ return await work();
581
+ } finally {
582
+ await rm(path, { force: true });
583
+ }
584
+ };
585
+ }
586
+ async function acquire(path, staleMs, waitMs, pollMs) {
587
+ await mkdir(dirname(path), { recursive: true });
588
+ const deadline = Date.now() + waitMs;
589
+ for (; ; ) {
590
+ try {
591
+ const handle = await open(path, "wx");
592
+ try {
593
+ await handle.writeFile(`${process.pid}
594
+ `);
595
+ } finally {
596
+ await handle.close();
597
+ }
598
+ return;
599
+ } catch (error2) {
600
+ if (!isContention(error2))
601
+ throw error2;
602
+ }
603
+ if (await isStale(path, staleMs)) {
604
+ await rm(path, { force: true });
605
+ continue;
606
+ }
607
+ if (Date.now() >= deadline) {
608
+ throw new Error("Timed out waiting for another imagegen process to refresh Codex credentials.");
609
+ }
610
+ await new Promise((resolve4) => setTimeout(resolve4, pollMs + Math.random() * pollMs));
611
+ }
612
+ }
613
+ function isContention(error2) {
614
+ const code = error2.code;
615
+ return code === "EEXIST" || process.platform === "win32" && (code === "EPERM" || code === "EACCES");
616
+ }
617
+ async function isStale(path, staleMs) {
618
+ try {
619
+ return Date.now() - (await stat(path)).mtimeMs > staleMs;
620
+ } catch {
621
+ return false;
622
+ }
623
+ }
624
+
625
+ // ../../packages/providers-codex/dist/codex-token-manager.js
626
+ var CodexTokenManager = class {
627
+ store;
628
+ pendingRefresh = null;
629
+ lock;
630
+ refresh;
631
+ constructor(store, options = {}) {
632
+ this.store = store;
633
+ this.lock = options.lock ?? createRefreshLock();
634
+ this.refresh = options.refresh ?? refreshTokens;
635
+ }
636
+ async exchangeAndSave(args) {
637
+ const tokens = await exchangeCodeForTokens(args);
638
+ await this.store.save(PROVIDER_NAME, tokens);
639
+ return tokens;
640
+ }
641
+ async status() {
642
+ if (!await this.store.load(PROVIDER_NAME))
643
+ return { connected: false };
644
+ try {
645
+ await this.getValidAccessToken();
646
+ const current = await this.store.load(PROVIDER_NAME);
647
+ return { connected: true, expiresAt: current?.expiresAt };
648
+ } catch {
649
+ return { connected: false };
650
+ }
651
+ }
652
+ async logout() {
653
+ await this.store.clear(PROVIDER_NAME);
654
+ }
655
+ async getValidAccessToken() {
656
+ const tokens = await this.store.load(PROVIDER_NAME);
657
+ if (!tokens)
658
+ throw new ReauthRequiredError();
659
+ if (!needsRefresh(tokens))
660
+ return tokens.accessToken;
661
+ return this.refreshFrom(tokens.accessToken);
662
+ }
663
+ // The Codex image endpoint rejected this access token before its recorded expiry.
664
+ async recoverFromUnauthorized(rejectedAccessToken) {
665
+ return this.refreshFrom(rejectedAccessToken);
666
+ }
667
+ refreshFrom(staleAccessToken) {
668
+ if (!this.pendingRefresh) {
669
+ this.pendingRefresh = this.lock(() => this.refreshLocked(staleAccessToken)).finally(() => {
670
+ this.pendingRefresh = null;
671
+ });
672
+ }
673
+ return this.pendingRefresh;
674
+ }
675
+ async refreshLocked(staleAccessToken) {
676
+ const current = await this.store.load(PROVIDER_NAME);
677
+ if (!current)
678
+ throw new ReauthRequiredError();
679
+ if (current.accessToken !== staleAccessToken && !needsRefresh(current))
680
+ return current.accessToken;
681
+ if (!current.refreshToken) {
682
+ await this.store.clear(PROVIDER_NAME);
683
+ throw new ReauthRequiredError();
684
+ }
685
+ const outcome = await this.refresh(current.refreshToken);
686
+ if (!outcome.ok) {
687
+ await this.store.clear(PROVIDER_NAME);
688
+ throw new ReauthRequiredError(`Codex rejected the stored refresh token (${outcome.reason}). Run \`imagegen auth codex login\` to reconnect.`);
689
+ }
690
+ await this.store.save(PROVIDER_NAME, outcome.tokens);
691
+ return outcome.tokens.accessToken;
692
+ }
693
+ };
694
+
695
+ // ../../packages/providers-codex/dist/token-store.js
696
+ async function resolveTokenStore() {
697
+ const store = await resolveCredentialStore();
698
+ return {
699
+ ...store,
700
+ load: async (provider) => {
701
+ const value = await store.load(provider);
702
+ if (value === null)
703
+ return null;
704
+ if (typeof value.accessToken !== "string" || typeof value.expiresAt !== "number" || value.refreshToken !== void 0 && typeof value.refreshToken !== "string") {
705
+ throw new Error("Stored Codex credentials are invalid. Run imagegen auth codex login.");
706
+ }
707
+ return value;
708
+ },
709
+ save: (provider, value) => store.save(provider, value),
710
+ clear: (provider) => store.clear(provider),
711
+ describe: () => store.describe()
712
+ };
713
+ }
714
+
584
715
  // ../../packages/providers-codex/dist/account-id.js
585
716
  function extractAccountId(token) {
586
717
  const parts = token.split(".");
@@ -601,13 +732,18 @@ function extractAccountId(token) {
601
732
  return accountId;
602
733
  }
603
734
 
604
- // ../../packages/providers-codex/dist/sse.js
735
+ // ../../packages/providers-codex/dist/codex-client.js
736
+ import sharp from "sharp";
737
+
738
+ // ../../packages/providers-codex/dist/errors.js
605
739
  function classifyImageError(message) {
606
740
  if (!message.trim())
607
741
  return "unknown";
608
742
  const lowerRate = message.includes("input-images") && message.includes("per min") && message.toLowerCase().includes("gpt-image");
609
743
  if (lowerRate)
610
744
  return "codex_image_input_rate_limit";
745
+ if (/usage limit/i.test(message))
746
+ return "usage_limit";
611
747
  if (/rate limit|429|too many requests|please try again/i.test(message))
612
748
  return "rate_limit";
613
749
  if (/403|forbidden|cloudflare/i.test(message))
@@ -616,107 +752,26 @@ function classifyImageError(message) {
616
752
  return "image_model_invalid";
617
753
  return "api_error";
618
754
  }
619
- function readString(value, property) {
620
- if (typeof value !== "object" || value === null)
621
- return void 0;
622
- const candidate = value[property];
623
- return typeof candidate === "string" ? candidate : void 0;
624
- }
625
- function readEventError(event) {
626
- const direct = readString(event, "message");
627
- if (direct)
628
- return direct;
629
- if (typeof event !== "object" || event === null)
630
- return void 0;
631
- const record = event;
632
- const nested = record["error"];
633
- if (typeof nested === "object" && nested !== null) {
634
- const message = readString(nested, "message");
635
- if (message)
636
- return message;
637
- }
638
- const response = record["response"];
639
- if (typeof response === "object" && response !== null) {
640
- const responseError = response["error"];
641
- if (typeof responseError === "object" && responseError !== null) {
642
- return readString(responseError, "message");
643
- }
644
- }
645
- return void 0;
646
- }
647
- function tryReadImageItem(item) {
648
- if (readString(item, "type") !== "image_generation_call")
649
- return null;
650
- const result = readString(item, "result");
651
- if (!result)
755
+ function readErrorMessage(body) {
756
+ if (!body.trim())
652
757
  return null;
653
- let data;
654
758
  try {
655
- data = Buffer.from(result, "base64");
656
- } catch {
657
- throw new Error("invalid_image_result: Codex returned an image result that was not valid base64.");
658
- }
659
- return { data, revisedPrompt: readString(item, "revised_prompt"), callId: readString(item, "id") };
660
- }
661
- function parseImageStreamEvents(events) {
662
- let eventCount = 0;
663
- let lastEventType;
664
- let responseId;
665
- const finish = (image) => ({
666
- ...image,
667
- responseId,
668
- eventCount,
669
- lastEventType
670
- });
671
- for (const raw of events) {
672
- const line = raw.trim();
673
- if (!line.startsWith("data:"))
674
- continue;
675
- const payload = line.slice("data:".length).trim();
676
- if (payload === "[DONE]")
677
- break;
678
- let event;
679
- try {
680
- event = JSON.parse(payload);
681
- } catch {
682
- continue;
683
- }
684
- const type = readString(event, "type");
685
- lastEventType = type ?? lastEventType;
686
- eventCount += 1;
687
- if (type === "response.created" && typeof event === "object" && event !== null) {
688
- const inner = event["response"];
689
- const id = readString(inner, "id");
690
- if (id)
691
- responseId = id;
692
- }
693
- if (type === "response.failed" || type === "error") {
694
- throw new Error(`${classifyImageError(readEventError(event) ?? "")}: ${readEventError(event) ?? "Codex image generation failed."}`);
695
- }
696
- if (type === "response.output_item.done" && typeof event === "object" && event !== null) {
697
- const item = event["item"];
698
- const image = tryReadImageItem(item);
699
- if (image)
700
- return finish(image);
701
- }
702
- if (type === "response.completed" && typeof event === "object" && event !== null) {
703
- const response = event["response"];
704
- const id = readString(response, "id");
705
- if (id)
706
- responseId = id;
707
- if (typeof response === "object" && response !== null) {
708
- const output = response["output"];
709
- if (Array.isArray(output)) {
710
- for (const item of output) {
711
- const image = tryReadImageItem(item);
712
- if (image)
713
- return finish(image);
714
- }
715
- }
759
+ const parsed = JSON.parse(body);
760
+ if (typeof parsed === "object" && parsed !== null) {
761
+ const record3 = parsed;
762
+ if (typeof record3["message"] === "string")
763
+ return record3["message"];
764
+ if (typeof record3["detail"] === "string")
765
+ return record3["detail"];
766
+ const error2 = record3["error"];
767
+ if (typeof error2 === "object" && error2 !== null && typeof error2["message"] === "string") {
768
+ return error2["message"];
716
769
  }
717
770
  }
771
+ } catch {
772
+ return body.slice(0, 500);
718
773
  }
719
- throw new Error(`stream_completed_without_image: Codex image generation completed without returning an image. responseId=${responseId ?? "unknown"}, events=${eventCount}, lastEvent=${lastEventType ?? "none"}.`);
774
+ return null;
720
775
  }
721
776
 
722
777
  // ../../packages/providers-codex/dist/codex-client.js
@@ -731,119 +786,46 @@ function normalizeCount(count) {
731
786
  function resolveSize(size) {
732
787
  try {
733
788
  return parseSize(size ?? "auto");
734
- } catch (error) {
789
+ } catch (error2) {
735
790
  throw new ProviderError({
736
791
  kind: "usage",
737
- message: error instanceof Error ? error.message : String(error)
792
+ message: error2 instanceof Error ? error2.message : String(error2)
738
793
  });
739
794
  }
740
795
  }
741
- function baseImageTool(args) {
742
- const tool = {
743
- type: "image_generation",
744
- model: args.imageModel,
745
- action: args.action,
746
- size: args.size && args.size.trim() ? args.size.trim() : "auto",
747
- output_format: args.outputFormat,
748
- background: "auto"
749
- };
750
- if (args.partialImages > 0)
751
- tool["partial_images"] = Math.min(3, Math.max(0, args.partialImages));
752
- if (args.quality && args.quality.trim() && args.quality.trim().toLowerCase() !== "auto") {
753
- tool["quality"] = args.quality.trim();
754
- }
755
- if (args.outputFormat === "jpeg" && typeof args.outputCompression === "number") {
756
- tool["output_compression"] = Math.min(100, Math.max(0, Math.trunc(args.outputCompression)));
757
- }
758
- return tool;
759
- }
760
- function buildPayload(args) {
761
- const content = [{ type: "input_text", text: args.prompt }];
762
- for (const reference of args.references.slice(0, MAX_REFERENCE_IMAGES2)) {
763
- content.push({ type: "input_image", image_url: toDataUrl(reference), detail: "auto" });
764
- }
765
- return {
796
+ function buildBody(args) {
797
+ const body = {
798
+ prompt: args.prompt,
766
799
  model: args.model,
767
- input: [{ role: "user", content }],
768
- tools: [args.tool],
769
- tool_choice: { type: "image_generation" },
770
- stream: true,
771
- store: false
800
+ size: args.size.size,
801
+ background: "auto"
772
802
  };
803
+ const quality = args.quality.trim();
804
+ if (quality && quality.toLowerCase() !== "auto")
805
+ body["quality"] = quality;
806
+ if (args.images.length > 0)
807
+ body["images"] = args.images.map((image) => ({ image_url: toDataUrl(image) }));
808
+ return body;
773
809
  }
774
810
  var CodexImageProvider = class {
775
811
  tokens;
776
812
  name = PROVIDER_NAME;
777
813
  sizePolicy = { resolveSize: (size) => resolveSize(size) };
778
- orchestrationModel;
779
814
  imageModel;
780
815
  timeoutSeconds;
781
- partialImages;
782
816
  fetchImpl;
783
817
  constructor(tokens, options = {}) {
784
818
  this.tokens = tokens;
785
- this.orchestrationModel = options.model ?? process.env["IMAGEGEN_CODEX_MODEL"] ?? DEFAULT_ORCHESTRATION_MODEL;
786
819
  this.imageModel = options.imageModel ?? process.env["IMAGEGEN_CODEX_IMAGE_MODEL"] ?? DEFAULT_IMAGE_MODEL;
787
820
  this.timeoutSeconds = options.timeoutSeconds ?? REQUEST_TIMEOUT_SECONDS;
788
- this.partialImages = options.partialImages ?? PARTIAL_IMAGES;
789
821
  this.fetchImpl = options.fetchImpl ?? fetch;
790
822
  }
791
823
  async generate(request) {
792
- const format = normalizeOutputFormat(request.format);
793
- const size = this.sizePolicy.resolveSize(request.size);
794
- const imageModel2 = this.resolveImageModel(request);
795
- return this.runAll({
796
- format,
797
- imageModel: imageModel2,
798
- count: normalizeCount(request.count),
799
- prompt: request.prompt,
800
- size,
801
- build: () => buildPayload({
802
- model: this.orchestrationModel,
803
- prompt: request.prompt,
804
- references: request.referenceImages ?? [],
805
- tool: baseImageTool({
806
- size: size.size,
807
- quality: request.quality ?? DEFAULT_QUALITY,
808
- outputFormat: format,
809
- imageModel: imageModel2,
810
- action: "generate",
811
- partialImages: this.partialImages
812
- })
813
- })
814
- });
824
+ const references = (request.referenceImages ?? []).slice(0, MAX_REFERENCE_IMAGES2);
825
+ return this.runAll(request, references.length > 0 ? "edits" : "generations", references);
815
826
  }
816
827
  async edit(request) {
817
- const format = normalizeOutputFormat(request.format);
818
- const size = this.sizePolicy.resolveSize(request.size);
819
- const imageModel2 = this.resolveImageModel(request);
820
- const references = [request.sourceImage, ...request.referenceImages ?? []];
821
- return this.runAll({
822
- format,
823
- imageModel: imageModel2,
824
- count: normalizeCount(request.count),
825
- prompt: request.prompt,
826
- size,
827
- build: () => {
828
- const tool = baseImageTool({
829
- size: size.size,
830
- quality: request.quality ?? DEFAULT_QUALITY,
831
- outputFormat: format,
832
- imageModel: imageModel2,
833
- action: "edit",
834
- partialImages: this.partialImages
835
- });
836
- if (request.mask) {
837
- tool["input_image_mask"] = { image_url: toDataUrl(request.mask) };
838
- }
839
- return buildPayload({
840
- model: this.orchestrationModel,
841
- prompt: request.prompt,
842
- references,
843
- tool
844
- });
845
- }
846
- });
828
+ return this.runAll(request, "edits", [request.sourceImage, ...request.referenceImages ?? []].slice(0, MAX_REFERENCE_IMAGES2));
847
829
  }
848
830
  resolveImageModel(request) {
849
831
  const model = (request.model ?? this.imageModel).trim();
@@ -855,116 +837,120 @@ var CodexImageProvider = class {
855
837
  }
856
838
  return model;
857
839
  }
858
- async runAll(args) {
859
- const images = [];
860
- for (let i = 0; i < args.count; i += 1) {
861
- images.push(await this.sendOnce(args.build(), args.format, args.prompt, args.size));
840
+ async runAll(request, endpoint, images) {
841
+ const format = normalizeOutputFormat(request.format);
842
+ const size = this.sizePolicy.resolveSize(request.size);
843
+ const imageModel2 = this.resolveImageModel(request);
844
+ const body = JSON.stringify(buildBody({ prompt: request.prompt, model: imageModel2, size, quality: request.quality ?? DEFAULT_QUALITY, images }));
845
+ const results = [];
846
+ for (let i = 0, count = normalizeCount(request.count); i < count; i += 1) {
847
+ results.push(await this.sendOnce(endpoint, body, format, request.prompt));
848
+ }
849
+ return { provider: this.name, imageModel: imageModel2, images: results };
850
+ }
851
+ async sendOnce(endpoint, body, format, prompt) {
852
+ let accessToken = await this.tokens.getValidAccessToken();
853
+ let response = await this.post(endpoint, accessToken, body);
854
+ if (response.status === 401) {
855
+ await response.body?.cancel();
856
+ accessToken = await this.tokens.recoverFromUnauthorized(accessToken);
857
+ response = await this.post(endpoint, accessToken, body);
858
+ }
859
+ const requestId = response.headers.get("x-codex-imagegen-request-id") ?? response.headers.get("x-oai-request-id") ?? response.headers.get("x-request-id") ?? void 0;
860
+ if (!response.ok) {
861
+ const errorText = await response.text().catch(() => "");
862
+ const kind = classifyImageError(errorText);
863
+ const detail = readErrorMessage(errorText);
864
+ const retryAfter = Number(response.headers.get("retry-after"));
865
+ throw new ProviderError({
866
+ kind,
867
+ message: detail ? `Codex image request returned ${response.status} (${kind}): ${detail}` : `Codex image request returned ${response.status} (${kind}).`,
868
+ statusCode: response.status,
869
+ requestId,
870
+ ...Number.isFinite(retryAfter) && retryAfter > 0 ? { retryAfterSeconds: retryAfter } : {}
871
+ });
872
+ }
873
+ const payload = record(await response.json().catch(() => null));
874
+ const item = Array.isArray(payload["data"]) ? record(payload["data"][0]) : {};
875
+ if (typeof item["b64_json"] !== "string" || !item["b64_json"]) {
876
+ throw new ProviderError({ kind: "no_image", message: "Codex image request returned no image data.", requestId });
862
877
  }
863
- return { provider: this.name, imageModel: args.imageModel, images };
878
+ let data = Buffer.from(item["b64_json"], "base64");
879
+ let width;
880
+ let height;
881
+ try {
882
+ const metadata = await sharp(data).metadata();
883
+ if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? ""))
884
+ throw new Error();
885
+ await sharp(data).stats();
886
+ if (metadata.format !== format)
887
+ data = await sharp(data).toFormat(format).toBuffer();
888
+ width = metadata.width;
889
+ height = metadata.height;
890
+ } catch {
891
+ throw new ProviderError({ kind: "malformed", message: "Codex returned an invalid raster image.", requestId });
892
+ }
893
+ return {
894
+ data,
895
+ mimeType: `image/${format}`,
896
+ width,
897
+ height,
898
+ prompt,
899
+ responseId: typeof item["generation_id"] === "string" ? item["generation_id"] : void 0,
900
+ usage: {
901
+ requestId,
902
+ imageGeneration: payload["usage"],
903
+ // What the backend actually rendered; it may override requested size and quality.
904
+ served: { size: payload["size"], quality: payload["quality"], background: payload["background"] },
905
+ planLimits: readPlanLimits(response.headers)
906
+ }
907
+ };
864
908
  }
865
- async sendOnce(payload, format, prompt, size) {
866
- const accessToken = await this.tokens.getValidAccessToken();
867
- const accountId = extractAccountId(accessToken);
909
+ async post(endpoint, accessToken, body) {
868
910
  const controller = new AbortController();
869
911
  const timer = setTimeout(() => controller.abort(), this.timeoutSeconds * 1e3);
870
- let response;
871
912
  try {
872
- response = await this.fetchImpl(RESPONSES_ENDPOINT, {
913
+ return await this.fetchImpl(`${IMAGES_ENDPOINT}/${endpoint}`, {
873
914
  method: "POST",
874
915
  headers: {
875
916
  authorization: `Bearer ${accessToken}`,
876
- "chatgpt-account-id": accountId,
877
- "OpenAI-Beta": "responses=experimental",
917
+ "chatgpt-account-id": extractAccountId(accessToken),
878
918
  originator: "pi",
879
919
  "user-agent": USER_AGENT,
880
- accept: "text/event-stream",
920
+ accept: "application/json",
881
921
  "content-type": "application/json"
882
922
  },
883
- body: JSON.stringify(payload),
923
+ body,
884
924
  signal: controller.signal
885
925
  });
886
926
  } finally {
887
927
  clearTimeout(timer);
888
928
  }
889
- const requestId = response.headers.get("x-request-id") ?? response.headers.get("openai-request-id") ?? response.headers.get("request-id") ?? void 0;
890
- if (!response.ok || !response.body) {
891
- const body = await response.text().catch(() => "");
892
- const kind = classifyImageError(body);
893
- const detail = readErrorMessage(body);
894
- throw new ProviderError({
895
- kind,
896
- message: detail ? `Codex image request returned ${response.status} (${kind}): ${detail}` : `Codex image request returned ${response.status} (${kind}).`,
897
- statusCode: response.status,
898
- requestId
899
- });
900
- }
901
- const lines = await readSseLines(response.body);
902
- let parsed;
903
- try {
904
- parsed = parseImageStreamEvents(lines);
905
- } catch (error) {
906
- throw toProviderError(error, requestId);
907
- }
908
- return {
909
- data: parsed.data,
910
- mimeType: `image/${format}`,
911
- width: size.width,
912
- height: size.height,
913
- prompt,
914
- responseId: parsed.responseId,
915
- callId: parsed.callId
916
- };
917
929
  }
918
930
  };
919
- async function readSseLines(body) {
920
- const reader = body.getReader();
921
- const decoder = new TextDecoder();
922
- let buffer = "";
923
- const lines = [];
924
- for (; ; ) {
925
- const { done, value } = await reader.read();
926
- if (done)
927
- break;
928
- buffer += decoder.decode(value, { stream: true });
929
- let index = buffer.indexOf("\n");
930
- while (index >= 0) {
931
- const line = buffer.slice(0, index);
932
- buffer = buffer.slice(index + 1);
933
- if (line.startsWith("data:"))
934
- lines.push(line);
935
- index = buffer.indexOf("\n");
936
- }
937
- }
938
- if (buffer.startsWith("data:"))
939
- lines.push(buffer);
940
- return lines;
941
- }
942
- function readErrorMessage(body) {
943
- if (!body.trim())
944
- return null;
945
- try {
946
- const parsed = JSON.parse(body);
947
- if (typeof parsed === "object" && parsed !== null) {
948
- const record = parsed;
949
- if (typeof record["message"] === "string")
950
- return record["message"];
951
- if (typeof record["detail"] === "string")
952
- return record["detail"];
953
- const error = record["error"];
954
- if (typeof error === "object" && error !== null && typeof error["message"] === "string") {
955
- return error["message"];
956
- }
957
- }
958
- } catch {
959
- return body.slice(0, 500);
960
- }
961
- return null;
962
- }
963
- function toProviderError(error, requestId) {
964
- const message = error instanceof Error ? error.message : String(error);
965
- const separator = message.indexOf(":");
966
- const kind = separator > 0 ? message.slice(0, separator) : "api_error";
967
- return new ProviderError({ kind, message, requestId });
931
+ function record(value) {
932
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
933
+ }
934
+ function readPlanLimits(headers) {
935
+ const number = (name) => {
936
+ const value = headers.get(`x-codex-${name}`);
937
+ const parsed = value === null ? Number.NaN : Number(value);
938
+ return Number.isFinite(parsed) ? parsed : void 0;
939
+ };
940
+ const window = (name) => {
941
+ const usedPercent = number(`${name}-used-percent`);
942
+ const windowMinutes = number(`${name}-window-minutes`);
943
+ if (usedPercent === void 0 || !windowMinutes)
944
+ return void 0;
945
+ return { usedPercent, windowMinutes, resetAt: number(`${name}-reset-at`) };
946
+ };
947
+ const limits = {
948
+ planType: headers.get("x-codex-plan-type") ?? void 0,
949
+ activeLimit: headers.get("x-codex-active-limit") ?? void 0,
950
+ primary: window("primary"),
951
+ secondary: window("secondary")
952
+ };
953
+ return Object.values(limits).some((value) => value !== void 0) ? limits : void 0;
968
954
  }
969
955
 
970
956
  // ../../packages/providers-codex/dist/codex-entry.js
@@ -1007,13 +993,876 @@ function createCodexEntry() {
1007
993
  };
1008
994
  }
1009
995
 
996
+ // ../../packages/providers-google/dist/gemini-client.js
997
+ import sharp2 from "sharp";
998
+
999
+ // ../../packages/providers-google/dist/image-size.js
1000
+ var DEFAULT_IMAGE_MODEL2 = "gemini-nano-banana-2.1";
1001
+ var native = {
1002
+ "1:1": [1024, 1024],
1003
+ "1:4": [512, 2048],
1004
+ "1:8": [384, 3072],
1005
+ "2:3": [848, 1264],
1006
+ "3:2": [1264, 848],
1007
+ "3:4": [896, 1200],
1008
+ "4:1": [2048, 512],
1009
+ "4:3": [1200, 896],
1010
+ "4:5": [928, 1152],
1011
+ "5:4": [1152, 928],
1012
+ "8:1": [3072, 384],
1013
+ "9:16": [768, 1376],
1014
+ "16:9": [1376, 768],
1015
+ "21:9": [1584, 672]
1016
+ };
1017
+ var legacy = {
1018
+ "1:1": [1024, 1024],
1019
+ "2:3": [832, 1248],
1020
+ "3:2": [1248, 832],
1021
+ "3:4": [864, 1184],
1022
+ "4:3": [1184, 864],
1023
+ "4:5": [896, 1152],
1024
+ "5:4": [1152, 896],
1025
+ "9:16": [768, 1344],
1026
+ "16:9": [1344, 768],
1027
+ "21:9": [1536, 672]
1028
+ };
1029
+ function resolveGeminiSize(size, model = DEFAULT_IMAGE_MODEL2) {
1030
+ const value = (size ?? "auto").trim().toLowerCase();
1031
+ const table = model === "gemini-2.5-flash-image" ? legacy : native;
1032
+ const restricted = model === "gemini-3-pro-image" || model === "nano-banana-pro-preview";
1033
+ const ratios = Object.keys(table).filter((r) => !restricted || !["1:4", "4:1", "1:8", "8:1"].includes(r));
1034
+ const presets = {
1035
+ square: "1:1",
1036
+ portrait: "2:3",
1037
+ landscape: "3:2",
1038
+ wide: "16:9",
1039
+ widescreen: "16:9"
1040
+ };
1041
+ const normalized = presets[value] ?? value;
1042
+ const pixels = /^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/.exec(normalized);
1043
+ const ratio = /^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)$/.exec(normalized);
1044
+ const auto = normalized === "auto" || normalized === "";
1045
+ const width = Number(pixels?.[1] ?? ratio?.[1] ?? (auto ? 1 : normalized));
1046
+ const height = Number(pixels?.[2] ?? ratio?.[2] ?? 1);
1047
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
1048
+ throw new ProviderError({
1049
+ kind: "usage",
1050
+ message: "Invalid Gemini size. Use WIDTHxHEIGHT, an aspect ratio, preset, or auto."
1051
+ });
1052
+ }
1053
+ const aspect = width / height;
1054
+ const selected = ratios.sort((a, b) => {
1055
+ const number = (r) => {
1056
+ const [x, y] = r.split(":").map(Number);
1057
+ return x / y;
1058
+ };
1059
+ return Math.abs(Math.log(number(a) / aspect)) - Math.abs(Math.log(number(b) / aspect));
1060
+ })[0];
1061
+ const scales = model === "gemini-2.5-flash-image" || model === "gemini-3.1-flash-lite-image" ? [1] : model === "gemini-3.1-flash-image" ? [0.5, 1, 2, 4] : [1, 2, 4];
1062
+ const candidates = scales.map((scale) => ({
1063
+ width: table[selected][0] * scale,
1064
+ height: table[selected][1] * scale,
1065
+ scale
1066
+ }));
1067
+ const chosen = pixels ? candidates.sort((a, b) => (a.width - width) ** 2 + (a.height - height) ** 2 - ((b.width - width) ** 2 + (b.height - height) ** 2) || a.scale - b.scale)[0] : candidates.find((c) => c.scale === 1);
1068
+ return {
1069
+ ...chosen,
1070
+ size: `${chosen.width}x${chosen.height}`,
1071
+ pixels: chosen.width * chosen.height,
1072
+ aspectRatio: auto ? void 0 : selected,
1073
+ imageSize: auto || model === "gemini-2.5-flash-image" ? void 0 : chosen.scale === 0.5 ? "512" : `${chosen.scale}K`
1074
+ };
1075
+ }
1076
+
1077
+ // ../../packages/providers-google/dist/gemini-client.js
1078
+ var error = (kind, message, statusCode) => new ProviderError({ kind, message, statusCode });
1079
+ var GeminiImageProvider = class {
1080
+ getKey;
1081
+ options;
1082
+ name = "gemini";
1083
+ sizePolicy = {
1084
+ resolveSize: (size) => resolveGeminiSize(size, this.options.imageModel)
1085
+ };
1086
+ constructor(getKey, options = {}) {
1087
+ this.getKey = getKey;
1088
+ this.options = options;
1089
+ }
1090
+ generate(request) {
1091
+ return this.run(request, request.referenceImages ?? []);
1092
+ }
1093
+ edit(request) {
1094
+ if (request.mask)
1095
+ throw error("usage", "Gemini does not support --mask. Describe the region to edit in the prompt.");
1096
+ return this.run(request, [
1097
+ request.sourceImage,
1098
+ ...request.referenceImages ?? []
1099
+ ]);
1100
+ }
1101
+ async run(request, inputs) {
1102
+ const model = (request.model ?? this.options.imageModel ?? process.env["IMAGEGEN_GEMINI_IMAGE_MODEL"] ?? DEFAULT_IMAGE_MODEL2).trim();
1103
+ if (!model)
1104
+ throw error("usage", "Image model must not be blank.");
1105
+ if (request.quality && request.quality.trim().toLowerCase() !== "auto")
1106
+ throw error("usage", "Gemini supports only --quality auto. Use --size to select native resolution.");
1107
+ if ((request.referenceImages?.length ?? 0) > MAX_REFERENCE_IMAGES)
1108
+ throw error("usage", "At most four reference images are allowed.");
1109
+ if (model === "gemini-2.5-flash-image" && inputs.length > 3)
1110
+ throw error("usage", "gemini-2.5-flash-image supports up to three input images in this tool, including the edit source.");
1111
+ if (inputs.some((i) => !["image/png", "image/jpeg", "image/webp"].includes(i.contentType)))
1112
+ throw error("usage", "Gemini inputs must be PNG, JPEG, or WebP.");
1113
+ const count = request.count ?? 1;
1114
+ if (!Number.isInteger(count) || count < 1 || count > 4)
1115
+ throw error("usage", "Image count must be from 1 to 4.");
1116
+ if (request.format && !["png", "jpeg", "jpg", "webp"].includes(request.format.trim().toLowerCase()))
1117
+ throw error("usage", "Output format must be png, jpeg, or webp.");
1118
+ const format = normalizeOutputFormat(request.format);
1119
+ const size = resolveGeminiSize(request.size, model);
1120
+ const payload = {
1121
+ model,
1122
+ store: false,
1123
+ stream: false,
1124
+ input: [
1125
+ { type: "text", text: request.prompt },
1126
+ ...inputs.map((i) => ({
1127
+ type: "image",
1128
+ mime_type: i.contentType,
1129
+ data: Buffer.from(i.data).toString("base64")
1130
+ }))
1131
+ ],
1132
+ // The live image endpoint rejects delivery even though the reference lists it.
1133
+ // Omit it and consume the default base64 response.
1134
+ response_format: {
1135
+ type: "image",
1136
+ ...format === "jpeg" ? { mime_type: "image/jpeg" } : {},
1137
+ ...size.aspectRatio ? { aspect_ratio: size.aspectRatio } : {},
1138
+ ...size.imageSize ? { image_size: size.imageSize } : {}
1139
+ }
1140
+ };
1141
+ if (Buffer.byteLength(JSON.stringify(payload)) > 2e7)
1142
+ throw error("usage", "Gemini inline request exceeds 20 MB. Use smaller reference images.");
1143
+ let key;
1144
+ try {
1145
+ key = await this.getKey();
1146
+ } catch {
1147
+ throw error("auth", "Could not load Gemini credentials. Run imagegen auth gemini login.");
1148
+ }
1149
+ const images = [];
1150
+ for (let i = 0; i < count; i++)
1151
+ images.push(await this.send(payload, key, format, request.prompt));
1152
+ return { provider: this.name, imageModel: model, images };
1153
+ }
1154
+ async send(payload, key, format, prompt) {
1155
+ const controller = new AbortController();
1156
+ const timer = setTimeout(() => controller.abort(), (this.options.timeoutSeconds ?? 180) * 1e3);
1157
+ try {
1158
+ const response = await (this.options.fetchImpl ?? fetch)("https://generativelanguage.googleapis.com/v1beta/interactions", {
1159
+ method: "POST",
1160
+ headers: {
1161
+ "content-type": "application/json",
1162
+ "x-goog-api-key": key
1163
+ },
1164
+ body: JSON.stringify(payload),
1165
+ signal: controller.signal
1166
+ });
1167
+ if (!response.ok) {
1168
+ const status = response.status;
1169
+ const failure2 = await response.json().catch(() => null);
1170
+ const invalidKey = Array.isArray(failure2?.error?.details) && failure2.error.details.some((detail) => detail.reason === "API_KEY_INVALID" || detail.reason === "API_KEY_EXPIRED");
1171
+ throw error(invalidKey || status === 401 || status === 403 ? "auth" : status === 429 ? "rate_limit" : status >= 500 ? "server" : "usage", invalidKey || status === 401 || status === 403 ? "Gemini rejected the API key or access. Check project access and run imagegen auth gemini login." : `Gemini returned HTTP ${status}. Check model access, request options, and project quota.`, status);
1172
+ }
1173
+ const body = await response.json().catch(() => {
1174
+ throw error("malformed", "Gemini returned invalid JSON.");
1175
+ });
1176
+ if (!body || typeof body !== "object")
1177
+ throw error("malformed", "Gemini returned an invalid interaction.");
1178
+ if (body.status !== "completed")
1179
+ throw error(body.status === "failed" ? "blocked" : "incomplete", "Gemini did not complete the image request. Check the prompt and model access.");
1180
+ if (body.steps !== void 0 && !Array.isArray(body.steps))
1181
+ throw error("malformed", "Gemini returned invalid output steps.");
1182
+ if ((body.steps ?? []).some((step) => !step || typeof step !== "object" || step.content !== void 0 && !Array.isArray(step.content))) {
1183
+ throw error("malformed", "Gemini returned invalid output content.");
1184
+ }
1185
+ const parts = (body.steps ?? []).filter((s) => s.type === "model_output").flatMap((s) => s.content ?? []).filter((p) => p && p.type === "image" && !p.thought);
1186
+ const image = parts.at(-1);
1187
+ if (!image)
1188
+ throw error("no_image", "Gemini returned no final image. Refine the image request and try again.");
1189
+ if (typeof image.data !== "string" || !image.data || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.data) || image.data.length % 4 !== 0)
1190
+ throw error("malformed", "Gemini returned invalid inline image data.");
1191
+ const original = Buffer.from(image.data, "base64");
1192
+ const metadata = await sharp2(original).metadata().catch(() => {
1193
+ throw error("malformed", "Gemini returned undecodable image bytes.");
1194
+ });
1195
+ if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || image.mime_type !== `image/${metadata.format}`)
1196
+ throw error("malformed", "Gemini returned an invalid image or inconsistent MIME type.");
1197
+ const data = metadata.format === format ? original : await sharp2(original).toFormat(format).toBuffer();
1198
+ return {
1199
+ data,
1200
+ mimeType: `image/${format}`,
1201
+ width: metadata.width,
1202
+ height: metadata.height,
1203
+ prompt,
1204
+ responseId: typeof body.id === "string" ? body.id : void 0
1205
+ };
1206
+ } catch (e) {
1207
+ if (controller.signal.aborted)
1208
+ throw error("timeout", "Gemini image request timed out. Try again when connectivity is restored.");
1209
+ if (e instanceof ProviderError)
1210
+ throw e;
1211
+ throw error("transport", "Gemini request failed while receiving or decoding the image. Check connectivity and try again.");
1212
+ } finally {
1213
+ clearTimeout(timer);
1214
+ }
1215
+ }
1216
+ };
1217
+
1218
+ // ../../packages/providers-google/dist/auth.js
1219
+ function validateKey(value) {
1220
+ const key = value.trim();
1221
+ if (!key || /\s/.test(key))
1222
+ throw new ProviderError({
1223
+ kind: "auth",
1224
+ message: "Enter a non-empty Gemini API key without whitespace."
1225
+ });
1226
+ return key;
1227
+ }
1228
+ async function geminiStore() {
1229
+ return resolveCredentialStore();
1230
+ }
1231
+ async function storedKey(store) {
1232
+ const value = await store.load("gemini");
1233
+ if (!value || typeof value.apiKey !== "string")
1234
+ throw new ProviderError({
1235
+ kind: "auth",
1236
+ message: "Run imagegen auth gemini login to configure an API key."
1237
+ });
1238
+ return validateKey(value.apiKey);
1239
+ }
1240
+
1241
+ // ../../packages/providers-google/dist/index.js
1242
+ function createGeminiEntry() {
1243
+ return {
1244
+ provider: new GeminiImageProvider(async () => storedKey(await geminiStore())),
1245
+ authStatus: async () => {
1246
+ const store = await geminiStore();
1247
+ let configured = false;
1248
+ try {
1249
+ await storedKey(store);
1250
+ configured = true;
1251
+ } catch {
1252
+ }
1253
+ return {
1254
+ name: "gemini",
1255
+ configured,
1256
+ detail: configured ? void 0 : "Run imagegen auth gemini login to configure an API key."
1257
+ };
1258
+ }
1259
+ };
1260
+ }
1261
+
1262
+ // ../../packages/providers-configurable/dist/profiles.js
1263
+ import { readFile, mkdir as mkdir2, writeFile, rename, rm as rm2 } from "node:fs/promises";
1264
+ import { dirname as dirname2, join as join3 } from "node:path";
1265
+ import { randomUUID } from "node:crypto";
1266
+ var usage = (message) => new ProviderError({ kind: "usage", message });
1267
+ var profileStore = () => resolveCredentialStore();
1268
+ function validateApiKey(value) {
1269
+ const key = value.trim();
1270
+ if (!key || /\s/.test(key))
1271
+ throw new ProviderError({
1272
+ kind: "auth",
1273
+ message: "Enter a non-empty API key without whitespace."
1274
+ });
1275
+ return key;
1276
+ }
1277
+ function validateProfile(value) {
1278
+ if (!value || typeof value !== "object" || Array.isArray(value))
1279
+ throw usage("Invalid endpoint profile.");
1280
+ const p = value;
1281
+ if (Object.keys(p).some((k) => !["name", "api", "baseUrl", "model", "auth"].includes(k)))
1282
+ throw usage("Endpoint profiles accept only name, api, baseUrl, model, and auth; store keys through auth login.");
1283
+ if (typeof p.name !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(p.name) || ["codex", "gemini"].includes(p.name))
1284
+ throw usage("Endpoint name must use lowercase letters, digits, and hyphens, and cannot be codex or gemini.");
1285
+ if (p.api !== "openrouter" && p.api !== "openai-images")
1286
+ throw usage("API format must be openrouter or openai-images.");
1287
+ if (p.auth !== void 0 && p.auth !== "bearer" && p.auth !== "none")
1288
+ throw usage("Auth must be bearer or none.");
1289
+ const raw = p.baseUrl ?? (p.api === "openrouter" ? "https://openrouter.ai/api/v1" : void 0);
1290
+ let url;
1291
+ try {
1292
+ if (typeof raw !== "string")
1293
+ throw new Error();
1294
+ url = new URL(raw);
1295
+ } catch {
1296
+ throw usage("Supply a valid --base-url including the API prefix.");
1297
+ }
1298
+ const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || url.hostname === "127.0.0.1";
1299
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
1300
+ throw usage("Base URL must use HTTPS (or loopback HTTP), without credentials, query, or fragment.");
1301
+ if (p.model !== void 0 && (typeof p.model !== "string" || !p.model.trim()))
1302
+ throw usage("Default model must not be blank.");
1303
+ return {
1304
+ name: p.name,
1305
+ api: p.api,
1306
+ baseUrl: url.toString().replace(/\/+$/, ""),
1307
+ auth: p.auth ?? "bearer",
1308
+ ...typeof p.model === "string" ? { model: p.model.trim() } : {}
1309
+ };
1310
+ }
1311
+ var ProfileRepository = class {
1312
+ path;
1313
+ credentials;
1314
+ constructor(path = join3(dirname2(defaultCredentialFilePath()), "providers.json"), credentials = profileStore) {
1315
+ this.path = path;
1316
+ this.credentials = credentials;
1317
+ }
1318
+ async list() {
1319
+ let text;
1320
+ try {
1321
+ text = await readFile(this.path, "utf8");
1322
+ } catch (e) {
1323
+ if (e.code === "ENOENT")
1324
+ return [];
1325
+ throw new ProviderError({
1326
+ kind: "io",
1327
+ message: "Could not read endpoint configuration."
1328
+ });
1329
+ }
1330
+ let value;
1331
+ try {
1332
+ value = JSON.parse(text);
1333
+ } catch {
1334
+ throw usage("Endpoint configuration is invalid JSON. Repair providers.json before retrying.");
1335
+ }
1336
+ const doc = value;
1337
+ if (!doc || doc.version !== 1 || !Array.isArray(doc.profiles))
1338
+ throw usage("Endpoint configuration requires version 1 and a profiles array.");
1339
+ const profiles = doc.profiles.map(validateProfile);
1340
+ if (new Set(profiles.map((p) => p.name)).size !== profiles.length)
1341
+ throw usage("Endpoint configuration contains duplicate names.");
1342
+ return profiles;
1343
+ }
1344
+ async get(name) {
1345
+ const p = (await this.list()).find((p2) => p2.name === name);
1346
+ if (!p)
1347
+ throw usage("Unknown endpoint profile. Run imagegen providers to list providers.");
1348
+ return p;
1349
+ }
1350
+ async add(value) {
1351
+ const p = validateProfile(value);
1352
+ const profiles = await this.list();
1353
+ if (profiles.some((v) => v.name === p.name))
1354
+ throw usage("Endpoint already exists. Use providers update.");
1355
+ await this.save([...profiles, p]);
1356
+ return p;
1357
+ }
1358
+ async update(name, patch) {
1359
+ const profiles = await this.list();
1360
+ const old = profiles.find((p2) => p2.name === name);
1361
+ if (!old)
1362
+ throw usage("Unknown endpoint profile.");
1363
+ const p = validateProfile({ ...old, ...patch, name });
1364
+ if (new URL(old.baseUrl).origin !== new URL(p.baseUrl).origin || old.auth !== p.auth)
1365
+ await (await this.credentials()).clear(name);
1366
+ await this.save(profiles.map((v) => v.name === name ? p : v));
1367
+ return p;
1368
+ }
1369
+ async remove(name) {
1370
+ const profiles = await this.list();
1371
+ if (!profiles.some((p) => p.name === name))
1372
+ throw usage("Unknown endpoint profile.");
1373
+ await (await this.credentials()).clear(name);
1374
+ await this.save(profiles.filter((p) => p.name !== name));
1375
+ }
1376
+ async save(profiles) {
1377
+ const temporary = `${this.path}.${randomUUID()}.tmp`;
1378
+ try {
1379
+ await mkdir2(dirname2(this.path), { recursive: true });
1380
+ await writeFile(temporary, JSON.stringify({ version: 1, profiles }, null, 2) + "\n", { mode: 384 });
1381
+ await rename(temporary, this.path);
1382
+ } catch {
1383
+ throw new ProviderError({
1384
+ kind: "io",
1385
+ message: "Could not save endpoint configuration."
1386
+ });
1387
+ } finally {
1388
+ await rm2(temporary, { force: true }).catch(() => {
1389
+ });
1390
+ }
1391
+ }
1392
+ };
1393
+ async function profileKey(profile, store) {
1394
+ if (profile.auth === "none")
1395
+ return void 0;
1396
+ try {
1397
+ const record3 = await store.load(profile.name);
1398
+ if (record3 && typeof record3.apiKey === "string")
1399
+ return validateApiKey(record3.apiKey);
1400
+ } catch {
1401
+ }
1402
+ throw new ProviderError({
1403
+ kind: "auth",
1404
+ message: `Run imagegen auth ${profile.name} login to configure an API key.`
1405
+ });
1406
+ }
1407
+
1408
+ // ../../packages/providers-configurable/dist/provider.js
1409
+ import sharp3 from "sharp";
1410
+
1411
+ // ../../packages/providers-configurable/dist/image-size.js
1412
+ function imageSize(value, api) {
1413
+ const size = (value ?? "auto").trim().toLowerCase();
1414
+ if (!size || size === "auto")
1415
+ return {};
1416
+ const presets = {
1417
+ square: "1024x1024",
1418
+ portrait: "1024x1536",
1419
+ landscape: "1536x1024",
1420
+ wide: "1536x864",
1421
+ widescreen: "1536x864"
1422
+ };
1423
+ if (presets[size])
1424
+ return { size: presets[size] };
1425
+ const pixels = /^(\d+)\s*x\s*(\d+)$/.exec(size);
1426
+ if (pixels && Number.isSafeInteger(Number(pixels[1])) && Number.isSafeInteger(Number(pixels[2])) && Number(pixels[1]) > 0 && Number(pixels[2]) > 0)
1427
+ return { size: `${Number(pixels[1])}x${Number(pixels[2])}` };
1428
+ const ratio = /^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)$/.exec(size);
1429
+ if (api === "openrouter" && ratio && Number(ratio[1]) > 0 && Number(ratio[2]) > 0 && Number.isFinite(Number(ratio[1]) / Number(ratio[2])))
1430
+ return { aspect_ratio: `${Number(ratio[1])}:${Number(ratio[2])}` };
1431
+ throw usage(api === "openai-images" ? "This API format requires WIDTHxHEIGHT or a preset; arbitrary aspect-only sizes are unsupported." : "Use WIDTHxHEIGHT, an aspect ratio, preset, or auto.");
1432
+ }
1433
+
1434
+ // ../../packages/providers-configurable/dist/http.js
1435
+ var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
1436
+ var EndpointHttp = class {
1437
+ fetchImpl;
1438
+ timeoutSeconds;
1439
+ constructor(fetchImpl = fetch, timeoutSeconds = 180) {
1440
+ this.fetchImpl = fetchImpl;
1441
+ this.timeoutSeconds = timeoutSeconds;
1442
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)
1443
+ throw new ProviderError({
1444
+ kind: "configuration",
1445
+ message: "Request timeout must be positive and finite."
1446
+ });
1447
+ }
1448
+ async run(action) {
1449
+ const controller = new AbortController();
1450
+ const timer = setTimeout(() => controller.abort(), this.timeoutSeconds * 1e3);
1451
+ const send = async (url, init = {}, download = false) => {
1452
+ const response = await this.fetchImpl(url, {
1453
+ ...init,
1454
+ redirect: download ? "follow" : "error",
1455
+ signal: controller.signal
1456
+ });
1457
+ if (!response.ok) {
1458
+ const status = response.status;
1459
+ const failure2 = record2(await response.json().catch(() => null));
1460
+ const detail = record2(failure2.error);
1461
+ const upstream = record2(detail.metadata).provider_name;
1462
+ const token = new Headers(init.headers).get("authorization")?.replace(/^Bearer\s+/i, "");
1463
+ const upstreamProvider = typeof upstream === "string" && /^[A-Za-z0-9][A-Za-z0-9 ._()-]{0,63}$/.test(upstream) && !(token && upstream.includes(token)) && !/(?:sk-|AIza)/.test(upstream) ? upstream : void 0;
1464
+ const retryHeader = response.headers.get("retry-after");
1465
+ const retryAfterSeconds = retryHeader && /^\d+$/.test(retryHeader) && Number.isSafeInteger(Number(retryHeader)) ? Number(retryHeader) : void 0;
1466
+ const upstreamQuota = status === 429 && upstreamProvider && typeof detail.message === "string" && /quota|resource.exhausted/i.test(detail.message);
1467
+ throw new ProviderError({
1468
+ kind: download ? "download" : status === 401 || status === 403 ? "auth" : status === 429 ? "rate_limit" : status >= 500 ? "server" : "usage",
1469
+ statusCode: status,
1470
+ upstreamProvider,
1471
+ retryAfterSeconds,
1472
+ message: download ? `Image download failed (HTTP ${status}).` : status === 401 || status === 403 ? "Endpoint authentication failed. Check the saved key and model access." : upstreamQuota ? `Upstream ${upstreamProvider} quota is exhausted (HTTP 429). Gateway credits may still be available. Select a different model or upstream, or resolve that upstream's quota.` : status === 429 && upstreamProvider ? `Upstream ${upstreamProvider} rate limit reached (HTTP 429). Wait for its limit to reset or select another model/upstream.` : status === 429 ? "Endpoint quota or rate limit reached (HTTP 429). Check gateway and upstream limits before retrying." : `Endpoint returned HTTP ${status}. Check the API format, model ID, and supported image options.`
1473
+ });
1474
+ }
1475
+ return response;
1476
+ };
1477
+ try {
1478
+ return await action({
1479
+ json: async (url, init) => {
1480
+ const response = await send(url, init);
1481
+ try {
1482
+ const value = await response.json();
1483
+ if (!value || typeof value !== "object" || Array.isArray(value))
1484
+ throw new Error();
1485
+ return record2(value);
1486
+ } catch {
1487
+ throw new ProviderError({
1488
+ kind: "malformed",
1489
+ message: "Endpoint returned an invalid JSON response."
1490
+ });
1491
+ }
1492
+ },
1493
+ bytes: async (url) => new Uint8Array(await (await send(url, {}, true)).arrayBuffer())
1494
+ });
1495
+ } catch (e) {
1496
+ if (controller.signal.aborted)
1497
+ throw new ProviderError({
1498
+ kind: "timeout",
1499
+ message: "Endpoint request timed out; no automatic retry was attempted."
1500
+ });
1501
+ if (e instanceof ProviderError)
1502
+ throw e;
1503
+ throw new ProviderError({
1504
+ kind: "transport",
1505
+ message: "Endpoint request failed. Check connectivity and the configured URL."
1506
+ });
1507
+ } finally {
1508
+ clearTimeout(timer);
1509
+ }
1510
+ }
1511
+ };
1512
+
1513
+ // ../../packages/providers-configurable/dist/openrouter.js
1514
+ function openRouterRequest(model, request, images) {
1515
+ return {
1516
+ path: "images",
1517
+ body: JSON.stringify({
1518
+ model,
1519
+ prompt: request.prompt,
1520
+ n: 1,
1521
+ ...imageSize(request.size, "openrouter"),
1522
+ ...request.quality && request.quality !== "auto" ? { quality: request.quality } : {},
1523
+ ...request.format ? { output_format: request.format === "jpg" ? "jpeg" : request.format } : {},
1524
+ ...images.length ? {
1525
+ input_references: images.map((image) => ({
1526
+ type: "image_url",
1527
+ image_url: {
1528
+ url: `data:${image.contentType};base64,${Buffer.from(image.data).toString("base64")}`
1529
+ }
1530
+ }))
1531
+ } : {}
1532
+ }),
1533
+ json: true
1534
+ };
1535
+ }
1536
+
1537
+ // ../../packages/providers-configurable/dist/openai-images.js
1538
+ function openAIRequest(model, request, images, mask) {
1539
+ const fields = {
1540
+ model,
1541
+ prompt: request.prompt,
1542
+ n: 1,
1543
+ ...imageSize(request.size, "openai-images"),
1544
+ ...request.quality && request.quality !== "auto" ? { quality: request.quality } : {},
1545
+ ...request.format ? { output_format: request.format === "jpg" ? "jpeg" : request.format } : {}
1546
+ };
1547
+ if (!images.length)
1548
+ return {
1549
+ path: "images/generations",
1550
+ body: JSON.stringify(fields),
1551
+ json: true
1552
+ };
1553
+ const body = new FormData();
1554
+ for (const [key, value] of Object.entries(fields))
1555
+ body.set(key, String(value));
1556
+ const add = (name, image, index) => body.append(name, new Blob([new Uint8Array(image.data)], { type: image.contentType }), image.filename ?? `image-${index}.png`);
1557
+ images.forEach((image, i) => add(images.length === 1 ? "image" : "image[]", image, i));
1558
+ if (mask)
1559
+ add("mask", mask, 0);
1560
+ return { path: "images/edits", body, json: false };
1561
+ }
1562
+
1563
+ // ../../packages/providers-configurable/dist/provider.js
1564
+ var ConfigurableImageProvider = class {
1565
+ getKey;
1566
+ name;
1567
+ profile;
1568
+ http;
1569
+ constructor(profile, getKey, options = {}) {
1570
+ this.getKey = getKey;
1571
+ this.profile = validateProfile(profile);
1572
+ this.name = profile.name;
1573
+ this.http = new EndpointHttp(options.fetchImpl, options.timeoutSeconds);
1574
+ }
1575
+ sizePolicy = {
1576
+ resolveSize: (size) => {
1577
+ const fields = imageSize(size, this.profile.api);
1578
+ if (!fields.size)
1579
+ return { size: fields.aspect_ratio ?? "auto" };
1580
+ const [width, height] = fields.size.split("x").map(Number);
1581
+ return {
1582
+ width,
1583
+ height,
1584
+ pixels: width * height,
1585
+ size: fields.size ?? fields.aspect_ratio ?? "auto"
1586
+ };
1587
+ }
1588
+ };
1589
+ generate(request) {
1590
+ return this.run(request, request.referenceImages ?? []);
1591
+ }
1592
+ edit(request) {
1593
+ return this.run(request, [request.sourceImage, ...request.referenceImages ?? []], request.mask);
1594
+ }
1595
+ async headers() {
1596
+ if (this.profile.auth === "none")
1597
+ return {};
1598
+ try {
1599
+ const key = await this.getKey();
1600
+ if (!key)
1601
+ throw new Error();
1602
+ return { authorization: `Bearer ${validateApiKey(key)}` };
1603
+ } catch {
1604
+ throw new ProviderError({
1605
+ kind: "auth",
1606
+ message: `Could not load endpoint credentials. Run imagegen auth ${this.name} login.`
1607
+ });
1608
+ }
1609
+ }
1610
+ async models() {
1611
+ const headers = await this.headers();
1612
+ return this.http.run(async (http) => {
1613
+ const body = await http.json(`${this.profile.baseUrl}/${this.profile.api === "openrouter" ? "images/models" : "models"}`, { headers });
1614
+ if (!Array.isArray(body.data))
1615
+ throw new ProviderError({
1616
+ kind: "malformed",
1617
+ message: "Model discovery response must contain a data array."
1618
+ });
1619
+ return body.data.map((value) => {
1620
+ const model = record2(value);
1621
+ if (typeof model.id !== "string")
1622
+ throw new ProviderError({
1623
+ kind: "malformed",
1624
+ message: "Discovered model has no string ID."
1625
+ });
1626
+ const architecture = record2(model.architecture);
1627
+ const supported = record2(model.supported_parameters);
1628
+ const strings = (v) => Array.isArray(v) && v.every((s) => typeof s === "string") ? v : void 0;
1629
+ return {
1630
+ id: model.id,
1631
+ ...typeof model.name === "string" ? { name: model.name } : {},
1632
+ capabilities: this.profile.api === "openrouter" ? "reported" : "unknown",
1633
+ ...this.profile.api === "openrouter" ? {
1634
+ supportedParameters: supported,
1635
+ inputModalities: strings(architecture.input_modalities),
1636
+ outputModalities: strings(architecture.output_modalities)
1637
+ } : {}
1638
+ };
1639
+ });
1640
+ });
1641
+ }
1642
+ async run(request, images, mask) {
1643
+ const model = (request.model ?? this.profile.model ?? "").trim();
1644
+ if (!model)
1645
+ throw usage("Supply --model or configure a default model with providers update.");
1646
+ if ((request.referenceImages?.length ?? 0) > 4)
1647
+ throw usage("At most four reference images are allowed.");
1648
+ const count = request.count ?? 1;
1649
+ if (!Number.isInteger(count) || count < 1 || count > 4)
1650
+ throw usage("Image count must be from 1 to 4.");
1651
+ if (mask && this.profile.api === "openrouter")
1652
+ throw usage("OpenRouter image API does not expose masks. Describe the edit region in the prompt.");
1653
+ for (const image of [...images, ...mask ? [mask] : []])
1654
+ if (!["image/png", "image/jpeg", "image/webp"].includes(image.contentType))
1655
+ throw usage("Input images must be PNG, JPEG, or WebP.");
1656
+ const format = (request.format ?? "png").trim().toLowerCase().replace(/^jpg$/, "jpeg");
1657
+ if (!["png", "jpeg", "webp"].includes(format))
1658
+ throw usage("Output format must be png, jpeg, or webp.");
1659
+ const quality = request.quality?.trim().toLowerCase();
1660
+ if (quality && !/^[a-z][a-z0-9_-]*$/.test(quality))
1661
+ throw usage("Quality must be a provider-supported quality name.");
1662
+ const normalized = {
1663
+ ...request,
1664
+ ...request.format ? { format } : {},
1665
+ quality
1666
+ };
1667
+ const payload = this.profile.api === "openrouter" ? openRouterRequest(model, normalized, images) : openAIRequest(model, normalized, images, mask);
1668
+ const headers = await this.headers();
1669
+ const results = [];
1670
+ for (let i = 0; i < count; i++)
1671
+ results.push(await this.http.run(async (http) => {
1672
+ const body = await http.json(`${this.profile.baseUrl}/${payload.path}`, {
1673
+ method: "POST",
1674
+ headers: {
1675
+ ...headers,
1676
+ ...payload.json ? { "content-type": "application/json" } : {}
1677
+ },
1678
+ body: payload.body
1679
+ });
1680
+ if (!Array.isArray(body.data) || body.data.length !== 1)
1681
+ throw new ProviderError({
1682
+ kind: "malformed",
1683
+ message: "Endpoint must return exactly one image for a single-image request."
1684
+ });
1685
+ const image = record2(body.data[0]);
1686
+ let data;
1687
+ if (typeof image.b64_json === "string") {
1688
+ if (!image.b64_json || image.b64_json.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.b64_json))
1689
+ throw new ProviderError({
1690
+ kind: "malformed",
1691
+ message: "Endpoint returned invalid base64 image data."
1692
+ });
1693
+ data = Buffer.from(image.b64_json, "base64");
1694
+ } else if (typeof image.url === "string") {
1695
+ let url;
1696
+ try {
1697
+ url = new URL(image.url);
1698
+ } catch {
1699
+ throw new ProviderError({
1700
+ kind: "malformed",
1701
+ message: "Endpoint returned an invalid image URL."
1702
+ });
1703
+ }
1704
+ if (headers.authorization && image.url.includes(headers.authorization.slice(7)) || url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
1705
+ throw new ProviderError({
1706
+ kind: "malformed",
1707
+ message: "Image download URL must use HTTPS or loopback HTTP without embedded credentials."
1708
+ });
1709
+ data = await http.bytes(url.toString());
1710
+ } else
1711
+ throw new ProviderError({
1712
+ kind: "no_image",
1713
+ message: "Endpoint returned no image bytes or download URL."
1714
+ });
1715
+ try {
1716
+ const metadata = await sharp3(data).metadata();
1717
+ if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || typeof image.media_type === "string" && image.media_type !== `image/${metadata.format}`)
1718
+ throw new Error();
1719
+ await sharp3(data).stats();
1720
+ if (metadata.format !== format)
1721
+ data = await sharp3(data).toFormat(format).toBuffer();
1722
+ return {
1723
+ data,
1724
+ width: metadata.width,
1725
+ height: metadata.height,
1726
+ mimeType: `image/${format}`,
1727
+ prompt: request.prompt,
1728
+ ...typeof body.id === "string" ? { responseId: body.id } : {}
1729
+ };
1730
+ } catch {
1731
+ throw new ProviderError({
1732
+ kind: "malformed",
1733
+ message: "Endpoint returned an invalid raster image or inconsistent image format."
1734
+ });
1735
+ }
1736
+ }));
1737
+ return { provider: this.name, imageModel: model, images: results };
1738
+ }
1739
+ };
1740
+ function createProfileEntry(profile) {
1741
+ return {
1742
+ provider: new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore())),
1743
+ authStatus: async () => {
1744
+ let configured = profile.auth === "none";
1745
+ if (!configured)
1746
+ try {
1747
+ await profileKey(profile, await profileStore());
1748
+ configured = true;
1749
+ } catch {
1750
+ }
1751
+ return {
1752
+ name: profile.name,
1753
+ configured,
1754
+ detail: `Run imagegen auth ${profile.name} login.`
1755
+ };
1756
+ }
1757
+ };
1758
+ }
1759
+
1760
+ // src/key-input.ts
1761
+ import { emitKeypressEvents } from "node:readline";
1762
+ var failure = (message) => new ProviderError({ kind: "auth", message });
1763
+ async function readApiKey(fromStdin, input = process.stdin, output = process.stderr, providerName = "gemini") {
1764
+ if (fromStdin) {
1765
+ let text = "";
1766
+ for await (const chunk of input) {
1767
+ text += chunk.toString();
1768
+ if (Buffer.byteLength(text) > 8192)
1769
+ throw failure("API key input is too long.");
1770
+ }
1771
+ return text;
1772
+ }
1773
+ if (!input.isTTY)
1774
+ throw failure(
1775
+ "API-key login requires a terminal, or explicit --key-stdin."
1776
+ );
1777
+ const terminal = input;
1778
+ const wasRaw = terminal.isRaw;
1779
+ const wasFlowing = input.readableFlowing === true;
1780
+ emitKeypressEvents(input);
1781
+ return new Promise((resolve4, reject) => {
1782
+ let text = "";
1783
+ const cleanup = () => {
1784
+ input.removeListener("keypress", onKey);
1785
+ input.removeListener("end", onEnd);
1786
+ input.removeListener("error", onError);
1787
+ terminal.setRawMode(wasRaw);
1788
+ if (!wasFlowing) input.pause();
1789
+ output.write("\n");
1790
+ };
1791
+ const onEnd = () => {
1792
+ cleanup();
1793
+ reject(failure("API-key login cancelled before a key was entered."));
1794
+ };
1795
+ const onError = () => {
1796
+ cleanup();
1797
+ reject(failure("Could not read the API key."));
1798
+ };
1799
+ const onKey = (value, key) => {
1800
+ if (key.ctrl && (key.name === "c" || key.name === "d")) {
1801
+ onEnd();
1802
+ return;
1803
+ }
1804
+ if (key.name === "return" || key.name === "enter") {
1805
+ cleanup();
1806
+ resolve4(text);
1807
+ return;
1808
+ }
1809
+ if (key.name === "backspace") {
1810
+ text = text.slice(0, -1);
1811
+ return;
1812
+ }
1813
+ if (value && !key.ctrl && !/[\x00-\x1f\x7f]/.test(value)) text += value;
1814
+ if (text.length > 8192) {
1815
+ cleanup();
1816
+ reject(failure("API key input is too long."));
1817
+ }
1818
+ };
1819
+ output.write(`${providerName} API key (hidden): `);
1820
+ input.on("keypress", onKey);
1821
+ input.once("end", onEnd);
1822
+ input.once("error", onError);
1823
+ terminal.setRawMode(true);
1824
+ input.resume();
1825
+ });
1826
+ }
1827
+
1010
1828
  // src/help.ts
1829
+ var PROFILE_HELP = `Named endpoint profiles:
1830
+ imagegen providers add <name> --api <openrouter|openai-images> [--base-url <url>]
1831
+ [--model <id>] [--auth <bearer|none>]
1832
+ imagegen providers show <name>
1833
+ imagegen providers update <name> [--api <format>] [--base-url <url>]
1834
+ [--model <id>] [--auth <bearer|none>]
1835
+ imagegen providers remove <name>
1836
+ imagegen auth <name> login [--key-stdin]
1837
+ imagegen auth <name> status [--show-path]
1838
+ imagegen auth <name> logout
1839
+ imagegen models --provider <name>
1840
+
1841
+ OpenRouter defaults to https://openrouter.ai/api/v1. openai-images requires
1842
+ --base-url including the API prefix (e.g. https://api.openai.com/v1).
1843
+ Keys use hidden terminal input or explicit --key-stdin and the existing vault/file
1844
+ store. --auth none supports endpoints without keys. Config is saved in providers.json
1845
+ alongside the credential file. Origin changes clear the saved key; removing a
1846
+ profile removes its credentials. Logout preserves profile configuration.
1847
+
1848
+ Select named profiles with --provider. Model precedence: --model > profile default;
1849
+ provide one if no default is saved. models explicitly discovers model metadata;
1850
+ OpenAI-compatible model listings do not establish image capabilities.
1851
+
1852
+ Supported protocols: OpenRouter dedicated Images API and OpenAI Images JSON /
1853
+ multipart endpoints, not arbitrary HTTP templates. Options vary by model.
1854
+ OpenRouter accepts aspect ratios; openai-images needs pixels or presets:
1855
+ square 1024x1024, portrait 1024x1536, landscape 1536x1024, wide 1536x864.
1856
+ Auto options are omitted. Native masks require openai-images.
1857
+ Outputs are measured and converted to PNG/JPEG/WebP without resizing. Count uses
1858
+ separate single-image requests without automatic retries or model substitution.
1859
+ `;
1011
1860
  var IMAGE_OPTIONS = `Image options (generate, edit):
1012
1861
  --prompt <text> Prompt text (required)
1013
1862
  --out <path|dir> Output file or directory (required)
1014
1863
  --provider <name> Repeatable. Defaults to the only configured provider;
1015
1864
  required when several are configured.
1016
- --model <id> Image model override (Codex default gpt-image-2.5-flare)
1865
+ --model <id> Image model override (defaults listed below)
1017
1866
  --reference <file> Reference image for continuity (repeatable, max 4)
1018
1867
  --size <size> auto, WIDTHxHEIGHT, aspect (16:9), or preset
1019
1868
  square, portrait, landscape, wide (validated per
@@ -1022,6 +1871,18 @@ var IMAGE_OPTIONS = `Image options (generate, edit):
1022
1871
  --format <format> png, jpeg, or webp (default png)
1023
1872
  --count <n> Number of images, 1-4 (default 1)
1024
1873
 
1874
+ Gemini provider:
1875
+ gemini-nano-banana-2.1 Default; generation, references, and independent edits
1876
+ gemini-3-pro-image Precision creative work and brand consistency
1877
+ gemini-3.1-flash-image Previous-generation Flash; also supports 512 resolution
1878
+ gemini-2.5-flash-image Legacy; up to three inputs including edit source
1879
+ --quality auto only; no --mask or guaranteed native alpha transparency.
1880
+ --size normalizes to model-native ratios and resolution tiers without resizing.
1881
+ auto keeps model defaults; aspect/preset requests use 1K. JSON reports actual pixels.
1882
+ PNG/JPEG/WebP output; format conversion happens locally when needed.
1883
+ Model precedence: --model > constructor imageModel > IMAGEGEN_GEMINI_IMAGE_MODEL
1884
+ > gemini-nano-banana-2.1. Calls are independent; use prior output as --source.
1885
+
1025
1886
  Supported Codex models (account availability is determined by the provider):
1026
1887
  gpt-image-2.5-flare Default; fast image generation and editing
1027
1888
  gpt-image-2.5-sunburst Higher-precision image generation and editing
@@ -1030,19 +1891,22 @@ Supported Codex models (account availability is determined by the provider):
1030
1891
  Use full model IDs; other provider-supported IDs may also be supplied.
1031
1892
  Model precedence: --model > constructor imageModel (library callers) >
1032
1893
  IMAGEGEN_CODEX_IMAGE_MODEL > gpt-image-2.5-flare.
1033
- IMAGEGEN_CODEX_MODEL configures orchestration separately; --model selects images.
1894
+ Codex may override the requested model, quality, and size; results report the
1895
+ returned dimensions. Codex ignores --mask; describe the region in the prompt.
1896
+ IMAGEGEN_USAGE_LOG=<file> appends one prompt-free JSON usage line per result.
1034
1897
 
1035
1898
  Codex quality: auto (default), low, medium, high; Flare/Sunburst also support
1036
1899
  xhigh and max. gpt-image-2 rejects xhigh/max.
1037
1900
  `;
1038
1901
  var GENERATE_EXAMPLES = `Examples:
1902
+ imagegen generate --provider gemini --size 16:9 --prompt "a lighthouse at dawn" --out ./gemini.png
1039
1903
  imagegen generate --prompt "a lighthouse at dawn" --out ./lighthouse.png
1040
1904
  imagegen generate --model gpt-image-2 --prompt "a lighthouse" --out ./classic.png
1041
1905
  imagegen generate --model gpt-image-2.5-sunburst --quality max --prompt "a product photo" --out ./product.png
1042
1906
  `;
1043
1907
  var EDIT_EXAMPLES = `Edit options:
1044
1908
  --source <file> Source image (required)
1045
- --mask <file> Optional edit mask
1909
+ --mask <file> Optional edit mask (openai-images; Codex ignores it)
1046
1910
 
1047
1911
  Examples:
1048
1912
  imagegen edit --format webp --source ./product.png --prompt "make the product blue" --out ./blue.webp
@@ -1051,21 +1915,28 @@ Examples:
1051
1915
  var USAGE = `imagegen - provider-agnostic image generation CLI
1052
1916
 
1053
1917
  Usage:
1918
+ imagegen auth gemini login [--key-stdin]
1919
+ imagegen auth gemini status [--show-path]
1920
+ imagegen auth gemini logout
1054
1921
  imagegen auth codex login [--timeout <minutes>] [--no-open]
1055
1922
  imagegen auth codex status [--show-path]
1056
1923
  imagegen auth codex logout
1057
1924
  imagegen generate --prompt <text> --out <path|dir> [--provider <name>] [options]
1058
1925
  imagegen edit --prompt <text> --source <file> [--mask <file>] --out <path|dir> [--provider <name>] [options]
1059
1926
  imagegen providers
1927
+ imagegen models --provider <profile-name>
1060
1928
  imagegen skill
1061
1929
  imagegen skill install --target <claude|codex|skills-dir> [--target <...>] [--force]
1062
1930
  imagegen help [command]
1063
1931
 
1932
+ ${PROFILE_HELP}
1064
1933
  ${IMAGE_OPTIONS}
1065
1934
  ${GENERATE_EXAMPLES}
1066
1935
  ${EDIT_EXAMPLES}
1067
1936
 
1068
1937
  Auth options:
1938
+ --key-stdin Read an API key from explicit stdin; otherwise hidden
1939
+ terminal entry. Keys are stored in the vault/file fallback.
1069
1940
  --timeout <minutes> Login wait timeout (default 10)
1070
1941
  --no-open Print the URL without opening a browser
1071
1942
  --show-path Show where tokens are stored (auth status)
@@ -1076,29 +1947,42 @@ Global:
1076
1947
  var COMMAND_HELP = {
1077
1948
  auth: `imagegen auth - manage provider credentials (one namespace per provider)
1078
1949
 
1950
+ ${PROFILE_HELP}
1951
+
1079
1952
  Usage:
1953
+ imagegen auth gemini login [--key-stdin]
1954
+ imagegen auth gemini status [--show-path]
1955
+ imagegen auth gemini logout
1080
1956
  imagegen auth codex login [--timeout <minutes>] [--no-open]
1081
1957
  imagegen auth codex status [--show-path]
1082
1958
  imagegen auth codex logout
1083
1959
  `,
1084
- login: `imagegen auth codex login - connect Codex via browser OAuth
1960
+ login: `imagegen auth login - connect Gemini with an API key or Codex via OAuth
1085
1961
 
1086
1962
  Usage:
1963
+ imagegen auth gemini login [--key-stdin]
1964
+ imagegen auth gemini status [--show-path]
1965
+ imagegen auth gemini logout
1087
1966
  imagegen auth codex login [--timeout <minutes>] [--no-open]
1088
1967
 
1089
- Starts a temporary localhost callback server, opens the browser,
1968
+ Gemini login reads a hidden API key (or explicit --key-stdin), stores it in the
1969
+ OS vault with file fallback, and makes no paid verification request.
1970
+ Codex login starts a temporary localhost callback server, opens the browser,
1090
1971
  exchanges the code, and stores tokens in the OS vault (file fallback).
1091
1972
  `,
1092
- status: `imagegen auth codex status - show Codex connection state
1973
+ status: `imagegen auth status - show provider connection state
1093
1974
 
1094
1975
  Usage:
1976
+ imagegen auth gemini status [--show-path]
1095
1977
  imagegen auth codex status [--show-path]
1096
1978
 
1097
- Prints JSON without secrets. --show-path reveals the token location.
1979
+ Prints JSON without secrets. --show-path reveals the credential location.
1980
+ Gemini status is local configured state, not API-key validity.
1098
1981
  `,
1099
- logout: `imagegen auth codex logout - remove stored Codex credentials
1982
+ logout: `imagegen auth logout - remove stored provider credentials
1100
1983
 
1101
1984
  Usage:
1985
+ imagegen auth gemini logout
1102
1986
  imagegen auth codex logout
1103
1987
  `,
1104
1988
  generate: `imagegen generate - create images from a prompt
@@ -1106,6 +1990,7 @@ Usage:
1106
1990
  Usage:
1107
1991
  imagegen generate --prompt <text> --out <path|dir> [--provider <name>] [options]
1108
1992
 
1993
+ ${PROFILE_HELP}
1109
1994
  ${IMAGE_OPTIONS}
1110
1995
  ${GENERATE_EXAMPLES}
1111
1996
  `,
@@ -1117,12 +2002,22 @@ Usage:
1117
2002
  ${IMAGE_OPTIONS}
1118
2003
  ${EDIT_EXAMPLES}
1119
2004
  `,
1120
- providers: `imagegen providers - list registered and configured providers
2005
+ providers: `imagegen providers - list providers and manage named endpoints
1121
2006
 
1122
2007
  Usage:
1123
2008
  imagegen providers
1124
2009
 
1125
2010
  Prints JSON with each provider and whether it is configured.
2011
+
2012
+ ${PROFILE_HELP}`,
2013
+ models: `imagegen models - discover models for a named endpoint profile
2014
+
2015
+ Usage:
2016
+ imagegen models --provider <profile-name>
2017
+
2018
+ Discovery is explicit and may require saved credentials. OpenRouter returns image
2019
+ models and reported capabilities; generic /models results have unknown image
2020
+ capabilities. This command does not generate images.
1126
2021
  `,
1127
2022
  skill: `imagegen skill - print or install the reusable agent skill
1128
2023
 
@@ -1154,9 +2049,9 @@ ${USAGE}`;
1154
2049
  }
1155
2050
 
1156
2051
  // src/skill.ts
1157
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2052
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1158
2053
  import { homedir as homedir2 } from "node:os";
1159
- import { dirname, join as join2, resolve } from "node:path";
2054
+ import { dirname as dirname3, join as join4, resolve } from "node:path";
1160
2055
  var SKILL_NAME = "imagegen-cli";
1161
2056
  var SkillExistsError = class extends Error {
1162
2057
  constructor(path) {
@@ -1165,18 +2060,18 @@ var SkillExistsError = class extends Error {
1165
2060
  }
1166
2061
  };
1167
2062
  async function skillText() {
1168
- return readFile(new URL("../skill/SKILL.md", import.meta.url), "utf8");
2063
+ return readFile2(new URL("../skill/SKILL.md", import.meta.url), "utf8");
1169
2064
  }
1170
2065
  function targetPath(target) {
1171
- const root = target === "claude" ? join2(homedir2(), ".claude", "skills") : target === "codex" ? join2(homedir2(), ".agents", "skills") : resolve(target);
1172
- return join2(root, SKILL_NAME, "SKILL.md");
2066
+ const root = target === "claude" ? join4(homedir2(), ".claude", "skills") : target === "codex" ? join4(homedir2(), ".agents", "skills") : resolve(target);
2067
+ return join4(root, SKILL_NAME, "SKILL.md");
1173
2068
  }
1174
2069
  async function existingText(path) {
1175
2070
  try {
1176
- return await readFile(path, "utf8");
1177
- } catch (error) {
1178
- if (error.code === "ENOENT") return void 0;
1179
- throw error;
2071
+ return await readFile2(path, "utf8");
2072
+ } catch (error2) {
2073
+ if (error2.code === "ENOENT") return void 0;
2074
+ throw error2;
1180
2075
  }
1181
2076
  }
1182
2077
  async function installSkill(targets, force) {
@@ -1195,19 +2090,60 @@ async function installSkill(targets, force) {
1195
2090
  }
1196
2091
  for (const item of planned.values()) {
1197
2092
  if (item.status === "up-to-date") continue;
1198
- await mkdir(dirname(item.path), { recursive: true });
1199
- await writeFile(item.path, content, { encoding: "utf8", flag: item.status === "installed" ? "wx" : "w" });
2093
+ await mkdir3(dirname3(item.path), { recursive: true });
2094
+ await writeFile2(item.path, content, { encoding: "utf8", flag: item.status === "installed" ? "wx" : "w" });
1200
2095
  }
1201
2096
  return [...planned.values()];
1202
2097
  }
1203
2098
 
2099
+ // src/usage-log.ts
2100
+ import { appendFile, mkdir as mkdir4 } from "node:fs/promises";
2101
+ import { dirname as dirname4, resolve as resolve2 } from "node:path";
2102
+ var USAGE_LOG_ENV = "IMAGEGEN_USAGE_LOG";
2103
+ function usageLogEntry(request, edit, result, paths) {
2104
+ return {
2105
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2106
+ command: edit ? "edit" : "generate",
2107
+ provider: result.provider,
2108
+ imageModel: result.imageModel,
2109
+ requested: {
2110
+ size: request.size ?? "auto",
2111
+ quality: request.quality ?? "auto",
2112
+ format: request.format ?? "png",
2113
+ count: request.count ?? 1,
2114
+ references: request.referenceImages?.length ?? 0,
2115
+ mask: edit ? edit.mask !== void 0 : void 0
2116
+ },
2117
+ images: result.images.map((image, i) => ({
2118
+ path: paths[i],
2119
+ width: image.width,
2120
+ height: image.height,
2121
+ bytes: image.data.byteLength,
2122
+ responseId: image.responseId,
2123
+ usage: image.usage
2124
+ }))
2125
+ };
2126
+ }
2127
+ async function appendUsageLog(entry) {
2128
+ const target = process.env[USAGE_LOG_ENV]?.trim();
2129
+ if (!target) return void 0;
2130
+ const path = resolve2(target);
2131
+ await mkdir4(dirname4(path), { recursive: true });
2132
+ await appendFile(path, `${JSON.stringify(entry)}
2133
+ `, "utf8");
2134
+ return path;
2135
+ }
2136
+
1204
2137
  // src/index.ts
1205
2138
  var EXIT_USAGE = 2;
1206
2139
  var EXIT_AUTH = 3;
1207
2140
  var EXIT_PROVIDER = 4;
1208
2141
  var EXIT_IO = 5;
1209
- function fail(kind, message, exitCode) {
1210
- process.stderr.write(`${JSON.stringify({ error: { kind, message } })}
2142
+ function errorDetails(error2) {
2143
+ return { statusCode: error2.statusCode, upstreamProvider: error2.upstreamProvider, retryAfterSeconds: error2.retryAfterSeconds };
2144
+ }
2145
+ function fail(kind, message, exitCode, details = {}) {
2146
+ process.stderr.write(`${JSON.stringify({ error: { kind, message, ...details } })}
1211
2147
  `);
1212
2148
  process.exit(exitCode);
1213
2149
  }
@@ -1222,10 +2158,10 @@ function parseArgs(argv) {
1222
2158
  }
1223
2159
  if (arg.startsWith("--")) {
1224
2160
  const key = arg.slice(2);
1225
- if (!["prompt", "out", "provider", "model", "reference", "size", "quality", "format", "count", "source", "mask", "timeout", "no-open", "show-path", "target", "force"].includes(key)) {
2161
+ if (!["prompt", "out", "provider", "model", "reference", "size", "quality", "format", "count", "source", "mask", "timeout", "api", "base-url", "auth", "key-stdin", "no-open", "show-path", "target", "force"].includes(key)) {
1226
2162
  fail("usage", `Unknown option --${key}. Run imagegen help.`, EXIT_USAGE);
1227
2163
  }
1228
- if (key === "no-open" || key === "show-path" || key === "force") {
2164
+ if (key === "key-stdin" || key === "no-open" || key === "show-path" || key === "force") {
1229
2165
  flags.set(key, true);
1230
2166
  continue;
1231
2167
  }
@@ -1273,7 +2209,7 @@ function redirectPort(uri) {
1273
2209
  async function loadImageInput(path, label) {
1274
2210
  let data;
1275
2211
  try {
1276
- data = await readFile2(path);
2212
+ data = await readFile3(path);
1277
2213
  } catch {
1278
2214
  fail("io", `${label} not found: ${path}`, EXIT_IO);
1279
2215
  }
@@ -1313,9 +2249,11 @@ function parseCount(flags) {
1313
2249
  }
1314
2250
  return count;
1315
2251
  }
1316
- function registry() {
2252
+ async function registry() {
1317
2253
  const registry2 = new ProviderRegistry();
1318
2254
  registry2.register(createCodexEntry());
2255
+ registry2.register(createGeminiEntry());
2256
+ for (const profile of await new ProfileRepository().list()) registry2.register(createProfileEntry(profile));
1319
2257
  return registry2;
1320
2258
  }
1321
2259
  function requestedProviders(flags) {
@@ -1324,10 +2262,10 @@ function requestedProviders(flags) {
1324
2262
  return raw;
1325
2263
  }
1326
2264
  async function resolveOutPaths(out, providerName, multiProvider, count, format) {
1327
- const resolved = resolve2(out);
2265
+ const resolved = resolve3(out);
1328
2266
  let isDir = false;
1329
2267
  try {
1330
- const info = await stat(resolved);
2268
+ const info = await stat2(resolved);
1331
2269
  isDir = info.isDirectory();
1332
2270
  } catch {
1333
2271
  isDir = extname(resolved) === "";
@@ -1335,12 +2273,12 @@ async function resolveOutPaths(out, providerName, multiProvider, count, format)
1335
2273
  const extension = `.${normalizeOutputFormat(format)}`;
1336
2274
  const prefix = multiProvider ? `${providerName}-` : "";
1337
2275
  if (isDir) {
1338
- return Array.from({ length: count }, (_, i) => join3(resolved, `${prefix}image-${i + 1}${extension}`));
2276
+ return Array.from({ length: count }, (_, i) => join5(resolved, `${prefix}image-${i + 1}${extension}`));
1339
2277
  }
1340
2278
  if (count > 1 || multiProvider) {
1341
- const dir = dirname2(resolved);
2279
+ const dir = dirname5(resolved);
1342
2280
  const base = basename(resolved, extname(resolved));
1343
- return Array.from({ length: count }, (_, i) => join3(dir, `${prefix}${base}-${i + 1}${extension}`));
2281
+ return Array.from({ length: count }, (_, i) => join5(dir, `${prefix}${base}-${i + 1}${extension}`));
1344
2282
  }
1345
2283
  return [resolved.endsWith(extension) ? resolved : `${resolved}${extension}`];
1346
2284
  }
@@ -1365,16 +2303,67 @@ async function saveImages(result, paths) {
1365
2303
  for (let i = 0; i < result.images.length; i += 1) {
1366
2304
  const path = paths[i];
1367
2305
  try {
1368
- await mkdir2(dirname2(path), { recursive: true });
1369
- await writeFile2(path, Buffer.from(result.images[i].data));
2306
+ await mkdir5(dirname5(path), { recursive: true });
2307
+ await writeFile3(path, Buffer.from(result.images[i].data));
1370
2308
  } catch {
1371
2309
  fail("io", `Could not write output file: ${path}`, EXIT_IO);
1372
2310
  }
1373
2311
  }
1374
2312
  }
2313
+ async function runApiKeyAuth(providerName, action, flags) {
2314
+ const profile = providerName === "gemini" ? void 0 : await new ProfileRepository().get(providerName);
2315
+ try {
2316
+ if (profile?.auth === "none") {
2317
+ process.stdout.write(`${JSON.stringify({ provider: providerName, ...action === "status" ? { connected: true, auth: "none" } : { status: "not-required" } })}
2318
+ `);
2319
+ return;
2320
+ }
2321
+ const store = profile ? await profileStore() : await geminiStore();
2322
+ if (action === "login") {
2323
+ const apiKey = validateApiKey(
2324
+ await readApiKey(flags.get("key-stdin") === true, process.stdin, process.stderr, providerName)
2325
+ );
2326
+ if (store.describe().backend === "file")
2327
+ process.stderr.write(
2328
+ `Warning: OS vault unavailable; credentials stored in a restricted file. Use auth ${providerName} status --show-path to inspect.
2329
+ `
2330
+ );
2331
+ await store.save(providerName, { apiKey });
2332
+ process.stdout.write(
2333
+ `${JSON.stringify({ provider: providerName, status: "connected" })}
2334
+ `
2335
+ );
2336
+ } else if (action === "logout") {
2337
+ await store.clear(providerName);
2338
+ process.stdout.write(
2339
+ `${JSON.stringify({ provider: providerName, status: "disconnected" })}
2340
+ `
2341
+ );
2342
+ } else {
2343
+ let connected = false;
2344
+ try {
2345
+ if (profile) await profileKey(profile, store);
2346
+ else await storedKey(store);
2347
+ connected = true;
2348
+ } catch (error2) {
2349
+ if (!(error2 instanceof ProviderError)) throw error2;
2350
+ }
2351
+ process.stdout.write(
2352
+ `${JSON.stringify({ provider: providerName, connected, ...flags.get("show-path") ? { store: store.describe() } : {} })}
2353
+ `
2354
+ );
2355
+ }
2356
+ } catch (error2) {
2357
+ fail(
2358
+ "auth",
2359
+ error2 instanceof ProviderError ? error2.message : "Could not access API credentials. Check the credential store and retry login.",
2360
+ EXIT_AUTH
2361
+ );
2362
+ }
2363
+ }
1375
2364
  async function runAuthLogin(providerName, flags) {
1376
2365
  if (providerName !== PROVIDER_NAME) {
1377
- fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
2366
+ fail("usage", `Unknown auth provider "${providerName}". Available: ${(await registry()).describe()}.`, EXIT_USAGE);
1378
2367
  }
1379
2368
  const uri = redirectUri();
1380
2369
  const port = redirectPort(uri);
@@ -1387,8 +2376,8 @@ async function runAuthLogin(providerName, flags) {
1387
2376
  const waiter = waitForOAuthCallback(timeoutMinutes * 60 * 1e3, port);
1388
2377
  if (!flags.get("no-open")) {
1389
2378
  try {
1390
- const { default: open } = await import("open");
1391
- await open(url);
2379
+ const { default: open2 } = await import("open");
2380
+ await open2(url);
1392
2381
  } catch {
1393
2382
  process.stderr.write("Could not open a browser automatically. Open this URL manually:\n");
1394
2383
  }
@@ -1398,9 +2387,9 @@ ${url}
1398
2387
  `);
1399
2388
  try {
1400
2389
  callback = await waiter;
1401
- } catch (error) {
1402
- if (error instanceof LoopbackError) fail("auth", error.message, EXIT_AUTH);
1403
- throw error;
2390
+ } catch (error2) {
2391
+ if (error2 instanceof LoopbackError) fail("auth", error2.message, EXIT_AUTH);
2392
+ throw error2;
1404
2393
  }
1405
2394
  if (callback.state !== state) fail("auth", "OAuth state mismatch. Retry `imagegen auth codex login`.", EXIT_AUTH);
1406
2395
  const verifier = consumeVerifier(state);
@@ -1412,15 +2401,16 @@ ${url}
1412
2401
  const manager = new CodexTokenManager(store);
1413
2402
  try {
1414
2403
  await manager.exchangeAndSave({ code: callback.code, redirectUri: uri, codeVerifier: verifier });
1415
- } catch (error) {
1416
- if (error instanceof ReauthRequiredError) fail("auth", error.message, EXIT_AUTH);
1417
- throw error;
2404
+ } catch (error2) {
2405
+ if (error2 instanceof ReauthRequiredError) fail("auth", error2.message, EXIT_AUTH);
2406
+ throw error2;
1418
2407
  }
1419
2408
  process.stdout.write(`${JSON.stringify({ provider: PROVIDER_NAME, status: "connected" })}
1420
2409
  `);
1421
2410
  }
1422
2411
  async function runProviders() {
1423
- const entries = registry().names().map((name) => registry().get(name));
2412
+ const providersRegistry = await registry();
2413
+ const entries = providersRegistry.names().map((name) => providersRegistry.get(name));
1424
2414
  const providers = [];
1425
2415
  for (const entry of entries) {
1426
2416
  const status = await entry.authStatus();
@@ -1431,7 +2421,7 @@ async function runProviders() {
1431
2421
  }
1432
2422
  async function runAuthStatus(providerName, flags) {
1433
2423
  if (providerName !== PROVIDER_NAME) {
1434
- fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
2424
+ fail("usage", `Unknown auth provider "${providerName}". Available: ${(await registry()).describe()}.`, EXIT_USAGE);
1435
2425
  }
1436
2426
  const store = await resolveTokenStore();
1437
2427
  const manager = new CodexTokenManager(store);
@@ -1447,7 +2437,7 @@ async function runAuthStatus(providerName, flags) {
1447
2437
  }
1448
2438
  async function runAuthLogout(providerName) {
1449
2439
  if (providerName !== PROVIDER_NAME) {
1450
- fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
2440
+ fail("usage", `Unknown auth provider "${providerName}". Available: ${(await registry()).describe()}.`, EXIT_USAGE);
1451
2441
  }
1452
2442
  const store = await resolveTokenStore();
1453
2443
  await store.clear(PROVIDER_NAME);
@@ -1475,9 +2465,9 @@ async function runGenerate(flags) {
1475
2465
  }
1476
2466
  async function resolveProviders(requested) {
1477
2467
  try {
1478
- return await registry().resolve(requested);
1479
- } catch (error) {
1480
- fail("usage", error instanceof Error ? error.message : String(error), EXIT_USAGE);
2468
+ return await (await registry()).resolve(requested);
2469
+ } catch (error2) {
2470
+ fail("usage", error2 instanceof Error ? error2.message : String(error2), EXIT_USAGE);
1481
2471
  }
1482
2472
  }
1483
2473
  async function runEdit(flags) {
@@ -1508,17 +2498,24 @@ async function runImageRequest(request, edit, providers) {
1508
2498
  let result;
1509
2499
  try {
1510
2500
  result = edit ? await entry.provider.edit(edit) : await entry.provider.generate(request);
1511
- } catch (error) {
1512
- if (error instanceof ReauthRequiredError) fail("auth", `${entry.provider.name}: ${error.message}`, EXIT_AUTH);
1513
- if (error instanceof ProviderError) {
1514
- if (error.kind === "configuration" || error.kind === "usage") fail(error.kind, `${entry.provider.name}: ${error.message}`, EXIT_USAGE);
1515
- fail("provider", `${entry.provider.name}: ${error.kind}: ${error.message}`, EXIT_PROVIDER);
2501
+ } catch (error2) {
2502
+ if (error2 instanceof ReauthRequiredError) fail("auth", `${entry.provider.name}: ${error2.message}`, EXIT_AUTH);
2503
+ if (error2 instanceof ProviderError) {
2504
+ if (error2.kind === "auth") fail("auth", `${entry.provider.name}: ${error2.message}`, EXIT_AUTH, errorDetails(error2));
2505
+ if (error2.kind === "configuration" || error2.kind === "usage") fail(error2.kind, `${entry.provider.name}: ${error2.message}`, EXIT_USAGE, errorDetails(error2));
2506
+ fail("provider", `${entry.provider.name}: ${error2.kind}: ${error2.message}`, EXIT_PROVIDER, errorDetails(error2));
1516
2507
  }
1517
- throw error;
2508
+ throw error2;
1518
2509
  }
1519
2510
  const paths = await resolveOutPaths(request.out, entry.provider.name, multiProvider, result.images.length, request.format ?? "png");
1520
2511
  await saveImages(result, paths);
1521
2512
  printResult(entry.provider.name, result, paths);
2513
+ try {
2514
+ await appendUsageLog(usageLogEntry(request, edit, result, paths));
2515
+ } catch {
2516
+ process.stderr.write(`${JSON.stringify({ warning: { kind: "io", message: `Could not append to ${USAGE_LOG_ENV} file.` } })}
2517
+ `);
2518
+ }
1522
2519
  }
1523
2520
  }
1524
2521
  async function main() {
@@ -1528,6 +2525,9 @@ async function main() {
1528
2525
  return;
1529
2526
  }
1530
2527
  const [command, sub, subSub] = positionals;
2528
+ if (flags.has("key-stdin") && !(command === "auth" && sub !== "codex" && subSub === "login")) {
2529
+ fail("usage", "--key-stdin is only supported by API-key auth login.", EXIT_USAGE);
2530
+ }
1531
2531
  try {
1532
2532
  if (command === "help") {
1533
2533
  process.stdout.write(helpFor(sub));
@@ -1546,12 +2546,42 @@ async function main() {
1546
2546
  const installed = await installSkill(targets, flags.get("force") === true);
1547
2547
  process.stdout.write(`${JSON.stringify({ skill: "imagegen-cli", installed })}
1548
2548
  `);
1549
- } catch (error) {
1550
- if (error instanceof SkillExistsError) fail("usage", error.message, EXIT_USAGE);
1551
- fail("io", error instanceof Error ? error.message : String(error), EXIT_IO);
2549
+ } catch (error2) {
2550
+ if (error2 instanceof SkillExistsError) fail("usage", error2.message, EXIT_USAGE);
2551
+ fail("io", error2 instanceof Error ? error2.message : String(error2), EXIT_IO);
1552
2552
  }
1553
2553
  return;
1554
2554
  }
2555
+ if (command === "providers" && sub) {
2556
+ if (!subSub || positionals.length !== 3) fail("usage", "Usage: imagegen providers add|show|update|remove <name> [options].", EXIT_USAGE);
2557
+ const repository = new ProfileRepository();
2558
+ if (sub === "show") process.stdout.write(`${JSON.stringify({ provider: await repository.get(subSub) })}
2559
+ `);
2560
+ else if (sub === "remove") {
2561
+ await repository.remove(subSub);
2562
+ process.stdout.write(`${JSON.stringify({ provider: subSub, status: "removed" })}
2563
+ `);
2564
+ } else if (sub === "add" || sub === "update") {
2565
+ const patch = {};
2566
+ for (const [flag, key] of [["api", "api"], ["base-url", "baseUrl"], ["auth", "auth"], ["model", "model"]]) {
2567
+ const value = flagValue(flags, flag);
2568
+ if (value !== void 0) patch[key] = value;
2569
+ }
2570
+ const profile = sub === "add" ? await repository.add({ name: subSub, ...patch }) : await repository.update(subSub, patch);
2571
+ process.stdout.write(`${JSON.stringify({ provider: profile })}
2572
+ `);
2573
+ } else fail("usage", "Unknown providers action. Use add, show, update, or remove.", EXIT_USAGE);
2574
+ return;
2575
+ }
2576
+ if (command === "models" && !sub) {
2577
+ const selected = requestedProviders(flags);
2578
+ if (!selected || selected.length !== 1) fail("usage", "Model discovery requires exactly one --provider <profile-name>.", EXIT_USAGE);
2579
+ const profile = await new ProfileRepository().get(selected[0]);
2580
+ const provider = new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore()));
2581
+ process.stdout.write(`${JSON.stringify({ provider: profile.name, models: await provider.models() })}
2582
+ `);
2583
+ return;
2584
+ }
1555
2585
  if (command === "providers" && !sub) {
1556
2586
  await runProviders();
1557
2587
  return;
@@ -1559,6 +2589,10 @@ async function main() {
1559
2589
  if (command === "auth" && sub && !subSub) {
1560
2590
  fail("usage", `Missing auth action. Usage: imagegen auth ${sub} login|status|logout.`, EXIT_USAGE);
1561
2591
  }
2592
+ if (command === "auth" && sub && sub !== "codex" && ["login", "status", "logout"].includes(subSub)) {
2593
+ await runApiKeyAuth(sub, subSub, flags);
2594
+ return;
2595
+ }
1562
2596
  if (command === "auth" && sub) {
1563
2597
  if (subSub === "login") {
1564
2598
  await runAuthLogin(sub, flags);
@@ -1582,9 +2616,12 @@ async function main() {
1582
2616
  return;
1583
2617
  }
1584
2618
  fail("usage", `Unknown command: ${positionals.join(" ")}. Run \`imagegen help\`.`, EXIT_USAGE);
1585
- } catch (error) {
1586
- if (error instanceof ProviderError) fail("provider", `${error.kind}: ${error.message}`, EXIT_PROVIDER);
1587
- fail("provider", error instanceof Error ? error.message : String(error), EXIT_PROVIDER);
2619
+ } catch (error2) {
2620
+ if (error2 instanceof ProviderError) {
2621
+ const exit = error2.kind === "auth" ? EXIT_AUTH : error2.kind === "usage" || error2.kind === "configuration" ? EXIT_USAGE : error2.kind === "io" ? EXIT_IO : EXIT_PROVIDER;
2622
+ fail(error2.kind, error2.message, exit, errorDetails(error2));
2623
+ }
2624
+ fail("provider", error2 instanceof Error ? error2.message : String(error2), EXIT_PROVIDER);
1588
2625
  }
1589
2626
  }
1590
2627
  await main();