@dorely13/ai-imagegen-cli 0.1.3 → 0.2.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 mkdir3, readFile as readFile3, stat, writeFile as writeFile3 } from "node:fs/promises";
5
+ import { basename, dirname as dirname3, extname, join as join4, resolve as resolve2 } 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;
@@ -288,9 +292,9 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
288
292
  clearTimeout(timer);
289
293
  server?.close();
290
294
  };
291
- const fail2 = (error) => {
295
+ const fail2 = (error2) => {
292
296
  closeServer();
293
- reject(error);
297
+ reject(error2);
294
298
  };
295
299
  server = createServer((req, res) => {
296
300
  try {
@@ -319,16 +323,16 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
319
323
  res.end(callbackPage("Connected", "ImageGenCli is connected. You can close this tab."));
320
324
  closeServer();
321
325
  resolve3({ code, state });
322
- } catch (error) {
323
- fail2(error instanceof Error ? error : new LoopbackError("OAuth callback failed.", "callback_error"));
326
+ } catch (error2) {
327
+ fail2(error2 instanceof Error ? error2 : new LoopbackError("OAuth callback failed.", "callback_error"));
324
328
  }
325
329
  });
326
- server.on("error", (error) => {
327
- if (error.code === "EADDRINUSE") {
330
+ server.on("error", (error2) => {
331
+ if (error2.code === "EADDRINUSE") {
328
332
  fail2(new LoopbackError(`Port ${port} is busy. Close the conflicting process and retry.`, "port_busy"));
329
333
  return;
330
334
  }
331
- fail2(error);
335
+ fail2(error2);
332
336
  });
333
337
  server.listen(port, "127.0.0.1");
334
338
  });
@@ -480,32 +484,32 @@ var CodexTokenManager = class {
480
484
  }
481
485
  };
482
486
 
483
- // ../../packages/providers-codex/dist/token-store.js
487
+ // ../../packages/credentials/dist/index.js
484
488
  import { homedir } from "node:os";
485
489
  import { join } from "node:path";
486
490
  function asKeytar(module) {
487
491
  if (typeof module !== "object" || module === null)
488
492
  return null;
489
- const record = module;
490
- const candidate = record["default"] ?? module;
493
+ const record2 = module;
494
+ const candidate = record2["default"] ?? module;
491
495
  if (typeof candidate.getPassword === "function" && typeof candidate.setPassword === "function" && typeof candidate.deletePassword === "function") {
492
496
  return candidate;
493
497
  }
494
498
  return null;
495
499
  }
496
- async function resolveTokenStore() {
500
+ async function resolveCredentialStore() {
497
501
  try {
498
502
  const keytar = asKeytar(await import("keytar"));
499
503
  if (!keytar)
500
- return new FileTokenStore();
501
- const store = new KeytarTokenStore(keytar);
504
+ return new FileCredentialStore();
505
+ const store = new KeytarCredentialStore(keytar);
502
506
  await store.probe();
503
507
  return store;
504
508
  } catch {
505
- return new FileTokenStore();
509
+ return new FileCredentialStore();
506
510
  }
507
511
  }
508
- var KeytarTokenStore = class {
512
+ var KeytarCredentialStore = class {
509
513
  keytar;
510
514
  constructor(keytar) {
511
515
  this.keytar = keytar;
@@ -517,7 +521,11 @@ var KeytarTokenStore = class {
517
521
  const raw = await this.keytar.getPassword("ImageGenCli", provider);
518
522
  if (!raw)
519
523
  return null;
520
- return JSON.parse(raw);
524
+ try {
525
+ return JSON.parse(raw);
526
+ } catch {
527
+ throw new Error("Stored credential record is invalid. Run the provider login command.");
528
+ }
521
529
  }
522
530
  async save(provider, tokens) {
523
531
  await this.keytar.setPassword("ImageGenCli", provider, JSON.stringify(tokens));
@@ -526,13 +534,16 @@ var KeytarTokenStore = class {
526
534
  await this.keytar.deletePassword("ImageGenCli", provider);
527
535
  }
528
536
  describe() {
529
- return { backend: "vault", location: "OS credential vault (service ImageGenCli)" };
537
+ return {
538
+ backend: "vault",
539
+ location: "OS credential vault (service ImageGenCli)"
540
+ };
530
541
  }
531
542
  };
532
- var FileTokenStore = class {
543
+ var FileCredentialStore = class {
533
544
  filePath;
534
545
  constructor(filePath) {
535
- this.filePath = filePath ?? defaultTokenFilePath();
546
+ this.filePath = filePath ?? defaultCredentialFilePath();
536
547
  }
537
548
  async load(provider) {
538
549
  const all = await this.readAll();
@@ -552,27 +563,37 @@ var FileTokenStore = class {
552
563
  return { backend: "file", location: this.filePath };
553
564
  }
554
565
  async readAll() {
555
- const { readFile: readFile3 } = await import("node:fs/promises");
566
+ const { readFile: readFile4 } = await import("node:fs/promises");
556
567
  try {
557
- const raw = await readFile3(this.filePath, "utf8");
558
- return JSON.parse(raw);
559
- } catch (error) {
560
- if (error.code === "ENOENT")
568
+ const raw = await readFile4(this.filePath, "utf8");
569
+ let value;
570
+ try {
571
+ value = JSON.parse(raw);
572
+ } catch {
573
+ throw new Error("Credential file is invalid. Restore a valid credential file before retrying.");
574
+ }
575
+ if (typeof value !== "object" || value === null || Array.isArray(value))
576
+ throw new Error("Credential file must contain provider records.");
577
+ return value;
578
+ } catch (error2) {
579
+ if (error2.code === "ENOENT")
561
580
  return {};
562
- throw error;
581
+ throw error2;
563
582
  }
564
583
  }
565
584
  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 });
585
+ const { mkdir: mkdir4, writeFile: writeFile4, chmod } = await import("node:fs/promises");
586
+ const { dirname: dirname4 } = await import("node:path");
587
+ await mkdir4(dirname4(this.filePath), { recursive: true });
588
+ await writeFile4(this.filePath, JSON.stringify(all, null, 2), {
589
+ mode: 384
590
+ });
570
591
  if (process.platform !== "win32") {
571
592
  await chmod(this.filePath, 384);
572
593
  }
573
594
  }
574
595
  };
575
- function defaultTokenFilePath() {
596
+ function defaultCredentialFilePath() {
576
597
  if (process.platform === "win32") {
577
598
  const base2 = process.env["LOCALAPPDATA"] ?? join(process.env["USERPROFILE"] ?? ".", "AppData", "Local");
578
599
  return join(base2, "ImageGenCli", "tokens.json");
@@ -581,6 +602,26 @@ function defaultTokenFilePath() {
581
602
  return join(base, "ImageGenCli", "tokens.json");
582
603
  }
583
604
 
605
+ // ../../packages/providers-codex/dist/token-store.js
606
+ async function resolveTokenStore() {
607
+ const store = await resolveCredentialStore();
608
+ return {
609
+ ...store,
610
+ load: async (provider) => {
611
+ const value = await store.load(provider);
612
+ if (value === null)
613
+ return null;
614
+ if (typeof value.accessToken !== "string" || typeof value.expiresAt !== "number" || value.refreshToken !== void 0 && typeof value.refreshToken !== "string") {
615
+ throw new Error("Stored Codex credentials are invalid. Run imagegen auth codex login.");
616
+ }
617
+ return value;
618
+ },
619
+ save: (provider, value) => store.save(provider, value),
620
+ clear: (provider) => store.clear(provider),
621
+ describe: () => store.describe()
622
+ };
623
+ }
624
+
584
625
  // ../../packages/providers-codex/dist/account-id.js
585
626
  function extractAccountId(token) {
586
627
  const parts = token.split(".");
@@ -628,14 +669,14 @@ function readEventError(event) {
628
669
  return direct;
629
670
  if (typeof event !== "object" || event === null)
630
671
  return void 0;
631
- const record = event;
632
- const nested = record["error"];
672
+ const record2 = event;
673
+ const nested = record2["error"];
633
674
  if (typeof nested === "object" && nested !== null) {
634
675
  const message = readString(nested, "message");
635
676
  if (message)
636
677
  return message;
637
678
  }
638
- const response = record["response"];
679
+ const response = record2["response"];
639
680
  if (typeof response === "object" && response !== null) {
640
681
  const responseError = response["error"];
641
682
  if (typeof responseError === "object" && responseError !== null) {
@@ -731,10 +772,10 @@ function normalizeCount(count) {
731
772
  function resolveSize(size) {
732
773
  try {
733
774
  return parseSize(size ?? "auto");
734
- } catch (error) {
775
+ } catch (error2) {
735
776
  throw new ProviderError({
736
777
  kind: "usage",
737
- message: error instanceof Error ? error.message : String(error)
778
+ message: error2 instanceof Error ? error2.message : String(error2)
738
779
  });
739
780
  }
740
781
  }
@@ -902,8 +943,8 @@ var CodexImageProvider = class {
902
943
  let parsed;
903
944
  try {
904
945
  parsed = parseImageStreamEvents(lines);
905
- } catch (error) {
906
- throw toProviderError(error, requestId);
946
+ } catch (error2) {
947
+ throw toProviderError(error2, requestId);
907
948
  }
908
949
  return {
909
950
  data: parsed.data,
@@ -945,14 +986,14 @@ function readErrorMessage(body) {
945
986
  try {
946
987
  const parsed = JSON.parse(body);
947
988
  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"];
989
+ const record2 = parsed;
990
+ if (typeof record2["message"] === "string")
991
+ return record2["message"];
992
+ if (typeof record2["detail"] === "string")
993
+ return record2["detail"];
994
+ const error2 = record2["error"];
995
+ if (typeof error2 === "object" && error2 !== null && typeof error2["message"] === "string") {
996
+ return error2["message"];
956
997
  }
957
998
  }
958
999
  } catch {
@@ -960,8 +1001,8 @@ function readErrorMessage(body) {
960
1001
  }
961
1002
  return null;
962
1003
  }
963
- function toProviderError(error, requestId) {
964
- const message = error instanceof Error ? error.message : String(error);
1004
+ function toProviderError(error2, requestId) {
1005
+ const message = error2 instanceof Error ? error2.message : String(error2);
965
1006
  const separator = message.indexOf(":");
966
1007
  const kind = separator > 0 ? message.slice(0, separator) : "api_error";
967
1008
  return new ProviderError({ kind, message, requestId });
@@ -1007,13 +1048,876 @@ function createCodexEntry() {
1007
1048
  };
1008
1049
  }
1009
1050
 
1051
+ // ../../packages/providers-google/dist/gemini-client.js
1052
+ import sharp from "sharp";
1053
+
1054
+ // ../../packages/providers-google/dist/image-size.js
1055
+ var DEFAULT_IMAGE_MODEL2 = "gemini-nano-banana-2.1";
1056
+ var native = {
1057
+ "1:1": [1024, 1024],
1058
+ "1:4": [512, 2048],
1059
+ "1:8": [384, 3072],
1060
+ "2:3": [848, 1264],
1061
+ "3:2": [1264, 848],
1062
+ "3:4": [896, 1200],
1063
+ "4:1": [2048, 512],
1064
+ "4:3": [1200, 896],
1065
+ "4:5": [928, 1152],
1066
+ "5:4": [1152, 928],
1067
+ "8:1": [3072, 384],
1068
+ "9:16": [768, 1376],
1069
+ "16:9": [1376, 768],
1070
+ "21:9": [1584, 672]
1071
+ };
1072
+ var legacy = {
1073
+ "1:1": [1024, 1024],
1074
+ "2:3": [832, 1248],
1075
+ "3:2": [1248, 832],
1076
+ "3:4": [864, 1184],
1077
+ "4:3": [1184, 864],
1078
+ "4:5": [896, 1152],
1079
+ "5:4": [1152, 896],
1080
+ "9:16": [768, 1344],
1081
+ "16:9": [1344, 768],
1082
+ "21:9": [1536, 672]
1083
+ };
1084
+ function resolveGeminiSize(size, model = DEFAULT_IMAGE_MODEL2) {
1085
+ const value = (size ?? "auto").trim().toLowerCase();
1086
+ const table = model === "gemini-2.5-flash-image" ? legacy : native;
1087
+ const restricted = model === "gemini-3-pro-image" || model === "nano-banana-pro-preview";
1088
+ const ratios = Object.keys(table).filter((r) => !restricted || !["1:4", "4:1", "1:8", "8:1"].includes(r));
1089
+ const presets = {
1090
+ square: "1:1",
1091
+ portrait: "2:3",
1092
+ landscape: "3:2",
1093
+ wide: "16:9",
1094
+ widescreen: "16:9"
1095
+ };
1096
+ const normalized = presets[value] ?? value;
1097
+ const pixels = /^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/.exec(normalized);
1098
+ const ratio = /^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)$/.exec(normalized);
1099
+ const auto = normalized === "auto" || normalized === "";
1100
+ const width = Number(pixels?.[1] ?? ratio?.[1] ?? (auto ? 1 : normalized));
1101
+ const height = Number(pixels?.[2] ?? ratio?.[2] ?? 1);
1102
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
1103
+ throw new ProviderError({
1104
+ kind: "usage",
1105
+ message: "Invalid Gemini size. Use WIDTHxHEIGHT, an aspect ratio, preset, or auto."
1106
+ });
1107
+ }
1108
+ const aspect = width / height;
1109
+ const selected = ratios.sort((a, b) => {
1110
+ const number = (r) => {
1111
+ const [x, y] = r.split(":").map(Number);
1112
+ return x / y;
1113
+ };
1114
+ return Math.abs(Math.log(number(a) / aspect)) - Math.abs(Math.log(number(b) / aspect));
1115
+ })[0];
1116
+ 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];
1117
+ const candidates = scales.map((scale) => ({
1118
+ width: table[selected][0] * scale,
1119
+ height: table[selected][1] * scale,
1120
+ scale
1121
+ }));
1122
+ 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);
1123
+ return {
1124
+ ...chosen,
1125
+ size: `${chosen.width}x${chosen.height}`,
1126
+ pixels: chosen.width * chosen.height,
1127
+ aspectRatio: auto ? void 0 : selected,
1128
+ imageSize: auto || model === "gemini-2.5-flash-image" ? void 0 : chosen.scale === 0.5 ? "512" : `${chosen.scale}K`
1129
+ };
1130
+ }
1131
+
1132
+ // ../../packages/providers-google/dist/gemini-client.js
1133
+ var error = (kind, message, statusCode) => new ProviderError({ kind, message, statusCode });
1134
+ var GeminiImageProvider = class {
1135
+ getKey;
1136
+ options;
1137
+ name = "gemini";
1138
+ sizePolicy = {
1139
+ resolveSize: (size) => resolveGeminiSize(size, this.options.imageModel)
1140
+ };
1141
+ constructor(getKey, options = {}) {
1142
+ this.getKey = getKey;
1143
+ this.options = options;
1144
+ }
1145
+ generate(request) {
1146
+ return this.run(request, request.referenceImages ?? []);
1147
+ }
1148
+ edit(request) {
1149
+ if (request.mask)
1150
+ throw error("usage", "Gemini does not support --mask. Describe the region to edit in the prompt.");
1151
+ return this.run(request, [
1152
+ request.sourceImage,
1153
+ ...request.referenceImages ?? []
1154
+ ]);
1155
+ }
1156
+ async run(request, inputs) {
1157
+ const model = (request.model ?? this.options.imageModel ?? process.env["IMAGEGEN_GEMINI_IMAGE_MODEL"] ?? DEFAULT_IMAGE_MODEL2).trim();
1158
+ if (!model)
1159
+ throw error("usage", "Image model must not be blank.");
1160
+ if (request.quality && request.quality.trim().toLowerCase() !== "auto")
1161
+ throw error("usage", "Gemini supports only --quality auto. Use --size to select native resolution.");
1162
+ if ((request.referenceImages?.length ?? 0) > MAX_REFERENCE_IMAGES)
1163
+ throw error("usage", "At most four reference images are allowed.");
1164
+ if (model === "gemini-2.5-flash-image" && inputs.length > 3)
1165
+ throw error("usage", "gemini-2.5-flash-image supports up to three input images in this tool, including the edit source.");
1166
+ if (inputs.some((i) => !["image/png", "image/jpeg", "image/webp"].includes(i.contentType)))
1167
+ throw error("usage", "Gemini inputs must be PNG, JPEG, or WebP.");
1168
+ const count = request.count ?? 1;
1169
+ if (!Number.isInteger(count) || count < 1 || count > 4)
1170
+ throw error("usage", "Image count must be from 1 to 4.");
1171
+ if (request.format && !["png", "jpeg", "jpg", "webp"].includes(request.format.trim().toLowerCase()))
1172
+ throw error("usage", "Output format must be png, jpeg, or webp.");
1173
+ const format = normalizeOutputFormat(request.format);
1174
+ const size = resolveGeminiSize(request.size, model);
1175
+ const payload = {
1176
+ model,
1177
+ store: false,
1178
+ stream: false,
1179
+ input: [
1180
+ { type: "text", text: request.prompt },
1181
+ ...inputs.map((i) => ({
1182
+ type: "image",
1183
+ mime_type: i.contentType,
1184
+ data: Buffer.from(i.data).toString("base64")
1185
+ }))
1186
+ ],
1187
+ // The live image endpoint rejects delivery even though the reference lists it.
1188
+ // Omit it and consume the default base64 response.
1189
+ response_format: {
1190
+ type: "image",
1191
+ ...format === "jpeg" ? { mime_type: "image/jpeg" } : {},
1192
+ ...size.aspectRatio ? { aspect_ratio: size.aspectRatio } : {},
1193
+ ...size.imageSize ? { image_size: size.imageSize } : {}
1194
+ }
1195
+ };
1196
+ if (Buffer.byteLength(JSON.stringify(payload)) > 2e7)
1197
+ throw error("usage", "Gemini inline request exceeds 20 MB. Use smaller reference images.");
1198
+ let key;
1199
+ try {
1200
+ key = await this.getKey();
1201
+ } catch {
1202
+ throw error("auth", "Could not load Gemini credentials. Run imagegen auth gemini login.");
1203
+ }
1204
+ const images = [];
1205
+ for (let i = 0; i < count; i++)
1206
+ images.push(await this.send(payload, key, format, request.prompt));
1207
+ return { provider: this.name, imageModel: model, images };
1208
+ }
1209
+ async send(payload, key, format, prompt) {
1210
+ const controller = new AbortController();
1211
+ const timer = setTimeout(() => controller.abort(), (this.options.timeoutSeconds ?? 180) * 1e3);
1212
+ try {
1213
+ const response = await (this.options.fetchImpl ?? fetch)("https://generativelanguage.googleapis.com/v1beta/interactions", {
1214
+ method: "POST",
1215
+ headers: {
1216
+ "content-type": "application/json",
1217
+ "x-goog-api-key": key
1218
+ },
1219
+ body: JSON.stringify(payload),
1220
+ signal: controller.signal
1221
+ });
1222
+ if (!response.ok) {
1223
+ const status = response.status;
1224
+ const failure2 = await response.json().catch(() => null);
1225
+ const invalidKey = Array.isArray(failure2?.error?.details) && failure2.error.details.some((detail) => detail.reason === "API_KEY_INVALID" || detail.reason === "API_KEY_EXPIRED");
1226
+ 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);
1227
+ }
1228
+ const body = await response.json().catch(() => {
1229
+ throw error("malformed", "Gemini returned invalid JSON.");
1230
+ });
1231
+ if (!body || typeof body !== "object")
1232
+ throw error("malformed", "Gemini returned an invalid interaction.");
1233
+ if (body.status !== "completed")
1234
+ throw error(body.status === "failed" ? "blocked" : "incomplete", "Gemini did not complete the image request. Check the prompt and model access.");
1235
+ if (body.steps !== void 0 && !Array.isArray(body.steps))
1236
+ throw error("malformed", "Gemini returned invalid output steps.");
1237
+ if ((body.steps ?? []).some((step) => !step || typeof step !== "object" || step.content !== void 0 && !Array.isArray(step.content))) {
1238
+ throw error("malformed", "Gemini returned invalid output content.");
1239
+ }
1240
+ const parts = (body.steps ?? []).filter((s) => s.type === "model_output").flatMap((s) => s.content ?? []).filter((p) => p && p.type === "image" && !p.thought);
1241
+ const image = parts.at(-1);
1242
+ if (!image)
1243
+ throw error("no_image", "Gemini returned no final image. Refine the image request and try again.");
1244
+ if (typeof image.data !== "string" || !image.data || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.data) || image.data.length % 4 !== 0)
1245
+ throw error("malformed", "Gemini returned invalid inline image data.");
1246
+ const original = Buffer.from(image.data, "base64");
1247
+ const metadata = await sharp(original).metadata().catch(() => {
1248
+ throw error("malformed", "Gemini returned undecodable image bytes.");
1249
+ });
1250
+ if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || image.mime_type !== `image/${metadata.format}`)
1251
+ throw error("malformed", "Gemini returned an invalid image or inconsistent MIME type.");
1252
+ const data = metadata.format === format ? original : await sharp(original).toFormat(format).toBuffer();
1253
+ return {
1254
+ data,
1255
+ mimeType: `image/${format}`,
1256
+ width: metadata.width,
1257
+ height: metadata.height,
1258
+ prompt,
1259
+ responseId: typeof body.id === "string" ? body.id : void 0
1260
+ };
1261
+ } catch (e) {
1262
+ if (controller.signal.aborted)
1263
+ throw error("timeout", "Gemini image request timed out. Try again when connectivity is restored.");
1264
+ if (e instanceof ProviderError)
1265
+ throw e;
1266
+ throw error("transport", "Gemini request failed while receiving or decoding the image. Check connectivity and try again.");
1267
+ } finally {
1268
+ clearTimeout(timer);
1269
+ }
1270
+ }
1271
+ };
1272
+
1273
+ // ../../packages/providers-google/dist/auth.js
1274
+ function validateKey(value) {
1275
+ const key = value.trim();
1276
+ if (!key || /\s/.test(key))
1277
+ throw new ProviderError({
1278
+ kind: "auth",
1279
+ message: "Enter a non-empty Gemini API key without whitespace."
1280
+ });
1281
+ return key;
1282
+ }
1283
+ async function geminiStore() {
1284
+ return resolveCredentialStore();
1285
+ }
1286
+ async function storedKey(store) {
1287
+ const value = await store.load("gemini");
1288
+ if (!value || typeof value.apiKey !== "string")
1289
+ throw new ProviderError({
1290
+ kind: "auth",
1291
+ message: "Run imagegen auth gemini login to configure an API key."
1292
+ });
1293
+ return validateKey(value.apiKey);
1294
+ }
1295
+
1296
+ // ../../packages/providers-google/dist/index.js
1297
+ function createGeminiEntry() {
1298
+ return {
1299
+ provider: new GeminiImageProvider(async () => storedKey(await geminiStore())),
1300
+ authStatus: async () => {
1301
+ const store = await geminiStore();
1302
+ let configured = false;
1303
+ try {
1304
+ await storedKey(store);
1305
+ configured = true;
1306
+ } catch {
1307
+ }
1308
+ return {
1309
+ name: "gemini",
1310
+ configured,
1311
+ detail: configured ? void 0 : "Run imagegen auth gemini login to configure an API key."
1312
+ };
1313
+ }
1314
+ };
1315
+ }
1316
+
1317
+ // ../../packages/providers-configurable/dist/profiles.js
1318
+ import { readFile, mkdir, writeFile, rename, rm } from "node:fs/promises";
1319
+ import { dirname, join as join2 } from "node:path";
1320
+ import { randomUUID } from "node:crypto";
1321
+ var usage = (message) => new ProviderError({ kind: "usage", message });
1322
+ var profileStore = () => resolveCredentialStore();
1323
+ function validateApiKey(value) {
1324
+ const key = value.trim();
1325
+ if (!key || /\s/.test(key))
1326
+ throw new ProviderError({
1327
+ kind: "auth",
1328
+ message: "Enter a non-empty API key without whitespace."
1329
+ });
1330
+ return key;
1331
+ }
1332
+ function validateProfile(value) {
1333
+ if (!value || typeof value !== "object" || Array.isArray(value))
1334
+ throw usage("Invalid endpoint profile.");
1335
+ const p = value;
1336
+ if (Object.keys(p).some((k) => !["name", "api", "baseUrl", "model", "auth"].includes(k)))
1337
+ throw usage("Endpoint profiles accept only name, api, baseUrl, model, and auth; store keys through auth login.");
1338
+ if (typeof p.name !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(p.name) || ["codex", "gemini"].includes(p.name))
1339
+ throw usage("Endpoint name must use lowercase letters, digits, and hyphens, and cannot be codex or gemini.");
1340
+ if (p.api !== "openrouter" && p.api !== "openai-images")
1341
+ throw usage("API format must be openrouter or openai-images.");
1342
+ if (p.auth !== void 0 && p.auth !== "bearer" && p.auth !== "none")
1343
+ throw usage("Auth must be bearer or none.");
1344
+ const raw = p.baseUrl ?? (p.api === "openrouter" ? "https://openrouter.ai/api/v1" : void 0);
1345
+ let url;
1346
+ try {
1347
+ if (typeof raw !== "string")
1348
+ throw new Error();
1349
+ url = new URL(raw);
1350
+ } catch {
1351
+ throw usage("Supply a valid --base-url including the API prefix.");
1352
+ }
1353
+ const loopback = url.hostname === "localhost" || url.hostname === "[::1]" || url.hostname === "127.0.0.1";
1354
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
1355
+ throw usage("Base URL must use HTTPS (or loopback HTTP), without credentials, query, or fragment.");
1356
+ if (p.model !== void 0 && (typeof p.model !== "string" || !p.model.trim()))
1357
+ throw usage("Default model must not be blank.");
1358
+ return {
1359
+ name: p.name,
1360
+ api: p.api,
1361
+ baseUrl: url.toString().replace(/\/+$/, ""),
1362
+ auth: p.auth ?? "bearer",
1363
+ ...typeof p.model === "string" ? { model: p.model.trim() } : {}
1364
+ };
1365
+ }
1366
+ var ProfileRepository = class {
1367
+ path;
1368
+ credentials;
1369
+ constructor(path = join2(dirname(defaultCredentialFilePath()), "providers.json"), credentials = profileStore) {
1370
+ this.path = path;
1371
+ this.credentials = credentials;
1372
+ }
1373
+ async list() {
1374
+ let text;
1375
+ try {
1376
+ text = await readFile(this.path, "utf8");
1377
+ } catch (e) {
1378
+ if (e.code === "ENOENT")
1379
+ return [];
1380
+ throw new ProviderError({
1381
+ kind: "io",
1382
+ message: "Could not read endpoint configuration."
1383
+ });
1384
+ }
1385
+ let value;
1386
+ try {
1387
+ value = JSON.parse(text);
1388
+ } catch {
1389
+ throw usage("Endpoint configuration is invalid JSON. Repair providers.json before retrying.");
1390
+ }
1391
+ const doc = value;
1392
+ if (!doc || doc.version !== 1 || !Array.isArray(doc.profiles))
1393
+ throw usage("Endpoint configuration requires version 1 and a profiles array.");
1394
+ const profiles = doc.profiles.map(validateProfile);
1395
+ if (new Set(profiles.map((p) => p.name)).size !== profiles.length)
1396
+ throw usage("Endpoint configuration contains duplicate names.");
1397
+ return profiles;
1398
+ }
1399
+ async get(name) {
1400
+ const p = (await this.list()).find((p2) => p2.name === name);
1401
+ if (!p)
1402
+ throw usage("Unknown endpoint profile. Run imagegen providers to list providers.");
1403
+ return p;
1404
+ }
1405
+ async add(value) {
1406
+ const p = validateProfile(value);
1407
+ const profiles = await this.list();
1408
+ if (profiles.some((v) => v.name === p.name))
1409
+ throw usage("Endpoint already exists. Use providers update.");
1410
+ await this.save([...profiles, p]);
1411
+ return p;
1412
+ }
1413
+ async update(name, patch) {
1414
+ const profiles = await this.list();
1415
+ const old = profiles.find((p2) => p2.name === name);
1416
+ if (!old)
1417
+ throw usage("Unknown endpoint profile.");
1418
+ const p = validateProfile({ ...old, ...patch, name });
1419
+ if (new URL(old.baseUrl).origin !== new URL(p.baseUrl).origin || old.auth !== p.auth)
1420
+ await (await this.credentials()).clear(name);
1421
+ await this.save(profiles.map((v) => v.name === name ? p : v));
1422
+ return p;
1423
+ }
1424
+ async remove(name) {
1425
+ const profiles = await this.list();
1426
+ if (!profiles.some((p) => p.name === name))
1427
+ throw usage("Unknown endpoint profile.");
1428
+ await (await this.credentials()).clear(name);
1429
+ await this.save(profiles.filter((p) => p.name !== name));
1430
+ }
1431
+ async save(profiles) {
1432
+ const temporary = `${this.path}.${randomUUID()}.tmp`;
1433
+ try {
1434
+ await mkdir(dirname(this.path), { recursive: true });
1435
+ await writeFile(temporary, JSON.stringify({ version: 1, profiles }, null, 2) + "\n", { mode: 384 });
1436
+ await rename(temporary, this.path);
1437
+ } catch {
1438
+ throw new ProviderError({
1439
+ kind: "io",
1440
+ message: "Could not save endpoint configuration."
1441
+ });
1442
+ } finally {
1443
+ await rm(temporary, { force: true }).catch(() => {
1444
+ });
1445
+ }
1446
+ }
1447
+ };
1448
+ async function profileKey(profile, store) {
1449
+ if (profile.auth === "none")
1450
+ return void 0;
1451
+ try {
1452
+ const record2 = await store.load(profile.name);
1453
+ if (record2 && typeof record2.apiKey === "string")
1454
+ return validateApiKey(record2.apiKey);
1455
+ } catch {
1456
+ }
1457
+ throw new ProviderError({
1458
+ kind: "auth",
1459
+ message: `Run imagegen auth ${profile.name} login to configure an API key.`
1460
+ });
1461
+ }
1462
+
1463
+ // ../../packages/providers-configurable/dist/provider.js
1464
+ import sharp2 from "sharp";
1465
+
1466
+ // ../../packages/providers-configurable/dist/image-size.js
1467
+ function imageSize(value, api) {
1468
+ const size = (value ?? "auto").trim().toLowerCase();
1469
+ if (!size || size === "auto")
1470
+ return {};
1471
+ const presets = {
1472
+ square: "1024x1024",
1473
+ portrait: "1024x1536",
1474
+ landscape: "1536x1024",
1475
+ wide: "1536x864",
1476
+ widescreen: "1536x864"
1477
+ };
1478
+ if (presets[size])
1479
+ return { size: presets[size] };
1480
+ const pixels = /^(\d+)\s*x\s*(\d+)$/.exec(size);
1481
+ if (pixels && Number.isSafeInteger(Number(pixels[1])) && Number.isSafeInteger(Number(pixels[2])) && Number(pixels[1]) > 0 && Number(pixels[2]) > 0)
1482
+ return { size: `${Number(pixels[1])}x${Number(pixels[2])}` };
1483
+ const ratio = /^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)$/.exec(size);
1484
+ if (api === "openrouter" && ratio && Number(ratio[1]) > 0 && Number(ratio[2]) > 0 && Number.isFinite(Number(ratio[1]) / Number(ratio[2])))
1485
+ return { aspect_ratio: `${Number(ratio[1])}:${Number(ratio[2])}` };
1486
+ 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.");
1487
+ }
1488
+
1489
+ // ../../packages/providers-configurable/dist/http.js
1490
+ var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
1491
+ var EndpointHttp = class {
1492
+ fetchImpl;
1493
+ timeoutSeconds;
1494
+ constructor(fetchImpl = fetch, timeoutSeconds = 180) {
1495
+ this.fetchImpl = fetchImpl;
1496
+ this.timeoutSeconds = timeoutSeconds;
1497
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0)
1498
+ throw new ProviderError({
1499
+ kind: "configuration",
1500
+ message: "Request timeout must be positive and finite."
1501
+ });
1502
+ }
1503
+ async run(action) {
1504
+ const controller = new AbortController();
1505
+ const timer = setTimeout(() => controller.abort(), this.timeoutSeconds * 1e3);
1506
+ const send = async (url, init = {}, download = false) => {
1507
+ const response = await this.fetchImpl(url, {
1508
+ ...init,
1509
+ redirect: download ? "follow" : "error",
1510
+ signal: controller.signal
1511
+ });
1512
+ if (!response.ok) {
1513
+ const status = response.status;
1514
+ const failure2 = record(await response.json().catch(() => null));
1515
+ const detail = record(failure2.error);
1516
+ const upstream = record(detail.metadata).provider_name;
1517
+ const token = new Headers(init.headers).get("authorization")?.replace(/^Bearer\s+/i, "");
1518
+ 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;
1519
+ const retryHeader = response.headers.get("retry-after");
1520
+ const retryAfterSeconds = retryHeader && /^\d+$/.test(retryHeader) && Number.isSafeInteger(Number(retryHeader)) ? Number(retryHeader) : void 0;
1521
+ const upstreamQuota = status === 429 && upstreamProvider && typeof detail.message === "string" && /quota|resource.exhausted/i.test(detail.message);
1522
+ throw new ProviderError({
1523
+ kind: download ? "download" : status === 401 || status === 403 ? "auth" : status === 429 ? "rate_limit" : status >= 500 ? "server" : "usage",
1524
+ statusCode: status,
1525
+ upstreamProvider,
1526
+ retryAfterSeconds,
1527
+ 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.`
1528
+ });
1529
+ }
1530
+ return response;
1531
+ };
1532
+ try {
1533
+ return await action({
1534
+ json: async (url, init) => {
1535
+ const response = await send(url, init);
1536
+ try {
1537
+ const value = await response.json();
1538
+ if (!value || typeof value !== "object" || Array.isArray(value))
1539
+ throw new Error();
1540
+ return record(value);
1541
+ } catch {
1542
+ throw new ProviderError({
1543
+ kind: "malformed",
1544
+ message: "Endpoint returned an invalid JSON response."
1545
+ });
1546
+ }
1547
+ },
1548
+ bytes: async (url) => new Uint8Array(await (await send(url, {}, true)).arrayBuffer())
1549
+ });
1550
+ } catch (e) {
1551
+ if (controller.signal.aborted)
1552
+ throw new ProviderError({
1553
+ kind: "timeout",
1554
+ message: "Endpoint request timed out; no automatic retry was attempted."
1555
+ });
1556
+ if (e instanceof ProviderError)
1557
+ throw e;
1558
+ throw new ProviderError({
1559
+ kind: "transport",
1560
+ message: "Endpoint request failed. Check connectivity and the configured URL."
1561
+ });
1562
+ } finally {
1563
+ clearTimeout(timer);
1564
+ }
1565
+ }
1566
+ };
1567
+
1568
+ // ../../packages/providers-configurable/dist/openrouter.js
1569
+ function openRouterRequest(model, request, images) {
1570
+ return {
1571
+ path: "images",
1572
+ body: JSON.stringify({
1573
+ model,
1574
+ prompt: request.prompt,
1575
+ n: 1,
1576
+ ...imageSize(request.size, "openrouter"),
1577
+ ...request.quality && request.quality !== "auto" ? { quality: request.quality } : {},
1578
+ ...request.format ? { output_format: request.format === "jpg" ? "jpeg" : request.format } : {},
1579
+ ...images.length ? {
1580
+ input_references: images.map((image) => ({
1581
+ type: "image_url",
1582
+ image_url: {
1583
+ url: `data:${image.contentType};base64,${Buffer.from(image.data).toString("base64")}`
1584
+ }
1585
+ }))
1586
+ } : {}
1587
+ }),
1588
+ json: true
1589
+ };
1590
+ }
1591
+
1592
+ // ../../packages/providers-configurable/dist/openai-images.js
1593
+ function openAIRequest(model, request, images, mask) {
1594
+ const fields = {
1595
+ model,
1596
+ prompt: request.prompt,
1597
+ n: 1,
1598
+ ...imageSize(request.size, "openai-images"),
1599
+ ...request.quality && request.quality !== "auto" ? { quality: request.quality } : {},
1600
+ ...request.format ? { output_format: request.format === "jpg" ? "jpeg" : request.format } : {}
1601
+ };
1602
+ if (!images.length)
1603
+ return {
1604
+ path: "images/generations",
1605
+ body: JSON.stringify(fields),
1606
+ json: true
1607
+ };
1608
+ const body = new FormData();
1609
+ for (const [key, value] of Object.entries(fields))
1610
+ body.set(key, String(value));
1611
+ const add = (name, image, index) => body.append(name, new Blob([new Uint8Array(image.data)], { type: image.contentType }), image.filename ?? `image-${index}.png`);
1612
+ images.forEach((image, i) => add(images.length === 1 ? "image" : "image[]", image, i));
1613
+ if (mask)
1614
+ add("mask", mask, 0);
1615
+ return { path: "images/edits", body, json: false };
1616
+ }
1617
+
1618
+ // ../../packages/providers-configurable/dist/provider.js
1619
+ var ConfigurableImageProvider = class {
1620
+ getKey;
1621
+ name;
1622
+ profile;
1623
+ http;
1624
+ constructor(profile, getKey, options = {}) {
1625
+ this.getKey = getKey;
1626
+ this.profile = validateProfile(profile);
1627
+ this.name = profile.name;
1628
+ this.http = new EndpointHttp(options.fetchImpl, options.timeoutSeconds);
1629
+ }
1630
+ sizePolicy = {
1631
+ resolveSize: (size) => {
1632
+ const fields = imageSize(size, this.profile.api);
1633
+ if (!fields.size)
1634
+ return { size: fields.aspect_ratio ?? "auto" };
1635
+ const [width, height] = fields.size.split("x").map(Number);
1636
+ return {
1637
+ width,
1638
+ height,
1639
+ pixels: width * height,
1640
+ size: fields.size ?? fields.aspect_ratio ?? "auto"
1641
+ };
1642
+ }
1643
+ };
1644
+ generate(request) {
1645
+ return this.run(request, request.referenceImages ?? []);
1646
+ }
1647
+ edit(request) {
1648
+ return this.run(request, [request.sourceImage, ...request.referenceImages ?? []], request.mask);
1649
+ }
1650
+ async headers() {
1651
+ if (this.profile.auth === "none")
1652
+ return {};
1653
+ try {
1654
+ const key = await this.getKey();
1655
+ if (!key)
1656
+ throw new Error();
1657
+ return { authorization: `Bearer ${validateApiKey(key)}` };
1658
+ } catch {
1659
+ throw new ProviderError({
1660
+ kind: "auth",
1661
+ message: `Could not load endpoint credentials. Run imagegen auth ${this.name} login.`
1662
+ });
1663
+ }
1664
+ }
1665
+ async models() {
1666
+ const headers = await this.headers();
1667
+ return this.http.run(async (http) => {
1668
+ const body = await http.json(`${this.profile.baseUrl}/${this.profile.api === "openrouter" ? "images/models" : "models"}`, { headers });
1669
+ if (!Array.isArray(body.data))
1670
+ throw new ProviderError({
1671
+ kind: "malformed",
1672
+ message: "Model discovery response must contain a data array."
1673
+ });
1674
+ return body.data.map((value) => {
1675
+ const model = record(value);
1676
+ if (typeof model.id !== "string")
1677
+ throw new ProviderError({
1678
+ kind: "malformed",
1679
+ message: "Discovered model has no string ID."
1680
+ });
1681
+ const architecture = record(model.architecture);
1682
+ const supported = record(model.supported_parameters);
1683
+ const strings = (v) => Array.isArray(v) && v.every((s) => typeof s === "string") ? v : void 0;
1684
+ return {
1685
+ id: model.id,
1686
+ ...typeof model.name === "string" ? { name: model.name } : {},
1687
+ capabilities: this.profile.api === "openrouter" ? "reported" : "unknown",
1688
+ ...this.profile.api === "openrouter" ? {
1689
+ supportedParameters: supported,
1690
+ inputModalities: strings(architecture.input_modalities),
1691
+ outputModalities: strings(architecture.output_modalities)
1692
+ } : {}
1693
+ };
1694
+ });
1695
+ });
1696
+ }
1697
+ async run(request, images, mask) {
1698
+ const model = (request.model ?? this.profile.model ?? "").trim();
1699
+ if (!model)
1700
+ throw usage("Supply --model or configure a default model with providers update.");
1701
+ if ((request.referenceImages?.length ?? 0) > 4)
1702
+ throw usage("At most four reference images are allowed.");
1703
+ const count = request.count ?? 1;
1704
+ if (!Number.isInteger(count) || count < 1 || count > 4)
1705
+ throw usage("Image count must be from 1 to 4.");
1706
+ if (mask && this.profile.api === "openrouter")
1707
+ throw usage("OpenRouter image API does not expose masks. Describe the edit region in the prompt.");
1708
+ for (const image of [...images, ...mask ? [mask] : []])
1709
+ if (!["image/png", "image/jpeg", "image/webp"].includes(image.contentType))
1710
+ throw usage("Input images must be PNG, JPEG, or WebP.");
1711
+ const format = (request.format ?? "png").trim().toLowerCase().replace(/^jpg$/, "jpeg");
1712
+ if (!["png", "jpeg", "webp"].includes(format))
1713
+ throw usage("Output format must be png, jpeg, or webp.");
1714
+ const quality = request.quality?.trim().toLowerCase();
1715
+ if (quality && !/^[a-z][a-z0-9_-]*$/.test(quality))
1716
+ throw usage("Quality must be a provider-supported quality name.");
1717
+ const normalized = {
1718
+ ...request,
1719
+ ...request.format ? { format } : {},
1720
+ quality
1721
+ };
1722
+ const payload = this.profile.api === "openrouter" ? openRouterRequest(model, normalized, images) : openAIRequest(model, normalized, images, mask);
1723
+ const headers = await this.headers();
1724
+ const results = [];
1725
+ for (let i = 0; i < count; i++)
1726
+ results.push(await this.http.run(async (http) => {
1727
+ const body = await http.json(`${this.profile.baseUrl}/${payload.path}`, {
1728
+ method: "POST",
1729
+ headers: {
1730
+ ...headers,
1731
+ ...payload.json ? { "content-type": "application/json" } : {}
1732
+ },
1733
+ body: payload.body
1734
+ });
1735
+ if (!Array.isArray(body.data) || body.data.length !== 1)
1736
+ throw new ProviderError({
1737
+ kind: "malformed",
1738
+ message: "Endpoint must return exactly one image for a single-image request."
1739
+ });
1740
+ const image = record(body.data[0]);
1741
+ let data;
1742
+ if (typeof image.b64_json === "string") {
1743
+ if (!image.b64_json || image.b64_json.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.b64_json))
1744
+ throw new ProviderError({
1745
+ kind: "malformed",
1746
+ message: "Endpoint returned invalid base64 image data."
1747
+ });
1748
+ data = Buffer.from(image.b64_json, "base64");
1749
+ } else if (typeof image.url === "string") {
1750
+ let url;
1751
+ try {
1752
+ url = new URL(image.url);
1753
+ } catch {
1754
+ throw new ProviderError({
1755
+ kind: "malformed",
1756
+ message: "Endpoint returned an invalid image URL."
1757
+ });
1758
+ }
1759
+ 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)))
1760
+ throw new ProviderError({
1761
+ kind: "malformed",
1762
+ message: "Image download URL must use HTTPS or loopback HTTP without embedded credentials."
1763
+ });
1764
+ data = await http.bytes(url.toString());
1765
+ } else
1766
+ throw new ProviderError({
1767
+ kind: "no_image",
1768
+ message: "Endpoint returned no image bytes or download URL."
1769
+ });
1770
+ try {
1771
+ const metadata = await sharp2(data).metadata();
1772
+ if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || typeof image.media_type === "string" && image.media_type !== `image/${metadata.format}`)
1773
+ throw new Error();
1774
+ await sharp2(data).stats();
1775
+ if (metadata.format !== format)
1776
+ data = await sharp2(data).toFormat(format).toBuffer();
1777
+ return {
1778
+ data,
1779
+ width: metadata.width,
1780
+ height: metadata.height,
1781
+ mimeType: `image/${format}`,
1782
+ prompt: request.prompt,
1783
+ ...typeof body.id === "string" ? { responseId: body.id } : {}
1784
+ };
1785
+ } catch {
1786
+ throw new ProviderError({
1787
+ kind: "malformed",
1788
+ message: "Endpoint returned an invalid raster image or inconsistent image format."
1789
+ });
1790
+ }
1791
+ }));
1792
+ return { provider: this.name, imageModel: model, images: results };
1793
+ }
1794
+ };
1795
+ function createProfileEntry(profile) {
1796
+ return {
1797
+ provider: new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore())),
1798
+ authStatus: async () => {
1799
+ let configured = profile.auth === "none";
1800
+ if (!configured)
1801
+ try {
1802
+ await profileKey(profile, await profileStore());
1803
+ configured = true;
1804
+ } catch {
1805
+ }
1806
+ return {
1807
+ name: profile.name,
1808
+ configured,
1809
+ detail: `Run imagegen auth ${profile.name} login.`
1810
+ };
1811
+ }
1812
+ };
1813
+ }
1814
+
1815
+ // src/key-input.ts
1816
+ import { emitKeypressEvents } from "node:readline";
1817
+ var failure = (message) => new ProviderError({ kind: "auth", message });
1818
+ async function readApiKey(fromStdin, input = process.stdin, output = process.stderr, providerName = "gemini") {
1819
+ if (fromStdin) {
1820
+ let text = "";
1821
+ for await (const chunk of input) {
1822
+ text += chunk.toString();
1823
+ if (Buffer.byteLength(text) > 8192)
1824
+ throw failure("API key input is too long.");
1825
+ }
1826
+ return text;
1827
+ }
1828
+ if (!input.isTTY)
1829
+ throw failure(
1830
+ "API-key login requires a terminal, or explicit --key-stdin."
1831
+ );
1832
+ const terminal = input;
1833
+ const wasRaw = terminal.isRaw;
1834
+ const wasFlowing = input.readableFlowing === true;
1835
+ emitKeypressEvents(input);
1836
+ return new Promise((resolve3, reject) => {
1837
+ let text = "";
1838
+ const cleanup = () => {
1839
+ input.removeListener("keypress", onKey);
1840
+ input.removeListener("end", onEnd);
1841
+ input.removeListener("error", onError);
1842
+ terminal.setRawMode(wasRaw);
1843
+ if (!wasFlowing) input.pause();
1844
+ output.write("\n");
1845
+ };
1846
+ const onEnd = () => {
1847
+ cleanup();
1848
+ reject(failure("API-key login cancelled before a key was entered."));
1849
+ };
1850
+ const onError = () => {
1851
+ cleanup();
1852
+ reject(failure("Could not read the API key."));
1853
+ };
1854
+ const onKey = (value, key) => {
1855
+ if (key.ctrl && (key.name === "c" || key.name === "d")) {
1856
+ onEnd();
1857
+ return;
1858
+ }
1859
+ if (key.name === "return" || key.name === "enter") {
1860
+ cleanup();
1861
+ resolve3(text);
1862
+ return;
1863
+ }
1864
+ if (key.name === "backspace") {
1865
+ text = text.slice(0, -1);
1866
+ return;
1867
+ }
1868
+ if (value && !key.ctrl && !/[\x00-\x1f\x7f]/.test(value)) text += value;
1869
+ if (text.length > 8192) {
1870
+ cleanup();
1871
+ reject(failure("API key input is too long."));
1872
+ }
1873
+ };
1874
+ output.write(`${providerName} API key (hidden): `);
1875
+ input.on("keypress", onKey);
1876
+ input.once("end", onEnd);
1877
+ input.once("error", onError);
1878
+ terminal.setRawMode(true);
1879
+ input.resume();
1880
+ });
1881
+ }
1882
+
1010
1883
  // src/help.ts
1884
+ var PROFILE_HELP = `Named endpoint profiles:
1885
+ imagegen providers add <name> --api <openrouter|openai-images> [--base-url <url>]
1886
+ [--model <id>] [--auth <bearer|none>]
1887
+ imagegen providers show <name>
1888
+ imagegen providers update <name> [--api <format>] [--base-url <url>]
1889
+ [--model <id>] [--auth <bearer|none>]
1890
+ imagegen providers remove <name>
1891
+ imagegen auth <name> login [--key-stdin]
1892
+ imagegen auth <name> status [--show-path]
1893
+ imagegen auth <name> logout
1894
+ imagegen models --provider <name>
1895
+
1896
+ OpenRouter defaults to https://openrouter.ai/api/v1. openai-images requires
1897
+ --base-url including the API prefix (e.g. https://api.openai.com/v1).
1898
+ Keys use hidden terminal input or explicit --key-stdin and the existing vault/file
1899
+ store. --auth none supports endpoints without keys. Config is saved in providers.json
1900
+ alongside the credential file. Origin changes clear the saved key; removing a
1901
+ profile removes its credentials. Logout preserves profile configuration.
1902
+
1903
+ Select named profiles with --provider. Model precedence: --model > profile default;
1904
+ provide one if no default is saved. models explicitly discovers model metadata;
1905
+ OpenAI-compatible model listings do not establish image capabilities.
1906
+
1907
+ Supported protocols: OpenRouter dedicated Images API and OpenAI Images JSON /
1908
+ multipart endpoints, not arbitrary HTTP templates. Options vary by model.
1909
+ OpenRouter accepts aspect ratios; openai-images needs pixels or presets:
1910
+ square 1024x1024, portrait 1024x1536, landscape 1536x1024, wide 1536x864.
1911
+ Auto options are omitted. Native masks require openai-images or Codex.
1912
+ Outputs are measured and converted to PNG/JPEG/WebP without resizing. Count uses
1913
+ separate single-image requests without automatic retries or model substitution.
1914
+ `;
1011
1915
  var IMAGE_OPTIONS = `Image options (generate, edit):
1012
1916
  --prompt <text> Prompt text (required)
1013
1917
  --out <path|dir> Output file or directory (required)
1014
1918
  --provider <name> Repeatable. Defaults to the only configured provider;
1015
1919
  required when several are configured.
1016
- --model <id> Image model override (Codex default gpt-image-2.5-flare)
1920
+ --model <id> Image model override (defaults listed below)
1017
1921
  --reference <file> Reference image for continuity (repeatable, max 4)
1018
1922
  --size <size> auto, WIDTHxHEIGHT, aspect (16:9), or preset
1019
1923
  square, portrait, landscape, wide (validated per
@@ -1022,6 +1926,18 @@ var IMAGE_OPTIONS = `Image options (generate, edit):
1022
1926
  --format <format> png, jpeg, or webp (default png)
1023
1927
  --count <n> Number of images, 1-4 (default 1)
1024
1928
 
1929
+ Gemini provider:
1930
+ gemini-nano-banana-2.1 Default; generation, references, and independent edits
1931
+ gemini-3-pro-image Precision creative work and brand consistency
1932
+ gemini-3.1-flash-image Previous-generation Flash; also supports 512 resolution
1933
+ gemini-2.5-flash-image Legacy; up to three inputs including edit source
1934
+ --quality auto only; no --mask or guaranteed native alpha transparency.
1935
+ --size normalizes to model-native ratios and resolution tiers without resizing.
1936
+ auto keeps model defaults; aspect/preset requests use 1K. JSON reports actual pixels.
1937
+ PNG/JPEG/WebP output; format conversion happens locally when needed.
1938
+ Model precedence: --model > constructor imageModel > IMAGEGEN_GEMINI_IMAGE_MODEL
1939
+ > gemini-nano-banana-2.1. Calls are independent; use prior output as --source.
1940
+
1025
1941
  Supported Codex models (account availability is determined by the provider):
1026
1942
  gpt-image-2.5-flare Default; fast image generation and editing
1027
1943
  gpt-image-2.5-sunburst Higher-precision image generation and editing
@@ -1036,13 +1952,14 @@ Codex quality: auto (default), low, medium, high; Flare/Sunburst also support
1036
1952
  xhigh and max. gpt-image-2 rejects xhigh/max.
1037
1953
  `;
1038
1954
  var GENERATE_EXAMPLES = `Examples:
1955
+ imagegen generate --provider gemini --size 16:9 --prompt "a lighthouse at dawn" --out ./gemini.png
1039
1956
  imagegen generate --prompt "a lighthouse at dawn" --out ./lighthouse.png
1040
1957
  imagegen generate --model gpt-image-2 --prompt "a lighthouse" --out ./classic.png
1041
1958
  imagegen generate --model gpt-image-2.5-sunburst --quality max --prompt "a product photo" --out ./product.png
1042
1959
  `;
1043
1960
  var EDIT_EXAMPLES = `Edit options:
1044
1961
  --source <file> Source image (required)
1045
- --mask <file> Optional edit mask
1962
+ --mask <file> Optional edit mask (Codex or openai-images)
1046
1963
 
1047
1964
  Examples:
1048
1965
  imagegen edit --format webp --source ./product.png --prompt "make the product blue" --out ./blue.webp
@@ -1051,21 +1968,28 @@ Examples:
1051
1968
  var USAGE = `imagegen - provider-agnostic image generation CLI
1052
1969
 
1053
1970
  Usage:
1971
+ imagegen auth gemini login [--key-stdin]
1972
+ imagegen auth gemini status [--show-path]
1973
+ imagegen auth gemini logout
1054
1974
  imagegen auth codex login [--timeout <minutes>] [--no-open]
1055
1975
  imagegen auth codex status [--show-path]
1056
1976
  imagegen auth codex logout
1057
1977
  imagegen generate --prompt <text> --out <path|dir> [--provider <name>] [options]
1058
1978
  imagegen edit --prompt <text> --source <file> [--mask <file>] --out <path|dir> [--provider <name>] [options]
1059
1979
  imagegen providers
1980
+ imagegen models --provider <profile-name>
1060
1981
  imagegen skill
1061
1982
  imagegen skill install --target <claude|codex|skills-dir> [--target <...>] [--force]
1062
1983
  imagegen help [command]
1063
1984
 
1985
+ ${PROFILE_HELP}
1064
1986
  ${IMAGE_OPTIONS}
1065
1987
  ${GENERATE_EXAMPLES}
1066
1988
  ${EDIT_EXAMPLES}
1067
1989
 
1068
1990
  Auth options:
1991
+ --key-stdin Read an API key from explicit stdin; otherwise hidden
1992
+ terminal entry. Keys are stored in the vault/file fallback.
1069
1993
  --timeout <minutes> Login wait timeout (default 10)
1070
1994
  --no-open Print the URL without opening a browser
1071
1995
  --show-path Show where tokens are stored (auth status)
@@ -1076,29 +2000,42 @@ Global:
1076
2000
  var COMMAND_HELP = {
1077
2001
  auth: `imagegen auth - manage provider credentials (one namespace per provider)
1078
2002
 
2003
+ ${PROFILE_HELP}
2004
+
1079
2005
  Usage:
2006
+ imagegen auth gemini login [--key-stdin]
2007
+ imagegen auth gemini status [--show-path]
2008
+ imagegen auth gemini logout
1080
2009
  imagegen auth codex login [--timeout <minutes>] [--no-open]
1081
2010
  imagegen auth codex status [--show-path]
1082
2011
  imagegen auth codex logout
1083
2012
  `,
1084
- login: `imagegen auth codex login - connect Codex via browser OAuth
2013
+ login: `imagegen auth login - connect Gemini with an API key or Codex via OAuth
1085
2014
 
1086
2015
  Usage:
2016
+ imagegen auth gemini login [--key-stdin]
2017
+ imagegen auth gemini status [--show-path]
2018
+ imagegen auth gemini logout
1087
2019
  imagegen auth codex login [--timeout <minutes>] [--no-open]
1088
2020
 
1089
- Starts a temporary localhost callback server, opens the browser,
2021
+ Gemini login reads a hidden API key (or explicit --key-stdin), stores it in the
2022
+ OS vault with file fallback, and makes no paid verification request.
2023
+ Codex login starts a temporary localhost callback server, opens the browser,
1090
2024
  exchanges the code, and stores tokens in the OS vault (file fallback).
1091
2025
  `,
1092
- status: `imagegen auth codex status - show Codex connection state
2026
+ status: `imagegen auth status - show provider connection state
1093
2027
 
1094
2028
  Usage:
2029
+ imagegen auth gemini status [--show-path]
1095
2030
  imagegen auth codex status [--show-path]
1096
2031
 
1097
- Prints JSON without secrets. --show-path reveals the token location.
2032
+ Prints JSON without secrets. --show-path reveals the credential location.
2033
+ Gemini status is local configured state, not API-key validity.
1098
2034
  `,
1099
- logout: `imagegen auth codex logout - remove stored Codex credentials
2035
+ logout: `imagegen auth logout - remove stored provider credentials
1100
2036
 
1101
2037
  Usage:
2038
+ imagegen auth gemini logout
1102
2039
  imagegen auth codex logout
1103
2040
  `,
1104
2041
  generate: `imagegen generate - create images from a prompt
@@ -1106,6 +2043,7 @@ Usage:
1106
2043
  Usage:
1107
2044
  imagegen generate --prompt <text> --out <path|dir> [--provider <name>] [options]
1108
2045
 
2046
+ ${PROFILE_HELP}
1109
2047
  ${IMAGE_OPTIONS}
1110
2048
  ${GENERATE_EXAMPLES}
1111
2049
  `,
@@ -1117,12 +2055,22 @@ Usage:
1117
2055
  ${IMAGE_OPTIONS}
1118
2056
  ${EDIT_EXAMPLES}
1119
2057
  `,
1120
- providers: `imagegen providers - list registered and configured providers
2058
+ providers: `imagegen providers - list providers and manage named endpoints
1121
2059
 
1122
2060
  Usage:
1123
2061
  imagegen providers
1124
2062
 
1125
2063
  Prints JSON with each provider and whether it is configured.
2064
+
2065
+ ${PROFILE_HELP}`,
2066
+ models: `imagegen models - discover models for a named endpoint profile
2067
+
2068
+ Usage:
2069
+ imagegen models --provider <profile-name>
2070
+
2071
+ Discovery is explicit and may require saved credentials. OpenRouter returns image
2072
+ models and reported capabilities; generic /models results have unknown image
2073
+ capabilities. This command does not generate images.
1126
2074
  `,
1127
2075
  skill: `imagegen skill - print or install the reusable agent skill
1128
2076
 
@@ -1154,9 +2102,9 @@ ${USAGE}`;
1154
2102
  }
1155
2103
 
1156
2104
  // src/skill.ts
1157
- import { mkdir, readFile, writeFile } from "node:fs/promises";
2105
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
1158
2106
  import { homedir as homedir2 } from "node:os";
1159
- import { dirname, join as join2, resolve } from "node:path";
2107
+ import { dirname as dirname2, join as join3, resolve } from "node:path";
1160
2108
  var SKILL_NAME = "imagegen-cli";
1161
2109
  var SkillExistsError = class extends Error {
1162
2110
  constructor(path) {
@@ -1165,18 +2113,18 @@ var SkillExistsError = class extends Error {
1165
2113
  }
1166
2114
  };
1167
2115
  async function skillText() {
1168
- return readFile(new URL("../skill/SKILL.md", import.meta.url), "utf8");
2116
+ return readFile2(new URL("../skill/SKILL.md", import.meta.url), "utf8");
1169
2117
  }
1170
2118
  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");
2119
+ const root = target === "claude" ? join3(homedir2(), ".claude", "skills") : target === "codex" ? join3(homedir2(), ".agents", "skills") : resolve(target);
2120
+ return join3(root, SKILL_NAME, "SKILL.md");
1173
2121
  }
1174
2122
  async function existingText(path) {
1175
2123
  try {
1176
- return await readFile(path, "utf8");
1177
- } catch (error) {
1178
- if (error.code === "ENOENT") return void 0;
1179
- throw error;
2124
+ return await readFile2(path, "utf8");
2125
+ } catch (error2) {
2126
+ if (error2.code === "ENOENT") return void 0;
2127
+ throw error2;
1180
2128
  }
1181
2129
  }
1182
2130
  async function installSkill(targets, force) {
@@ -1195,8 +2143,8 @@ async function installSkill(targets, force) {
1195
2143
  }
1196
2144
  for (const item of planned.values()) {
1197
2145
  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" });
2146
+ await mkdir2(dirname2(item.path), { recursive: true });
2147
+ await writeFile2(item.path, content, { encoding: "utf8", flag: item.status === "installed" ? "wx" : "w" });
1200
2148
  }
1201
2149
  return [...planned.values()];
1202
2150
  }
@@ -1206,8 +2154,11 @@ var EXIT_USAGE = 2;
1206
2154
  var EXIT_AUTH = 3;
1207
2155
  var EXIT_PROVIDER = 4;
1208
2156
  var EXIT_IO = 5;
1209
- function fail(kind, message, exitCode) {
1210
- process.stderr.write(`${JSON.stringify({ error: { kind, message } })}
2157
+ function errorDetails(error2) {
2158
+ return { statusCode: error2.statusCode, upstreamProvider: error2.upstreamProvider, retryAfterSeconds: error2.retryAfterSeconds };
2159
+ }
2160
+ function fail(kind, message, exitCode, details = {}) {
2161
+ process.stderr.write(`${JSON.stringify({ error: { kind, message, ...details } })}
1211
2162
  `);
1212
2163
  process.exit(exitCode);
1213
2164
  }
@@ -1222,10 +2173,10 @@ function parseArgs(argv) {
1222
2173
  }
1223
2174
  if (arg.startsWith("--")) {
1224
2175
  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)) {
2176
+ 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
2177
  fail("usage", `Unknown option --${key}. Run imagegen help.`, EXIT_USAGE);
1227
2178
  }
1228
- if (key === "no-open" || key === "show-path" || key === "force") {
2179
+ if (key === "key-stdin" || key === "no-open" || key === "show-path" || key === "force") {
1229
2180
  flags.set(key, true);
1230
2181
  continue;
1231
2182
  }
@@ -1273,7 +2224,7 @@ function redirectPort(uri) {
1273
2224
  async function loadImageInput(path, label) {
1274
2225
  let data;
1275
2226
  try {
1276
- data = await readFile2(path);
2227
+ data = await readFile3(path);
1277
2228
  } catch {
1278
2229
  fail("io", `${label} not found: ${path}`, EXIT_IO);
1279
2230
  }
@@ -1313,9 +2264,11 @@ function parseCount(flags) {
1313
2264
  }
1314
2265
  return count;
1315
2266
  }
1316
- function registry() {
2267
+ async function registry() {
1317
2268
  const registry2 = new ProviderRegistry();
1318
2269
  registry2.register(createCodexEntry());
2270
+ registry2.register(createGeminiEntry());
2271
+ for (const profile of await new ProfileRepository().list()) registry2.register(createProfileEntry(profile));
1319
2272
  return registry2;
1320
2273
  }
1321
2274
  function requestedProviders(flags) {
@@ -1335,12 +2288,12 @@ async function resolveOutPaths(out, providerName, multiProvider, count, format)
1335
2288
  const extension = `.${normalizeOutputFormat(format)}`;
1336
2289
  const prefix = multiProvider ? `${providerName}-` : "";
1337
2290
  if (isDir) {
1338
- return Array.from({ length: count }, (_, i) => join3(resolved, `${prefix}image-${i + 1}${extension}`));
2291
+ return Array.from({ length: count }, (_, i) => join4(resolved, `${prefix}image-${i + 1}${extension}`));
1339
2292
  }
1340
2293
  if (count > 1 || multiProvider) {
1341
- const dir = dirname2(resolved);
2294
+ const dir = dirname3(resolved);
1342
2295
  const base = basename(resolved, extname(resolved));
1343
- return Array.from({ length: count }, (_, i) => join3(dir, `${prefix}${base}-${i + 1}${extension}`));
2296
+ return Array.from({ length: count }, (_, i) => join4(dir, `${prefix}${base}-${i + 1}${extension}`));
1344
2297
  }
1345
2298
  return [resolved.endsWith(extension) ? resolved : `${resolved}${extension}`];
1346
2299
  }
@@ -1365,16 +2318,67 @@ async function saveImages(result, paths) {
1365
2318
  for (let i = 0; i < result.images.length; i += 1) {
1366
2319
  const path = paths[i];
1367
2320
  try {
1368
- await mkdir2(dirname2(path), { recursive: true });
1369
- await writeFile2(path, Buffer.from(result.images[i].data));
2321
+ await mkdir3(dirname3(path), { recursive: true });
2322
+ await writeFile3(path, Buffer.from(result.images[i].data));
1370
2323
  } catch {
1371
2324
  fail("io", `Could not write output file: ${path}`, EXIT_IO);
1372
2325
  }
1373
2326
  }
1374
2327
  }
2328
+ async function runApiKeyAuth(providerName, action, flags) {
2329
+ const profile = providerName === "gemini" ? void 0 : await new ProfileRepository().get(providerName);
2330
+ try {
2331
+ if (profile?.auth === "none") {
2332
+ process.stdout.write(`${JSON.stringify({ provider: providerName, ...action === "status" ? { connected: true, auth: "none" } : { status: "not-required" } })}
2333
+ `);
2334
+ return;
2335
+ }
2336
+ const store = profile ? await profileStore() : await geminiStore();
2337
+ if (action === "login") {
2338
+ const apiKey = validateApiKey(
2339
+ await readApiKey(flags.get("key-stdin") === true, process.stdin, process.stderr, providerName)
2340
+ );
2341
+ if (store.describe().backend === "file")
2342
+ process.stderr.write(
2343
+ `Warning: OS vault unavailable; credentials stored in a restricted file. Use auth ${providerName} status --show-path to inspect.
2344
+ `
2345
+ );
2346
+ await store.save(providerName, { apiKey });
2347
+ process.stdout.write(
2348
+ `${JSON.stringify({ provider: providerName, status: "connected" })}
2349
+ `
2350
+ );
2351
+ } else if (action === "logout") {
2352
+ await store.clear(providerName);
2353
+ process.stdout.write(
2354
+ `${JSON.stringify({ provider: providerName, status: "disconnected" })}
2355
+ `
2356
+ );
2357
+ } else {
2358
+ let connected = false;
2359
+ try {
2360
+ if (profile) await profileKey(profile, store);
2361
+ else await storedKey(store);
2362
+ connected = true;
2363
+ } catch (error2) {
2364
+ if (!(error2 instanceof ProviderError)) throw error2;
2365
+ }
2366
+ process.stdout.write(
2367
+ `${JSON.stringify({ provider: providerName, connected, ...flags.get("show-path") ? { store: store.describe() } : {} })}
2368
+ `
2369
+ );
2370
+ }
2371
+ } catch (error2) {
2372
+ fail(
2373
+ "auth",
2374
+ error2 instanceof ProviderError ? error2.message : "Could not access API credentials. Check the credential store and retry login.",
2375
+ EXIT_AUTH
2376
+ );
2377
+ }
2378
+ }
1375
2379
  async function runAuthLogin(providerName, flags) {
1376
2380
  if (providerName !== PROVIDER_NAME) {
1377
- fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
2381
+ fail("usage", `Unknown auth provider "${providerName}". Available: ${(await registry()).describe()}.`, EXIT_USAGE);
1378
2382
  }
1379
2383
  const uri = redirectUri();
1380
2384
  const port = redirectPort(uri);
@@ -1398,9 +2402,9 @@ ${url}
1398
2402
  `);
1399
2403
  try {
1400
2404
  callback = await waiter;
1401
- } catch (error) {
1402
- if (error instanceof LoopbackError) fail("auth", error.message, EXIT_AUTH);
1403
- throw error;
2405
+ } catch (error2) {
2406
+ if (error2 instanceof LoopbackError) fail("auth", error2.message, EXIT_AUTH);
2407
+ throw error2;
1404
2408
  }
1405
2409
  if (callback.state !== state) fail("auth", "OAuth state mismatch. Retry `imagegen auth codex login`.", EXIT_AUTH);
1406
2410
  const verifier = consumeVerifier(state);
@@ -1412,15 +2416,16 @@ ${url}
1412
2416
  const manager = new CodexTokenManager(store);
1413
2417
  try {
1414
2418
  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;
2419
+ } catch (error2) {
2420
+ if (error2 instanceof ReauthRequiredError) fail("auth", error2.message, EXIT_AUTH);
2421
+ throw error2;
1418
2422
  }
1419
2423
  process.stdout.write(`${JSON.stringify({ provider: PROVIDER_NAME, status: "connected" })}
1420
2424
  `);
1421
2425
  }
1422
2426
  async function runProviders() {
1423
- const entries = registry().names().map((name) => registry().get(name));
2427
+ const providersRegistry = await registry();
2428
+ const entries = providersRegistry.names().map((name) => providersRegistry.get(name));
1424
2429
  const providers = [];
1425
2430
  for (const entry of entries) {
1426
2431
  const status = await entry.authStatus();
@@ -1431,7 +2436,7 @@ async function runProviders() {
1431
2436
  }
1432
2437
  async function runAuthStatus(providerName, flags) {
1433
2438
  if (providerName !== PROVIDER_NAME) {
1434
- fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
2439
+ fail("usage", `Unknown auth provider "${providerName}". Available: ${(await registry()).describe()}.`, EXIT_USAGE);
1435
2440
  }
1436
2441
  const store = await resolveTokenStore();
1437
2442
  const manager = new CodexTokenManager(store);
@@ -1447,7 +2452,7 @@ async function runAuthStatus(providerName, flags) {
1447
2452
  }
1448
2453
  async function runAuthLogout(providerName) {
1449
2454
  if (providerName !== PROVIDER_NAME) {
1450
- fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
2455
+ fail("usage", `Unknown auth provider "${providerName}". Available: ${(await registry()).describe()}.`, EXIT_USAGE);
1451
2456
  }
1452
2457
  const store = await resolveTokenStore();
1453
2458
  await store.clear(PROVIDER_NAME);
@@ -1475,9 +2480,9 @@ async function runGenerate(flags) {
1475
2480
  }
1476
2481
  async function resolveProviders(requested) {
1477
2482
  try {
1478
- return await registry().resolve(requested);
1479
- } catch (error) {
1480
- fail("usage", error instanceof Error ? error.message : String(error), EXIT_USAGE);
2483
+ return await (await registry()).resolve(requested);
2484
+ } catch (error2) {
2485
+ fail("usage", error2 instanceof Error ? error2.message : String(error2), EXIT_USAGE);
1481
2486
  }
1482
2487
  }
1483
2488
  async function runEdit(flags) {
@@ -1508,13 +2513,14 @@ async function runImageRequest(request, edit, providers) {
1508
2513
  let result;
1509
2514
  try {
1510
2515
  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);
2516
+ } catch (error2) {
2517
+ if (error2 instanceof ReauthRequiredError) fail("auth", `${entry.provider.name}: ${error2.message}`, EXIT_AUTH);
2518
+ if (error2 instanceof ProviderError) {
2519
+ if (error2.kind === "auth") fail("auth", `${entry.provider.name}: ${error2.message}`, EXIT_AUTH, errorDetails(error2));
2520
+ if (error2.kind === "configuration" || error2.kind === "usage") fail(error2.kind, `${entry.provider.name}: ${error2.message}`, EXIT_USAGE, errorDetails(error2));
2521
+ fail("provider", `${entry.provider.name}: ${error2.kind}: ${error2.message}`, EXIT_PROVIDER, errorDetails(error2));
1516
2522
  }
1517
- throw error;
2523
+ throw error2;
1518
2524
  }
1519
2525
  const paths = await resolveOutPaths(request.out, entry.provider.name, multiProvider, result.images.length, request.format ?? "png");
1520
2526
  await saveImages(result, paths);
@@ -1528,6 +2534,9 @@ async function main() {
1528
2534
  return;
1529
2535
  }
1530
2536
  const [command, sub, subSub] = positionals;
2537
+ if (flags.has("key-stdin") && !(command === "auth" && sub !== "codex" && subSub === "login")) {
2538
+ fail("usage", "--key-stdin is only supported by API-key auth login.", EXIT_USAGE);
2539
+ }
1531
2540
  try {
1532
2541
  if (command === "help") {
1533
2542
  process.stdout.write(helpFor(sub));
@@ -1546,12 +2555,42 @@ async function main() {
1546
2555
  const installed = await installSkill(targets, flags.get("force") === true);
1547
2556
  process.stdout.write(`${JSON.stringify({ skill: "imagegen-cli", installed })}
1548
2557
  `);
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);
2558
+ } catch (error2) {
2559
+ if (error2 instanceof SkillExistsError) fail("usage", error2.message, EXIT_USAGE);
2560
+ fail("io", error2 instanceof Error ? error2.message : String(error2), EXIT_IO);
1552
2561
  }
1553
2562
  return;
1554
2563
  }
2564
+ if (command === "providers" && sub) {
2565
+ if (!subSub || positionals.length !== 3) fail("usage", "Usage: imagegen providers add|show|update|remove <name> [options].", EXIT_USAGE);
2566
+ const repository = new ProfileRepository();
2567
+ if (sub === "show") process.stdout.write(`${JSON.stringify({ provider: await repository.get(subSub) })}
2568
+ `);
2569
+ else if (sub === "remove") {
2570
+ await repository.remove(subSub);
2571
+ process.stdout.write(`${JSON.stringify({ provider: subSub, status: "removed" })}
2572
+ `);
2573
+ } else if (sub === "add" || sub === "update") {
2574
+ const patch = {};
2575
+ for (const [flag, key] of [["api", "api"], ["base-url", "baseUrl"], ["auth", "auth"], ["model", "model"]]) {
2576
+ const value = flagValue(flags, flag);
2577
+ if (value !== void 0) patch[key] = value;
2578
+ }
2579
+ const profile = sub === "add" ? await repository.add({ name: subSub, ...patch }) : await repository.update(subSub, patch);
2580
+ process.stdout.write(`${JSON.stringify({ provider: profile })}
2581
+ `);
2582
+ } else fail("usage", "Unknown providers action. Use add, show, update, or remove.", EXIT_USAGE);
2583
+ return;
2584
+ }
2585
+ if (command === "models" && !sub) {
2586
+ const selected = requestedProviders(flags);
2587
+ if (!selected || selected.length !== 1) fail("usage", "Model discovery requires exactly one --provider <profile-name>.", EXIT_USAGE);
2588
+ const profile = await new ProfileRepository().get(selected[0]);
2589
+ const provider = new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore()));
2590
+ process.stdout.write(`${JSON.stringify({ provider: profile.name, models: await provider.models() })}
2591
+ `);
2592
+ return;
2593
+ }
1555
2594
  if (command === "providers" && !sub) {
1556
2595
  await runProviders();
1557
2596
  return;
@@ -1559,6 +2598,10 @@ async function main() {
1559
2598
  if (command === "auth" && sub && !subSub) {
1560
2599
  fail("usage", `Missing auth action. Usage: imagegen auth ${sub} login|status|logout.`, EXIT_USAGE);
1561
2600
  }
2601
+ if (command === "auth" && sub && sub !== "codex" && ["login", "status", "logout"].includes(subSub)) {
2602
+ await runApiKeyAuth(sub, subSub, flags);
2603
+ return;
2604
+ }
1562
2605
  if (command === "auth" && sub) {
1563
2606
  if (subSub === "login") {
1564
2607
  await runAuthLogin(sub, flags);
@@ -1582,9 +2625,12 @@ async function main() {
1582
2625
  return;
1583
2626
  }
1584
2627
  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);
2628
+ } catch (error2) {
2629
+ if (error2 instanceof ProviderError) {
2630
+ const exit = error2.kind === "auth" ? EXIT_AUTH : error2.kind === "usage" || error2.kind === "configuration" ? EXIT_USAGE : error2.kind === "io" ? EXIT_IO : EXIT_PROVIDER;
2631
+ fail(error2.kind, error2.message, exit, errorDetails(error2));
2632
+ }
2633
+ fail("provider", error2 instanceof Error ? error2.message : String(error2), EXIT_PROVIDER);
1588
2634
  }
1589
2635
  }
1590
2636
  await main();