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