@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/help.js +196 -121
- package/dist/index.js +1150 -104
- package/dist/index.js.map +1 -1
- package/dist/key-input.js +68 -0
- package/package.json +3 -2
- package/skill/SKILL.md +100 -16
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
|
|
5
|
-
import { basename, dirname as
|
|
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 = (
|
|
295
|
+
const fail2 = (error2) => {
|
|
292
296
|
closeServer();
|
|
293
|
-
reject(
|
|
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 (
|
|
323
|
-
fail2(
|
|
326
|
+
} catch (error2) {
|
|
327
|
+
fail2(error2 instanceof Error ? error2 : new LoopbackError("OAuth callback failed.", "callback_error"));
|
|
324
328
|
}
|
|
325
329
|
});
|
|
326
|
-
server.on("error", (
|
|
327
|
-
if (
|
|
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(
|
|
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/
|
|
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
|
|
490
|
-
const candidate =
|
|
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
|
|
500
|
+
async function resolveCredentialStore() {
|
|
497
501
|
try {
|
|
498
502
|
const keytar = asKeytar(await import("keytar"));
|
|
499
503
|
if (!keytar)
|
|
500
|
-
return new
|
|
501
|
-
const store = new
|
|
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
|
|
509
|
+
return new FileCredentialStore();
|
|
506
510
|
}
|
|
507
511
|
}
|
|
508
|
-
var
|
|
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
|
-
|
|
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 {
|
|
537
|
+
return {
|
|
538
|
+
backend: "vault",
|
|
539
|
+
location: "OS credential vault (service ImageGenCli)"
|
|
540
|
+
};
|
|
530
541
|
}
|
|
531
542
|
};
|
|
532
|
-
var
|
|
543
|
+
var FileCredentialStore = class {
|
|
533
544
|
filePath;
|
|
534
545
|
constructor(filePath) {
|
|
535
|
-
this.filePath = filePath ??
|
|
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:
|
|
566
|
+
const { readFile: readFile4 } = await import("node:fs/promises");
|
|
556
567
|
try {
|
|
557
|
-
const raw = await
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
581
|
+
throw error2;
|
|
563
582
|
}
|
|
564
583
|
}
|
|
565
584
|
async writeAll(all) {
|
|
566
|
-
const { mkdir:
|
|
567
|
-
const { dirname:
|
|
568
|
-
await
|
|
569
|
-
await
|
|
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
|
|
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
|
|
632
|
-
const nested =
|
|
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 =
|
|
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 (
|
|
775
|
+
} catch (error2) {
|
|
735
776
|
throw new ProviderError({
|
|
736
777
|
kind: "usage",
|
|
737
|
-
message:
|
|
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 (
|
|
906
|
-
throw toProviderError(
|
|
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
|
|
949
|
-
if (typeof
|
|
950
|
-
return
|
|
951
|
-
if (typeof
|
|
952
|
-
return
|
|
953
|
-
const
|
|
954
|
-
if (typeof
|
|
955
|
-
return
|
|
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(
|
|
964
|
-
const message =
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2116
|
+
return readFile2(new URL("../skill/SKILL.md", import.meta.url), "utf8");
|
|
1169
2117
|
}
|
|
1170
2118
|
function targetPath(target) {
|
|
1171
|
-
const root = target === "claude" ?
|
|
1172
|
-
return
|
|
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
|
|
1177
|
-
} catch (
|
|
1178
|
-
if (
|
|
1179
|
-
throw
|
|
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
|
|
1199
|
-
await
|
|
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
|
|
1210
|
-
|
|
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
|
|
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) =>
|
|
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 =
|
|
2294
|
+
const dir = dirname3(resolved);
|
|
1342
2295
|
const base = basename(resolved, extname(resolved));
|
|
1343
|
-
return Array.from({ length: count }, (_, i) =>
|
|
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
|
|
1369
|
-
await
|
|
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 (
|
|
1402
|
-
if (
|
|
1403
|
-
throw
|
|
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 (
|
|
1416
|
-
if (
|
|
1417
|
-
throw
|
|
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
|
|
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 (
|
|
1480
|
-
fail("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 (
|
|
1512
|
-
if (
|
|
1513
|
-
if (
|
|
1514
|
-
if (
|
|
1515
|
-
|
|
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
|
|
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 (
|
|
1550
|
-
if (
|
|
1551
|
-
fail("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 (
|
|
1586
|
-
if (
|
|
1587
|
-
|
|
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();
|