@dorely13/ai-imagegen-cli 0.1.2 → 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, readFile, stat, writeFile } from "node:fs/promises";
5
- import { basename, dirname, extname, join as join2, resolve } 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;
@@ -278,7 +282,7 @@ function callbackPage(title, message) {
278
282
  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
283
  }
280
284
  async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3, port = LOOPBACK_PORT) {
281
- return new Promise((resolve2, reject) => {
285
+ return new Promise((resolve3, reject) => {
282
286
  let server;
283
287
  const timer = setTimeout(() => {
284
288
  closeServer();
@@ -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 {
@@ -318,17 +322,17 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
318
322
  res.writeHead(200, { "content-type": "text/html" });
319
323
  res.end(callbackPage("Connected", "ImageGenCli is connected. You can close this tab."));
320
324
  closeServer();
321
- resolve2({ code, state });
322
- } catch (error) {
323
- fail2(error instanceof Error ? error : new LoopbackError("OAuth callback failed.", "callback_error"));
325
+ resolve3({ code, state });
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: readFile2 } = await import("node:fs/promises");
566
+ const { readFile: readFile4 } = await import("node:fs/promises");
556
567
  try {
557
- const raw = await readFile2(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: mkdir2, writeFile: writeFile2, chmod } = await import("node:fs/promises");
567
- const { dirname: dirname2 } = await import("node:path");
568
- await mkdir2(dirname2(this.filePath), { recursive: true });
569
- await writeFile2(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,19 +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>
1981
+ imagegen skill
1982
+ imagegen skill install --target <claude|codex|skills-dir> [--target <...>] [--force]
1060
1983
  imagegen help [command]
1061
1984
 
1985
+ ${PROFILE_HELP}
1062
1986
  ${IMAGE_OPTIONS}
1063
1987
  ${GENERATE_EXAMPLES}
1064
1988
  ${EDIT_EXAMPLES}
1065
1989
 
1066
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.
1067
1993
  --timeout <minutes> Login wait timeout (default 10)
1068
1994
  --no-open Print the URL without opening a browser
1069
1995
  --show-path Show where tokens are stored (auth status)
@@ -1074,29 +2000,42 @@ Global:
1074
2000
  var COMMAND_HELP = {
1075
2001
  auth: `imagegen auth - manage provider credentials (one namespace per provider)
1076
2002
 
2003
+ ${PROFILE_HELP}
2004
+
1077
2005
  Usage:
2006
+ imagegen auth gemini login [--key-stdin]
2007
+ imagegen auth gemini status [--show-path]
2008
+ imagegen auth gemini logout
1078
2009
  imagegen auth codex login [--timeout <minutes>] [--no-open]
1079
2010
  imagegen auth codex status [--show-path]
1080
2011
  imagegen auth codex logout
1081
2012
  `,
1082
- 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
1083
2014
 
1084
2015
  Usage:
2016
+ imagegen auth gemini login [--key-stdin]
2017
+ imagegen auth gemini status [--show-path]
2018
+ imagegen auth gemini logout
1085
2019
  imagegen auth codex login [--timeout <minutes>] [--no-open]
1086
2020
 
1087
- 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,
1088
2024
  exchanges the code, and stores tokens in the OS vault (file fallback).
1089
2025
  `,
1090
- status: `imagegen auth codex status - show Codex connection state
2026
+ status: `imagegen auth status - show provider connection state
1091
2027
 
1092
2028
  Usage:
2029
+ imagegen auth gemini status [--show-path]
1093
2030
  imagegen auth codex status [--show-path]
1094
2031
 
1095
- 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.
1096
2034
  `,
1097
- logout: `imagegen auth codex logout - remove stored Codex credentials
2035
+ logout: `imagegen auth logout - remove stored provider credentials
1098
2036
 
1099
2037
  Usage:
2038
+ imagegen auth gemini logout
1100
2039
  imagegen auth codex logout
1101
2040
  `,
1102
2041
  generate: `imagegen generate - create images from a prompt
@@ -1104,6 +2043,7 @@ Usage:
1104
2043
  Usage:
1105
2044
  imagegen generate --prompt <text> --out <path|dir> [--provider <name>] [options]
1106
2045
 
2046
+ ${PROFILE_HELP}
1107
2047
  ${IMAGE_OPTIONS}
1108
2048
  ${GENERATE_EXAMPLES}
1109
2049
  `,
@@ -1115,12 +2055,36 @@ Usage:
1115
2055
  ${IMAGE_OPTIONS}
1116
2056
  ${EDIT_EXAMPLES}
1117
2057
  `,
1118
- providers: `imagegen providers - list registered and configured providers
2058
+ providers: `imagegen providers - list providers and manage named endpoints
1119
2059
 
1120
2060
  Usage:
1121
2061
  imagegen providers
1122
2062
 
1123
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.
2074
+ `,
2075
+ skill: `imagegen skill - print or install the reusable agent skill
2076
+
2077
+ Usage:
2078
+ imagegen skill
2079
+ imagegen skill install --target <claude|codex|skills-dir> [--target <...>] [--force]
2080
+
2081
+ Without install, prints SKILL.md as Markdown. Install accepts repeatable targets:
2082
+ claude ~/.claude/skills/imagegen-cli/SKILL.md
2083
+ codex ~/.agents/skills/imagegen-cli/SKILL.md
2084
+ <skills-dir> <skills-dir>/imagegen-cli/SKILL.md
2085
+
2086
+ Install prints JSON with each path and status. Existing different content is
2087
+ preserved unless --force is passed.
1124
2088
  `,
1125
2089
  help: `imagegen help - show human-readable usage
1126
2090
 
@@ -1137,13 +2101,64 @@ ${USAGE}`;
1137
2101
  return text;
1138
2102
  }
1139
2103
 
2104
+ // src/skill.ts
2105
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2106
+ import { homedir as homedir2 } from "node:os";
2107
+ import { dirname as dirname2, join as join3, resolve } from "node:path";
2108
+ var SKILL_NAME = "imagegen-cli";
2109
+ var SkillExistsError = class extends Error {
2110
+ constructor(path) {
2111
+ super(`Skill differs at ${path}. Re-run with --force to replace it.`);
2112
+ this.name = "SkillExistsError";
2113
+ }
2114
+ };
2115
+ async function skillText() {
2116
+ return readFile2(new URL("../skill/SKILL.md", import.meta.url), "utf8");
2117
+ }
2118
+ function targetPath(target) {
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");
2121
+ }
2122
+ async function existingText(path) {
2123
+ try {
2124
+ return await readFile2(path, "utf8");
2125
+ } catch (error2) {
2126
+ if (error2.code === "ENOENT") return void 0;
2127
+ throw error2;
2128
+ }
2129
+ }
2130
+ async function installSkill(targets, force) {
2131
+ const content = await skillText();
2132
+ const planned = /* @__PURE__ */ new Map();
2133
+ for (const target of targets) {
2134
+ const path = targetPath(target);
2135
+ if (planned.has(path)) continue;
2136
+ const existing = await existingText(path);
2137
+ if (existing !== void 0 && existing !== content && !force) throw new SkillExistsError(path);
2138
+ planned.set(path, {
2139
+ target,
2140
+ path,
2141
+ status: existing === void 0 ? "installed" : existing === content ? "up-to-date" : "updated"
2142
+ });
2143
+ }
2144
+ for (const item of planned.values()) {
2145
+ if (item.status === "up-to-date") continue;
2146
+ await mkdir2(dirname2(item.path), { recursive: true });
2147
+ await writeFile2(item.path, content, { encoding: "utf8", flag: item.status === "installed" ? "wx" : "w" });
2148
+ }
2149
+ return [...planned.values()];
2150
+ }
2151
+
1140
2152
  // src/index.ts
1141
2153
  var EXIT_USAGE = 2;
1142
2154
  var EXIT_AUTH = 3;
1143
2155
  var EXIT_PROVIDER = 4;
1144
2156
  var EXIT_IO = 5;
1145
- function fail(kind, message, exitCode) {
1146
- 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 } })}
1147
2162
  `);
1148
2163
  process.exit(exitCode);
1149
2164
  }
@@ -1158,14 +2173,14 @@ function parseArgs(argv) {
1158
2173
  }
1159
2174
  if (arg.startsWith("--")) {
1160
2175
  const key = arg.slice(2);
1161
- if (!["prompt", "out", "provider", "model", "reference", "size", "quality", "format", "count", "source", "mask", "timeout", "no-open", "show-path"].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)) {
1162
2177
  fail("usage", `Unknown option --${key}. Run imagegen help.`, EXIT_USAGE);
1163
2178
  }
1164
- if (key === "no-open" || key === "show-path") {
2179
+ if (key === "key-stdin" || key === "no-open" || key === "show-path" || key === "force") {
1165
2180
  flags.set(key, true);
1166
2181
  continue;
1167
2182
  }
1168
- if (key === "reference" || key === "provider") {
2183
+ if (key === "reference" || key === "provider" || key === "target") {
1169
2184
  const value2 = argv[i + 1];
1170
2185
  if (!value2 || value2.startsWith("--")) fail("usage", `Missing value for --${key}.`, EXIT_USAGE);
1171
2186
  i += 1;
@@ -1209,7 +2224,7 @@ function redirectPort(uri) {
1209
2224
  async function loadImageInput(path, label) {
1210
2225
  let data;
1211
2226
  try {
1212
- data = await readFile(path);
2227
+ data = await readFile3(path);
1213
2228
  } catch {
1214
2229
  fail("io", `${label} not found: ${path}`, EXIT_IO);
1215
2230
  }
@@ -1249,9 +2264,11 @@ function parseCount(flags) {
1249
2264
  }
1250
2265
  return count;
1251
2266
  }
1252
- function registry() {
2267
+ async function registry() {
1253
2268
  const registry2 = new ProviderRegistry();
1254
2269
  registry2.register(createCodexEntry());
2270
+ registry2.register(createGeminiEntry());
2271
+ for (const profile of await new ProfileRepository().list()) registry2.register(createProfileEntry(profile));
1255
2272
  return registry2;
1256
2273
  }
1257
2274
  function requestedProviders(flags) {
@@ -1260,7 +2277,7 @@ function requestedProviders(flags) {
1260
2277
  return raw;
1261
2278
  }
1262
2279
  async function resolveOutPaths(out, providerName, multiProvider, count, format) {
1263
- const resolved = resolve(out);
2280
+ const resolved = resolve2(out);
1264
2281
  let isDir = false;
1265
2282
  try {
1266
2283
  const info = await stat(resolved);
@@ -1271,12 +2288,12 @@ async function resolveOutPaths(out, providerName, multiProvider, count, format)
1271
2288
  const extension = `.${normalizeOutputFormat(format)}`;
1272
2289
  const prefix = multiProvider ? `${providerName}-` : "";
1273
2290
  if (isDir) {
1274
- return Array.from({ length: count }, (_, i) => join2(resolved, `${prefix}image-${i + 1}${extension}`));
2291
+ return Array.from({ length: count }, (_, i) => join4(resolved, `${prefix}image-${i + 1}${extension}`));
1275
2292
  }
1276
2293
  if (count > 1 || multiProvider) {
1277
- const dir = dirname(resolved);
2294
+ const dir = dirname3(resolved);
1278
2295
  const base = basename(resolved, extname(resolved));
1279
- return Array.from({ length: count }, (_, i) => join2(dir, `${prefix}${base}-${i + 1}${extension}`));
2296
+ return Array.from({ length: count }, (_, i) => join4(dir, `${prefix}${base}-${i + 1}${extension}`));
1280
2297
  }
1281
2298
  return [resolved.endsWith(extension) ? resolved : `${resolved}${extension}`];
1282
2299
  }
@@ -1301,16 +2318,67 @@ async function saveImages(result, paths) {
1301
2318
  for (let i = 0; i < result.images.length; i += 1) {
1302
2319
  const path = paths[i];
1303
2320
  try {
1304
- await mkdir(dirname(path), { recursive: true });
1305
- await writeFile(path, Buffer.from(result.images[i].data));
2321
+ await mkdir3(dirname3(path), { recursive: true });
2322
+ await writeFile3(path, Buffer.from(result.images[i].data));
1306
2323
  } catch {
1307
2324
  fail("io", `Could not write output file: ${path}`, EXIT_IO);
1308
2325
  }
1309
2326
  }
1310
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
+ }
1311
2379
  async function runAuthLogin(providerName, flags) {
1312
2380
  if (providerName !== PROVIDER_NAME) {
1313
- fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
2381
+ fail("usage", `Unknown auth provider "${providerName}". Available: ${(await registry()).describe()}.`, EXIT_USAGE);
1314
2382
  }
1315
2383
  const uri = redirectUri();
1316
2384
  const port = redirectPort(uri);
@@ -1334,9 +2402,9 @@ ${url}
1334
2402
  `);
1335
2403
  try {
1336
2404
  callback = await waiter;
1337
- } catch (error) {
1338
- if (error instanceof LoopbackError) fail("auth", error.message, EXIT_AUTH);
1339
- throw error;
2405
+ } catch (error2) {
2406
+ if (error2 instanceof LoopbackError) fail("auth", error2.message, EXIT_AUTH);
2407
+ throw error2;
1340
2408
  }
1341
2409
  if (callback.state !== state) fail("auth", "OAuth state mismatch. Retry `imagegen auth codex login`.", EXIT_AUTH);
1342
2410
  const verifier = consumeVerifier(state);
@@ -1348,15 +2416,16 @@ ${url}
1348
2416
  const manager = new CodexTokenManager(store);
1349
2417
  try {
1350
2418
  await manager.exchangeAndSave({ code: callback.code, redirectUri: uri, codeVerifier: verifier });
1351
- } catch (error) {
1352
- if (error instanceof ReauthRequiredError) fail("auth", error.message, EXIT_AUTH);
1353
- throw error;
2419
+ } catch (error2) {
2420
+ if (error2 instanceof ReauthRequiredError) fail("auth", error2.message, EXIT_AUTH);
2421
+ throw error2;
1354
2422
  }
1355
2423
  process.stdout.write(`${JSON.stringify({ provider: PROVIDER_NAME, status: "connected" })}
1356
2424
  `);
1357
2425
  }
1358
2426
  async function runProviders() {
1359
- const entries = registry().names().map((name) => registry().get(name));
2427
+ const providersRegistry = await registry();
2428
+ const entries = providersRegistry.names().map((name) => providersRegistry.get(name));
1360
2429
  const providers = [];
1361
2430
  for (const entry of entries) {
1362
2431
  const status = await entry.authStatus();
@@ -1367,7 +2436,7 @@ async function runProviders() {
1367
2436
  }
1368
2437
  async function runAuthStatus(providerName, flags) {
1369
2438
  if (providerName !== PROVIDER_NAME) {
1370
- fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
2439
+ fail("usage", `Unknown auth provider "${providerName}". Available: ${(await registry()).describe()}.`, EXIT_USAGE);
1371
2440
  }
1372
2441
  const store = await resolveTokenStore();
1373
2442
  const manager = new CodexTokenManager(store);
@@ -1383,7 +2452,7 @@ async function runAuthStatus(providerName, flags) {
1383
2452
  }
1384
2453
  async function runAuthLogout(providerName) {
1385
2454
  if (providerName !== PROVIDER_NAME) {
1386
- fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
2455
+ fail("usage", `Unknown auth provider "${providerName}". Available: ${(await registry()).describe()}.`, EXIT_USAGE);
1387
2456
  }
1388
2457
  const store = await resolveTokenStore();
1389
2458
  await store.clear(PROVIDER_NAME);
@@ -1411,9 +2480,9 @@ async function runGenerate(flags) {
1411
2480
  }
1412
2481
  async function resolveProviders(requested) {
1413
2482
  try {
1414
- return await registry().resolve(requested);
1415
- } catch (error) {
1416
- 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);
1417
2486
  }
1418
2487
  }
1419
2488
  async function runEdit(flags) {
@@ -1444,13 +2513,14 @@ async function runImageRequest(request, edit, providers) {
1444
2513
  let result;
1445
2514
  try {
1446
2515
  result = edit ? await entry.provider.edit(edit) : await entry.provider.generate(request);
1447
- } catch (error) {
1448
- if (error instanceof ReauthRequiredError) fail("auth", `${entry.provider.name}: ${error.message}`, EXIT_AUTH);
1449
- if (error instanceof ProviderError) {
1450
- if (error.kind === "configuration" || error.kind === "usage") fail(error.kind, `${entry.provider.name}: ${error.message}`, EXIT_USAGE);
1451
- 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));
1452
2522
  }
1453
- throw error;
2523
+ throw error2;
1454
2524
  }
1455
2525
  const paths = await resolveOutPaths(request.out, entry.provider.name, multiProvider, result.images.length, request.format ?? "png");
1456
2526
  await saveImages(result, paths);
@@ -1464,11 +2534,63 @@ async function main() {
1464
2534
  return;
1465
2535
  }
1466
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
+ }
1467
2540
  try {
1468
2541
  if (command === "help") {
1469
2542
  process.stdout.write(helpFor(sub));
1470
2543
  return;
1471
2544
  }
2545
+ if (command === "skill" && !sub) {
2546
+ process.stdout.write(await skillText());
2547
+ return;
2548
+ }
2549
+ if (command === "skill" && sub === "install" && !subSub) {
2550
+ const targets = flags.get("target");
2551
+ if (!Array.isArray(targets) || targets.length === 0) {
2552
+ fail("usage", "Missing --target. Run imagegen help skill.", EXIT_USAGE);
2553
+ }
2554
+ try {
2555
+ const installed = await installSkill(targets, flags.get("force") === true);
2556
+ process.stdout.write(`${JSON.stringify({ skill: "imagegen-cli", installed })}
2557
+ `);
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);
2561
+ }
2562
+ return;
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
+ }
1472
2594
  if (command === "providers" && !sub) {
1473
2595
  await runProviders();
1474
2596
  return;
@@ -1476,6 +2598,10 @@ async function main() {
1476
2598
  if (command === "auth" && sub && !subSub) {
1477
2599
  fail("usage", `Missing auth action. Usage: imagegen auth ${sub} login|status|logout.`, EXIT_USAGE);
1478
2600
  }
2601
+ if (command === "auth" && sub && sub !== "codex" && ["login", "status", "logout"].includes(subSub)) {
2602
+ await runApiKeyAuth(sub, subSub, flags);
2603
+ return;
2604
+ }
1479
2605
  if (command === "auth" && sub) {
1480
2606
  if (subSub === "login") {
1481
2607
  await runAuthLogin(sub, flags);
@@ -1499,9 +2625,12 @@ async function main() {
1499
2625
  return;
1500
2626
  }
1501
2627
  fail("usage", `Unknown command: ${positionals.join(" ")}. Run \`imagegen help\`.`, EXIT_USAGE);
1502
- } catch (error) {
1503
- if (error instanceof ProviderError) fail("provider", `${error.kind}: ${error.message}`, EXIT_PROVIDER);
1504
- 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);
1505
2634
  }
1506
2635
  }
1507
2636
  await main();