@dorely13/ai-imagegen-cli 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { mkdir as mkdir3, readFile as readFile3, stat, writeFile as writeFile3 } from "node:fs/promises";
5
- import { basename, dirname as dirname3, extname, join as join4, resolve as resolve2 } from "node:path";
4
+ import { mkdir as mkdir5, readFile as readFile4, 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 {
@@ -85,7 +85,9 @@ function normalizeOutputFormat(value) {
85
85
  return "jpeg";
86
86
  if (normalized === "webp")
87
87
  return "webp";
88
- return "png";
88
+ if (normalized === "png" || normalized === "")
89
+ return "png";
90
+ throw new ProviderError({ kind: "usage", message: "Output format must be png, jpeg, or webp." });
89
91
  }
90
92
 
91
93
  // ../../packages/core/dist/image-size.js
@@ -226,20 +228,24 @@ var AUTH_ENDPOINT = "https://auth.openai.com/oauth/authorize";
226
228
  var TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
227
229
  var CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
228
230
  var SCOPE = "openid profile email offline_access";
229
- var RESPONSES_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
231
+ var IMAGES_ENDPOINT = "https://chatgpt.com/backend-api/codex/images";
230
232
  var DEFAULT_REDIRECT_URI = "http://localhost:1455/auth/callback";
231
233
  var LOOPBACK_PORT = 1455;
232
234
  var CALLBACK_PATH = "/auth/callback";
233
- var DEFAULT_IMAGE_MODEL = "gpt-image-2.5-flare";
234
- var DEFAULT_ORCHESTRATION_MODEL = "gpt-5.6-luna";
235
+ var DEFAULT_IMAGE_MODEL = "gpt-image-2.6-flare";
235
236
  var DEFAULT_QUALITY = "auto";
236
237
  var REQUEST_TIMEOUT_SECONDS = 600;
237
- var PARTIAL_IMAGES = 2;
238
238
  var MAX_REFERENCE_IMAGES2 = 4;
239
239
  var MAX_IMAGE_COUNT2 = 4;
240
240
  var MIN_IMAGE_COUNT2 = 1;
241
241
  var FLOW_TIMEOUT_MINUTES = 10;
242
242
  var USER_AGENT = "ImageGenCli";
243
+ var TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1e3;
244
+ var TOKEN_REQUEST_TIMEOUT_MS = 30 * 1e3;
245
+ var REFRESH_LOCK_FILE = "codex-refresh.lock";
246
+ var REFRESH_LOCK_STALE_MS = 60 * 1e3;
247
+ var REFRESH_LOCK_WAIT_MS = 90 * 1e3;
248
+ var REFRESH_LOCK_POLL_MS = 100;
243
249
 
244
250
  // ../../packages/providers-codex/dist/pkce.js
245
251
  import { createHash, randomBytes } from "node:crypto";
@@ -282,7 +288,7 @@ function callbackPage(title, message) {
282
288
  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>`;
283
289
  }
284
290
  async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3, port = LOOPBACK_PORT) {
285
- return new Promise((resolve3, reject) => {
291
+ return new Promise((resolve4, reject) => {
286
292
  let server;
287
293
  const timer = setTimeout(() => {
288
294
  closeServer();
@@ -322,7 +328,7 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
322
328
  res.writeHead(200, { "content-type": "text/html" });
323
329
  res.end(callbackPage("Connected", "ImageGenCli is connected. You can close this tab."));
324
330
  closeServer();
325
- resolve3({ code, state });
331
+ resolve4({ code, state });
326
332
  } catch (error2) {
327
333
  fail2(error2 instanceof Error ? error2 : new LoopbackError("OAuth callback failed.", "callback_error"));
328
334
  }
@@ -340,7 +346,7 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
340
346
 
341
347
  // ../../packages/providers-codex/dist/codex-auth.js
342
348
  var ReauthRequiredError = class extends Error {
343
- constructor(message = "Codex credentials are missing or expired. Run `imagegen auth login` to reconnect.") {
349
+ constructor(message = "Codex credentials are missing or expired. Run `imagegen auth codex login` to reconnect.") {
344
350
  super(message);
345
351
  this.name = "ReauthRequiredError";
346
352
  }
@@ -353,10 +359,24 @@ function toTokenSet(body, fallbackRefreshToken) {
353
359
  scope: body.scope
354
360
  };
355
361
  }
362
+ var REJECTED_GRANT_CODES = /* @__PURE__ */ new Set([
363
+ "invalid_grant",
364
+ "invalid_token",
365
+ "refresh_token_expired",
366
+ "refresh_token_reused",
367
+ "refresh_token_invalidated"
368
+ ]);
356
369
  async function readOAuthErrorCode(response) {
357
370
  try {
358
371
  const body = await response.json();
359
- return typeof body.error === "string" ? body.error : null;
372
+ if (typeof body.error === "string")
373
+ return body.error;
374
+ if (typeof body.error === "object" && body.error !== null) {
375
+ const code = body.error["code"];
376
+ if (typeof code === "string")
377
+ return code;
378
+ }
379
+ return null;
360
380
  } catch {
361
381
  return null;
362
382
  }
@@ -365,30 +385,31 @@ async function postTokenRequest(params) {
365
385
  const response = await fetch(TOKEN_ENDPOINT, {
366
386
  method: "POST",
367
387
  headers: { "content-type": "application/x-www-form-urlencoded" },
368
- body: new URLSearchParams(params)
388
+ body: new URLSearchParams(params),
389
+ signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)
369
390
  });
370
391
  if (!response.ok) {
371
392
  const errorCode = await readOAuthErrorCode(response.clone());
372
- if (response.status === 401 || response.status === 400 && (errorCode === "invalid_grant" || errorCode === "invalid_token")) {
373
- return null;
393
+ if (response.status === 401 || errorCode !== null && REJECTED_GRANT_CODES.has(errorCode)) {
394
+ return { ok: false, reason: errorCode ?? `status ${response.status}` };
374
395
  }
375
- throw new Error(`Token request failed with status ${response.status}.`);
396
+ throw new Error(`Token request failed with status ${response.status}${errorCode ? ` (${errorCode})` : ""}.`);
376
397
  }
377
398
  const body = await response.json();
378
- return toTokenSet(body, params["refresh_token"]);
399
+ return { ok: true, tokens: toTokenSet(body, params["refresh_token"]) };
379
400
  }
380
401
  async function exchangeCodeForTokens(args) {
381
- const tokens = await postTokenRequest({
402
+ const outcome = await postTokenRequest({
382
403
  grant_type: "authorization_code",
383
404
  client_id: CLIENT_ID,
384
405
  code: args.code,
385
406
  redirect_uri: args.redirectUri,
386
407
  code_verifier: args.codeVerifier
387
408
  });
388
- if (!tokens) {
389
- throw new ReauthRequiredError("The authorization code was rejected. Run `imagegen auth login` again.");
409
+ if (!outcome.ok) {
410
+ throw new ReauthRequiredError(`The authorization code was rejected (${outcome.reason}). Run \`imagegen auth codex login\` again.`);
390
411
  }
391
- return tokens;
412
+ return outcome.tokens;
392
413
  }
393
414
  async function refreshTokens(refreshToken) {
394
415
  return postTokenRequest({
@@ -397,8 +418,8 @@ async function refreshTokens(refreshToken) {
397
418
  refresh_token: refreshToken
398
419
  });
399
420
  }
400
- function isTokenExpired(tokens, now = Date.now()) {
401
- return tokens.expiresAt <= now;
421
+ function needsRefresh(tokens, now = Date.now()) {
422
+ return tokens.expiresAt - TOKEN_REFRESH_MARGIN_MS <= now;
402
423
  }
403
424
  var pendingVerifiers = /* @__PURE__ */ new Map();
404
425
  function startPkceFlow(redirectUri2) {
@@ -424,65 +445,9 @@ function prunePendingVerifiers() {
424
445
  }
425
446
  }
426
447
 
427
- // ../../packages/providers-codex/dist/codex-token-manager.js
428
- var CodexTokenManager = class {
429
- store;
430
- pendingRefresh = null;
431
- constructor(store) {
432
- this.store = store;
433
- }
434
- async exchangeAndSave(args) {
435
- const tokens = await exchangeCodeForTokens(args);
436
- await this.store.save(PROVIDER_NAME, tokens);
437
- return tokens;
438
- }
439
- async status() {
440
- const tokens = await this.store.load(PROVIDER_NAME);
441
- if (!tokens)
442
- return { connected: false };
443
- if (!isTokenExpired(tokens))
444
- return { connected: true, expiresAt: tokens.expiresAt };
445
- if (!tokens.refreshToken)
446
- return { connected: false };
447
- try {
448
- const refreshed = await this.getValidAccessToken();
449
- void refreshed;
450
- const current = await this.store.load(PROVIDER_NAME);
451
- return { connected: true, expiresAt: current?.expiresAt };
452
- } catch {
453
- return { connected: false };
454
- }
455
- }
456
- async logout() {
457
- await this.store.clear(PROVIDER_NAME);
458
- }
459
- async getValidAccessToken() {
460
- const tokens = await this.store.load(PROVIDER_NAME);
461
- if (!tokens)
462
- throw new ReauthRequiredError();
463
- if (!isTokenExpired(tokens))
464
- return tokens.accessToken;
465
- if (!tokens.refreshToken) {
466
- await this.store.clear(PROVIDER_NAME);
467
- throw new ReauthRequiredError();
468
- }
469
- if (!this.pendingRefresh) {
470
- this.pendingRefresh = this.performRefresh(tokens.refreshToken).finally(() => {
471
- this.pendingRefresh = null;
472
- });
473
- }
474
- return this.pendingRefresh;
475
- }
476
- async performRefresh(refreshToken) {
477
- const refreshed = await refreshTokens(refreshToken);
478
- if (!refreshed) {
479
- await this.store.clear(PROVIDER_NAME);
480
- throw new ReauthRequiredError();
481
- }
482
- await this.store.save(PROVIDER_NAME, refreshed);
483
- return refreshed.accessToken;
484
- }
485
- };
448
+ // ../../packages/providers-codex/dist/refresh-lock.js
449
+ import { mkdir, open, rm, stat } from "node:fs/promises";
450
+ import { dirname, join as join2 } from "node:path";
486
451
 
487
452
  // ../../packages/credentials/dist/index.js
488
453
  import { homedir } from "node:os";
@@ -490,8 +455,8 @@ import { join } from "node:path";
490
455
  function asKeytar(module) {
491
456
  if (typeof module !== "object" || module === null)
492
457
  return null;
493
- const record2 = module;
494
- const candidate = record2["default"] ?? module;
458
+ const record3 = module;
459
+ const candidate = record3["default"] ?? module;
495
460
  if (typeof candidate.getPassword === "function" && typeof candidate.setPassword === "function" && typeof candidate.deletePassword === "function") {
496
461
  return candidate;
497
462
  }
@@ -563,9 +528,9 @@ var FileCredentialStore = class {
563
528
  return { backend: "file", location: this.filePath };
564
529
  }
565
530
  async readAll() {
566
- const { readFile: readFile4 } = await import("node:fs/promises");
531
+ const { readFile: readFile5 } = await import("node:fs/promises");
567
532
  try {
568
- const raw = await readFile4(this.filePath, "utf8");
533
+ const raw = await readFile5(this.filePath, "utf8");
569
534
  let value;
570
535
  try {
571
536
  value = JSON.parse(raw);
@@ -582,9 +547,9 @@ var FileCredentialStore = class {
582
547
  }
583
548
  }
584
549
  async writeAll(all) {
585
- const { mkdir: mkdir4, writeFile: writeFile4, chmod } = await import("node:fs/promises");
586
- const { dirname: dirname4 } = await import("node:path");
587
- await mkdir4(dirname4(this.filePath), { recursive: true });
550
+ const { mkdir: mkdir6, writeFile: writeFile4, chmod } = await import("node:fs/promises");
551
+ const { dirname: dirname6 } = await import("node:path");
552
+ await mkdir6(dirname6(this.filePath), { recursive: true });
588
553
  await writeFile4(this.filePath, JSON.stringify(all, null, 2), {
589
554
  mode: 384
590
555
  });
@@ -602,6 +567,133 @@ function defaultCredentialFilePath() {
602
567
  return join(base, "ImageGenCli", "tokens.json");
603
568
  }
604
569
 
570
+ // ../../packages/providers-codex/dist/refresh-lock.js
571
+ function defaultRefreshLockPath() {
572
+ return join2(dirname(defaultCredentialFilePath()), REFRESH_LOCK_FILE);
573
+ }
574
+ function createRefreshLock(options = {}) {
575
+ const path = options.path ?? defaultRefreshLockPath();
576
+ const staleMs = options.staleMs ?? REFRESH_LOCK_STALE_MS;
577
+ const waitMs = options.waitMs ?? REFRESH_LOCK_WAIT_MS;
578
+ const pollMs = options.pollMs ?? REFRESH_LOCK_POLL_MS;
579
+ return async (work) => {
580
+ await acquire(path, staleMs, waitMs, pollMs);
581
+ try {
582
+ return await work();
583
+ } finally {
584
+ await rm(path, { force: true });
585
+ }
586
+ };
587
+ }
588
+ async function acquire(path, staleMs, waitMs, pollMs) {
589
+ await mkdir(dirname(path), { recursive: true });
590
+ const deadline = Date.now() + waitMs;
591
+ for (; ; ) {
592
+ try {
593
+ const handle = await open(path, "wx");
594
+ try {
595
+ await handle.writeFile(`${process.pid}
596
+ `);
597
+ } finally {
598
+ await handle.close();
599
+ }
600
+ return;
601
+ } catch (error2) {
602
+ if (!isContention(error2))
603
+ throw error2;
604
+ }
605
+ if (await isStale(path, staleMs)) {
606
+ await rm(path, { force: true });
607
+ continue;
608
+ }
609
+ if (Date.now() >= deadline) {
610
+ throw new Error("Timed out waiting for another imagegen process to refresh Codex credentials.");
611
+ }
612
+ await new Promise((resolve4) => setTimeout(resolve4, pollMs + Math.random() * pollMs));
613
+ }
614
+ }
615
+ function isContention(error2) {
616
+ const code = error2.code;
617
+ return code === "EEXIST" || process.platform === "win32" && (code === "EPERM" || code === "EACCES");
618
+ }
619
+ async function isStale(path, staleMs) {
620
+ try {
621
+ return Date.now() - (await stat(path)).mtimeMs > staleMs;
622
+ } catch {
623
+ return false;
624
+ }
625
+ }
626
+
627
+ // ../../packages/providers-codex/dist/codex-token-manager.js
628
+ var CodexTokenManager = class {
629
+ store;
630
+ pendingRefresh = null;
631
+ lock;
632
+ refresh;
633
+ constructor(store, options = {}) {
634
+ this.store = store;
635
+ this.lock = options.lock ?? createRefreshLock();
636
+ this.refresh = options.refresh ?? refreshTokens;
637
+ }
638
+ async exchangeAndSave(args) {
639
+ const tokens = await exchangeCodeForTokens(args);
640
+ await this.store.save(PROVIDER_NAME, tokens);
641
+ return tokens;
642
+ }
643
+ async status() {
644
+ if (!await this.store.load(PROVIDER_NAME))
645
+ return { connected: false };
646
+ try {
647
+ await this.getValidAccessToken();
648
+ const current = await this.store.load(PROVIDER_NAME);
649
+ return { connected: true, expiresAt: current?.expiresAt };
650
+ } catch {
651
+ return { connected: false };
652
+ }
653
+ }
654
+ async logout() {
655
+ await this.store.clear(PROVIDER_NAME);
656
+ }
657
+ async getValidAccessToken() {
658
+ const tokens = await this.store.load(PROVIDER_NAME);
659
+ if (!tokens)
660
+ throw new ReauthRequiredError();
661
+ if (!needsRefresh(tokens))
662
+ return tokens.accessToken;
663
+ return this.refreshFrom(tokens.accessToken);
664
+ }
665
+ // The Codex image endpoint rejected this access token before its recorded expiry.
666
+ async recoverFromUnauthorized(rejectedAccessToken) {
667
+ return this.refreshFrom(rejectedAccessToken);
668
+ }
669
+ refreshFrom(staleAccessToken) {
670
+ if (!this.pendingRefresh) {
671
+ this.pendingRefresh = this.lock(() => this.refreshLocked(staleAccessToken)).finally(() => {
672
+ this.pendingRefresh = null;
673
+ });
674
+ }
675
+ return this.pendingRefresh;
676
+ }
677
+ async refreshLocked(staleAccessToken) {
678
+ const current = await this.store.load(PROVIDER_NAME);
679
+ if (!current)
680
+ throw new ReauthRequiredError();
681
+ if (current.accessToken !== staleAccessToken && !needsRefresh(current))
682
+ return current.accessToken;
683
+ if (!current.refreshToken) {
684
+ await this.store.clear(PROVIDER_NAME);
685
+ throw new ReauthRequiredError();
686
+ }
687
+ const outcome = await this.refresh(current.refreshToken);
688
+ if (!outcome.ok) {
689
+ await this.store.clear(PROVIDER_NAME);
690
+ throw new ReauthRequiredError(`Codex rejected the stored refresh token (${outcome.reason}). Run \`imagegen auth codex login\` to reconnect.`);
691
+ }
692
+ await this.store.save(PROVIDER_NAME, outcome.tokens);
693
+ return outcome.tokens.accessToken;
694
+ }
695
+ };
696
+
605
697
  // ../../packages/providers-codex/dist/token-store.js
606
698
  async function resolveTokenStore() {
607
699
  const store = await resolveCredentialStore();
@@ -642,13 +734,18 @@ function extractAccountId(token) {
642
734
  return accountId;
643
735
  }
644
736
 
645
- // ../../packages/providers-codex/dist/sse.js
737
+ // ../../packages/providers-codex/dist/codex-client.js
738
+ import sharp from "sharp";
739
+
740
+ // ../../packages/providers-codex/dist/errors.js
646
741
  function classifyImageError(message) {
647
742
  if (!message.trim())
648
743
  return "unknown";
649
744
  const lowerRate = message.includes("input-images") && message.includes("per min") && message.toLowerCase().includes("gpt-image");
650
745
  if (lowerRate)
651
746
  return "codex_image_input_rate_limit";
747
+ if (/usage limit/i.test(message))
748
+ return "usage_limit";
652
749
  if (/rate limit|429|too many requests|please try again/i.test(message))
653
750
  return "rate_limit";
654
751
  if (/403|forbidden|cloudflare/i.test(message))
@@ -657,107 +754,26 @@ function classifyImageError(message) {
657
754
  return "image_model_invalid";
658
755
  return "api_error";
659
756
  }
660
- function readString(value, property) {
661
- if (typeof value !== "object" || value === null)
662
- return void 0;
663
- const candidate = value[property];
664
- return typeof candidate === "string" ? candidate : void 0;
665
- }
666
- function readEventError(event) {
667
- const direct = readString(event, "message");
668
- if (direct)
669
- return direct;
670
- if (typeof event !== "object" || event === null)
671
- return void 0;
672
- const record2 = event;
673
- const nested = record2["error"];
674
- if (typeof nested === "object" && nested !== null) {
675
- const message = readString(nested, "message");
676
- if (message)
677
- return message;
678
- }
679
- const response = record2["response"];
680
- if (typeof response === "object" && response !== null) {
681
- const responseError = response["error"];
682
- if (typeof responseError === "object" && responseError !== null) {
683
- return readString(responseError, "message");
684
- }
685
- }
686
- return void 0;
687
- }
688
- function tryReadImageItem(item) {
689
- if (readString(item, "type") !== "image_generation_call")
690
- return null;
691
- const result = readString(item, "result");
692
- if (!result)
757
+ function readErrorMessage(body) {
758
+ if (!body.trim())
693
759
  return null;
694
- let data;
695
760
  try {
696
- data = Buffer.from(result, "base64");
697
- } catch {
698
- throw new Error("invalid_image_result: Codex returned an image result that was not valid base64.");
699
- }
700
- return { data, revisedPrompt: readString(item, "revised_prompt"), callId: readString(item, "id") };
701
- }
702
- function parseImageStreamEvents(events) {
703
- let eventCount = 0;
704
- let lastEventType;
705
- let responseId;
706
- const finish = (image) => ({
707
- ...image,
708
- responseId,
709
- eventCount,
710
- lastEventType
711
- });
712
- for (const raw of events) {
713
- const line = raw.trim();
714
- if (!line.startsWith("data:"))
715
- continue;
716
- const payload = line.slice("data:".length).trim();
717
- if (payload === "[DONE]")
718
- break;
719
- let event;
720
- try {
721
- event = JSON.parse(payload);
722
- } catch {
723
- continue;
724
- }
725
- const type = readString(event, "type");
726
- lastEventType = type ?? lastEventType;
727
- eventCount += 1;
728
- if (type === "response.created" && typeof event === "object" && event !== null) {
729
- const inner = event["response"];
730
- const id = readString(inner, "id");
731
- if (id)
732
- responseId = id;
733
- }
734
- if (type === "response.failed" || type === "error") {
735
- throw new Error(`${classifyImageError(readEventError(event) ?? "")}: ${readEventError(event) ?? "Codex image generation failed."}`);
736
- }
737
- if (type === "response.output_item.done" && typeof event === "object" && event !== null) {
738
- const item = event["item"];
739
- const image = tryReadImageItem(item);
740
- if (image)
741
- return finish(image);
742
- }
743
- if (type === "response.completed" && typeof event === "object" && event !== null) {
744
- const response = event["response"];
745
- const id = readString(response, "id");
746
- if (id)
747
- responseId = id;
748
- if (typeof response === "object" && response !== null) {
749
- const output = response["output"];
750
- if (Array.isArray(output)) {
751
- for (const item of output) {
752
- const image = tryReadImageItem(item);
753
- if (image)
754
- return finish(image);
755
- }
756
- }
761
+ const parsed = JSON.parse(body);
762
+ if (typeof parsed === "object" && parsed !== null) {
763
+ const record3 = parsed;
764
+ if (typeof record3["message"] === "string")
765
+ return record3["message"];
766
+ if (typeof record3["detail"] === "string")
767
+ return record3["detail"];
768
+ const error2 = record3["error"];
769
+ if (typeof error2 === "object" && error2 !== null && typeof error2["message"] === "string") {
770
+ return error2["message"];
757
771
  }
758
772
  }
773
+ } catch {
774
+ return body.slice(0, 500);
759
775
  }
760
- throw new Error(`stream_completed_without_image: Codex image generation completed without returning an image. responseId=${responseId ?? "unknown"}, events=${eventCount}, lastEvent=${lastEventType ?? "none"}.`);
776
+ return null;
761
777
  }
762
778
 
763
779
  // ../../packages/providers-codex/dist/codex-client.js
@@ -779,112 +795,78 @@ function resolveSize(size) {
779
795
  });
780
796
  }
781
797
  }
782
- function baseImageTool(args) {
783
- const tool = {
784
- type: "image_generation",
785
- model: args.imageModel,
786
- action: args.action,
787
- size: args.size && args.size.trim() ? args.size.trim() : "auto",
788
- output_format: args.outputFormat,
798
+ function buildBody(args) {
799
+ const body = {
800
+ prompt: args.prompt,
801
+ model: args.model,
802
+ size: args.size.size,
789
803
  background: "auto"
790
804
  };
791
- if (args.partialImages > 0)
792
- tool["partial_images"] = Math.min(3, Math.max(0, args.partialImages));
793
- if (args.quality && args.quality.trim() && args.quality.trim().toLowerCase() !== "auto") {
794
- tool["quality"] = args.quality.trim();
805
+ const quality = args.quality.trim();
806
+ if (quality && quality.toLowerCase() !== "auto")
807
+ body["quality"] = quality;
808
+ if (args.images.length > 0)
809
+ body["images"] = args.images.map((image) => ({ image_url: toDataUrl(image) }));
810
+ return body;
811
+ }
812
+ var MASK_INSTRUCTION = "The last image is an edit mask, not content to reproduce. Apply the requested change only where the mask is transparent. Keep every area where the mask is opaque exactly as it is in the first image.";
813
+ async function normalizeMask(mask, source) {
814
+ const usage2 = (message) => new ProviderError({ kind: "usage", message });
815
+ let pixels;
816
+ let width;
817
+ let height;
818
+ let sourceWidth;
819
+ let sourceHeight;
820
+ try {
821
+ const decoded = await sharp(mask.data).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
822
+ ({ width, height } = decoded.info);
823
+ pixels = decoded.data;
824
+ ({ width: sourceWidth, height: sourceHeight } = await sharp(source.data).metadata());
825
+ } catch {
826
+ throw usage2("Mask and source must be readable PNG, JPEG, or WebP images.");
795
827
  }
796
- if (args.outputFormat === "jpeg" && typeof args.outputCompression === "number") {
797
- tool["output_compression"] = Math.min(100, Math.max(0, Math.trunc(args.outputCompression)));
828
+ if (width !== sourceWidth || height !== sourceHeight) {
829
+ throw usage2(`Mask is ${width}x${height} but the source is ${sourceWidth}x${sourceHeight}; they must match.`);
798
830
  }
799
- return tool;
800
- }
801
- function buildPayload(args) {
802
- const content = [{ type: "input_text", text: args.prompt }];
803
- for (const reference of args.references.slice(0, MAX_REFERENCE_IMAGES2)) {
804
- content.push({ type: "input_image", image_url: toDataUrl(reference), detail: "auto" });
831
+ let editable = false;
832
+ for (let i = 0; i < pixels.length; i += 4) {
833
+ const edit = pixels[i + 3] < 128;
834
+ editable ||= edit;
835
+ pixels.fill(edit ? 0 : 255, i, i + 4);
805
836
  }
806
- return {
807
- model: args.model,
808
- input: [{ role: "user", content }],
809
- tools: [args.tool],
810
- tool_choice: { type: "image_generation" },
811
- stream: true,
812
- store: false
813
- };
837
+ if (!editable)
838
+ throw usage2("Mask has no transparent pixels; make the area to edit transparent.");
839
+ const data = await sharp(pixels, { raw: { width, height, channels: 4 } }).png().toBuffer();
840
+ return { data, contentType: "image/png" };
814
841
  }
815
842
  var CodexImageProvider = class {
816
843
  tokens;
817
844
  name = PROVIDER_NAME;
818
845
  sizePolicy = { resolveSize: (size) => resolveSize(size) };
819
- orchestrationModel;
820
846
  imageModel;
821
847
  timeoutSeconds;
822
- partialImages;
823
848
  fetchImpl;
824
849
  constructor(tokens, options = {}) {
825
850
  this.tokens = tokens;
826
- this.orchestrationModel = options.model ?? process.env["IMAGEGEN_CODEX_MODEL"] ?? DEFAULT_ORCHESTRATION_MODEL;
827
851
  this.imageModel = options.imageModel ?? process.env["IMAGEGEN_CODEX_IMAGE_MODEL"] ?? DEFAULT_IMAGE_MODEL;
828
852
  this.timeoutSeconds = options.timeoutSeconds ?? REQUEST_TIMEOUT_SECONDS;
829
- this.partialImages = options.partialImages ?? PARTIAL_IMAGES;
830
853
  this.fetchImpl = options.fetchImpl ?? fetch;
831
854
  }
832
855
  async generate(request) {
833
- const format = normalizeOutputFormat(request.format);
834
- const size = this.sizePolicy.resolveSize(request.size);
835
- const imageModel2 = this.resolveImageModel(request);
836
- return this.runAll({
837
- format,
838
- imageModel: imageModel2,
839
- count: normalizeCount(request.count),
840
- prompt: request.prompt,
841
- size,
842
- build: () => buildPayload({
843
- model: this.orchestrationModel,
844
- prompt: request.prompt,
845
- references: request.referenceImages ?? [],
846
- tool: baseImageTool({
847
- size: size.size,
848
- quality: request.quality ?? DEFAULT_QUALITY,
849
- outputFormat: format,
850
- imageModel: imageModel2,
851
- action: "generate",
852
- partialImages: this.partialImages
853
- })
854
- })
855
- });
856
+ const references = (request.referenceImages ?? []).slice(0, MAX_REFERENCE_IMAGES2);
857
+ return this.runAll(request, references.length > 0 ? "edits" : "generations", references);
856
858
  }
857
859
  async edit(request) {
858
- const format = normalizeOutputFormat(request.format);
859
- const size = this.sizePolicy.resolveSize(request.size);
860
- const imageModel2 = this.resolveImageModel(request);
861
- const references = [request.sourceImage, ...request.referenceImages ?? []];
862
- return this.runAll({
863
- format,
864
- imageModel: imageModel2,
865
- count: normalizeCount(request.count),
866
- prompt: request.prompt,
867
- size,
868
- build: () => {
869
- const tool = baseImageTool({
870
- size: size.size,
871
- quality: request.quality ?? DEFAULT_QUALITY,
872
- outputFormat: format,
873
- imageModel: imageModel2,
874
- action: "edit",
875
- partialImages: this.partialImages
876
- });
877
- if (request.mask) {
878
- tool["input_image_mask"] = { image_url: toDataUrl(request.mask) };
879
- }
880
- return buildPayload({
881
- model: this.orchestrationModel,
882
- prompt: request.prompt,
883
- references,
884
- tool
885
- });
886
- }
887
- });
860
+ const images = [request.sourceImage, ...request.referenceImages ?? []];
861
+ if (!request.mask)
862
+ return this.runAll(request, "edits", images.slice(0, MAX_REFERENCE_IMAGES2));
863
+ if (images.length + 1 > MAX_REFERENCE_IMAGES2) {
864
+ throw new ProviderError({ kind: "usage", message: `Codex accepts at most ${MAX_REFERENCE_IMAGES2} edit images; a mask uses one, leaving room for ${MAX_REFERENCE_IMAGES2 - 2} --reference images.` });
865
+ }
866
+ const mask = await normalizeMask(request.mask, request.sourceImage);
867
+ return this.runAll(request, "edits", [...images, mask], `${request.prompt}
868
+
869
+ ${MASK_INSTRUCTION}`);
888
870
  }
889
871
  resolveImageModel(request) {
890
872
  const model = (request.model ?? this.imageModel).trim();
@@ -896,116 +878,122 @@ var CodexImageProvider = class {
896
878
  }
897
879
  return model;
898
880
  }
899
- async runAll(args) {
900
- const images = [];
901
- for (let i = 0; i < args.count; i += 1) {
902
- images.push(await this.sendOnce(args.build(), args.format, args.prompt, args.size));
881
+ async runAll(request, endpoint, images, modelPrompt = request.prompt) {
882
+ const format = normalizeOutputFormat(request.format);
883
+ const size = this.sizePolicy.resolveSize(request.size);
884
+ const requestedModel = this.resolveImageModel(request);
885
+ const body = JSON.stringify(buildBody({ prompt: modelPrompt, model: requestedModel, size, quality: request.quality ?? DEFAULT_QUALITY, images }));
886
+ const results = [];
887
+ for (let i = 0, count = normalizeCount(request.count); i < count; i += 1) {
888
+ results.push(await this.sendOnce(endpoint, body, format, request.prompt));
889
+ }
890
+ return { provider: this.name, requestedModel, images: results };
891
+ }
892
+ async sendOnce(endpoint, body, format, prompt) {
893
+ let accessToken = await this.tokens.getValidAccessToken();
894
+ let response = await this.post(endpoint, accessToken, body);
895
+ if (response.status === 401) {
896
+ await response.body?.cancel();
897
+ accessToken = await this.tokens.recoverFromUnauthorized(accessToken);
898
+ response = await this.post(endpoint, accessToken, body);
899
+ }
900
+ 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;
901
+ if (!response.ok) {
902
+ const errorText = await response.text().catch(() => "");
903
+ const kind = classifyImageError(errorText);
904
+ const detail = readErrorMessage(errorText);
905
+ const retryAfter = Number(response.headers.get("retry-after"));
906
+ throw new ProviderError({
907
+ kind,
908
+ message: detail ? `Codex image request returned ${response.status} (${kind}): ${detail}` : `Codex image request returned ${response.status} (${kind}).`,
909
+ statusCode: response.status,
910
+ requestId,
911
+ ...Number.isFinite(retryAfter) && retryAfter > 0 ? { retryAfterSeconds: retryAfter } : {}
912
+ });
913
+ }
914
+ const payload = record(await response.json().catch(() => null));
915
+ const item = Array.isArray(payload["data"]) ? record(payload["data"][0]) : {};
916
+ if (typeof item["b64_json"] !== "string" || !item["b64_json"]) {
917
+ throw new ProviderError({ kind: "no_image", message: "Codex image request returned no image data.", requestId });
903
918
  }
904
- return { provider: this.name, imageModel: args.imageModel, images };
919
+ let data = Buffer.from(item["b64_json"], "base64");
920
+ let width;
921
+ let height;
922
+ try {
923
+ const metadata = await sharp(data).metadata();
924
+ if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? ""))
925
+ throw new Error();
926
+ await sharp(data).stats();
927
+ if (metadata.format !== format)
928
+ data = await sharp(data).toFormat(format).toBuffer();
929
+ width = metadata.width;
930
+ height = metadata.height;
931
+ } catch {
932
+ throw new ProviderError({ kind: "malformed", message: "Codex returned an invalid raster image.", requestId });
933
+ }
934
+ return {
935
+ data,
936
+ mimeType: `image/${format}`,
937
+ width,
938
+ height,
939
+ prompt,
940
+ responseId: typeof item["generation_id"] === "string" ? item["generation_id"] : void 0,
941
+ // The endpoint accepts any requested model and currently names none in its response.
942
+ servedModel: typeof payload["model"] === "string" ? payload["model"] : void 0,
943
+ usage: {
944
+ requestId,
945
+ imageGeneration: payload["usage"],
946
+ // What the backend actually rendered; it may override requested size and quality.
947
+ served: { size: payload["size"], quality: payload["quality"], background: payload["background"] },
948
+ planLimits: readPlanLimits(response.headers)
949
+ }
950
+ };
905
951
  }
906
- async sendOnce(payload, format, prompt, size) {
907
- const accessToken = await this.tokens.getValidAccessToken();
908
- const accountId = extractAccountId(accessToken);
952
+ async post(endpoint, accessToken, body) {
909
953
  const controller = new AbortController();
910
954
  const timer = setTimeout(() => controller.abort(), this.timeoutSeconds * 1e3);
911
- let response;
912
955
  try {
913
- response = await this.fetchImpl(RESPONSES_ENDPOINT, {
956
+ return await this.fetchImpl(`${IMAGES_ENDPOINT}/${endpoint}`, {
914
957
  method: "POST",
915
958
  headers: {
916
959
  authorization: `Bearer ${accessToken}`,
917
- "chatgpt-account-id": accountId,
918
- "OpenAI-Beta": "responses=experimental",
960
+ "chatgpt-account-id": extractAccountId(accessToken),
919
961
  originator: "pi",
920
962
  "user-agent": USER_AGENT,
921
- accept: "text/event-stream",
963
+ accept: "application/json",
922
964
  "content-type": "application/json"
923
965
  },
924
- body: JSON.stringify(payload),
966
+ body,
925
967
  signal: controller.signal
926
968
  });
927
969
  } finally {
928
970
  clearTimeout(timer);
929
971
  }
930
- const requestId = response.headers.get("x-request-id") ?? response.headers.get("openai-request-id") ?? response.headers.get("request-id") ?? void 0;
931
- if (!response.ok || !response.body) {
932
- const body = await response.text().catch(() => "");
933
- const kind = classifyImageError(body);
934
- const detail = readErrorMessage(body);
935
- throw new ProviderError({
936
- kind,
937
- message: detail ? `Codex image request returned ${response.status} (${kind}): ${detail}` : `Codex image request returned ${response.status} (${kind}).`,
938
- statusCode: response.status,
939
- requestId
940
- });
941
- }
942
- const lines = await readSseLines(response.body);
943
- let parsed;
944
- try {
945
- parsed = parseImageStreamEvents(lines);
946
- } catch (error2) {
947
- throw toProviderError(error2, requestId);
948
- }
949
- return {
950
- data: parsed.data,
951
- mimeType: `image/${format}`,
952
- width: size.width,
953
- height: size.height,
954
- prompt,
955
- responseId: parsed.responseId,
956
- callId: parsed.callId
957
- };
958
972
  }
959
973
  };
960
- async function readSseLines(body) {
961
- const reader = body.getReader();
962
- const decoder = new TextDecoder();
963
- let buffer = "";
964
- const lines = [];
965
- for (; ; ) {
966
- const { done, value } = await reader.read();
967
- if (done)
968
- break;
969
- buffer += decoder.decode(value, { stream: true });
970
- let index = buffer.indexOf("\n");
971
- while (index >= 0) {
972
- const line = buffer.slice(0, index);
973
- buffer = buffer.slice(index + 1);
974
- if (line.startsWith("data:"))
975
- lines.push(line);
976
- index = buffer.indexOf("\n");
977
- }
978
- }
979
- if (buffer.startsWith("data:"))
980
- lines.push(buffer);
981
- return lines;
982
- }
983
- function readErrorMessage(body) {
984
- if (!body.trim())
985
- return null;
986
- try {
987
- const parsed = JSON.parse(body);
988
- if (typeof parsed === "object" && parsed !== null) {
989
- const record2 = parsed;
990
- if (typeof record2["message"] === "string")
991
- return record2["message"];
992
- if (typeof record2["detail"] === "string")
993
- return record2["detail"];
994
- const error2 = record2["error"];
995
- if (typeof error2 === "object" && error2 !== null && typeof error2["message"] === "string") {
996
- return error2["message"];
997
- }
998
- }
999
- } catch {
1000
- return body.slice(0, 500);
1001
- }
1002
- return null;
1003
- }
1004
- function toProviderError(error2, requestId) {
1005
- const message = error2 instanceof Error ? error2.message : String(error2);
1006
- const separator = message.indexOf(":");
1007
- const kind = separator > 0 ? message.slice(0, separator) : "api_error";
1008
- return new ProviderError({ kind, message, requestId });
974
+ function record(value) {
975
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
976
+ }
977
+ function readPlanLimits(headers) {
978
+ const number = (name) => {
979
+ const value = headers.get(`x-codex-${name}`);
980
+ const parsed = value === null ? Number.NaN : Number(value);
981
+ return Number.isFinite(parsed) ? parsed : void 0;
982
+ };
983
+ const window = (name) => {
984
+ const usedPercent = number(`${name}-used-percent`);
985
+ const windowMinutes = number(`${name}-window-minutes`);
986
+ if (usedPercent === void 0 || !windowMinutes)
987
+ return void 0;
988
+ return { usedPercent, windowMinutes, resetAt: number(`${name}-reset-at`) };
989
+ };
990
+ const limits = {
991
+ planType: headers.get("x-codex-plan-type") ?? void 0,
992
+ activeLimit: headers.get("x-codex-active-limit") ?? void 0,
993
+ primary: window("primary"),
994
+ secondary: window("secondary")
995
+ };
996
+ return Object.values(limits).some((value) => value !== void 0) ? limits : void 0;
1009
997
  }
1010
998
 
1011
999
  // ../../packages/providers-codex/dist/codex-entry.js
@@ -1049,7 +1037,7 @@ function createCodexEntry() {
1049
1037
  }
1050
1038
 
1051
1039
  // ../../packages/providers-google/dist/gemini-client.js
1052
- import sharp from "sharp";
1040
+ import sharp2 from "sharp";
1053
1041
 
1054
1042
  // ../../packages/providers-google/dist/image-size.js
1055
1043
  var DEFAULT_IMAGE_MODEL2 = "gemini-nano-banana-2.1";
@@ -1168,8 +1156,6 @@ var GeminiImageProvider = class {
1168
1156
  const count = request.count ?? 1;
1169
1157
  if (!Number.isInteger(count) || count < 1 || count > 4)
1170
1158
  throw error("usage", "Image count must be from 1 to 4.");
1171
- if (request.format && !["png", "jpeg", "jpg", "webp"].includes(request.format.trim().toLowerCase()))
1172
- throw error("usage", "Output format must be png, jpeg, or webp.");
1173
1159
  const format = normalizeOutputFormat(request.format);
1174
1160
  const size = resolveGeminiSize(request.size, model);
1175
1161
  const payload = {
@@ -1204,7 +1190,7 @@ var GeminiImageProvider = class {
1204
1190
  const images = [];
1205
1191
  for (let i = 0; i < count; i++)
1206
1192
  images.push(await this.send(payload, key, format, request.prompt));
1207
- return { provider: this.name, imageModel: model, images };
1193
+ return { provider: this.name, requestedModel: model, images };
1208
1194
  }
1209
1195
  async send(payload, key, format, prompt) {
1210
1196
  const controller = new AbortController();
@@ -1244,12 +1230,12 @@ var GeminiImageProvider = class {
1244
1230
  if (typeof image.data !== "string" || !image.data || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.data) || image.data.length % 4 !== 0)
1245
1231
  throw error("malformed", "Gemini returned invalid inline image data.");
1246
1232
  const original = Buffer.from(image.data, "base64");
1247
- const metadata = await sharp(original).metadata().catch(() => {
1233
+ const metadata = await sharp2(original).metadata().catch(() => {
1248
1234
  throw error("malformed", "Gemini returned undecodable image bytes.");
1249
1235
  });
1250
1236
  if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || image.mime_type !== `image/${metadata.format}`)
1251
1237
  throw error("malformed", "Gemini returned an invalid image or inconsistent MIME type.");
1252
- const data = metadata.format === format ? original : await sharp(original).toFormat(format).toBuffer();
1238
+ const data = metadata.format === format ? original : await sharp2(original).toFormat(format).toBuffer();
1253
1239
  return {
1254
1240
  data,
1255
1241
  mimeType: `image/${format}`,
@@ -1315,9 +1301,10 @@ function createGeminiEntry() {
1315
1301
  }
1316
1302
 
1317
1303
  // ../../packages/providers-configurable/dist/profiles.js
1318
- import { readFile, mkdir, writeFile, rename, rm } from "node:fs/promises";
1319
- import { dirname, join as join2 } from "node:path";
1304
+ import { readFile, mkdir as mkdir2, writeFile, rename, rm as rm2 } from "node:fs/promises";
1305
+ import { dirname as dirname2, isAbsolute, join as join3 } from "node:path";
1320
1306
  import { randomUUID } from "node:crypto";
1307
+ var COMFY_FIELDS = ["startCommand", "workflow", "editWorkflow"];
1321
1308
  var usage = (message) => new ProviderError({ kind: "usage", message });
1322
1309
  var profileStore = () => resolveCredentialStore();
1323
1310
  function validateApiKey(value) {
@@ -1333,14 +1320,25 @@ function validateProfile(value) {
1333
1320
  if (!value || typeof value !== "object" || Array.isArray(value))
1334
1321
  throw usage("Invalid endpoint profile.");
1335
1322
  const p = value;
1336
- if (Object.keys(p).some((k) => !["name", "api", "baseUrl", "model", "auth"].includes(k)))
1337
- throw usage("Endpoint profiles accept only name, api, baseUrl, model, and auth; store keys through auth login.");
1323
+ if (Object.keys(p).some((k) => !["name", "api", "baseUrl", "model", "auth", ...COMFY_FIELDS].includes(k)))
1324
+ throw usage("Endpoint profiles accept only name, api, baseUrl, model, auth, startCommand, workflow, and editWorkflow; store keys through auth login.");
1338
1325
  if (typeof p.name !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(p.name) || ["codex", "gemini"].includes(p.name))
1339
1326
  throw usage("Endpoint name must use lowercase letters, digits, and hyphens, and cannot be codex or gemini.");
1340
- if (p.api !== "openrouter" && p.api !== "openai-images")
1341
- throw usage("API format must be openrouter or openai-images.");
1327
+ if (p.api !== "openrouter" && p.api !== "openai-images" && p.api !== "comfyui")
1328
+ throw usage("API format must be openrouter, openai-images, or comfyui.");
1342
1329
  if (p.auth !== void 0 && p.auth !== "bearer" && p.auth !== "none")
1343
1330
  throw usage("Auth must be bearer or none.");
1331
+ if (p.api === "comfyui" && p.auth === "bearer")
1332
+ throw usage("ComfyUI profiles do not use API keys. Use --auth none.");
1333
+ if (p.api !== "comfyui" && COMFY_FIELDS.some((k) => p[k] !== void 0))
1334
+ throw usage("--start-command, --workflow, and --edit-workflow apply only to comfyui profiles.");
1335
+ for (const field of COMFY_FIELDS) {
1336
+ const value2 = p[field];
1337
+ if (value2 !== void 0 && (typeof value2 !== "string" || !value2.trim()))
1338
+ throw usage(`${field} must not be blank.`);
1339
+ if (field !== "startCommand" && value2 && !isAbsolute(value2))
1340
+ throw usage(`${field} must be an absolute path.`);
1341
+ }
1344
1342
  const raw = p.baseUrl ?? (p.api === "openrouter" ? "https://openrouter.ai/api/v1" : void 0);
1345
1343
  let url;
1346
1344
  try {
@@ -1355,18 +1353,24 @@ function validateProfile(value) {
1355
1353
  throw usage("Base URL must use HTTPS (or loopback HTTP), without credentials, query, or fragment.");
1356
1354
  if (p.model !== void 0 && (typeof p.model !== "string" || !p.model.trim()))
1357
1355
  throw usage("Default model must not be blank.");
1356
+ if (p.api === "comfyui" && p.model === void 0)
1357
+ throw usage("ComfyUI profiles need --model: flux3-klein, flux3-klein-raw, or your name for the model in --workflow.");
1358
1358
  return {
1359
1359
  name: p.name,
1360
1360
  api: p.api,
1361
1361
  baseUrl: url.toString().replace(/\/+$/, ""),
1362
- auth: p.auth ?? "bearer",
1363
- ...typeof p.model === "string" ? { model: p.model.trim() } : {}
1362
+ auth: p.auth ?? (p.api === "comfyui" ? "none" : "bearer"),
1363
+ ...typeof p.model === "string" ? { model: p.model.trim() } : {},
1364
+ ...Object.fromEntries(COMFY_FIELDS.flatMap((field) => {
1365
+ const value2 = p[field];
1366
+ return typeof value2 === "string" ? [[field, value2.trim()]] : [];
1367
+ }))
1364
1368
  };
1365
1369
  }
1366
1370
  var ProfileRepository = class {
1367
1371
  path;
1368
1372
  credentials;
1369
- constructor(path = join2(dirname(defaultCredentialFilePath()), "providers.json"), credentials = profileStore) {
1373
+ constructor(path = join3(dirname2(defaultCredentialFilePath()), "providers.json"), credentials = profileStore) {
1370
1374
  this.path = path;
1371
1375
  this.credentials = credentials;
1372
1376
  }
@@ -1431,7 +1435,7 @@ var ProfileRepository = class {
1431
1435
  async save(profiles) {
1432
1436
  const temporary = `${this.path}.${randomUUID()}.tmp`;
1433
1437
  try {
1434
- await mkdir(dirname(this.path), { recursive: true });
1438
+ await mkdir2(dirname2(this.path), { recursive: true });
1435
1439
  await writeFile(temporary, JSON.stringify({ version: 1, profiles }, null, 2) + "\n", { mode: 384 });
1436
1440
  await rename(temporary, this.path);
1437
1441
  } catch {
@@ -1440,7 +1444,7 @@ var ProfileRepository = class {
1440
1444
  message: "Could not save endpoint configuration."
1441
1445
  });
1442
1446
  } finally {
1443
- await rm(temporary, { force: true }).catch(() => {
1447
+ await rm2(temporary, { force: true }).catch(() => {
1444
1448
  });
1445
1449
  }
1446
1450
  }
@@ -1449,9 +1453,9 @@ async function profileKey(profile, store) {
1449
1453
  if (profile.auth === "none")
1450
1454
  return void 0;
1451
1455
  try {
1452
- const record2 = await store.load(profile.name);
1453
- if (record2 && typeof record2.apiKey === "string")
1454
- return validateApiKey(record2.apiKey);
1456
+ const record3 = await store.load(profile.name);
1457
+ if (record3 && typeof record3.apiKey === "string")
1458
+ return validateApiKey(record3.apiKey);
1455
1459
  } catch {
1456
1460
  }
1457
1461
  throw new ProviderError({
@@ -1461,7 +1465,7 @@ async function profileKey(profile, store) {
1461
1465
  }
1462
1466
 
1463
1467
  // ../../packages/providers-configurable/dist/provider.js
1464
- import sharp2 from "sharp";
1468
+ import sharp4 from "sharp";
1465
1469
 
1466
1470
  // ../../packages/providers-configurable/dist/image-size.js
1467
1471
  function imageSize(value, api) {
@@ -1481,13 +1485,30 @@ function imageSize(value, api) {
1481
1485
  if (pixels && Number.isSafeInteger(Number(pixels[1])) && Number.isSafeInteger(Number(pixels[2])) && Number(pixels[1]) > 0 && Number(pixels[2]) > 0)
1482
1486
  return { size: `${Number(pixels[1])}x${Number(pixels[2])}` };
1483
1487
  const ratio = /^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)$/.exec(size);
1484
- if (api === "openrouter" && ratio && Number(ratio[1]) > 0 && Number(ratio[2]) > 0 && Number.isFinite(Number(ratio[1]) / Number(ratio[2])))
1488
+ if (api !== "openai-images" && ratio && Number(ratio[1]) > 0 && Number(ratio[2]) > 0 && Number.isFinite(Number(ratio[1]) / Number(ratio[2])))
1485
1489
  return { aspect_ratio: `${Number(ratio[1])}:${Number(ratio[2])}` };
1486
1490
  throw usage(api === "openai-images" ? "This API format requires WIDTHxHEIGHT or a preset; arbitrary aspect-only sizes are unsupported." : "Use WIDTHxHEIGHT, an aspect ratio, preset, or auto.");
1487
1491
  }
1492
+ function comfySize(value) {
1493
+ const fields = imageSize(value, "comfyui");
1494
+ let width, height;
1495
+ if (fields.aspect_ratio) {
1496
+ const [w, h] = fields.aspect_ratio.split(":").map(Number);
1497
+ const scale = Math.sqrt(1024 * 1024 / (w * h));
1498
+ [width, height] = [w * scale, h * scale];
1499
+ } else if (fields.size)
1500
+ [width, height] = fields.size.split("x").map(Number);
1501
+ else
1502
+ return void 0;
1503
+ width = Math.round(width / 16) * 16;
1504
+ height = Math.round(height / 16) * 16;
1505
+ if (Math.min(width, height) < 256 || Math.max(width, height) > 2048)
1506
+ throw usage("ComfyUI image edges must be from 256 to 2048 pixels.");
1507
+ return { width, height };
1508
+ }
1488
1509
 
1489
1510
  // ../../packages/providers-configurable/dist/http.js
1490
- var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
1511
+ var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
1491
1512
  var EndpointHttp = class {
1492
1513
  fetchImpl;
1493
1514
  timeoutSeconds;
@@ -1511,9 +1532,9 @@ var EndpointHttp = class {
1511
1532
  });
1512
1533
  if (!response.ok) {
1513
1534
  const status = response.status;
1514
- const failure2 = record(await response.json().catch(() => null));
1515
- const detail = record(failure2.error);
1516
- const upstream = record(detail.metadata).provider_name;
1535
+ const failure2 = record2(await response.json().catch(() => null));
1536
+ const detail = record2(failure2.error);
1537
+ const upstream = record2(detail.metadata).provider_name;
1517
1538
  const token = new Headers(init.headers).get("authorization")?.replace(/^Bearer\s+/i, "");
1518
1539
  const upstreamProvider = typeof upstream === "string" && /^[A-Za-z0-9][A-Za-z0-9 ._()-]{0,63}$/.test(upstream) && !(token && upstream.includes(token)) && !/(?:sk-|AIza)/.test(upstream) ? upstream : void 0;
1519
1540
  const retryHeader = response.headers.get("retry-after");
@@ -1537,7 +1558,7 @@ var EndpointHttp = class {
1537
1558
  const value = await response.json();
1538
1559
  if (!value || typeof value !== "object" || Array.isArray(value))
1539
1560
  throw new Error();
1540
- return record(value);
1561
+ return record2(value);
1541
1562
  } catch {
1542
1563
  throw new ProviderError({
1543
1564
  kind: "malformed",
@@ -1615,6 +1636,625 @@ function openAIRequest(model, request, images, mask) {
1615
1636
  return { path: "images/edits", body, json: false };
1616
1637
  }
1617
1638
 
1639
+ // ../../packages/providers-configurable/dist/comfyui.js
1640
+ import { randomInt, randomUUID as randomUUID2 } from "node:crypto";
1641
+ import sharp3 from "sharp";
1642
+
1643
+ // ../../packages/providers-configurable/dist/comfyui-workflow.js
1644
+ import { readFile as readFile2 } from "node:fs/promises";
1645
+ var TOKEN = /\{\{(\w+)\}\}/g;
1646
+ function usedTokens(graph) {
1647
+ const used = /* @__PURE__ */ new Set();
1648
+ for (const node of Object.values(graph))
1649
+ for (const value of Object.values(node.inputs))
1650
+ if (typeof value === "string")
1651
+ for (const match of value.matchAll(TOKEN))
1652
+ used.add(match[1]);
1653
+ return used;
1654
+ }
1655
+ async function loadWorkflow(path) {
1656
+ const invalid = (detail) => new ProviderError({
1657
+ kind: "configuration",
1658
+ message: `ComfyUI workflow ${path} ${detail}`
1659
+ });
1660
+ let parsed;
1661
+ try {
1662
+ parsed = JSON.parse(await readFile2(path, "utf8"));
1663
+ } catch {
1664
+ throw invalid("could not be read as JSON.");
1665
+ }
1666
+ const nodes = Object.values(record2(parsed));
1667
+ if (!nodes.length || nodes.some((node) => typeof record2(node).class_type !== "string" || typeof record2(node).inputs !== "object"))
1668
+ throw invalid("is not in API format. Export it from ComfyUI as API JSON.");
1669
+ const graph = parsed;
1670
+ const used = usedTokens(graph);
1671
+ const unknown = [...used].filter((t) => !/^(prompt|seed|width|height|image[1-9]\d*)$/.test(t));
1672
+ if (unknown.length)
1673
+ throw invalid(`uses unknown tokens: ${unknown.join(", ")}.`);
1674
+ if (!used.has("prompt") || !used.has("seed"))
1675
+ throw invalid("must use {{prompt}} and {{seed}}.");
1676
+ if (used.has("width") !== used.has("height"))
1677
+ throw invalid("must use {{width}} and {{height}} together.");
1678
+ const images = [...used].filter((t) => t.startsWith("image")).length;
1679
+ for (let i = 1; i <= images; i++)
1680
+ if (!used.has(`image${i}`))
1681
+ throw invalid(`must number its image tokens from {{image1}} in order.`);
1682
+ return { graph, images, sized: used.has("width") };
1683
+ }
1684
+ function fillWorkflow({ graph }, { prompt, seed, size, images }) {
1685
+ const values = {
1686
+ prompt,
1687
+ seed,
1688
+ ...size ?? {},
1689
+ ...Object.fromEntries(images.map((image, i) => [`image${i + 1}`, image]))
1690
+ };
1691
+ const fill = (value) => {
1692
+ if (typeof value !== "string")
1693
+ return value;
1694
+ const whole = /^\{\{(\w+)\}\}$/.exec(value);
1695
+ if (whole)
1696
+ return values[whole[1]];
1697
+ return value.replace(TOKEN, (_, token) => String(values[token]));
1698
+ };
1699
+ return Object.fromEntries(Object.entries(graph).map(([id, node]) => [
1700
+ id,
1701
+ {
1702
+ ...node,
1703
+ inputs: Object.fromEntries(Object.entries(node.inputs).map(([key, value]) => [key, fill(value)]))
1704
+ }
1705
+ ]));
1706
+ }
1707
+
1708
+ // ../../packages/providers-configurable/dist/validation.js
1709
+ function validateImageRequest(request, images) {
1710
+ if ((request.referenceImages?.length ?? 0) > 4)
1711
+ throw usage("At most four reference images are allowed.");
1712
+ const count = request.count ?? 1;
1713
+ if (!Number.isInteger(count) || count < 1 || count > 4)
1714
+ throw usage("Image count must be from 1 to 4.");
1715
+ for (const image of images)
1716
+ if (!["image/png", "image/jpeg", "image/webp"].includes(image.contentType))
1717
+ throw usage("Input images must be PNG, JPEG, or WebP.");
1718
+ const format = (request.format ?? "png").trim().toLowerCase().replace(/^jpg$/, "jpeg");
1719
+ if (!["png", "jpeg", "webp"].includes(format))
1720
+ throw usage("Output format must be png, jpeg, or webp.");
1721
+ return { count, format };
1722
+ }
1723
+
1724
+ // ../../packages/providers-configurable/dist/flux3-klein-prompts.js
1725
+ var FLUX3_KLEIN_GENERATE_PROMPT = `You are the prompt upsampler of a text-to-image model. Expand the user's short request into ONE detailed, self-contained caption: the exact text the image model receives. Output only the caption, with no greetings, explanations, refusals, questions, Markdown, headings, JSON, coordinates, ids or angle-bracket tokens, and no quotes around it. If the input is not clearly an image request, caption a visualization of its concept. The request may begin with a "TARGET CANVAS: W:H" line: compose for that canvas (wide spreads content sideways, tall stacks it) and never mention the ratio or the canvas.
1726
+
1727
+ Fidelity and planning: keep every explicit subject, attribute, count, spatial constraint, style cue and piece of wording; never contradict, substitute, drop or narrow the request, and keep an abstract concept abstract. Decide everything left open as ONE concrete, consistent plan; never offer alternatives or hedge. Never re-express a stated constraint through another mechanism; add camera optics (depth of field, bokeh, blur) only when asked for or implied. Without a stated style, plan an actual photograph and open with the medium convention ("Full-color outdoor photograph, ...", "Full-color indoor photograph, ...", "Studio photograph, ...", "Close-up photograph, ..."); keep "Photorealistic" for renders, and turn "photorealistic" or "realistic" requests for photos into the photograph convention. Give a photograph a few ordinary, non-ideal specifics drawn from the scene's own objects instead of the stock-photo ideal. Do not invent names, brands, logos or celebrities; keep user-named ones. A long request keeps its content but not its wording: drop appraisal and sales vocabulary, turn moods into observable physical facts, and add only new concrete content with positions. Time of day is lighting, not rendered text.
1728
+
1729
+ Caption format: one continuous prose block in the generative voice, never "this image shows", "we can see" or "is visible". Lead with medium and style, then scene, lighting and camera, then the subjects with concrete colors, materials and state. Every person in focus gets an observable profile: ethnicity, gender, age range, skin tone ("fair skin", "brown skin"), hair and clothing. Make the layout reconstructable from words: frame-relative positions (frame-left, frame-right, upper, lower, foreground, midground, background), sentence-initial anchors ("On the left, ...", "In the background, ..."), relations (holds, faces, behind) and a consistent order; describe up to about ten instances one by one, beyond that give the exact count and arrangement. Out-of-focus or distant planes are shapes, colors and silhouettes, never identities or readable text, and the background takes about a sixth of the caption. Be definite: no hedging adverbs, no quality or emotion judgments, no closing sentence about mood; state negations as what is present. Rendered text: transcribe every wording verbatim inside double quotes in its original language and script, keep the line breaks of multi-line text, and give its typography, color and placement; author concrete, domain-correct wording when the request needs text it does not supply, and name canonical structures (calendar, keyboard, periodic table) exactly with their full layout. All prose is English; only quoted text keeps its language. Typical length is 100 to 350 words, more for complex scenes.
1730
+
1731
+ Output only the caption.`;
1732
+ var FLUX3_KLEIN_EDIT_PROMPT = `You are the prompt upsampler of an image-editing model. You see the reference image(s), attached in order (the first image, the second image, ...), and the user's short request. Expand it into ONE detailed, self-contained instruction: the caption the editing model receives together with the same images. Output only the instruction as one continuous English prose block, with no greetings, explanations, Markdown, JSON, coordinates, ids or angle-bracket tokens. Read a vague or conversational request as the most plausible edit of the supplied images.
1733
+
1734
+ Pick the voice by task shape. When one reference stays and gets bounded changes, write an imperative editing instruction on it ("Remove the ...", "Change the ... to ...", "Zoom in on ..."); a whole-image transformation opens with its scope ("Convert the entire image into ...", "Apply a ... filter over the entire scene", "Horizontally flip the entire image so that ...", "Shift the camera viewpoint to ...", then where the major elements should now be). When the references are drawn into a newly composed frame (combining subjects, a new scene, a garment, object or style donor), write a generation request ("Create a photograph of ...", "Create an anime-style illustration of ...", "Generate a new image featuring ...") that says what to take from each image, by ordinal, and what is newly generated.
1735
+
1736
+ Keep every explicit edit, constraint, count, position, style cue and piece of wording, and decide what is left open as one concrete plan. Ground everything in what is actually visible: identify each referent unambiguously by its real appearance, never mention content that is not there, and use every image the user attached. State every end state concretely (which color, how large, which pose, which material) and give new content enough appearance to be generated. Cover the knock-on effects of the change (shadows, reflections, occlusion, perspective, scale, lighting) and say what fills a removed region. A zoom-in names what stays in frame and which neighbors are cropped out; an outpainting continues the scene in the same medium. Describe the change, not the whole scene, and close an editing instruction with one compact clause naming what stays unchanged; transferred people keep their identity, face and accessories unless asked otherwise. Transcribe on-image text verbatim in its original language inside double quotes, with line breaks as \\n, and match the original font, size and color when replacing it. Name recognizable public figures, brands and landmarks you can see. Directions are frame-relative; use no evaluative or quality words. Typical length is 50 to 150 words.
1737
+
1738
+ Output only the instruction.`;
1739
+
1740
+ // ../../packages/providers-configurable/dist/flux3-klein.js
1741
+ var FLUX3_KLEIN_CHECKPOINT = "flux3-image-klein-int8-convrot";
1742
+ var FLUX3_KLEIN_MODELS = [
1743
+ {
1744
+ id: "flux3-klein",
1745
+ name: "FLUX 3 Klein, prompt expanded by its upsampler",
1746
+ upsample: true
1747
+ },
1748
+ {
1749
+ id: "flux3-klein-raw",
1750
+ name: "FLUX 3 Klein, prompt used verbatim as the full caption",
1751
+ upsample: false
1752
+ }
1753
+ ];
1754
+ var UPSAMPLER = "qwen3.5_9b_flux3_prompt_upsampler_w4a8.safetensors";
1755
+ var ENCODER = "qwen3vl_4b_int8_convrot.safetensors";
1756
+ var VAE = "flux2-vae.safetensors";
1757
+ var WEIGHTS = [
1758
+ {
1759
+ node: "UNETLoader",
1760
+ input: "unet_name",
1761
+ folder: "diffusion_models",
1762
+ file: `${FLUX3_KLEIN_CHECKPOINT}.safetensors`
1763
+ },
1764
+ {
1765
+ node: "CLIPLoader",
1766
+ input: "clip_name",
1767
+ folder: "text_encoders",
1768
+ file: ENCODER
1769
+ },
1770
+ {
1771
+ node: "VAELoader",
1772
+ input: "vae_name",
1773
+ folder: "vae",
1774
+ file: VAE
1775
+ }
1776
+ ];
1777
+ var flux3KleinWeights = (model) => model.upsample ? [
1778
+ ...WEIGHTS,
1779
+ {
1780
+ node: "CLIPLoader",
1781
+ input: "clip_name",
1782
+ folder: "text_encoders",
1783
+ file: UPSAMPLER
1784
+ }
1785
+ ] : WEIGHTS;
1786
+ function flux3KleinModel(id) {
1787
+ const model = FLUX3_KLEIN_MODELS.find((m) => m.id === id);
1788
+ if (!model)
1789
+ throw usage(`Built-in ComfyUI models are ${FLUX3_KLEIN_MODELS.map((m) => m.id).join(" and ")}. Add --workflow to the profile for any other model.`);
1790
+ return model;
1791
+ }
1792
+ var STEPS = { low: 4, medium: 8, high: 16, auto: 16 };
1793
+ function flux3KleinSteps(quality) {
1794
+ const steps = STEPS[(quality ?? "auto").trim().toLowerCase()];
1795
+ if (!steps)
1796
+ throw usage("Quality must be low, medium, high, or auto.");
1797
+ return steps;
1798
+ }
1799
+ function upsamplerRequest(job) {
1800
+ if (job.images.length || !job.size || job.size.width === job.size.height)
1801
+ return job.prompt;
1802
+ const divisor = (a, b) => b ? divisor(b, a % b) : a;
1803
+ const shared = divisor(job.size.width, job.size.height);
1804
+ return `TARGET CANVAS: ${job.size.width / shared}:${job.size.height / shared}
1805
+ ${job.prompt}`;
1806
+ }
1807
+ function flux3KleinWorkflow(job) {
1808
+ const editing = job.images.length > 0;
1809
+ const nodes = {
1810
+ model: {
1811
+ class_type: "UNETLoader",
1812
+ inputs: {
1813
+ unet_name: `${FLUX3_KLEIN_CHECKPOINT}.safetensors`,
1814
+ weight_dtype: "default"
1815
+ }
1816
+ },
1817
+ encoder: {
1818
+ class_type: "CLIPLoader",
1819
+ inputs: { clip_name: ENCODER, type: "flux3", device: "default" }
1820
+ },
1821
+ vae: { class_type: "VAELoader", inputs: { vae_name: VAE } }
1822
+ };
1823
+ job.images.forEach((image, i) => {
1824
+ nodes[`image${i}`] = { class_type: "LoadImage", inputs: { image } };
1825
+ });
1826
+ if (job.upsample) {
1827
+ nodes.upsampler_clip = {
1828
+ class_type: "CLIPLoader",
1829
+ inputs: { clip_name: UPSAMPLER, type: "flux3", device: "default" }
1830
+ };
1831
+ nodes.upsampler = {
1832
+ class_type: "TextGenerateV2",
1833
+ inputs: {
1834
+ clip: ["upsampler_clip", 0],
1835
+ prompt: upsamplerRequest(job),
1836
+ system_prompt: editing ? FLUX3_KLEIN_EDIT_PROMPT : FLUX3_KLEIN_GENERATE_PROMPT,
1837
+ max_length: 3584,
1838
+ sampling_mode: "on",
1839
+ "sampling_mode.temperature": 1,
1840
+ "sampling_mode.top_k": 1,
1841
+ "sampling_mode.top_p": 0.95,
1842
+ "sampling_mode.min_p": 0,
1843
+ "sampling_mode.repetition_penalty": 1.05,
1844
+ "sampling_mode.presence_penalty": editing ? 0 : 1.5,
1845
+ "sampling_mode.seed": 0,
1846
+ thinking: false,
1847
+ use_default_template: true,
1848
+ mtp: "off",
1849
+ ...Object.fromEntries(job.images.map((_, i) => [`images.image${i + 1}`, [`image${i}`, 0]]))
1850
+ }
1851
+ };
1852
+ nodes.caption = {
1853
+ class_type: "PreviewAny",
1854
+ inputs: { source: ["upsampler", 0] }
1855
+ };
1856
+ }
1857
+ nodes.positive = {
1858
+ class_type: "CLIPTextEncode",
1859
+ inputs: {
1860
+ clip: ["encoder", 0],
1861
+ text: job.upsample ? ["upsampler", 0] : job.prompt
1862
+ }
1863
+ };
1864
+ nodes.negative = editing ? {
1865
+ class_type: "ConditioningZeroOut",
1866
+ inputs: { conditioning: ["positive", 0] }
1867
+ } : {
1868
+ class_type: "CLIPTextEncode",
1869
+ inputs: { clip: ["encoder", 0], text: "" }
1870
+ };
1871
+ if (editing)
1872
+ nodes.references = {
1873
+ class_type: "ReferenceMultiLatent",
1874
+ inputs: {
1875
+ positive: ["positive", 0],
1876
+ negative: ["negative", 0],
1877
+ vae: ["vae", 0],
1878
+ resolution: "resize",
1879
+ "resolution.megapixels": 1,
1880
+ ...Object.fromEntries(job.images.map((_, i) => [`images.image${i}`, [`image${i}`, 0]]))
1881
+ }
1882
+ };
1883
+ if (!editing || job.size) {
1884
+ const { width, height } = job.size ?? { width: 1024, height: 1024 };
1885
+ nodes.latent = {
1886
+ class_type: "EmptyFlux2LatentImage",
1887
+ inputs: { width, height, batch_size: 1 }
1888
+ };
1889
+ }
1890
+ nodes.sampler = {
1891
+ class_type: "KSampler",
1892
+ inputs: {
1893
+ model: ["model", 0],
1894
+ seed: job.seed,
1895
+ steps: job.steps,
1896
+ cfg: 1,
1897
+ sampler_name: "euler",
1898
+ scheduler: "simple",
1899
+ denoise: 1,
1900
+ positive: editing ? ["references", 0] : ["positive", 0],
1901
+ negative: editing ? ["references", 1] : ["negative", 0],
1902
+ // Without a requested size, edits keep the first reference's shape.
1903
+ latent_image: nodes.latent ? ["latent", 0] : ["references", 2]
1904
+ }
1905
+ };
1906
+ nodes.decode = {
1907
+ class_type: "VAEDecode",
1908
+ inputs: { samples: ["sampler", 0], vae: ["vae", 0] }
1909
+ };
1910
+ nodes.output = {
1911
+ class_type: "PreviewImage",
1912
+ inputs: { images: ["decode", 0] }
1913
+ };
1914
+ return nodes;
1915
+ }
1916
+
1917
+ // ../../packages/providers-configurable/dist/comfyui.js
1918
+ var EXTENSIONS = {
1919
+ "image/png": "png",
1920
+ "image/jpeg": "jpg",
1921
+ "image/webp": "webp"
1922
+ };
1923
+ function comboOptions(spec) {
1924
+ if (!Array.isArray(spec))
1925
+ return void 0;
1926
+ const choices = spec[0] === "COMBO" ? record2(spec[1]).options : spec[0];
1927
+ return Array.isArray(choices) ? choices : void 0;
1928
+ }
1929
+ var comboChoices = (spec) => comboOptions(spec) ?? [];
1930
+ async function sourceShape(image) {
1931
+ try {
1932
+ const { width, height } = await sharp3(image.data).metadata();
1933
+ return comfySize(`${width}:${height}`);
1934
+ } catch (e) {
1935
+ if (e instanceof ProviderError)
1936
+ throw e;
1937
+ throw usage("Could not read the source image's dimensions.");
1938
+ }
1939
+ }
1940
+ function executionFailure(messages) {
1941
+ const failure2 = Array.isArray(messages) ? messages.find((m) => Array.isArray(m) && m[0] === "execution_error") : void 0;
1942
+ const detail = record2(Array.isArray(failure2) ? failure2[1] : void 0);
1943
+ const node = typeof detail.node_type === "string" ? detail.node_type : "the workflow";
1944
+ const reason = typeof detail.exception_message === "string" ? detail.exception_message.replace(/\s+/g, " ").trim().slice(0, 300) : "";
1945
+ const memory = /out ?of ?memory/i.test(`${detail.exception_type} ${reason}`);
1946
+ return new ProviderError({
1947
+ kind: "server",
1948
+ message: `ComfyUI failed in ${node}${reason ? `: ${reason}` : "."}${memory ? " Close other GPU applications or request a smaller --size." : ""}`
1949
+ });
1950
+ }
1951
+ async function encoded(data, format) {
1952
+ try {
1953
+ const result = await sharp3(data).toFormat(format).toBuffer({ resolveWithObject: true });
1954
+ return {
1955
+ data: result.data,
1956
+ width: result.info.width,
1957
+ height: result.info.height
1958
+ };
1959
+ } catch {
1960
+ throw new ProviderError({
1961
+ kind: "malformed",
1962
+ message: "ComfyUI returned an invalid raster image."
1963
+ });
1964
+ }
1965
+ }
1966
+ var ComfyUIImageProvider = class {
1967
+ name;
1968
+ profile;
1969
+ baseUrl;
1970
+ defaultModel;
1971
+ http;
1972
+ pollIntervalMs;
1973
+ constructor(profile, options = {}) {
1974
+ const valid = validateProfile(profile);
1975
+ this.profile = valid;
1976
+ this.name = valid.name;
1977
+ this.baseUrl = valid.baseUrl;
1978
+ this.defaultModel = valid.model;
1979
+ this.http = new EndpointHttp(options.fetchImpl, options.timeoutSeconds ?? 600);
1980
+ this.pollIntervalMs = options.pollIntervalMs ?? 500;
1981
+ }
1982
+ sizePolicy = {
1983
+ resolveSize: (size) => {
1984
+ const resolved = comfySize(size);
1985
+ if (!resolved)
1986
+ return { size: "auto" };
1987
+ const { width, height } = resolved;
1988
+ return {
1989
+ width,
1990
+ height,
1991
+ pixels: width * height,
1992
+ size: `${width}x${height}`
1993
+ };
1994
+ }
1995
+ };
1996
+ generate(request) {
1997
+ return this.run(request, request.referenceImages ?? []);
1998
+ }
1999
+ async edit(request) {
2000
+ if (request.mask)
2001
+ throw usage("ComfyUI profiles have no mask input. Describe the edit region in the prompt.");
2002
+ return this.run(request, [
2003
+ request.sourceImage,
2004
+ ...request.referenceImages ?? []
2005
+ ]);
2006
+ }
2007
+ get usesWorkflowFiles() {
2008
+ return Boolean(this.profile.workflow || this.profile.editWorkflow);
2009
+ }
2010
+ async models() {
2011
+ if (this.usesWorkflowFiles) {
2012
+ const { workflow, editWorkflow } = this.profile;
2013
+ const files = await Promise.all([workflow, editWorkflow].flatMap((f) => f ? [loadWorkflow(f)] : []));
2014
+ return this.reachable(() => this.http.run(async (http) => {
2015
+ for (const file of files)
2016
+ await this.requireInputs(http, file.graph);
2017
+ return [
2018
+ {
2019
+ id: this.defaultModel,
2020
+ name: `${this.defaultModel}, from this profile's workflow files`,
2021
+ capabilities: "reported",
2022
+ inputModalities: editWorkflow ? ["text", "image"] : ["text"],
2023
+ outputModalities: ["image"]
2024
+ }
2025
+ ];
2026
+ }));
2027
+ }
2028
+ flux3KleinModel(this.defaultModel);
2029
+ return this.reachable(() => this.http.run(async (http) => {
2030
+ const models = [];
2031
+ for (const { id, name, upsample } of FLUX3_KLEIN_MODELS)
2032
+ if (!(await this.missingWeights(http, { id, name, upsample })).length)
2033
+ models.push({
2034
+ id,
2035
+ name,
2036
+ capabilities: "reported",
2037
+ inputModalities: ["text", "image"],
2038
+ outputModalities: ["image"]
2039
+ });
2040
+ return models;
2041
+ }));
2042
+ }
2043
+ async run(request, images) {
2044
+ const { count, format } = validateImageRequest(request, images);
2045
+ const plan = this.usesWorkflowFiles ? await this.workflowPlan(request, images) : this.kleinPlan(request);
2046
+ return this.reachable(async () => {
2047
+ await this.http.run((http) => plan.preflight(http));
2048
+ const uploaded = [];
2049
+ for (const image of images)
2050
+ uploaded.push(await this.upload(image));
2051
+ const results = [];
2052
+ for (let i = 0; i < count; i++)
2053
+ results.push(await this.render(plan, uploaded, randomInt(281474976710655), format));
2054
+ return {
2055
+ provider: this.name,
2056
+ requestedModel: plan.model,
2057
+ images: results
2058
+ };
2059
+ });
2060
+ }
2061
+ kleinPlan(request) {
2062
+ const model = flux3KleinModel((request.model ?? this.defaultModel).trim());
2063
+ const steps = flux3KleinSteps(request.quality);
2064
+ const size = comfySize(request.size);
2065
+ return {
2066
+ model: model.id,
2067
+ preflight: async (http) => {
2068
+ const missing = await this.missingWeights(http, model);
2069
+ if (missing.length)
2070
+ throw new ProviderError({
2071
+ kind: "configuration",
2072
+ message: `ComfyUI is missing FLUX 3 Klein weights: ${missing.join(", ")}. Download them from black-forest-labs/flux-3-image-klein-comfy into its models folder.`
2073
+ });
2074
+ },
2075
+ graph: (images, seed) => flux3KleinWorkflow({
2076
+ prompt: request.prompt,
2077
+ upsample: model.upsample,
2078
+ images,
2079
+ size,
2080
+ steps,
2081
+ seed
2082
+ }),
2083
+ described: (outputs, seed) => {
2084
+ const caption = record2(outputs.caption).text;
2085
+ return {
2086
+ prompt: model.upsample && Array.isArray(caption) && typeof caption[0] === "string" ? caption[0] : request.prompt,
2087
+ servedModel: FLUX3_KLEIN_CHECKPOINT,
2088
+ usage: { served: { steps, seed, upsampled: model.upsample } }
2089
+ };
2090
+ }
2091
+ };
2092
+ }
2093
+ async workflowPlan(request, images) {
2094
+ const model = this.defaultModel;
2095
+ if (request.model && request.model.trim() !== model)
2096
+ throw usage(`Profile ${this.name} renders ${model} from its workflow files. Update the profile to change models.`);
2097
+ if ((request.quality ?? "auto").trim().toLowerCase() !== "auto")
2098
+ throw usage("Workflow files set their own sampling, so only --quality auto applies.");
2099
+ const path = images.length ? this.profile.editWorkflow : this.profile.workflow;
2100
+ if (!path)
2101
+ throw usage(images.length ? `Profile ${this.name} has no --edit-workflow, so it cannot take input images.` : `Profile ${this.name} has no --workflow, so it needs a source image.`);
2102
+ const workflow = await loadWorkflow(path);
2103
+ if (workflow.images !== images.length)
2104
+ throw usage(`This workflow takes ${workflow.images} input images, but the request has ${images.length}.`);
2105
+ const requested = comfySize(request.size);
2106
+ if (requested && !workflow.sized)
2107
+ throw usage("This workflow has fixed dimensions, so only --size auto applies.");
2108
+ const size = !workflow.sized ? void 0 : requested ?? (images[0] ? await sourceShape(images[0]) : { width: 1024, height: 1024 });
2109
+ return {
2110
+ model,
2111
+ preflight: (http) => this.requireInputs(http, workflow.graph),
2112
+ graph: (uploaded, seed) => fillWorkflow(workflow, {
2113
+ prompt: request.prompt,
2114
+ seed,
2115
+ size,
2116
+ images: uploaded
2117
+ }),
2118
+ described: (_, seed) => ({
2119
+ prompt: request.prompt,
2120
+ servedModel: model,
2121
+ usage: { served: { seed } }
2122
+ })
2123
+ };
2124
+ }
2125
+ async reachable(action) {
2126
+ try {
2127
+ return await action();
2128
+ } catch (e) {
2129
+ if (e instanceof ProviderError && e.kind === "transport") {
2130
+ const start = this.profile.startCommand;
2131
+ throw new ProviderError({
2132
+ kind: "transport",
2133
+ message: `ComfyUI is not reachable at ${this.baseUrl}. Start it${start ? ` with \`${start}\`` : ""} and retry.`
2134
+ });
2135
+ }
2136
+ throw e;
2137
+ }
2138
+ }
2139
+ // Names what a workflow file needs instead of queueing a doomed job.
2140
+ async requireInputs(http, graph) {
2141
+ const specs = /* @__PURE__ */ new Map();
2142
+ const missing = /* @__PURE__ */ new Set();
2143
+ for (const node of Object.values(graph)) {
2144
+ if (!specs.has(node.class_type)) {
2145
+ const info = await http.json(`${this.baseUrl}/object_info/${encodeURIComponent(node.class_type)}`);
2146
+ specs.set(node.class_type, record2(info[node.class_type]));
2147
+ }
2148
+ const spec = record2(specs.get(node.class_type));
2149
+ if (!Object.keys(spec).length) {
2150
+ missing.add(`node type ${node.class_type}`);
2151
+ continue;
2152
+ }
2153
+ const inputs = {
2154
+ ...record2(record2(spec.input).required),
2155
+ ...record2(record2(spec.input).optional)
2156
+ };
2157
+ for (const [name, value] of Object.entries(node.inputs)) {
2158
+ if (typeof value !== "string" || value.includes("{{"))
2159
+ continue;
2160
+ if (comboOptions(inputs[name])?.includes(value) === false)
2161
+ missing.add(`${value} for ${node.class_type}.${name}`);
2162
+ }
2163
+ }
2164
+ if (missing.size)
2165
+ throw new ProviderError({
2166
+ kind: "configuration",
2167
+ message: `ComfyUI is missing what this workflow needs: ${[...missing].join(", ")}. Install it under ComfyUI, restart the server, and retry.`
2168
+ });
2169
+ }
2170
+ async missingWeights(http, model) {
2171
+ const choices = /* @__PURE__ */ new Map();
2172
+ const missing = [];
2173
+ for (const weight of flux3KleinWeights(model)) {
2174
+ if (!choices.has(weight.node)) {
2175
+ const info = await http.json(`${this.baseUrl}/object_info/${weight.node}`);
2176
+ const inputs = record2(record2(record2(info[weight.node]).input).required);
2177
+ choices.set(weight.node, comboChoices(inputs[weight.input]));
2178
+ }
2179
+ if (!choices.get(weight.node)?.includes(weight.file))
2180
+ missing.push(`${weight.folder}/${weight.file}`);
2181
+ }
2182
+ return missing;
2183
+ }
2184
+ // The temp folder keeps inputs out of ComfyUI's library; it clears on restart.
2185
+ upload(image) {
2186
+ return this.http.run(async (http) => {
2187
+ const form = new FormData();
2188
+ form.set("image", new Blob([new Uint8Array(image.data)], { type: image.contentType }), `imagegen-${randomUUID2()}.${EXTENSIONS[image.contentType]}`);
2189
+ form.set("type", "temp");
2190
+ const body = await http.json(`${this.baseUrl}/upload/image`, {
2191
+ method: "POST",
2192
+ body: form
2193
+ });
2194
+ if (typeof body.name !== "string")
2195
+ throw new ProviderError({
2196
+ kind: "malformed",
2197
+ message: "ComfyUI did not return an uploaded image name."
2198
+ });
2199
+ return `${body.name} [temp]`;
2200
+ });
2201
+ }
2202
+ render(plan, images, seed, format) {
2203
+ return this.http.run(async (http) => {
2204
+ const queued = await http.json(`${this.baseUrl}/prompt`, {
2205
+ method: "POST",
2206
+ headers: { "content-type": "application/json" },
2207
+ body: JSON.stringify({ prompt: plan.graph(images, seed) })
2208
+ }).catch((e) => {
2209
+ if (e instanceof ProviderError && e.statusCode === 400)
2210
+ throw new ProviderError({
2211
+ kind: "server",
2212
+ statusCode: 400,
2213
+ message: `ComfyUI rejected the ${plan.model} workflow. Check its console for the failing node, and update ComfyUI if a node is unknown.`
2214
+ });
2215
+ throw e;
2216
+ });
2217
+ if (typeof queued.prompt_id !== "string")
2218
+ throw new ProviderError({
2219
+ kind: "malformed",
2220
+ message: "ComfyUI did not return a prompt ID."
2221
+ });
2222
+ const outputs = record2((await this.finished(http, queued.prompt_id)).outputs);
2223
+ const saved = Object.values(outputs).map((output) => record2(output).images).find((list) => Array.isArray(list) && list.length);
2224
+ const file = record2(Array.isArray(saved) ? saved[0] : void 0);
2225
+ if (typeof file.filename !== "string")
2226
+ throw new ProviderError({
2227
+ kind: "no_image",
2228
+ message: "ComfyUI finished without an output image."
2229
+ });
2230
+ const query = new URLSearchParams({
2231
+ filename: file.filename,
2232
+ subfolder: typeof file.subfolder === "string" ? file.subfolder : "",
2233
+ type: typeof file.type === "string" ? file.type : "temp"
2234
+ });
2235
+ const data = await http.bytes(`${this.baseUrl}/view?${query}`);
2236
+ return {
2237
+ ...await encoded(data, format),
2238
+ mimeType: `image/${format}`,
2239
+ responseId: queued.prompt_id,
2240
+ ...plan.described(outputs, seed)
2241
+ };
2242
+ });
2243
+ }
2244
+ async finished(http, id) {
2245
+ for (; ; ) {
2246
+ const history = await http.json(`${this.baseUrl}/history/${id}`);
2247
+ const entry = record2(history[id]);
2248
+ const status = record2(entry.status);
2249
+ if (status.status_str === "error")
2250
+ throw executionFailure(status.messages);
2251
+ if (status.completed === true)
2252
+ return entry;
2253
+ await new Promise((resolve4) => setTimeout(resolve4, this.pollIntervalMs));
2254
+ }
2255
+ }
2256
+ };
2257
+
1618
2258
  // ../../packages/providers-configurable/dist/provider.js
1619
2259
  var ConfigurableImageProvider = class {
1620
2260
  getKey;
@@ -1672,14 +2312,14 @@ var ConfigurableImageProvider = class {
1672
2312
  message: "Model discovery response must contain a data array."
1673
2313
  });
1674
2314
  return body.data.map((value) => {
1675
- const model = record(value);
2315
+ const model = record2(value);
1676
2316
  if (typeof model.id !== "string")
1677
2317
  throw new ProviderError({
1678
2318
  kind: "malformed",
1679
2319
  message: "Discovered model has no string ID."
1680
2320
  });
1681
- const architecture = record(model.architecture);
1682
- const supported = record(model.supported_parameters);
2321
+ const architecture = record2(model.architecture);
2322
+ const supported = record2(model.supported_parameters);
1683
2323
  const strings = (v) => Array.isArray(v) && v.every((s) => typeof s === "string") ? v : void 0;
1684
2324
  return {
1685
2325
  id: model.id,
@@ -1698,19 +2338,12 @@ var ConfigurableImageProvider = class {
1698
2338
  const model = (request.model ?? this.profile.model ?? "").trim();
1699
2339
  if (!model)
1700
2340
  throw usage("Supply --model or configure a default model with providers update.");
1701
- if ((request.referenceImages?.length ?? 0) > 4)
1702
- throw usage("At most four reference images are allowed.");
1703
- const count = request.count ?? 1;
1704
- if (!Number.isInteger(count) || count < 1 || count > 4)
1705
- throw usage("Image count must be from 1 to 4.");
1706
2341
  if (mask && this.profile.api === "openrouter")
1707
2342
  throw usage("OpenRouter image API does not expose masks. Describe the edit region in the prompt.");
1708
- for (const image of [...images, ...mask ? [mask] : []])
1709
- if (!["image/png", "image/jpeg", "image/webp"].includes(image.contentType))
1710
- throw usage("Input images must be PNG, JPEG, or WebP.");
1711
- const format = (request.format ?? "png").trim().toLowerCase().replace(/^jpg$/, "jpeg");
1712
- if (!["png", "jpeg", "webp"].includes(format))
1713
- throw usage("Output format must be png, jpeg, or webp.");
2343
+ const { count, format } = validateImageRequest(request, [
2344
+ ...images,
2345
+ ...mask ? [mask] : []
2346
+ ]);
1714
2347
  const quality = request.quality?.trim().toLowerCase();
1715
2348
  if (quality && !/^[a-z][a-z0-9_-]*$/.test(quality))
1716
2349
  throw usage("Quality must be a provider-supported quality name.");
@@ -1737,7 +2370,7 @@ var ConfigurableImageProvider = class {
1737
2370
  kind: "malformed",
1738
2371
  message: "Endpoint must return exactly one image for a single-image request."
1739
2372
  });
1740
- const image = record(body.data[0]);
2373
+ const image = record2(body.data[0]);
1741
2374
  let data;
1742
2375
  if (typeof image.b64_json === "string") {
1743
2376
  if (!image.b64_json || image.b64_json.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.b64_json))
@@ -1768,12 +2401,12 @@ var ConfigurableImageProvider = class {
1768
2401
  message: "Endpoint returned no image bytes or download URL."
1769
2402
  });
1770
2403
  try {
1771
- const metadata = await sharp2(data).metadata();
2404
+ const metadata = await sharp4(data).metadata();
1772
2405
  if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || typeof image.media_type === "string" && image.media_type !== `image/${metadata.format}`)
1773
2406
  throw new Error();
1774
- await sharp2(data).stats();
2407
+ await sharp4(data).stats();
1775
2408
  if (metadata.format !== format)
1776
- data = await sharp2(data).toFormat(format).toBuffer();
2409
+ data = await sharp4(data).toFormat(format).toBuffer();
1777
2410
  return {
1778
2411
  data,
1779
2412
  width: metadata.width,
@@ -1789,12 +2422,15 @@ var ConfigurableImageProvider = class {
1789
2422
  });
1790
2423
  }
1791
2424
  }));
1792
- return { provider: this.name, imageModel: model, images: results };
2425
+ return { provider: this.name, requestedModel: model, images: results };
1793
2426
  }
1794
2427
  };
2428
+ function createProfileProvider(profile) {
2429
+ return profile.api === "comfyui" ? new ComfyUIImageProvider(profile) : new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore()));
2430
+ }
1795
2431
  function createProfileEntry(profile) {
1796
2432
  return {
1797
- provider: new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore())),
2433
+ provider: createProfileProvider(profile),
1798
2434
  authStatus: async () => {
1799
2435
  let configured = profile.auth === "none";
1800
2436
  if (!configured)
@@ -1833,7 +2469,7 @@ async function readApiKey(fromStdin, input = process.stdin, output = process.std
1833
2469
  const wasRaw = terminal.isRaw;
1834
2470
  const wasFlowing = input.readableFlowing === true;
1835
2471
  emitKeypressEvents(input);
1836
- return new Promise((resolve3, reject) => {
2472
+ return new Promise((resolve4, reject) => {
1837
2473
  let text = "";
1838
2474
  const cleanup = () => {
1839
2475
  input.removeListener("keypress", onKey);
@@ -1858,7 +2494,7 @@ async function readApiKey(fromStdin, input = process.stdin, output = process.std
1858
2494
  }
1859
2495
  if (key.name === "return" || key.name === "enter") {
1860
2496
  cleanup();
1861
- resolve3(text);
2497
+ resolve4(text);
1862
2498
  return;
1863
2499
  }
1864
2500
  if (key.name === "backspace") {
@@ -1882,11 +2518,15 @@ async function readApiKey(fromStdin, input = process.stdin, output = process.std
1882
2518
 
1883
2519
  // src/help.ts
1884
2520
  var PROFILE_HELP = `Named endpoint profiles:
1885
- imagegen providers add <name> --api <openrouter|openai-images> [--base-url <url>]
1886
- [--model <id>] [--auth <bearer|none>]
2521
+ imagegen providers add <name> --api <openrouter|openai-images|comfyui>
2522
+ [--base-url <url>] [--model <id>] [--auth <bearer|none>]
2523
+ [--start-command <text>] [--workflow <file>]
2524
+ [--edit-workflow <file>]
1887
2525
  imagegen providers show <name>
1888
2526
  imagegen providers update <name> [--api <format>] [--base-url <url>]
1889
2527
  [--model <id>] [--auth <bearer|none>]
2528
+ [--start-command <text>] [--workflow <file>]
2529
+ [--edit-workflow <file>]
1890
2530
  imagegen providers remove <name>
1891
2531
  imagegen auth <name> login [--key-stdin]
1892
2532
  imagegen auth <name> status [--show-path]
@@ -1894,8 +2534,9 @@ var PROFILE_HELP = `Named endpoint profiles:
1894
2534
  imagegen models --provider <name>
1895
2535
 
1896
2536
  OpenRouter defaults to https://openrouter.ai/api/v1. openai-images requires
1897
- --base-url including the API prefix (e.g. https://api.openai.com/v1).
1898
- Keys use hidden terminal input or explicit --key-stdin and the existing vault/file
2537
+ --base-url including the API prefix (e.g. https://api.openai.com/v1). comfyui
2538
+ requires --base-url of its server (e.g. http://127.0.0.1:8188) and --model, and
2539
+ uses no key. Keys use hidden terminal input or explicit --key-stdin and the existing vault/file
1899
2540
  store. --auth none supports endpoints without keys. Config is saved in providers.json
1900
2541
  alongside the credential file. Origin changes clear the saved key; removing a
1901
2542
  profile removes its credentials. Logout preserves profile configuration.
@@ -1904,13 +2545,25 @@ Select named profiles with --provider. Model precedence: --model > profile defau
1904
2545
  provide one if no default is saved. models explicitly discovers model metadata;
1905
2546
  OpenAI-compatible model listings do not establish image capabilities.
1906
2547
 
1907
- Supported protocols: OpenRouter dedicated Images API and OpenAI Images JSON /
1908
- multipart endpoints, not arbitrary HTTP templates. Options vary by model.
2548
+ Supported protocols: OpenRouter dedicated Images API, OpenAI Images JSON /
2549
+ multipart endpoints, and local models through ComfyUI, not arbitrary HTTP
2550
+ templates. Options vary by model.
1909
2551
  OpenRouter accepts aspect ratios; openai-images needs pixels or presets:
1910
2552
  square 1024x1024, portrait 1024x1536, landscape 1536x1024, wide 1536x864.
1911
- Auto options are omitted. Native masks require openai-images or Codex.
2553
+ Auto options are omitted. Native masks require openai-images.
1912
2554
  Outputs are measured and converted to PNG/JPEG/WebP without resizing. Count uses
1913
2555
  separate single-image requests without automatic retries or model substitution.
2556
+
2557
+ comfyui profiles: --model names what the profile renders. Built in: flux3-klein
2558
+ expands the prompt and reports the rendered caption as prompt; flux3-klein-raw
2559
+ uses the prompt as the full caption; quality low, medium, and high run 4, 8, and
2560
+ 16 steps. Any other model needs --workflow (text-to-image) and/or --edit-workflow
2561
+ (input images): ComfyUI API-format JSON using {{prompt}} and {{seed}}, optionally
2562
+ {{width}} and {{height}}, and {{image1}}, {{image2}}, ... for inputs. Workflow
2563
+ files set their own sampling, so only --quality auto applies.
2564
+ Sizes accept pixels, presets, or aspect ratios, round to multiples of 16, and need
2565
+ edges from 256 to 2048. Masks are rejected. The CLI does not start or stop ComfyUI;
2566
+ --start-command records how to start it and is quoted when it is not reachable.
1914
2567
  `;
1915
2568
  var IMAGE_OPTIONS = `Image options (generate, edit):
1916
2569
  --prompt <text> Prompt text (required)
@@ -1938,32 +2591,41 @@ Gemini provider:
1938
2591
  Model precedence: --model > constructor imageModel > IMAGEGEN_GEMINI_IMAGE_MODEL
1939
2592
  > gemini-nano-banana-2.1. Calls are independent; use prior output as --source.
1940
2593
 
1941
- Supported Codex models (account availability is determined by the provider):
1942
- gpt-image-2.5-flare Default; fast image generation and editing
1943
- gpt-image-2.5-sunburst Higher-precision image generation and editing
1944
- gpt-image-2 Previous model; retained for compatibility
2594
+ Codex model requests (the backend chooses what actually runs):
2595
+ gpt-image-2.6-flare Requested by default
2596
+ gpt-image-2.6-sunburst Requested with --model
2597
+ gpt-image-2.5-flare Requested with --model
2598
+ gpt-image-2.5-sunburst Requested with --model
2599
+ gpt-image-2 Requested with --model
1945
2600
 
1946
- Use full model IDs; other provider-supported IDs may also be supplied.
2601
+ Codex accepts any model ID, including made-up ones, and does not report what ran;
2602
+ its Responses route rewrites these requests to gpt-image-2-codex. Use a named
2603
+ profile such as OpenRouter when a specific model matters.
1947
2604
  Model precedence: --model > constructor imageModel (library callers) >
1948
- IMAGEGEN_CODEX_IMAGE_MODEL > gpt-image-2.5-flare.
1949
- IMAGEGEN_CODEX_MODEL configures orchestration separately; --model selects images.
1950
-
1951
- Codex quality: auto (default), low, medium, high; Flare/Sunburst also support
1952
- xhigh and max. gpt-image-2 rejects xhigh/max.
2605
+ IMAGEGEN_CODEX_IMAGE_MODEL > gpt-image-2.6-flare.
2606
+ JSON results report requestedModel, and servedModel per image (null unless the
2607
+ provider names one). Codex also reports served quality, size, and background, and
2608
+ may override what was requested. A Codex --mask is sent as the last edit image with a fixed
2609
+ instruction appended to the prompt; it steers the edit but does not guarantee
2610
+ untouched pixels outside it. It must match the source size, and uses one of the
2611
+ four edit-image slots.
2612
+ IMAGEGEN_USAGE_LOG=<file> appends one prompt-free JSON usage line per result.
2613
+
2614
+ Codex quality requests: auto (default), low, medium, high; Flare/Sunburst IDs also
2615
+ accept xhigh and max. gpt-image-2 rejects xhigh/max. The backend may serve another.
1953
2616
  `;
1954
2617
  var GENERATE_EXAMPLES = `Examples:
1955
2618
  imagegen generate --provider gemini --size 16:9 --prompt "a lighthouse at dawn" --out ./gemini.png
1956
2619
  imagegen generate --prompt "a lighthouse at dawn" --out ./lighthouse.png
1957
- imagegen generate --model gpt-image-2 --prompt "a lighthouse" --out ./classic.png
1958
- imagegen generate --model gpt-image-2.5-sunburst --quality max --prompt "a product photo" --out ./product.png
2620
+ imagegen generate --provider openrouter --model openai/gpt-image-2.6-sunburst --quality high --prompt "a product photo" --out ./product.png
1959
2621
  `;
1960
2622
  var EDIT_EXAMPLES = `Edit options:
1961
2623
  --source <file> Source image (required)
1962
- --mask <file> Optional edit mask (Codex or openai-images)
2624
+ --mask <file> Optional edit mask; transparent pixels mark the area to edit
1963
2625
 
1964
2626
  Examples:
1965
2627
  imagegen edit --format webp --source ./product.png --prompt "make the product blue" --out ./blue.webp
1966
- imagegen edit --model gpt-image-2.5-sunburst --quality max --source ./product.png --prompt "add a ribbon" --out ./ribbon.png
2628
+ imagegen edit --provider openrouter --model openai/gpt-image-2.6-sunburst --source ./product.png --prompt "add a ribbon" --out ./ribbon.png
1967
2629
  `;
1968
2630
  var USAGE = `imagegen - provider-agnostic image generation CLI
1969
2631
 
@@ -1981,6 +2643,7 @@ Usage:
1981
2643
  imagegen skill
1982
2644
  imagegen skill install --target <claude|codex|skills-dir> [--target <...>] [--force]
1983
2645
  imagegen help [command]
2646
+ imagegen --version
1984
2647
 
1985
2648
  ${PROFILE_HELP}
1986
2649
  ${IMAGE_OPTIONS}
@@ -1996,6 +2659,7 @@ Auth options:
1996
2659
 
1997
2660
  Global:
1998
2661
  -h, --help Show help and exit 0
2662
+ --version Print the CLI version and exit 0
1999
2663
  `;
2000
2664
  var COMMAND_HELP = {
2001
2665
  auth: `imagegen auth - manage provider credentials (one namespace per provider)
@@ -2102,9 +2766,9 @@ ${USAGE}`;
2102
2766
  }
2103
2767
 
2104
2768
  // src/skill.ts
2105
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2769
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
2106
2770
  import { homedir as homedir2 } from "node:os";
2107
- import { dirname as dirname2, join as join3, resolve } from "node:path";
2771
+ import { dirname as dirname3, join as join4, resolve } from "node:path";
2108
2772
  var SKILL_NAME = "imagegen-cli";
2109
2773
  var SkillExistsError = class extends Error {
2110
2774
  constructor(path) {
@@ -2113,15 +2777,15 @@ var SkillExistsError = class extends Error {
2113
2777
  }
2114
2778
  };
2115
2779
  async function skillText() {
2116
- return readFile2(new URL("../skill/SKILL.md", import.meta.url), "utf8");
2780
+ return readFile3(new URL("../skill/SKILL.md", import.meta.url), "utf8");
2117
2781
  }
2118
2782
  function targetPath(target) {
2119
- const root = target === "claude" ? join3(homedir2(), ".claude", "skills") : target === "codex" ? join3(homedir2(), ".agents", "skills") : resolve(target);
2120
- return join3(root, SKILL_NAME, "SKILL.md");
2783
+ const root = target === "claude" ? join4(homedir2(), ".claude", "skills") : target === "codex" ? join4(homedir2(), ".agents", "skills") : resolve(target);
2784
+ return join4(root, SKILL_NAME, "SKILL.md");
2121
2785
  }
2122
2786
  async function existingText(path) {
2123
2787
  try {
2124
- return await readFile2(path, "utf8");
2788
+ return await readFile3(path, "utf8");
2125
2789
  } catch (error2) {
2126
2790
  if (error2.code === "ENOENT") return void 0;
2127
2791
  throw error2;
@@ -2143,12 +2807,51 @@ async function installSkill(targets, force) {
2143
2807
  }
2144
2808
  for (const item of planned.values()) {
2145
2809
  if (item.status === "up-to-date") continue;
2146
- await mkdir2(dirname2(item.path), { recursive: true });
2810
+ await mkdir3(dirname3(item.path), { recursive: true });
2147
2811
  await writeFile2(item.path, content, { encoding: "utf8", flag: item.status === "installed" ? "wx" : "w" });
2148
2812
  }
2149
2813
  return [...planned.values()];
2150
2814
  }
2151
2815
 
2816
+ // src/usage-log.ts
2817
+ import { appendFile, mkdir as mkdir4 } from "node:fs/promises";
2818
+ import { dirname as dirname4, resolve as resolve2 } from "node:path";
2819
+ var USAGE_LOG_ENV = "IMAGEGEN_USAGE_LOG";
2820
+ function usageLogEntry(request, edit, result, paths) {
2821
+ return {
2822
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2823
+ command: edit ? "edit" : "generate",
2824
+ provider: result.provider,
2825
+ requestedModel: result.requestedModel,
2826
+ requested: {
2827
+ size: request.size ?? "auto",
2828
+ quality: request.quality ?? "auto",
2829
+ format: request.format ?? "png",
2830
+ count: request.count ?? 1,
2831
+ references: request.referenceImages?.length ?? 0,
2832
+ mask: edit ? edit.mask !== void 0 : void 0
2833
+ },
2834
+ images: result.images.map((image, i) => ({
2835
+ path: paths[i],
2836
+ width: image.width,
2837
+ height: image.height,
2838
+ bytes: image.data.byteLength,
2839
+ responseId: image.responseId,
2840
+ servedModel: image.servedModel ?? null,
2841
+ usage: image.usage
2842
+ }))
2843
+ };
2844
+ }
2845
+ async function appendUsageLog(entry) {
2846
+ const target = process.env[USAGE_LOG_ENV]?.trim();
2847
+ if (!target) return void 0;
2848
+ const path = resolve2(target);
2849
+ await mkdir4(dirname4(path), { recursive: true });
2850
+ await appendFile(path, `${JSON.stringify(entry)}
2851
+ `, "utf8");
2852
+ return path;
2853
+ }
2854
+
2152
2855
  // src/index.ts
2153
2856
  var EXIT_USAGE = 2;
2154
2857
  var EXIT_AUTH = 3;
@@ -2171,9 +2874,13 @@ function parseArgs(argv) {
2171
2874
  flags.set("help", true);
2172
2875
  continue;
2173
2876
  }
2877
+ if (arg === "--version") {
2878
+ flags.set("version", true);
2879
+ continue;
2880
+ }
2174
2881
  if (arg.startsWith("--")) {
2175
2882
  const key = arg.slice(2);
2176
- if (!["prompt", "out", "provider", "model", "reference", "size", "quality", "format", "count", "source", "mask", "timeout", "api", "base-url", "auth", "key-stdin", "no-open", "show-path", "target", "force"].includes(key)) {
2883
+ if (!["prompt", "out", "provider", "model", "reference", "size", "quality", "format", "count", "source", "mask", "timeout", "api", "base-url", "auth", "start-command", "workflow", "edit-workflow", "key-stdin", "no-open", "show-path", "target", "force"].includes(key)) {
2177
2884
  fail("usage", `Unknown option --${key}. Run imagegen help.`, EXIT_USAGE);
2178
2885
  }
2179
2886
  if (key === "key-stdin" || key === "no-open" || key === "show-path" || key === "force") {
@@ -2207,6 +2914,10 @@ function imageModel(flags) {
2207
2914
  if (model !== void 0 && !model) fail("usage", "--model must not be blank.", EXIT_USAGE);
2208
2915
  return model;
2209
2916
  }
2917
+ async function cliVersion() {
2918
+ const manifest = JSON.parse(await readFile4(new URL("../package.json", import.meta.url), "utf8"));
2919
+ return manifest.version;
2920
+ }
2210
2921
  function redirectUri() {
2211
2922
  return process.env["IMAGEGEN_CODEX_REDIRECT_URI"] ?? DEFAULT_REDIRECT_URI;
2212
2923
  }
@@ -2224,7 +2935,7 @@ function redirectPort(uri) {
2224
2935
  async function loadImageInput(path, label) {
2225
2936
  let data;
2226
2937
  try {
2227
- data = await readFile3(path);
2938
+ data = await readFile4(path);
2228
2939
  } catch {
2229
2940
  fail("io", `${label} not found: ${path}`, EXIT_IO);
2230
2941
  }
@@ -2277,10 +2988,10 @@ function requestedProviders(flags) {
2277
2988
  return raw;
2278
2989
  }
2279
2990
  async function resolveOutPaths(out, providerName, multiProvider, count, format) {
2280
- const resolved = resolve2(out);
2991
+ const resolved = resolve3(out);
2281
2992
  let isDir = false;
2282
2993
  try {
2283
- const info = await stat(resolved);
2994
+ const info = await stat2(resolved);
2284
2995
  isDir = info.isDirectory();
2285
2996
  } catch {
2286
2997
  isDir = extname(resolved) === "";
@@ -2288,12 +2999,12 @@ async function resolveOutPaths(out, providerName, multiProvider, count, format)
2288
2999
  const extension = `.${normalizeOutputFormat(format)}`;
2289
3000
  const prefix = multiProvider ? `${providerName}-` : "";
2290
3001
  if (isDir) {
2291
- return Array.from({ length: count }, (_, i) => join4(resolved, `${prefix}image-${i + 1}${extension}`));
3002
+ return Array.from({ length: count }, (_, i) => join5(resolved, `${prefix}image-${i + 1}${extension}`));
2292
3003
  }
2293
3004
  if (count > 1 || multiProvider) {
2294
- const dir = dirname3(resolved);
3005
+ const dir = dirname5(resolved);
2295
3006
  const base = basename(resolved, extname(resolved));
2296
- return Array.from({ length: count }, (_, i) => join4(dir, `${prefix}${base}-${i + 1}${extension}`));
3007
+ return Array.from({ length: count }, (_, i) => join5(dir, `${prefix}${base}-${i + 1}${extension}`));
2297
3008
  }
2298
3009
  return [resolved.endsWith(extension) ? resolved : `${resolved}${extension}`];
2299
3010
  }
@@ -2301,14 +3012,16 @@ function printResult(providerName, result, paths) {
2301
3012
  process.stdout.write(
2302
3013
  `${JSON.stringify({
2303
3014
  provider: providerName,
2304
- imageModel: result.imageModel,
3015
+ requestedModel: result.requestedModel,
2305
3016
  images: result.images.map((image, i) => ({
2306
3017
  path: paths[i],
2307
3018
  mimeType: image.mimeType,
2308
3019
  width: image.width,
2309
3020
  height: image.height,
2310
3021
  prompt: image.prompt,
2311
- responseId: image.responseId
3022
+ responseId: image.responseId,
3023
+ servedModel: image.servedModel ?? null,
3024
+ served: image.usage?.["served"]
2312
3025
  }))
2313
3026
  })}
2314
3027
  `
@@ -2318,7 +3031,7 @@ async function saveImages(result, paths) {
2318
3031
  for (let i = 0; i < result.images.length; i += 1) {
2319
3032
  const path = paths[i];
2320
3033
  try {
2321
- await mkdir3(dirname3(path), { recursive: true });
3034
+ await mkdir5(dirname5(path), { recursive: true });
2322
3035
  await writeFile3(path, Buffer.from(result.images[i].data));
2323
3036
  } catch {
2324
3037
  fail("io", `Could not write output file: ${path}`, EXIT_IO);
@@ -2391,8 +3104,8 @@ async function runAuthLogin(providerName, flags) {
2391
3104
  const waiter = waitForOAuthCallback(timeoutMinutes * 60 * 1e3, port);
2392
3105
  if (!flags.get("no-open")) {
2393
3106
  try {
2394
- const { default: open } = await import("open");
2395
- await open(url);
3107
+ const { default: open2 } = await import("open");
3108
+ await open2(url);
2396
3109
  } catch {
2397
3110
  process.stderr.write("Could not open a browser automatically. Open this URL manually:\n");
2398
3111
  }
@@ -2525,10 +3238,21 @@ async function runImageRequest(request, edit, providers) {
2525
3238
  const paths = await resolveOutPaths(request.out, entry.provider.name, multiProvider, result.images.length, request.format ?? "png");
2526
3239
  await saveImages(result, paths);
2527
3240
  printResult(entry.provider.name, result, paths);
3241
+ try {
3242
+ await appendUsageLog(usageLogEntry(request, edit, result, paths));
3243
+ } catch {
3244
+ process.stderr.write(`${JSON.stringify({ warning: { kind: "io", message: `Could not append to ${USAGE_LOG_ENV} file.` } })}
3245
+ `);
3246
+ }
2528
3247
  }
2529
3248
  }
2530
3249
  async function main() {
2531
3250
  const { flags, positionals } = parseArgs(process.argv.slice(2));
3251
+ if (flags.get("version")) {
3252
+ process.stdout.write(`${await cliVersion()}
3253
+ `);
3254
+ return;
3255
+ }
2532
3256
  if (flags.get("help") || positionals.length === 0) {
2533
3257
  process.stdout.write(helpFor(positionals[0]));
2534
3258
  return;
@@ -2572,10 +3296,14 @@ async function main() {
2572
3296
  `);
2573
3297
  } else if (sub === "add" || sub === "update") {
2574
3298
  const patch = {};
2575
- for (const [flag, key] of [["api", "api"], ["base-url", "baseUrl"], ["auth", "auth"], ["model", "model"]]) {
3299
+ for (const [flag, key] of [["api", "api"], ["base-url", "baseUrl"], ["auth", "auth"], ["model", "model"], ["start-command", "startCommand"]]) {
2576
3300
  const value = flagValue(flags, flag);
2577
3301
  if (value !== void 0) patch[key] = value;
2578
3302
  }
3303
+ for (const [flag, key] of [["workflow", "workflow"], ["edit-workflow", "editWorkflow"]]) {
3304
+ const value = flagValue(flags, flag);
3305
+ if (value !== void 0) patch[key] = resolve3(value);
3306
+ }
2579
3307
  const profile = sub === "add" ? await repository.add({ name: subSub, ...patch }) : await repository.update(subSub, patch);
2580
3308
  process.stdout.write(`${JSON.stringify({ provider: profile })}
2581
3309
  `);
@@ -2586,8 +3314,7 @@ async function main() {
2586
3314
  const selected = requestedProviders(flags);
2587
3315
  if (!selected || selected.length !== 1) fail("usage", "Model discovery requires exactly one --provider <profile-name>.", EXIT_USAGE);
2588
3316
  const profile = await new ProfileRepository().get(selected[0]);
2589
- const provider = new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore()));
2590
- process.stdout.write(`${JSON.stringify({ provider: profile.name, models: await provider.models() })}
3317
+ process.stdout.write(`${JSON.stringify({ provider: profile.name, models: await createProfileProvider(profile).models() })}
2591
3318
  `);
2592
3319
  return;
2593
3320
  }