@dorely13/ai-imagegen-cli 0.1.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 +98 -0
- package/dist/index.js +1452 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
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";
|
|
6
|
+
|
|
7
|
+
// ../../packages/core/dist/types.js
|
|
8
|
+
var ProviderError = class extends Error {
|
|
9
|
+
kind;
|
|
10
|
+
statusCode;
|
|
11
|
+
requestId;
|
|
12
|
+
responseId;
|
|
13
|
+
constructor(details) {
|
|
14
|
+
super(details.message);
|
|
15
|
+
this.name = "ProviderError";
|
|
16
|
+
this.kind = details.kind;
|
|
17
|
+
this.statusCode = details.statusCode;
|
|
18
|
+
this.requestId = details.requestId;
|
|
19
|
+
this.responseId = details.responseId;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var MAX_REFERENCE_IMAGES = 4;
|
|
23
|
+
var MAX_IMAGE_COUNT = 4;
|
|
24
|
+
var MIN_IMAGE_COUNT = 1;
|
|
25
|
+
|
|
26
|
+
// ../../packages/core/dist/provider-registry.js
|
|
27
|
+
var ProviderRegistry = class {
|
|
28
|
+
entries = /* @__PURE__ */ new Map();
|
|
29
|
+
register(entry) {
|
|
30
|
+
this.entries.set(entry.provider.name, entry);
|
|
31
|
+
}
|
|
32
|
+
names() {
|
|
33
|
+
return [...this.entries.keys()].sort();
|
|
34
|
+
}
|
|
35
|
+
get(name) {
|
|
36
|
+
const entry = this.entries.get(name);
|
|
37
|
+
if (!entry) {
|
|
38
|
+
throw new Error(`Unknown provider "${name}". Configured providers: ${this.describe()}.`);
|
|
39
|
+
}
|
|
40
|
+
return entry;
|
|
41
|
+
}
|
|
42
|
+
async configuredNames() {
|
|
43
|
+
const names = [];
|
|
44
|
+
for (const [name, entry] of this.entries) {
|
|
45
|
+
const status = await entry.authStatus();
|
|
46
|
+
if (status.configured)
|
|
47
|
+
names.push(name);
|
|
48
|
+
}
|
|
49
|
+
return names.sort();
|
|
50
|
+
}
|
|
51
|
+
async resolve(requested) {
|
|
52
|
+
if (requested && requested.length > 0) {
|
|
53
|
+
const resolved = requested.map((name) => this.get(name));
|
|
54
|
+
for (const entry of resolved) {
|
|
55
|
+
const status = await entry.authStatus();
|
|
56
|
+
if (!status.configured) {
|
|
57
|
+
throw new Error(`Provider "${entry.provider.name}" is not configured. ${status.detail ?? "Run its auth command first."}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return resolved;
|
|
61
|
+
}
|
|
62
|
+
const configured = await this.configuredNames();
|
|
63
|
+
if (configured.length === 0) {
|
|
64
|
+
throw new Error(`No providers are configured. Available providers: ${this.describe()}.`);
|
|
65
|
+
}
|
|
66
|
+
if (configured.length > 1) {
|
|
67
|
+
throw new Error(`Multiple providers are configured (${configured.join(", ")}). Pass --provider to choose one or more.`);
|
|
68
|
+
}
|
|
69
|
+
return [this.get(configured[0])];
|
|
70
|
+
}
|
|
71
|
+
describe() {
|
|
72
|
+
const names = this.names();
|
|
73
|
+
return names.length > 0 ? names.join(", ") : "(none registered)";
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// ../../packages/core/dist/format.js
|
|
78
|
+
function normalizeOutputFormat(value) {
|
|
79
|
+
const normalized = (value ?? "").trim().toLowerCase();
|
|
80
|
+
if (normalized === "jpg" || normalized === "jpeg")
|
|
81
|
+
return "jpeg";
|
|
82
|
+
if (normalized === "webp")
|
|
83
|
+
return "webp";
|
|
84
|
+
return "png";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ../../packages/core/dist/image-size.js
|
|
88
|
+
var SIZE_MULTIPLE = 16;
|
|
89
|
+
var MINIMUM_PIXELS = 655360;
|
|
90
|
+
var MAXIMUM_PIXELS = 8294400;
|
|
91
|
+
var MAXIMUM_EDGE = 3840;
|
|
92
|
+
var TARGET_PIXELS = 1572864;
|
|
93
|
+
var MAXIMUM_ASPECT_RATIO = 3;
|
|
94
|
+
var PREFERRED_ASPECT_ERROR = 1e-3;
|
|
95
|
+
var SizeError = class extends Error {
|
|
96
|
+
constructor(message) {
|
|
97
|
+
super(message);
|
|
98
|
+
this.name = "SizeError";
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
function toSize(width, height) {
|
|
102
|
+
return { width, height, size: `${width}x${height}`, pixels: width * height };
|
|
103
|
+
}
|
|
104
|
+
function targetAspect(width, height) {
|
|
105
|
+
return validatedAspect(width / height);
|
|
106
|
+
}
|
|
107
|
+
function parseSize(value) {
|
|
108
|
+
const normalized = value.trim().toLowerCase();
|
|
109
|
+
if (normalized === "" || normalized === "auto")
|
|
110
|
+
return resolveDefault();
|
|
111
|
+
if (normalized === "square")
|
|
112
|
+
return resolveAspect(1);
|
|
113
|
+
if (normalized === "portrait")
|
|
114
|
+
return resolveAspect(2 / 3);
|
|
115
|
+
if (normalized === "landscape")
|
|
116
|
+
return resolveAspect(3 / 2);
|
|
117
|
+
if (normalized === "wide" || normalized === "widescreen")
|
|
118
|
+
return resolveAspect(16 / 9);
|
|
119
|
+
const match = /^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/.exec(normalized);
|
|
120
|
+
if (match) {
|
|
121
|
+
return resolveNearestPixels(Number(match[1]), Number(match[2]));
|
|
122
|
+
}
|
|
123
|
+
const aspect = parseAspect(normalized);
|
|
124
|
+
return resolveNearestAspect(aspect);
|
|
125
|
+
}
|
|
126
|
+
function parseAspect(value) {
|
|
127
|
+
const normalized = value.trim().toLowerCase();
|
|
128
|
+
const ratio = /^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)$/.exec(normalized);
|
|
129
|
+
if (ratio) {
|
|
130
|
+
const width = Number(ratio[1]);
|
|
131
|
+
const height = Number(ratio[2]);
|
|
132
|
+
if (width > 0 && height > 0)
|
|
133
|
+
return validatedAspect(width / height);
|
|
134
|
+
}
|
|
135
|
+
const decimal = Number(normalized);
|
|
136
|
+
if (Number.isFinite(decimal) && decimal > 0)
|
|
137
|
+
return validatedAspect(decimal);
|
|
138
|
+
throw new SizeError(`Invalid size '${value}'. Use WIDTHxHEIGHT, an aspect like 16:9, a preset (square, portrait, landscape, wide), or auto.`);
|
|
139
|
+
}
|
|
140
|
+
function validatedAspect(aspect) {
|
|
141
|
+
if (!Number.isFinite(aspect) || aspect < 1 / MAXIMUM_ASPECT_RATIO || aspect > MAXIMUM_ASPECT_RATIO) {
|
|
142
|
+
throw new SizeError("Image aspect ratio must be between 1:3 and 3:1.");
|
|
143
|
+
}
|
|
144
|
+
return aspect;
|
|
145
|
+
}
|
|
146
|
+
function resolveDefault() {
|
|
147
|
+
return resolveAspect(1);
|
|
148
|
+
}
|
|
149
|
+
function resolveAspect(aspect) {
|
|
150
|
+
const valid = validatedAspect(aspect);
|
|
151
|
+
const candidates = [];
|
|
152
|
+
for (let width = SIZE_MULTIPLE; width <= MAXIMUM_EDGE; width += SIZE_MULTIPLE) {
|
|
153
|
+
const nearestHeight = Math.round(width / valid / SIZE_MULTIPLE) * SIZE_MULTIPLE;
|
|
154
|
+
for (let offset = -1; offset <= 1; offset += 1) {
|
|
155
|
+
const height = nearestHeight + offset * SIZE_MULTIPLE;
|
|
156
|
+
if (height <= 0 || height > MAXIMUM_EDGE)
|
|
157
|
+
continue;
|
|
158
|
+
const pixels = width * height;
|
|
159
|
+
if (pixels < MINIMUM_PIXELS || pixels > MAXIMUM_PIXELS)
|
|
160
|
+
continue;
|
|
161
|
+
const actualAspect = width / height;
|
|
162
|
+
if (actualAspect < 1 / MAXIMUM_ASPECT_RATIO || actualAspect > MAXIMUM_ASPECT_RATIO)
|
|
163
|
+
continue;
|
|
164
|
+
candidates.push({
|
|
165
|
+
width,
|
|
166
|
+
height,
|
|
167
|
+
aspectError: Math.abs(Math.log(actualAspect / valid)),
|
|
168
|
+
areaDistance: Math.abs(pixels - TARGET_PIXELS)
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (candidates.length === 0) {
|
|
173
|
+
throw new SizeError("Could not derive a valid raster size for the requested aspect ratio.");
|
|
174
|
+
}
|
|
175
|
+
const exact = candidates.filter((candidate) => candidate.aspectError <= 1e-10);
|
|
176
|
+
const pool = exact.length > 0 ? exact : candidates.filter((candidate) => candidate.aspectError <= PREFERRED_ASPECT_ERROR);
|
|
177
|
+
const ranked = (pool.length > 0 ? pool : candidates).sort((a, b) => a.areaDistance - b.areaDistance || a.aspectError - b.aspectError || a.width - b.width || a.height - b.height);
|
|
178
|
+
const selected = ranked[0];
|
|
179
|
+
return toSize(selected.width, selected.height);
|
|
180
|
+
}
|
|
181
|
+
function resolveNearestAspect(aspect) {
|
|
182
|
+
return resolveAspect(aspect);
|
|
183
|
+
}
|
|
184
|
+
function resolveNearestPixels(width, height) {
|
|
185
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
|
186
|
+
throw new SizeError("Desired image pixels must be finite positive values.");
|
|
187
|
+
}
|
|
188
|
+
const aspect = targetAspect(width, height);
|
|
189
|
+
const candidates = [];
|
|
190
|
+
for (let candidateWidth = SIZE_MULTIPLE; candidateWidth <= MAXIMUM_EDGE; candidateWidth += SIZE_MULTIPLE) {
|
|
191
|
+
const nearestHeight = Math.round(candidateWidth / aspect / SIZE_MULTIPLE) * SIZE_MULTIPLE;
|
|
192
|
+
for (let offset = -1; offset <= 1; offset += 1) {
|
|
193
|
+
const candidateHeight = nearestHeight + offset * SIZE_MULTIPLE;
|
|
194
|
+
if (candidateHeight <= 0 || candidateHeight > MAXIMUM_EDGE)
|
|
195
|
+
continue;
|
|
196
|
+
const pixels = candidateWidth * candidateHeight;
|
|
197
|
+
if (pixels < MINIMUM_PIXELS || pixels > MAXIMUM_PIXELS)
|
|
198
|
+
continue;
|
|
199
|
+
const actualAspect = candidateWidth / candidateHeight;
|
|
200
|
+
const aspectError = Math.abs(Math.log(actualAspect / aspect));
|
|
201
|
+
if (aspectError > PREFERRED_ASPECT_ERROR)
|
|
202
|
+
continue;
|
|
203
|
+
candidates.push({
|
|
204
|
+
width: candidateWidth,
|
|
205
|
+
height: candidateHeight,
|
|
206
|
+
aspectError,
|
|
207
|
+
distance: Math.round((candidateWidth - width) ** 2 + (candidateHeight - height) ** 2)
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (candidates.length === 0) {
|
|
212
|
+
throw new SizeError("Could not derive a provider-valid raster near the requested pixel dimensions.");
|
|
213
|
+
}
|
|
214
|
+
const ranked = candidates.sort((a, b) => a.distance - b.distance || a.aspectError - b.aspectError || a.width - b.width || a.height - b.height);
|
|
215
|
+
const selected = ranked[0];
|
|
216
|
+
return toSize(selected.width, selected.height);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ../../packages/providers-codex/dist/codex-constants.js
|
|
220
|
+
var PROVIDER_NAME = "codex";
|
|
221
|
+
var AUTH_ENDPOINT = "https://auth.openai.com/oauth/authorize";
|
|
222
|
+
var TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
|
|
223
|
+
var CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
224
|
+
var SCOPE = "openid profile email offline_access";
|
|
225
|
+
var RESPONSES_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
|
|
226
|
+
var DEFAULT_REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
227
|
+
var LOOPBACK_PORT = 1455;
|
|
228
|
+
var CALLBACK_PATH = "/auth/callback";
|
|
229
|
+
var DEFAULT_IMAGE_MODEL = "gpt-image-2";
|
|
230
|
+
var DEFAULT_ORCHESTRATION_MODEL = "gpt-5.6-luna";
|
|
231
|
+
var DEFAULT_QUALITY = "auto";
|
|
232
|
+
var REQUEST_TIMEOUT_SECONDS = 600;
|
|
233
|
+
var PARTIAL_IMAGES = 2;
|
|
234
|
+
var MAX_REFERENCE_IMAGES2 = 4;
|
|
235
|
+
var MAX_IMAGE_COUNT2 = 4;
|
|
236
|
+
var MIN_IMAGE_COUNT2 = 1;
|
|
237
|
+
var FLOW_TIMEOUT_MINUTES = 10;
|
|
238
|
+
var USER_AGENT = "ImageGenCli";
|
|
239
|
+
|
|
240
|
+
// ../../packages/providers-codex/dist/pkce.js
|
|
241
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
242
|
+
function base64UrlEncode(data) {
|
|
243
|
+
return Buffer.from(data).toString("base64url");
|
|
244
|
+
}
|
|
245
|
+
function createPkcePair() {
|
|
246
|
+
const verifier = base64UrlEncode(randomBytes(32));
|
|
247
|
+
const challenge = base64UrlEncode(createHash("sha256").update(verifier, "ascii").digest());
|
|
248
|
+
const state = randomBytes(16).toString("hex");
|
|
249
|
+
return { verifier, challenge, state };
|
|
250
|
+
}
|
|
251
|
+
function buildAuthorizeUrl(pair, redirectUri2) {
|
|
252
|
+
const params = new URLSearchParams({
|
|
253
|
+
response_type: "code",
|
|
254
|
+
client_id: CLIENT_ID,
|
|
255
|
+
redirect_uri: redirectUri2,
|
|
256
|
+
scope: SCOPE,
|
|
257
|
+
state: pair.state,
|
|
258
|
+
code_challenge: pair.challenge,
|
|
259
|
+
code_challenge_method: "S256",
|
|
260
|
+
id_token_add_organizations: "true",
|
|
261
|
+
codex_cli_simplified_flow: "true",
|
|
262
|
+
originator: "pi"
|
|
263
|
+
});
|
|
264
|
+
return `${AUTH_ENDPOINT}?${params.toString()}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ../../packages/providers-codex/dist/loopback-server.js
|
|
268
|
+
import { createServer } from "node:http";
|
|
269
|
+
var LoopbackError = class extends Error {
|
|
270
|
+
code;
|
|
271
|
+
constructor(message, code) {
|
|
272
|
+
super(message);
|
|
273
|
+
this.name = "LoopbackError";
|
|
274
|
+
this.code = code;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
function callbackPage(title, message) {
|
|
278
|
+
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
|
+
}
|
|
280
|
+
async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3, port = LOOPBACK_PORT) {
|
|
281
|
+
return new Promise((resolve2, reject) => {
|
|
282
|
+
let server;
|
|
283
|
+
const timer = setTimeout(() => {
|
|
284
|
+
closeServer();
|
|
285
|
+
reject(new LoopbackError("OAuth flow timed out waiting for the browser callback.", "timeout"));
|
|
286
|
+
}, timeoutMs);
|
|
287
|
+
const closeServer = () => {
|
|
288
|
+
clearTimeout(timer);
|
|
289
|
+
server?.close();
|
|
290
|
+
};
|
|
291
|
+
const fail2 = (error) => {
|
|
292
|
+
closeServer();
|
|
293
|
+
reject(error);
|
|
294
|
+
};
|
|
295
|
+
server = createServer((req, res) => {
|
|
296
|
+
try {
|
|
297
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
298
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
299
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
300
|
+
res.end("Not found.");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const code = url.searchParams.get("code");
|
|
304
|
+
const state = url.searchParams.get("state");
|
|
305
|
+
const errorParam = url.searchParams.get("error");
|
|
306
|
+
if (errorParam) {
|
|
307
|
+
res.writeHead(400, { "content-type": "text/html" });
|
|
308
|
+
res.end(callbackPage("Authorization failed", "The provider returned an error. You can close this tab."));
|
|
309
|
+
fail2(new LoopbackError("OAuth provider returned an error.", "callback_error"));
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (!code || !state) {
|
|
313
|
+
res.writeHead(400, { "content-type": "text/html" });
|
|
314
|
+
res.end(callbackPage("Authorization failed", "The callback was missing the authorization code."));
|
|
315
|
+
fail2(new LoopbackError("OAuth callback was missing the authorization code.", "callback_error"));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
319
|
+
res.end(callbackPage("Connected", "ImageGenCli is connected. You can close this tab."));
|
|
320
|
+
closeServer();
|
|
321
|
+
resolve2({ code, state });
|
|
322
|
+
} catch (error) {
|
|
323
|
+
fail2(error instanceof Error ? error : new LoopbackError("OAuth callback failed.", "callback_error"));
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
server.on("error", (error) => {
|
|
327
|
+
if (error.code === "EADDRINUSE") {
|
|
328
|
+
fail2(new LoopbackError(`Port ${port} is busy. Close the conflicting process and retry.`, "port_busy"));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
fail2(error);
|
|
332
|
+
});
|
|
333
|
+
server.listen(port, "127.0.0.1");
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ../../packages/providers-codex/dist/codex-auth.js
|
|
338
|
+
var ReauthRequiredError = class extends Error {
|
|
339
|
+
constructor(message = "Codex credentials are missing or expired. Run `imagegen auth login` to reconnect.") {
|
|
340
|
+
super(message);
|
|
341
|
+
this.name = "ReauthRequiredError";
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
function toTokenSet(body, fallbackRefreshToken) {
|
|
345
|
+
return {
|
|
346
|
+
accessToken: body.access_token,
|
|
347
|
+
refreshToken: body.refresh_token ?? fallbackRefreshToken,
|
|
348
|
+
expiresAt: Date.now() + body.expires_in * 1e3,
|
|
349
|
+
scope: body.scope
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
async function readOAuthErrorCode(response) {
|
|
353
|
+
try {
|
|
354
|
+
const body = await response.json();
|
|
355
|
+
return typeof body.error === "string" ? body.error : null;
|
|
356
|
+
} catch {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function postTokenRequest(params) {
|
|
361
|
+
const response = await fetch(TOKEN_ENDPOINT, {
|
|
362
|
+
method: "POST",
|
|
363
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
364
|
+
body: new URLSearchParams(params)
|
|
365
|
+
});
|
|
366
|
+
if (!response.ok) {
|
|
367
|
+
const errorCode = await readOAuthErrorCode(response.clone());
|
|
368
|
+
if (response.status === 401 || response.status === 400 && (errorCode === "invalid_grant" || errorCode === "invalid_token")) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
throw new Error(`Token request failed with status ${response.status}.`);
|
|
372
|
+
}
|
|
373
|
+
const body = await response.json();
|
|
374
|
+
return toTokenSet(body, params["refresh_token"]);
|
|
375
|
+
}
|
|
376
|
+
async function exchangeCodeForTokens(args) {
|
|
377
|
+
const tokens = await postTokenRequest({
|
|
378
|
+
grant_type: "authorization_code",
|
|
379
|
+
client_id: CLIENT_ID,
|
|
380
|
+
code: args.code,
|
|
381
|
+
redirect_uri: args.redirectUri,
|
|
382
|
+
code_verifier: args.codeVerifier
|
|
383
|
+
});
|
|
384
|
+
if (!tokens) {
|
|
385
|
+
throw new ReauthRequiredError("The authorization code was rejected. Run `imagegen auth login` again.");
|
|
386
|
+
}
|
|
387
|
+
return tokens;
|
|
388
|
+
}
|
|
389
|
+
async function refreshTokens(refreshToken) {
|
|
390
|
+
return postTokenRequest({
|
|
391
|
+
grant_type: "refresh_token",
|
|
392
|
+
client_id: CLIENT_ID,
|
|
393
|
+
refresh_token: refreshToken
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function isTokenExpired(tokens, now = Date.now()) {
|
|
397
|
+
return tokens.expiresAt <= now;
|
|
398
|
+
}
|
|
399
|
+
var pendingVerifiers = /* @__PURE__ */ new Map();
|
|
400
|
+
function startPkceFlow(redirectUri2) {
|
|
401
|
+
const pair = createPkcePair();
|
|
402
|
+
prunePendingVerifiers();
|
|
403
|
+
pendingVerifiers.set(pair.state, { verifier: pair.verifier, createdAt: Date.now() });
|
|
404
|
+
return { url: buildAuthorizeUrl(pair, redirectUri2), state: pair.state };
|
|
405
|
+
}
|
|
406
|
+
function consumeVerifier(state) {
|
|
407
|
+
const entry = pendingVerifiers.get(state);
|
|
408
|
+
pendingVerifiers.delete(state);
|
|
409
|
+
if (!entry)
|
|
410
|
+
return null;
|
|
411
|
+
if (Date.now() - entry.createdAt > FLOW_TIMEOUT_MINUTES * 60 * 1e3)
|
|
412
|
+
return null;
|
|
413
|
+
return entry.verifier;
|
|
414
|
+
}
|
|
415
|
+
function prunePendingVerifiers() {
|
|
416
|
+
const cutoff = Date.now() - FLOW_TIMEOUT_MINUTES * 60 * 1e3;
|
|
417
|
+
for (const [state, entry] of pendingVerifiers) {
|
|
418
|
+
if (entry.createdAt < cutoff)
|
|
419
|
+
pendingVerifiers.delete(state);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ../../packages/providers-codex/dist/codex-token-manager.js
|
|
424
|
+
var CodexTokenManager = class {
|
|
425
|
+
store;
|
|
426
|
+
pendingRefresh = null;
|
|
427
|
+
constructor(store) {
|
|
428
|
+
this.store = store;
|
|
429
|
+
}
|
|
430
|
+
async exchangeAndSave(args) {
|
|
431
|
+
const tokens = await exchangeCodeForTokens(args);
|
|
432
|
+
await this.store.save(PROVIDER_NAME, tokens);
|
|
433
|
+
return tokens;
|
|
434
|
+
}
|
|
435
|
+
async status() {
|
|
436
|
+
const tokens = await this.store.load(PROVIDER_NAME);
|
|
437
|
+
if (!tokens)
|
|
438
|
+
return { connected: false };
|
|
439
|
+
if (!isTokenExpired(tokens))
|
|
440
|
+
return { connected: true, expiresAt: tokens.expiresAt };
|
|
441
|
+
if (!tokens.refreshToken)
|
|
442
|
+
return { connected: false };
|
|
443
|
+
try {
|
|
444
|
+
const refreshed = await this.getValidAccessToken();
|
|
445
|
+
void refreshed;
|
|
446
|
+
const current = await this.store.load(PROVIDER_NAME);
|
|
447
|
+
return { connected: true, expiresAt: current?.expiresAt };
|
|
448
|
+
} catch {
|
|
449
|
+
return { connected: false };
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
async logout() {
|
|
453
|
+
await this.store.clear(PROVIDER_NAME);
|
|
454
|
+
}
|
|
455
|
+
async getValidAccessToken() {
|
|
456
|
+
const tokens = await this.store.load(PROVIDER_NAME);
|
|
457
|
+
if (!tokens)
|
|
458
|
+
throw new ReauthRequiredError();
|
|
459
|
+
if (!isTokenExpired(tokens))
|
|
460
|
+
return tokens.accessToken;
|
|
461
|
+
if (!tokens.refreshToken) {
|
|
462
|
+
await this.store.clear(PROVIDER_NAME);
|
|
463
|
+
throw new ReauthRequiredError();
|
|
464
|
+
}
|
|
465
|
+
if (!this.pendingRefresh) {
|
|
466
|
+
this.pendingRefresh = this.performRefresh(tokens.refreshToken).finally(() => {
|
|
467
|
+
this.pendingRefresh = null;
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
return this.pendingRefresh;
|
|
471
|
+
}
|
|
472
|
+
async performRefresh(refreshToken) {
|
|
473
|
+
const refreshed = await refreshTokens(refreshToken);
|
|
474
|
+
if (!refreshed) {
|
|
475
|
+
await this.store.clear(PROVIDER_NAME);
|
|
476
|
+
throw new ReauthRequiredError();
|
|
477
|
+
}
|
|
478
|
+
await this.store.save(PROVIDER_NAME, refreshed);
|
|
479
|
+
return refreshed.accessToken;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// ../../packages/providers-codex/dist/token-store.js
|
|
484
|
+
import { homedir } from "node:os";
|
|
485
|
+
import { join } from "node:path";
|
|
486
|
+
function asKeytar(module) {
|
|
487
|
+
if (typeof module !== "object" || module === null)
|
|
488
|
+
return null;
|
|
489
|
+
const record = module;
|
|
490
|
+
const candidate = record["default"] ?? module;
|
|
491
|
+
if (typeof candidate.getPassword === "function" && typeof candidate.setPassword === "function" && typeof candidate.deletePassword === "function") {
|
|
492
|
+
return candidate;
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
async function resolveTokenStore() {
|
|
497
|
+
try {
|
|
498
|
+
const keytar = asKeytar(await import("keytar"));
|
|
499
|
+
if (!keytar)
|
|
500
|
+
return new FileTokenStore();
|
|
501
|
+
const store = new KeytarTokenStore(keytar);
|
|
502
|
+
await store.probe();
|
|
503
|
+
return store;
|
|
504
|
+
} catch {
|
|
505
|
+
return new FileTokenStore();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
var KeytarTokenStore = class {
|
|
509
|
+
keytar;
|
|
510
|
+
constructor(keytar) {
|
|
511
|
+
this.keytar = keytar;
|
|
512
|
+
}
|
|
513
|
+
async probe() {
|
|
514
|
+
await this.keytar.getPassword("ImageGenCli", "__probe__");
|
|
515
|
+
}
|
|
516
|
+
async load(provider) {
|
|
517
|
+
const raw = await this.keytar.getPassword("ImageGenCli", provider);
|
|
518
|
+
if (!raw)
|
|
519
|
+
return null;
|
|
520
|
+
return JSON.parse(raw);
|
|
521
|
+
}
|
|
522
|
+
async save(provider, tokens) {
|
|
523
|
+
await this.keytar.setPassword("ImageGenCli", provider, JSON.stringify(tokens));
|
|
524
|
+
}
|
|
525
|
+
async clear(provider) {
|
|
526
|
+
await this.keytar.deletePassword("ImageGenCli", provider);
|
|
527
|
+
}
|
|
528
|
+
describe() {
|
|
529
|
+
return { backend: "vault", location: "OS credential vault (service ImageGenCli)" };
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
var FileTokenStore = class {
|
|
533
|
+
filePath;
|
|
534
|
+
constructor(filePath) {
|
|
535
|
+
this.filePath = filePath ?? defaultTokenFilePath();
|
|
536
|
+
}
|
|
537
|
+
async load(provider) {
|
|
538
|
+
const all = await this.readAll();
|
|
539
|
+
return all[provider] ?? null;
|
|
540
|
+
}
|
|
541
|
+
async save(provider, tokens) {
|
|
542
|
+
const all = await this.readAll();
|
|
543
|
+
all[provider] = tokens;
|
|
544
|
+
await this.writeAll(all);
|
|
545
|
+
}
|
|
546
|
+
async clear(provider) {
|
|
547
|
+
const all = await this.readAll();
|
|
548
|
+
delete all[provider];
|
|
549
|
+
await this.writeAll(all);
|
|
550
|
+
}
|
|
551
|
+
describe() {
|
|
552
|
+
return { backend: "file", location: this.filePath };
|
|
553
|
+
}
|
|
554
|
+
async readAll() {
|
|
555
|
+
const { readFile: readFile2 } = await import("node:fs/promises");
|
|
556
|
+
try {
|
|
557
|
+
const raw = await readFile2(this.filePath, "utf8");
|
|
558
|
+
return JSON.parse(raw);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
if (error.code === "ENOENT")
|
|
561
|
+
return {};
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
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 });
|
|
570
|
+
if (process.platform !== "win32") {
|
|
571
|
+
await chmod(this.filePath, 384);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
function defaultTokenFilePath() {
|
|
576
|
+
if (process.platform === "win32") {
|
|
577
|
+
const base2 = process.env["LOCALAPPDATA"] ?? join(process.env["USERPROFILE"] ?? ".", "AppData", "Local");
|
|
578
|
+
return join(base2, "ImageGenCli", "tokens.json");
|
|
579
|
+
}
|
|
580
|
+
const base = process.env["XDG_DATA_HOME"] ?? join(homedir(), ".local", "share");
|
|
581
|
+
return join(base, "ImageGenCli", "tokens.json");
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// ../../packages/providers-codex/dist/account-id.js
|
|
585
|
+
function extractAccountId(token) {
|
|
586
|
+
const parts = token.split(".");
|
|
587
|
+
if (parts.length !== 3) {
|
|
588
|
+
throw new Error("OAuth token is not a valid JWT. Run `imagegen auth login` to reconnect.");
|
|
589
|
+
}
|
|
590
|
+
let payload;
|
|
591
|
+
try {
|
|
592
|
+
const decoded = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
593
|
+
payload = JSON.parse(decoded);
|
|
594
|
+
} catch {
|
|
595
|
+
throw new Error("OAuth token payload could not be decoded. Run `imagegen auth login` to reconnect.");
|
|
596
|
+
}
|
|
597
|
+
const accountId = typeof payload === "object" && payload !== null && "https://api.openai.com/auth" in payload && typeof payload["https://api.openai.com/auth"] === "object" && payload["https://api.openai.com/auth"] !== null ? payload["https://api.openai.com/auth"]["chatgpt_account_id"] : void 0;
|
|
598
|
+
if (typeof accountId !== "string" || accountId.length === 0) {
|
|
599
|
+
throw new Error("OAuth token does not contain a ChatGPT account id. Run `imagegen auth login` to reconnect.");
|
|
600
|
+
}
|
|
601
|
+
return accountId;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ../../packages/providers-codex/dist/sse.js
|
|
605
|
+
function classifyImageError(message) {
|
|
606
|
+
if (!message.trim())
|
|
607
|
+
return "unknown";
|
|
608
|
+
const lowerRate = message.includes("input-images") && message.includes("per min") && message.toLowerCase().includes("gpt-image");
|
|
609
|
+
if (lowerRate)
|
|
610
|
+
return "codex_image_input_rate_limit";
|
|
611
|
+
if (/rate limit|429|too many requests|please try again/i.test(message))
|
|
612
|
+
return "rate_limit";
|
|
613
|
+
if (/403|forbidden|cloudflare/i.test(message))
|
|
614
|
+
return "codex_transport_forbidden";
|
|
615
|
+
if (/invalid_value/i.test(message) && /gpt-image/i.test(message))
|
|
616
|
+
return "image_model_invalid";
|
|
617
|
+
return "api_error";
|
|
618
|
+
}
|
|
619
|
+
function readString(value, property) {
|
|
620
|
+
if (typeof value !== "object" || value === null)
|
|
621
|
+
return void 0;
|
|
622
|
+
const candidate = value[property];
|
|
623
|
+
return typeof candidate === "string" ? candidate : void 0;
|
|
624
|
+
}
|
|
625
|
+
function readEventError(event) {
|
|
626
|
+
const direct = readString(event, "message");
|
|
627
|
+
if (direct)
|
|
628
|
+
return direct;
|
|
629
|
+
if (typeof event !== "object" || event === null)
|
|
630
|
+
return void 0;
|
|
631
|
+
const record = event;
|
|
632
|
+
const nested = record["error"];
|
|
633
|
+
if (typeof nested === "object" && nested !== null) {
|
|
634
|
+
const message = readString(nested, "message");
|
|
635
|
+
if (message)
|
|
636
|
+
return message;
|
|
637
|
+
}
|
|
638
|
+
const response = record["response"];
|
|
639
|
+
if (typeof response === "object" && response !== null) {
|
|
640
|
+
const responseError = response["error"];
|
|
641
|
+
if (typeof responseError === "object" && responseError !== null) {
|
|
642
|
+
return readString(responseError, "message");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return void 0;
|
|
646
|
+
}
|
|
647
|
+
function tryReadImageItem(item) {
|
|
648
|
+
if (readString(item, "type") !== "image_generation_call")
|
|
649
|
+
return null;
|
|
650
|
+
const result = readString(item, "result");
|
|
651
|
+
if (!result)
|
|
652
|
+
return null;
|
|
653
|
+
let data;
|
|
654
|
+
try {
|
|
655
|
+
data = Buffer.from(result, "base64");
|
|
656
|
+
} catch {
|
|
657
|
+
throw new Error("invalid_image_result: Codex returned an image result that was not valid base64.");
|
|
658
|
+
}
|
|
659
|
+
return { data, revisedPrompt: readString(item, "revised_prompt"), callId: readString(item, "id") };
|
|
660
|
+
}
|
|
661
|
+
function parseImageStreamEvents(events) {
|
|
662
|
+
let eventCount = 0;
|
|
663
|
+
let lastEventType;
|
|
664
|
+
let responseId;
|
|
665
|
+
const finish = (image) => ({
|
|
666
|
+
...image,
|
|
667
|
+
responseId,
|
|
668
|
+
eventCount,
|
|
669
|
+
lastEventType
|
|
670
|
+
});
|
|
671
|
+
for (const raw of events) {
|
|
672
|
+
const line = raw.trim();
|
|
673
|
+
if (!line.startsWith("data:"))
|
|
674
|
+
continue;
|
|
675
|
+
const payload = line.slice("data:".length).trim();
|
|
676
|
+
if (payload === "[DONE]")
|
|
677
|
+
break;
|
|
678
|
+
let event;
|
|
679
|
+
try {
|
|
680
|
+
event = JSON.parse(payload);
|
|
681
|
+
} catch {
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
const type = readString(event, "type");
|
|
685
|
+
lastEventType = type ?? lastEventType;
|
|
686
|
+
eventCount += 1;
|
|
687
|
+
if (type === "response.created" && typeof event === "object" && event !== null) {
|
|
688
|
+
const inner = event["response"];
|
|
689
|
+
const id = readString(inner, "id");
|
|
690
|
+
if (id)
|
|
691
|
+
responseId = id;
|
|
692
|
+
}
|
|
693
|
+
if (type === "response.failed" || type === "error") {
|
|
694
|
+
throw new Error(`${classifyImageError(readEventError(event) ?? "")}: ${readEventError(event) ?? "Codex image generation failed."}`);
|
|
695
|
+
}
|
|
696
|
+
if (type === "response.output_item.done" && typeof event === "object" && event !== null) {
|
|
697
|
+
const item = event["item"];
|
|
698
|
+
const image = tryReadImageItem(item);
|
|
699
|
+
if (image)
|
|
700
|
+
return finish(image);
|
|
701
|
+
}
|
|
702
|
+
if (type === "response.completed" && typeof event === "object" && event !== null) {
|
|
703
|
+
const response = event["response"];
|
|
704
|
+
const id = readString(response, "id");
|
|
705
|
+
if (id)
|
|
706
|
+
responseId = id;
|
|
707
|
+
if (typeof response === "object" && response !== null) {
|
|
708
|
+
const output = response["output"];
|
|
709
|
+
if (Array.isArray(output)) {
|
|
710
|
+
for (const item of output) {
|
|
711
|
+
const image = tryReadImageItem(item);
|
|
712
|
+
if (image)
|
|
713
|
+
return finish(image);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
throw new Error(`stream_completed_without_image: Codex image generation completed without returning an image. responseId=${responseId ?? "unknown"}, events=${eventCount}, lastEvent=${lastEventType ?? "none"}.`);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ../../packages/providers-codex/dist/codex-client.js
|
|
723
|
+
function toDataUrl(image) {
|
|
724
|
+
return `data:${image.contentType};base64,${Buffer.from(image.data).toString("base64")}`;
|
|
725
|
+
}
|
|
726
|
+
function normalizeCount(count) {
|
|
727
|
+
if (!count)
|
|
728
|
+
return 1;
|
|
729
|
+
return Math.min(MAX_IMAGE_COUNT2, Math.max(MIN_IMAGE_COUNT2, Math.trunc(count)));
|
|
730
|
+
}
|
|
731
|
+
function resolveSize(size) {
|
|
732
|
+
try {
|
|
733
|
+
return parseSize(size ?? "auto");
|
|
734
|
+
} catch (error) {
|
|
735
|
+
throw new ProviderError({
|
|
736
|
+
kind: "usage",
|
|
737
|
+
message: error instanceof Error ? error.message : String(error)
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function baseImageTool(args) {
|
|
742
|
+
const tool = {
|
|
743
|
+
type: "image_generation",
|
|
744
|
+
model: args.imageModel,
|
|
745
|
+
action: args.action,
|
|
746
|
+
size: args.size && args.size.trim() ? args.size.trim() : "auto",
|
|
747
|
+
output_format: args.outputFormat,
|
|
748
|
+
background: "auto"
|
|
749
|
+
};
|
|
750
|
+
if (args.partialImages > 0)
|
|
751
|
+
tool["partial_images"] = Math.min(3, Math.max(0, args.partialImages));
|
|
752
|
+
if (args.quality && args.quality.trim() && args.quality.trim().toLowerCase() !== "auto") {
|
|
753
|
+
tool["quality"] = args.quality.trim();
|
|
754
|
+
}
|
|
755
|
+
if (args.outputFormat === "jpeg" && typeof args.outputCompression === "number") {
|
|
756
|
+
tool["output_compression"] = Math.min(100, Math.max(0, Math.trunc(args.outputCompression)));
|
|
757
|
+
}
|
|
758
|
+
return tool;
|
|
759
|
+
}
|
|
760
|
+
function buildPayload(args) {
|
|
761
|
+
const content = [{ type: "input_text", text: args.prompt }];
|
|
762
|
+
for (const reference of args.references.slice(0, MAX_REFERENCE_IMAGES2)) {
|
|
763
|
+
content.push({ type: "input_image", image_url: toDataUrl(reference), detail: "auto" });
|
|
764
|
+
}
|
|
765
|
+
return {
|
|
766
|
+
model: args.model,
|
|
767
|
+
input: [{ role: "user", content }],
|
|
768
|
+
tools: [args.tool],
|
|
769
|
+
tool_choice: { type: "image_generation" },
|
|
770
|
+
stream: true,
|
|
771
|
+
store: false
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
var CodexImageProvider = class {
|
|
775
|
+
tokens;
|
|
776
|
+
name = PROVIDER_NAME;
|
|
777
|
+
sizePolicy = { resolveSize: (size) => resolveSize(size) };
|
|
778
|
+
orchestrationModel;
|
|
779
|
+
imageModel;
|
|
780
|
+
timeoutSeconds;
|
|
781
|
+
partialImages;
|
|
782
|
+
fetchImpl;
|
|
783
|
+
constructor(tokens, options = {}) {
|
|
784
|
+
this.tokens = tokens;
|
|
785
|
+
this.orchestrationModel = options.model ?? process.env["IMAGEGEN_CODEX_MODEL"] ?? DEFAULT_ORCHESTRATION_MODEL;
|
|
786
|
+
this.imageModel = options.imageModel ?? process.env["IMAGEGEN_CODEX_IMAGE_MODEL"] ?? DEFAULT_IMAGE_MODEL;
|
|
787
|
+
this.timeoutSeconds = options.timeoutSeconds ?? REQUEST_TIMEOUT_SECONDS;
|
|
788
|
+
this.partialImages = options.partialImages ?? PARTIAL_IMAGES;
|
|
789
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
790
|
+
}
|
|
791
|
+
async generate(request) {
|
|
792
|
+
const format = normalizeOutputFormat(request.format);
|
|
793
|
+
const size = this.sizePolicy.resolveSize(request.size);
|
|
794
|
+
return this.runAll({
|
|
795
|
+
format,
|
|
796
|
+
count: normalizeCount(request.count),
|
|
797
|
+
prompt: request.prompt,
|
|
798
|
+
size,
|
|
799
|
+
build: () => buildPayload({
|
|
800
|
+
model: this.orchestrationModel,
|
|
801
|
+
prompt: request.prompt,
|
|
802
|
+
references: request.referenceImages ?? [],
|
|
803
|
+
tool: baseImageTool({
|
|
804
|
+
size: size.size,
|
|
805
|
+
quality: request.quality ?? DEFAULT_QUALITY,
|
|
806
|
+
outputFormat: format,
|
|
807
|
+
imageModel: this.imageModel,
|
|
808
|
+
action: "generate",
|
|
809
|
+
partialImages: this.partialImages
|
|
810
|
+
})
|
|
811
|
+
})
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
async edit(request) {
|
|
815
|
+
const format = normalizeOutputFormat(request.format);
|
|
816
|
+
const size = this.sizePolicy.resolveSize(request.size);
|
|
817
|
+
const references = [request.sourceImage, ...request.referenceImages ?? []];
|
|
818
|
+
return this.runAll({
|
|
819
|
+
format,
|
|
820
|
+
count: normalizeCount(request.count),
|
|
821
|
+
prompt: request.prompt,
|
|
822
|
+
size,
|
|
823
|
+
build: () => {
|
|
824
|
+
const tool = baseImageTool({
|
|
825
|
+
size: size.size,
|
|
826
|
+
quality: request.quality ?? DEFAULT_QUALITY,
|
|
827
|
+
outputFormat: format,
|
|
828
|
+
imageModel: this.imageModel,
|
|
829
|
+
action: "edit",
|
|
830
|
+
partialImages: this.partialImages
|
|
831
|
+
});
|
|
832
|
+
if (request.mask) {
|
|
833
|
+
tool["input_image_mask"] = { image_url: toDataUrl(request.mask) };
|
|
834
|
+
}
|
|
835
|
+
return buildPayload({
|
|
836
|
+
model: this.orchestrationModel,
|
|
837
|
+
prompt: request.prompt,
|
|
838
|
+
references,
|
|
839
|
+
tool
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
async runAll(args) {
|
|
845
|
+
const images = [];
|
|
846
|
+
for (let i = 0; i < args.count; i += 1) {
|
|
847
|
+
images.push(await this.sendOnce(args.build(), args.format, args.prompt, args.size));
|
|
848
|
+
}
|
|
849
|
+
return { provider: this.name, imageModel: this.imageModel, images };
|
|
850
|
+
}
|
|
851
|
+
async sendOnce(payload, format, prompt, size) {
|
|
852
|
+
const accessToken = await this.tokens.getValidAccessToken();
|
|
853
|
+
const accountId = extractAccountId(accessToken);
|
|
854
|
+
const controller = new AbortController();
|
|
855
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutSeconds * 1e3);
|
|
856
|
+
let response;
|
|
857
|
+
try {
|
|
858
|
+
response = await this.fetchImpl(RESPONSES_ENDPOINT, {
|
|
859
|
+
method: "POST",
|
|
860
|
+
headers: {
|
|
861
|
+
authorization: `Bearer ${accessToken}`,
|
|
862
|
+
"chatgpt-account-id": accountId,
|
|
863
|
+
"OpenAI-Beta": "responses=experimental",
|
|
864
|
+
originator: "pi",
|
|
865
|
+
"user-agent": USER_AGENT,
|
|
866
|
+
accept: "text/event-stream",
|
|
867
|
+
"content-type": "application/json"
|
|
868
|
+
},
|
|
869
|
+
body: JSON.stringify(payload),
|
|
870
|
+
signal: controller.signal
|
|
871
|
+
});
|
|
872
|
+
} finally {
|
|
873
|
+
clearTimeout(timer);
|
|
874
|
+
}
|
|
875
|
+
const requestId = response.headers.get("x-request-id") ?? response.headers.get("openai-request-id") ?? response.headers.get("request-id") ?? void 0;
|
|
876
|
+
if (!response.ok || !response.body) {
|
|
877
|
+
const body = await response.text().catch(() => "");
|
|
878
|
+
const kind = classifyImageError(body);
|
|
879
|
+
const detail = readErrorMessage(body);
|
|
880
|
+
throw new ProviderError({
|
|
881
|
+
kind,
|
|
882
|
+
message: detail ? `Codex image request returned ${response.status} (${kind}): ${detail}` : `Codex image request returned ${response.status} (${kind}).`,
|
|
883
|
+
statusCode: response.status,
|
|
884
|
+
requestId
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
const lines = await readSseLines(response.body);
|
|
888
|
+
let parsed;
|
|
889
|
+
try {
|
|
890
|
+
parsed = parseImageStreamEvents(lines);
|
|
891
|
+
} catch (error) {
|
|
892
|
+
throw toProviderError(error, requestId);
|
|
893
|
+
}
|
|
894
|
+
return {
|
|
895
|
+
data: parsed.data,
|
|
896
|
+
mimeType: `image/${format}`,
|
|
897
|
+
width: size.width,
|
|
898
|
+
height: size.height,
|
|
899
|
+
prompt,
|
|
900
|
+
responseId: parsed.responseId,
|
|
901
|
+
callId: parsed.callId
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
};
|
|
905
|
+
async function readSseLines(body) {
|
|
906
|
+
const reader = body.getReader();
|
|
907
|
+
const decoder = new TextDecoder();
|
|
908
|
+
let buffer = "";
|
|
909
|
+
const lines = [];
|
|
910
|
+
for (; ; ) {
|
|
911
|
+
const { done, value } = await reader.read();
|
|
912
|
+
if (done)
|
|
913
|
+
break;
|
|
914
|
+
buffer += decoder.decode(value, { stream: true });
|
|
915
|
+
let index = buffer.indexOf("\n");
|
|
916
|
+
while (index >= 0) {
|
|
917
|
+
const line = buffer.slice(0, index);
|
|
918
|
+
buffer = buffer.slice(index + 1);
|
|
919
|
+
if (line.startsWith("data:"))
|
|
920
|
+
lines.push(line);
|
|
921
|
+
index = buffer.indexOf("\n");
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (buffer.startsWith("data:"))
|
|
925
|
+
lines.push(buffer);
|
|
926
|
+
return lines;
|
|
927
|
+
}
|
|
928
|
+
function readErrorMessage(body) {
|
|
929
|
+
if (!body.trim())
|
|
930
|
+
return null;
|
|
931
|
+
try {
|
|
932
|
+
const parsed = JSON.parse(body);
|
|
933
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
934
|
+
const record = parsed;
|
|
935
|
+
if (typeof record["message"] === "string")
|
|
936
|
+
return record["message"];
|
|
937
|
+
if (typeof record["detail"] === "string")
|
|
938
|
+
return record["detail"];
|
|
939
|
+
const error = record["error"];
|
|
940
|
+
if (typeof error === "object" && error !== null && typeof error["message"] === "string") {
|
|
941
|
+
return error["message"];
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
} catch {
|
|
945
|
+
return body.slice(0, 500);
|
|
946
|
+
}
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
function toProviderError(error, requestId) {
|
|
950
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
951
|
+
const separator = message.indexOf(":");
|
|
952
|
+
const kind = separator > 0 ? message.slice(0, separator) : "api_error";
|
|
953
|
+
return new ProviderError({ kind, message, requestId });
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// ../../packages/providers-codex/dist/codex-entry.js
|
|
957
|
+
function createCodexEntry() {
|
|
958
|
+
const buildProvider = async () => {
|
|
959
|
+
const store = await resolveTokenStore();
|
|
960
|
+
return new CodexImageProvider(new CodexTokenManager(store));
|
|
961
|
+
};
|
|
962
|
+
const authStatus = async () => {
|
|
963
|
+
const store = await resolveTokenStore();
|
|
964
|
+
const manager = new CodexTokenManager(store);
|
|
965
|
+
const status = await manager.status();
|
|
966
|
+
return {
|
|
967
|
+
name: PROVIDER_NAME,
|
|
968
|
+
configured: status.connected,
|
|
969
|
+
detail: status.connected ? void 0 : "Run `imagegen auth codex login` to connect."
|
|
970
|
+
};
|
|
971
|
+
};
|
|
972
|
+
let sizePolicy;
|
|
973
|
+
return {
|
|
974
|
+
provider: {
|
|
975
|
+
name: PROVIDER_NAME,
|
|
976
|
+
get sizePolicy() {
|
|
977
|
+
if (!sizePolicy)
|
|
978
|
+
throw new Error("Codex provider is not initialized.");
|
|
979
|
+
return sizePolicy;
|
|
980
|
+
},
|
|
981
|
+
generate: async (request) => {
|
|
982
|
+
const provider = await buildProvider();
|
|
983
|
+
sizePolicy = provider.sizePolicy;
|
|
984
|
+
return provider.generate(request);
|
|
985
|
+
},
|
|
986
|
+
edit: async (request) => {
|
|
987
|
+
const provider = await buildProvider();
|
|
988
|
+
sizePolicy = provider.sizePolicy;
|
|
989
|
+
return provider.edit(request);
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
authStatus
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/help.ts
|
|
997
|
+
var USAGE = `imagegen - provider-agnostic image generation CLI
|
|
998
|
+
|
|
999
|
+
Usage:
|
|
1000
|
+
imagegen auth codex login [--timeout <minutes>] [--no-open]
|
|
1001
|
+
imagegen auth codex status [--show-path]
|
|
1002
|
+
imagegen auth codex logout
|
|
1003
|
+
imagegen generate --prompt <text> --out <path|dir> [--provider <name>] [options]
|
|
1004
|
+
imagegen edit --prompt <text> --source <file> [--mask <file>] --out <path|dir> [--provider <name>] [options]
|
|
1005
|
+
imagegen providers
|
|
1006
|
+
imagegen help [command]
|
|
1007
|
+
|
|
1008
|
+
Image options (generate, edit):
|
|
1009
|
+
--prompt <text> Prompt text (required)
|
|
1010
|
+
--out <path|dir> Output file or directory (required)
|
|
1011
|
+
--provider <name> Repeatable. Defaults to the only configured provider;
|
|
1012
|
+
required when several are configured.
|
|
1013
|
+
--reference <file> Reference image for continuity (repeatable, max 4)
|
|
1014
|
+
--size <size> auto, WIDTHxHEIGHT, aspect (16:9), or preset
|
|
1015
|
+
square, portrait, landscape, wide (validated per
|
|
1016
|
+
provider; bad aspects are rejected)
|
|
1017
|
+
--quality <quality> Quality hint (provider-specific, default auto)
|
|
1018
|
+
--format <format> png, jpeg, or webp (default png)
|
|
1019
|
+
--count <n> Number of images, 1-4 (default 1)
|
|
1020
|
+
|
|
1021
|
+
Auth options:
|
|
1022
|
+
--timeout <minutes> Login wait timeout (default 10)
|
|
1023
|
+
--no-open Print the URL without opening a browser
|
|
1024
|
+
--show-path Show where tokens are stored (auth status)
|
|
1025
|
+
|
|
1026
|
+
Global:
|
|
1027
|
+
-h, --help Show help and exit 0
|
|
1028
|
+
`;
|
|
1029
|
+
var COMMAND_HELP = {
|
|
1030
|
+
auth: `imagegen auth - manage provider credentials (one namespace per provider)
|
|
1031
|
+
|
|
1032
|
+
Usage:
|
|
1033
|
+
imagegen auth codex login [--timeout <minutes>] [--no-open]
|
|
1034
|
+
imagegen auth codex status [--show-path]
|
|
1035
|
+
imagegen auth codex logout
|
|
1036
|
+
`,
|
|
1037
|
+
login: `imagegen auth codex login - connect Codex via browser OAuth
|
|
1038
|
+
|
|
1039
|
+
Usage:
|
|
1040
|
+
imagegen auth codex login [--timeout <minutes>] [--no-open]
|
|
1041
|
+
|
|
1042
|
+
Starts a temporary localhost callback server, opens the browser,
|
|
1043
|
+
exchanges the code, and stores tokens in the OS vault (file fallback).
|
|
1044
|
+
`,
|
|
1045
|
+
status: `imagegen auth codex status - show Codex connection state
|
|
1046
|
+
|
|
1047
|
+
Usage:
|
|
1048
|
+
imagegen auth codex status [--show-path]
|
|
1049
|
+
|
|
1050
|
+
Prints JSON without secrets. --show-path reveals the token location.
|
|
1051
|
+
`,
|
|
1052
|
+
logout: `imagegen auth codex logout - remove stored Codex credentials
|
|
1053
|
+
|
|
1054
|
+
Usage:
|
|
1055
|
+
imagegen auth codex logout
|
|
1056
|
+
`,
|
|
1057
|
+
generate: `imagegen generate - create images from a prompt
|
|
1058
|
+
|
|
1059
|
+
Usage:
|
|
1060
|
+
imagegen generate --prompt <text> --out <path|dir> [--provider <name>] [options]
|
|
1061
|
+
|
|
1062
|
+
Options: --provider <name> (repeatable; run on several providers),
|
|
1063
|
+
--reference <file> (repeatable, max 4), --size, --quality,
|
|
1064
|
+
--format png|jpeg|webp, --count 1-4.
|
|
1065
|
+
`,
|
|
1066
|
+
edit: `imagegen edit - edit a source image with a prompt
|
|
1067
|
+
|
|
1068
|
+
Usage:
|
|
1069
|
+
imagegen edit --prompt <text> --source <file> [--mask <file>] --out <path|dir> [--provider <name>] [options]
|
|
1070
|
+
|
|
1071
|
+
Same image options as generate.
|
|
1072
|
+
`,
|
|
1073
|
+
providers: `imagegen providers - list registered and configured providers
|
|
1074
|
+
|
|
1075
|
+
Usage:
|
|
1076
|
+
imagegen providers
|
|
1077
|
+
|
|
1078
|
+
Prints JSON with each provider and whether it is configured.
|
|
1079
|
+
`,
|
|
1080
|
+
help: `imagegen help - show human-readable usage
|
|
1081
|
+
|
|
1082
|
+
Usage:
|
|
1083
|
+
imagegen help [command]
|
|
1084
|
+
`
|
|
1085
|
+
};
|
|
1086
|
+
function helpFor(topic) {
|
|
1087
|
+
if (!topic) return USAGE;
|
|
1088
|
+
const text = COMMAND_HELP[topic];
|
|
1089
|
+
if (!text) return `Unknown help topic "${topic}".
|
|
1090
|
+
|
|
1091
|
+
${USAGE}`;
|
|
1092
|
+
return text;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// src/index.ts
|
|
1096
|
+
var EXIT_USAGE = 2;
|
|
1097
|
+
var EXIT_AUTH = 3;
|
|
1098
|
+
var EXIT_PROVIDER = 4;
|
|
1099
|
+
var EXIT_IO = 5;
|
|
1100
|
+
function fail(kind, message, exitCode) {
|
|
1101
|
+
process.stderr.write(`${JSON.stringify({ error: { kind, message } })}
|
|
1102
|
+
`);
|
|
1103
|
+
process.exit(exitCode);
|
|
1104
|
+
}
|
|
1105
|
+
function parseArgs(argv) {
|
|
1106
|
+
const flags = /* @__PURE__ */ new Map();
|
|
1107
|
+
const positionals = [];
|
|
1108
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1109
|
+
const arg = argv[i];
|
|
1110
|
+
if (arg === "--help" || arg === "-h") {
|
|
1111
|
+
flags.set("help", true);
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
if (arg.startsWith("--")) {
|
|
1115
|
+
const key = arg.slice(2);
|
|
1116
|
+
if (key === "no-open" || key === "show-path") {
|
|
1117
|
+
flags.set(key, true);
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
if (key === "reference" || key === "provider") {
|
|
1121
|
+
const value2 = argv[i + 1];
|
|
1122
|
+
if (!value2 || value2.startsWith("--")) fail("usage", `Missing value for --${key}.`, EXIT_USAGE);
|
|
1123
|
+
i += 1;
|
|
1124
|
+
const existing = flags.get(key);
|
|
1125
|
+
flags.set(key, [...Array.isArray(existing) ? existing : [], value2]);
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const value = argv[i + 1];
|
|
1129
|
+
if (!value || value.startsWith("--")) fail("usage", `Missing value for --${key}.`, EXIT_USAGE);
|
|
1130
|
+
i += 1;
|
|
1131
|
+
flags.set(key, value);
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
positionals.push(arg);
|
|
1135
|
+
}
|
|
1136
|
+
return { flags, positionals };
|
|
1137
|
+
}
|
|
1138
|
+
function flagValue(flags, key) {
|
|
1139
|
+
const value = flags.get(key);
|
|
1140
|
+
return typeof value === "string" ? value : void 0;
|
|
1141
|
+
}
|
|
1142
|
+
function redirectUri() {
|
|
1143
|
+
return process.env["IMAGEGEN_CODEX_REDIRECT_URI"] ?? DEFAULT_REDIRECT_URI;
|
|
1144
|
+
}
|
|
1145
|
+
function redirectPort(uri) {
|
|
1146
|
+
try {
|
|
1147
|
+
const parsed = new URL(uri);
|
|
1148
|
+
if (parsed.hostname !== "localhost" && parsed.hostname !== "127.0.0.1") {
|
|
1149
|
+
fail("usage", "Custom redirect URI must use localhost.", EXIT_USAGE);
|
|
1150
|
+
}
|
|
1151
|
+
return parsed.port ? Number(parsed.port) : LOOPBACK_PORT;
|
|
1152
|
+
} catch {
|
|
1153
|
+
fail("usage", "IMAGEGEN_CODEX_REDIRECT_URI is not a valid URL.", EXIT_USAGE);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
async function loadImageInput(path, label) {
|
|
1157
|
+
let data;
|
|
1158
|
+
try {
|
|
1159
|
+
data = await readFile(path);
|
|
1160
|
+
} catch {
|
|
1161
|
+
fail("io", `${label} not found: ${path}`, EXIT_IO);
|
|
1162
|
+
}
|
|
1163
|
+
return { data: new Uint8Array(data), contentType: contentTypeFor(path), filename: basename(path) };
|
|
1164
|
+
}
|
|
1165
|
+
function contentTypeFor(path) {
|
|
1166
|
+
switch (extname(path).toLowerCase()) {
|
|
1167
|
+
case ".jpg":
|
|
1168
|
+
case ".jpeg":
|
|
1169
|
+
return "image/jpeg";
|
|
1170
|
+
case ".webp":
|
|
1171
|
+
return "image/webp";
|
|
1172
|
+
case ".png":
|
|
1173
|
+
return "image/png";
|
|
1174
|
+
default:
|
|
1175
|
+
return "image/png";
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
async function loadReferences(flags) {
|
|
1179
|
+
const raw = flags.get("reference");
|
|
1180
|
+
const paths = Array.isArray(raw) ? raw : [];
|
|
1181
|
+
if (paths.length > MAX_REFERENCE_IMAGES) {
|
|
1182
|
+
fail("usage", `At most ${MAX_REFERENCE_IMAGES} --reference images are allowed.`, EXIT_USAGE);
|
|
1183
|
+
}
|
|
1184
|
+
const images = [];
|
|
1185
|
+
for (const path of paths) {
|
|
1186
|
+
images.push(await loadImageInput(path, "Reference image"));
|
|
1187
|
+
}
|
|
1188
|
+
return images;
|
|
1189
|
+
}
|
|
1190
|
+
function parseCount(flags) {
|
|
1191
|
+
const raw = flagValue(flags, "count");
|
|
1192
|
+
if (!raw) return 1;
|
|
1193
|
+
const count = Number(raw);
|
|
1194
|
+
if (!Number.isInteger(count) || count < MIN_IMAGE_COUNT || count > MAX_IMAGE_COUNT) {
|
|
1195
|
+
fail("usage", `--count must be an integer from ${MIN_IMAGE_COUNT} to ${MAX_IMAGE_COUNT}.`, EXIT_USAGE);
|
|
1196
|
+
}
|
|
1197
|
+
return count;
|
|
1198
|
+
}
|
|
1199
|
+
function registry() {
|
|
1200
|
+
const registry2 = new ProviderRegistry();
|
|
1201
|
+
registry2.register(createCodexEntry());
|
|
1202
|
+
return registry2;
|
|
1203
|
+
}
|
|
1204
|
+
function requestedProviders(flags) {
|
|
1205
|
+
const raw = flags.get("provider");
|
|
1206
|
+
if (!Array.isArray(raw) || raw.length === 0) return void 0;
|
|
1207
|
+
return raw;
|
|
1208
|
+
}
|
|
1209
|
+
async function resolveOutPaths(out, providerName, multiProvider, count, format) {
|
|
1210
|
+
const resolved = resolve(out);
|
|
1211
|
+
let isDir = false;
|
|
1212
|
+
try {
|
|
1213
|
+
const info = await stat(resolved);
|
|
1214
|
+
isDir = info.isDirectory();
|
|
1215
|
+
} catch {
|
|
1216
|
+
isDir = extname(resolved) === "";
|
|
1217
|
+
}
|
|
1218
|
+
const extension = `.${normalizeOutputFormat(format)}`;
|
|
1219
|
+
const prefix = multiProvider ? `${providerName}-` : "";
|
|
1220
|
+
if (isDir) {
|
|
1221
|
+
return Array.from({ length: count }, (_, i) => join2(resolved, `${prefix}image-${i + 1}${extension}`));
|
|
1222
|
+
}
|
|
1223
|
+
if (count > 1 || multiProvider) {
|
|
1224
|
+
const dir = dirname(resolved);
|
|
1225
|
+
const base = basename(resolved, extname(resolved));
|
|
1226
|
+
return Array.from({ length: count }, (_, i) => join2(dir, `${prefix}${base}-${i + 1}${extension}`));
|
|
1227
|
+
}
|
|
1228
|
+
return [resolved.endsWith(extension) ? resolved : `${resolved}${extension}`];
|
|
1229
|
+
}
|
|
1230
|
+
function printResult(providerName, result, paths) {
|
|
1231
|
+
process.stdout.write(
|
|
1232
|
+
`${JSON.stringify({
|
|
1233
|
+
provider: providerName,
|
|
1234
|
+
imageModel: result.imageModel,
|
|
1235
|
+
images: result.images.map((image, i) => ({
|
|
1236
|
+
path: paths[i],
|
|
1237
|
+
mimeType: image.mimeType,
|
|
1238
|
+
width: image.width,
|
|
1239
|
+
height: image.height,
|
|
1240
|
+
prompt: image.prompt,
|
|
1241
|
+
responseId: image.responseId
|
|
1242
|
+
}))
|
|
1243
|
+
})}
|
|
1244
|
+
`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
async function saveImages(result, paths) {
|
|
1248
|
+
for (let i = 0; i < result.images.length; i += 1) {
|
|
1249
|
+
const path = paths[i];
|
|
1250
|
+
try {
|
|
1251
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1252
|
+
await writeFile(path, Buffer.from(result.images[i].data));
|
|
1253
|
+
} catch {
|
|
1254
|
+
fail("io", `Could not write output file: ${path}`, EXIT_IO);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
async function runAuthLogin(providerName, flags) {
|
|
1259
|
+
if (providerName !== PROVIDER_NAME) {
|
|
1260
|
+
fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
|
|
1261
|
+
}
|
|
1262
|
+
const uri = redirectUri();
|
|
1263
|
+
const port = redirectPort(uri);
|
|
1264
|
+
const timeoutMinutes = Number(flagValue(flags, "timeout") ?? "10");
|
|
1265
|
+
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0 || timeoutMinutes > 60) {
|
|
1266
|
+
fail("usage", "--timeout must be a number of minutes from 1 to 60.", EXIT_USAGE);
|
|
1267
|
+
}
|
|
1268
|
+
const { url, state } = startPkceFlow(uri);
|
|
1269
|
+
let callback;
|
|
1270
|
+
const waiter = waitForOAuthCallback(timeoutMinutes * 60 * 1e3, port);
|
|
1271
|
+
if (!flags.get("no-open")) {
|
|
1272
|
+
try {
|
|
1273
|
+
const { default: open } = await import("open");
|
|
1274
|
+
await open(url);
|
|
1275
|
+
} catch {
|
|
1276
|
+
process.stderr.write("Could not open a browser automatically. Open this URL manually:\n");
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
process.stderr.write(`Authorize in the browser:
|
|
1280
|
+
${url}
|
|
1281
|
+
`);
|
|
1282
|
+
try {
|
|
1283
|
+
callback = await waiter;
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
if (error instanceof LoopbackError) fail("auth", error.message, EXIT_AUTH);
|
|
1286
|
+
throw error;
|
|
1287
|
+
}
|
|
1288
|
+
if (callback.state !== state) fail("auth", "OAuth state mismatch. Retry `imagegen auth codex login`.", EXIT_AUTH);
|
|
1289
|
+
const verifier = consumeVerifier(state);
|
|
1290
|
+
if (!verifier) fail("auth", "OAuth flow expired. Retry `imagegen auth codex login`.", EXIT_AUTH);
|
|
1291
|
+
const store = await resolveTokenStore();
|
|
1292
|
+
if (store.describe().backend === "file") {
|
|
1293
|
+
process.stderr.write("Warning: OS vault unavailable; tokens stored in a restricted file. Use `auth status --show-path` to inspect.\n");
|
|
1294
|
+
}
|
|
1295
|
+
const manager = new CodexTokenManager(store);
|
|
1296
|
+
try {
|
|
1297
|
+
await manager.exchangeAndSave({ code: callback.code, redirectUri: uri, codeVerifier: verifier });
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
if (error instanceof ReauthRequiredError) fail("auth", error.message, EXIT_AUTH);
|
|
1300
|
+
throw error;
|
|
1301
|
+
}
|
|
1302
|
+
process.stdout.write(`${JSON.stringify({ provider: PROVIDER_NAME, status: "connected" })}
|
|
1303
|
+
`);
|
|
1304
|
+
}
|
|
1305
|
+
async function runProviders() {
|
|
1306
|
+
const entries = registry().names().map((name) => registry().get(name));
|
|
1307
|
+
const providers = [];
|
|
1308
|
+
for (const entry of entries) {
|
|
1309
|
+
const status = await entry.authStatus();
|
|
1310
|
+
providers.push({ name: status.name, configured: status.configured });
|
|
1311
|
+
}
|
|
1312
|
+
process.stdout.write(`${JSON.stringify({ providers })}
|
|
1313
|
+
`);
|
|
1314
|
+
}
|
|
1315
|
+
async function runAuthStatus(providerName, flags) {
|
|
1316
|
+
if (providerName !== PROVIDER_NAME) {
|
|
1317
|
+
fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
|
|
1318
|
+
}
|
|
1319
|
+
const store = await resolveTokenStore();
|
|
1320
|
+
const manager = new CodexTokenManager(store);
|
|
1321
|
+
const status = await manager.status();
|
|
1322
|
+
const output = { provider: PROVIDER_NAME, connected: status.connected };
|
|
1323
|
+
if (status.expiresAt) output["expiresAt"] = new Date(status.expiresAt).toISOString();
|
|
1324
|
+
if (flags.get("show-path")) {
|
|
1325
|
+
const info = store.describe();
|
|
1326
|
+
output["store"] = { backend: info.backend, location: info.location };
|
|
1327
|
+
}
|
|
1328
|
+
process.stdout.write(`${JSON.stringify(output)}
|
|
1329
|
+
`);
|
|
1330
|
+
}
|
|
1331
|
+
async function runAuthLogout(providerName) {
|
|
1332
|
+
if (providerName !== PROVIDER_NAME) {
|
|
1333
|
+
fail("usage", `Unknown auth provider "${providerName}". Available: ${registry().describe()}.`, EXIT_USAGE);
|
|
1334
|
+
}
|
|
1335
|
+
const store = await resolveTokenStore();
|
|
1336
|
+
await store.clear(PROVIDER_NAME);
|
|
1337
|
+
process.stdout.write(`${JSON.stringify({ provider: PROVIDER_NAME, status: "disconnected" })}
|
|
1338
|
+
`);
|
|
1339
|
+
}
|
|
1340
|
+
async function runGenerate(flags) {
|
|
1341
|
+
const prompt = flagValue(flags, "prompt");
|
|
1342
|
+
const out = flagValue(flags, "out");
|
|
1343
|
+
if (!prompt) fail("usage", "Missing required --prompt.", EXIT_USAGE);
|
|
1344
|
+
if (!out) fail("usage", "Missing required --out.", EXIT_USAGE);
|
|
1345
|
+
const providers = await resolveProviders(requestedProviders(flags));
|
|
1346
|
+
const request = {
|
|
1347
|
+
prompt,
|
|
1348
|
+
out,
|
|
1349
|
+
size: flagValue(flags, "size"),
|
|
1350
|
+
quality: flagValue(flags, "quality"),
|
|
1351
|
+
format: flagValue(flags, "format"),
|
|
1352
|
+
count: parseCount(flags),
|
|
1353
|
+
referenceImages: await loadReferences(flags)
|
|
1354
|
+
};
|
|
1355
|
+
await runImageRequest(request, void 0, providers);
|
|
1356
|
+
}
|
|
1357
|
+
async function resolveProviders(requested) {
|
|
1358
|
+
try {
|
|
1359
|
+
return await registry().resolve(requested);
|
|
1360
|
+
} catch (error) {
|
|
1361
|
+
fail("usage", error instanceof Error ? error.message : String(error), EXIT_USAGE);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
async function runEdit(flags) {
|
|
1365
|
+
const prompt = flagValue(flags, "prompt");
|
|
1366
|
+
const out = flagValue(flags, "out");
|
|
1367
|
+
const source = flagValue(flags, "source");
|
|
1368
|
+
const mask = flagValue(flags, "mask");
|
|
1369
|
+
if (!prompt) fail("usage", "Missing required --prompt.", EXIT_USAGE);
|
|
1370
|
+
if (!out) fail("usage", "Missing required --out.", EXIT_USAGE);
|
|
1371
|
+
if (!source) fail("usage", "Missing required --source.", EXIT_USAGE);
|
|
1372
|
+
const request = {
|
|
1373
|
+
prompt,
|
|
1374
|
+
out,
|
|
1375
|
+
size: flagValue(flags, "size"),
|
|
1376
|
+
quality: flagValue(flags, "quality"),
|
|
1377
|
+
format: flagValue(flags, "format"),
|
|
1378
|
+
count: parseCount(flags),
|
|
1379
|
+
referenceImages: await loadReferences(flags),
|
|
1380
|
+
sourceImage: await loadImageInput(source, "Source image"),
|
|
1381
|
+
mask: mask ? await loadImageInput(mask, "Mask image") : void 0
|
|
1382
|
+
};
|
|
1383
|
+
await runImageRequest(request, request, await resolveProviders(requestedProviders(flags)));
|
|
1384
|
+
}
|
|
1385
|
+
async function runImageRequest(request, edit, providers) {
|
|
1386
|
+
const multiProvider = providers.length > 1;
|
|
1387
|
+
for (const entry of providers) {
|
|
1388
|
+
let result;
|
|
1389
|
+
try {
|
|
1390
|
+
result = edit ? await entry.provider.edit(edit) : await entry.provider.generate(request);
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
if (error instanceof ReauthRequiredError) fail("auth", `${entry.provider.name}: ${error.message}`, EXIT_AUTH);
|
|
1393
|
+
if (error instanceof ProviderError) {
|
|
1394
|
+
if (error.kind === "configuration" || error.kind === "usage") fail(error.kind, `${entry.provider.name}: ${error.message}`, EXIT_USAGE);
|
|
1395
|
+
fail("provider", `${entry.provider.name}: ${error.kind}: ${error.message}`, EXIT_PROVIDER);
|
|
1396
|
+
}
|
|
1397
|
+
throw error;
|
|
1398
|
+
}
|
|
1399
|
+
const paths = await resolveOutPaths(request.out, entry.provider.name, multiProvider, result.images.length, request.format ?? "png");
|
|
1400
|
+
await saveImages(result, paths);
|
|
1401
|
+
printResult(entry.provider.name, result, paths);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
async function main() {
|
|
1405
|
+
const { flags, positionals } = parseArgs(process.argv.slice(2));
|
|
1406
|
+
if (flags.get("help") || positionals.length === 0) {
|
|
1407
|
+
process.stdout.write(helpFor(positionals[0]));
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
const [command, sub, subSub] = positionals;
|
|
1411
|
+
try {
|
|
1412
|
+
if (command === "help") {
|
|
1413
|
+
process.stdout.write(helpFor(sub));
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
if (command === "providers" && !sub) {
|
|
1417
|
+
await runProviders();
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (command === "auth" && sub && !subSub) {
|
|
1421
|
+
fail("usage", `Missing auth action. Usage: imagegen auth ${sub} login|status|logout.`, EXIT_USAGE);
|
|
1422
|
+
}
|
|
1423
|
+
if (command === "auth" && sub) {
|
|
1424
|
+
if (subSub === "login") {
|
|
1425
|
+
await runAuthLogin(sub, flags);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (subSub === "status") {
|
|
1429
|
+
await runAuthStatus(sub, flags);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
if (subSub === "logout") {
|
|
1433
|
+
await runAuthLogout(sub);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (command === "generate" && !sub) {
|
|
1438
|
+
await runGenerate(flags);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (command === "edit" && !sub) {
|
|
1442
|
+
await runEdit(flags);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
fail("usage", `Unknown command: ${positionals.join(" ")}. Run \`imagegen help\`.`, EXIT_USAGE);
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
if (error instanceof ProviderError) fail("provider", `${error.kind}: ${error.message}`, EXIT_PROVIDER);
|
|
1448
|
+
fail("provider", error instanceof Error ? error.message : String(error), EXIT_PROVIDER);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
await main();
|
|
1452
|
+
//# sourceMappingURL=index.mjs.map
|