@dorely13/ai-imagegen-cli 0.3.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,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { mkdir as mkdir5, readFile as readFile3, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
4
+ import { mkdir as mkdir5, readFile as readFile4, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
5
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
@@ -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
@@ -230,7 +232,7 @@ 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";
235
+ var DEFAULT_IMAGE_MODEL = "gpt-image-2.6-flare";
234
236
  var DEFAULT_QUALITY = "auto";
235
237
  var REQUEST_TIMEOUT_SECONDS = 600;
236
238
  var MAX_REFERENCE_IMAGES2 = 4;
@@ -526,9 +528,9 @@ var FileCredentialStore = class {
526
528
  return { backend: "file", location: this.filePath };
527
529
  }
528
530
  async readAll() {
529
- const { readFile: readFile4 } = await import("node:fs/promises");
531
+ const { readFile: readFile5 } = await import("node:fs/promises");
530
532
  try {
531
- const raw = await readFile4(this.filePath, "utf8");
533
+ const raw = await readFile5(this.filePath, "utf8");
532
534
  let value;
533
535
  try {
534
536
  value = JSON.parse(raw);
@@ -807,6 +809,36 @@ function buildBody(args) {
807
809
  body["images"] = args.images.map((image) => ({ image_url: toDataUrl(image) }));
808
810
  return body;
809
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.");
827
+ }
828
+ if (width !== sourceWidth || height !== sourceHeight) {
829
+ throw usage2(`Mask is ${width}x${height} but the source is ${sourceWidth}x${sourceHeight}; they must match.`);
830
+ }
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);
836
+ }
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" };
841
+ }
810
842
  var CodexImageProvider = class {
811
843
  tokens;
812
844
  name = PROVIDER_NAME;
@@ -825,7 +857,16 @@ var CodexImageProvider = class {
825
857
  return this.runAll(request, references.length > 0 ? "edits" : "generations", references);
826
858
  }
827
859
  async edit(request) {
828
- return this.runAll(request, "edits", [request.sourceImage, ...request.referenceImages ?? []].slice(0, MAX_REFERENCE_IMAGES2));
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}`);
829
870
  }
830
871
  resolveImageModel(request) {
831
872
  const model = (request.model ?? this.imageModel).trim();
@@ -837,16 +878,16 @@ var CodexImageProvider = class {
837
878
  }
838
879
  return model;
839
880
  }
840
- async runAll(request, endpoint, images) {
881
+ async runAll(request, endpoint, images, modelPrompt = request.prompt) {
841
882
  const format = normalizeOutputFormat(request.format);
842
883
  const size = this.sizePolicy.resolveSize(request.size);
843
- const imageModel2 = this.resolveImageModel(request);
844
- const body = JSON.stringify(buildBody({ prompt: request.prompt, model: imageModel2, size, quality: request.quality ?? DEFAULT_QUALITY, images }));
884
+ const requestedModel = this.resolveImageModel(request);
885
+ const body = JSON.stringify(buildBody({ prompt: modelPrompt, model: requestedModel, size, quality: request.quality ?? DEFAULT_QUALITY, images }));
845
886
  const results = [];
846
887
  for (let i = 0, count = normalizeCount(request.count); i < count; i += 1) {
847
888
  results.push(await this.sendOnce(endpoint, body, format, request.prompt));
848
889
  }
849
- return { provider: this.name, imageModel: imageModel2, images: results };
890
+ return { provider: this.name, requestedModel, images: results };
850
891
  }
851
892
  async sendOnce(endpoint, body, format, prompt) {
852
893
  let accessToken = await this.tokens.getValidAccessToken();
@@ -897,6 +938,8 @@ var CodexImageProvider = class {
897
938
  height,
898
939
  prompt,
899
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,
900
943
  usage: {
901
944
  requestId,
902
945
  imageGeneration: payload["usage"],
@@ -1113,8 +1156,6 @@ var GeminiImageProvider = class {
1113
1156
  const count = request.count ?? 1;
1114
1157
  if (!Number.isInteger(count) || count < 1 || count > 4)
1115
1158
  throw error("usage", "Image count must be from 1 to 4.");
1116
- if (request.format && !["png", "jpeg", "jpg", "webp"].includes(request.format.trim().toLowerCase()))
1117
- throw error("usage", "Output format must be png, jpeg, or webp.");
1118
1159
  const format = normalizeOutputFormat(request.format);
1119
1160
  const size = resolveGeminiSize(request.size, model);
1120
1161
  const payload = {
@@ -1149,7 +1190,7 @@ var GeminiImageProvider = class {
1149
1190
  const images = [];
1150
1191
  for (let i = 0; i < count; i++)
1151
1192
  images.push(await this.send(payload, key, format, request.prompt));
1152
- return { provider: this.name, imageModel: model, images };
1193
+ return { provider: this.name, requestedModel: model, images };
1153
1194
  }
1154
1195
  async send(payload, key, format, prompt) {
1155
1196
  const controller = new AbortController();
@@ -1261,8 +1302,9 @@ function createGeminiEntry() {
1261
1302
 
1262
1303
  // ../../packages/providers-configurable/dist/profiles.js
1263
1304
  import { readFile, mkdir as mkdir2, writeFile, rename, rm as rm2 } from "node:fs/promises";
1264
- import { dirname as dirname2, join as join3 } from "node:path";
1305
+ import { dirname as dirname2, isAbsolute, join as join3 } from "node:path";
1265
1306
  import { randomUUID } from "node:crypto";
1307
+ var COMFY_FIELDS = ["startCommand", "workflow", "editWorkflow"];
1266
1308
  var usage = (message) => new ProviderError({ kind: "usage", message });
1267
1309
  var profileStore = () => resolveCredentialStore();
1268
1310
  function validateApiKey(value) {
@@ -1278,14 +1320,25 @@ function validateProfile(value) {
1278
1320
  if (!value || typeof value !== "object" || Array.isArray(value))
1279
1321
  throw usage("Invalid endpoint profile.");
1280
1322
  const p = value;
1281
- if (Object.keys(p).some((k) => !["name", "api", "baseUrl", "model", "auth"].includes(k)))
1282
- throw usage("Endpoint profiles accept only name, api, baseUrl, model, and auth; store keys through auth login.");
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.");
1283
1325
  if (typeof p.name !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(p.name) || ["codex", "gemini"].includes(p.name))
1284
1326
  throw usage("Endpoint name must use lowercase letters, digits, and hyphens, and cannot be codex or gemini.");
1285
- if (p.api !== "openrouter" && p.api !== "openai-images")
1286
- throw usage("API format must be openrouter or openai-images.");
1327
+ if (p.api !== "openrouter" && p.api !== "openai-images" && p.api !== "comfyui")
1328
+ throw usage("API format must be openrouter, openai-images, or comfyui.");
1287
1329
  if (p.auth !== void 0 && p.auth !== "bearer" && p.auth !== "none")
1288
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
+ }
1289
1342
  const raw = p.baseUrl ?? (p.api === "openrouter" ? "https://openrouter.ai/api/v1" : void 0);
1290
1343
  let url;
1291
1344
  try {
@@ -1300,12 +1353,18 @@ function validateProfile(value) {
1300
1353
  throw usage("Base URL must use HTTPS (or loopback HTTP), without credentials, query, or fragment.");
1301
1354
  if (p.model !== void 0 && (typeof p.model !== "string" || !p.model.trim()))
1302
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.");
1303
1358
  return {
1304
1359
  name: p.name,
1305
1360
  api: p.api,
1306
1361
  baseUrl: url.toString().replace(/\/+$/, ""),
1307
- auth: p.auth ?? "bearer",
1308
- ...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
+ }))
1309
1368
  };
1310
1369
  }
1311
1370
  var ProfileRepository = class {
@@ -1406,7 +1465,7 @@ async function profileKey(profile, store) {
1406
1465
  }
1407
1466
 
1408
1467
  // ../../packages/providers-configurable/dist/provider.js
1409
- import sharp3 from "sharp";
1468
+ import sharp4 from "sharp";
1410
1469
 
1411
1470
  // ../../packages/providers-configurable/dist/image-size.js
1412
1471
  function imageSize(value, api) {
@@ -1426,10 +1485,27 @@ function imageSize(value, api) {
1426
1485
  if (pixels && Number.isSafeInteger(Number(pixels[1])) && Number.isSafeInteger(Number(pixels[2])) && Number(pixels[1]) > 0 && Number(pixels[2]) > 0)
1427
1486
  return { size: `${Number(pixels[1])}x${Number(pixels[2])}` };
1428
1487
  const ratio = /^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)$/.exec(size);
1429
- if (api === "openrouter" && ratio && Number(ratio[1]) > 0 && Number(ratio[2]) > 0 && Number.isFinite(Number(ratio[1]) / Number(ratio[2])))
1488
+ if (api !== "openai-images" && ratio && Number(ratio[1]) > 0 && Number(ratio[2]) > 0 && Number.isFinite(Number(ratio[1]) / Number(ratio[2])))
1430
1489
  return { aspect_ratio: `${Number(ratio[1])}:${Number(ratio[2])}` };
1431
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.");
1432
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
+ }
1433
1509
 
1434
1510
  // ../../packages/providers-configurable/dist/http.js
1435
1511
  var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
@@ -1560,6 +1636,625 @@ function openAIRequest(model, request, images, mask) {
1560
1636
  return { path: "images/edits", body, json: false };
1561
1637
  }
1562
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
+
1563
2258
  // ../../packages/providers-configurable/dist/provider.js
1564
2259
  var ConfigurableImageProvider = class {
1565
2260
  getKey;
@@ -1643,19 +2338,12 @@ var ConfigurableImageProvider = class {
1643
2338
  const model = (request.model ?? this.profile.model ?? "").trim();
1644
2339
  if (!model)
1645
2340
  throw usage("Supply --model or configure a default model with providers update.");
1646
- if ((request.referenceImages?.length ?? 0) > 4)
1647
- throw usage("At most four reference images are allowed.");
1648
- const count = request.count ?? 1;
1649
- if (!Number.isInteger(count) || count < 1 || count > 4)
1650
- throw usage("Image count must be from 1 to 4.");
1651
2341
  if (mask && this.profile.api === "openrouter")
1652
2342
  throw usage("OpenRouter image API does not expose masks. Describe the edit region in the prompt.");
1653
- for (const image of [...images, ...mask ? [mask] : []])
1654
- if (!["image/png", "image/jpeg", "image/webp"].includes(image.contentType))
1655
- throw usage("Input images must be PNG, JPEG, or WebP.");
1656
- const format = (request.format ?? "png").trim().toLowerCase().replace(/^jpg$/, "jpeg");
1657
- if (!["png", "jpeg", "webp"].includes(format))
1658
- throw usage("Output format must be png, jpeg, or webp.");
2343
+ const { count, format } = validateImageRequest(request, [
2344
+ ...images,
2345
+ ...mask ? [mask] : []
2346
+ ]);
1659
2347
  const quality = request.quality?.trim().toLowerCase();
1660
2348
  if (quality && !/^[a-z][a-z0-9_-]*$/.test(quality))
1661
2349
  throw usage("Quality must be a provider-supported quality name.");
@@ -1713,12 +2401,12 @@ var ConfigurableImageProvider = class {
1713
2401
  message: "Endpoint returned no image bytes or download URL."
1714
2402
  });
1715
2403
  try {
1716
- const metadata = await sharp3(data).metadata();
2404
+ const metadata = await sharp4(data).metadata();
1717
2405
  if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || typeof image.media_type === "string" && image.media_type !== `image/${metadata.format}`)
1718
2406
  throw new Error();
1719
- await sharp3(data).stats();
2407
+ await sharp4(data).stats();
1720
2408
  if (metadata.format !== format)
1721
- data = await sharp3(data).toFormat(format).toBuffer();
2409
+ data = await sharp4(data).toFormat(format).toBuffer();
1722
2410
  return {
1723
2411
  data,
1724
2412
  width: metadata.width,
@@ -1734,12 +2422,15 @@ var ConfigurableImageProvider = class {
1734
2422
  });
1735
2423
  }
1736
2424
  }));
1737
- return { provider: this.name, imageModel: model, images: results };
2425
+ return { provider: this.name, requestedModel: model, images: results };
1738
2426
  }
1739
2427
  };
2428
+ function createProfileProvider(profile) {
2429
+ return profile.api === "comfyui" ? new ComfyUIImageProvider(profile) : new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore()));
2430
+ }
1740
2431
  function createProfileEntry(profile) {
1741
2432
  return {
1742
- provider: new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore())),
2433
+ provider: createProfileProvider(profile),
1743
2434
  authStatus: async () => {
1744
2435
  let configured = profile.auth === "none";
1745
2436
  if (!configured)
@@ -1827,11 +2518,15 @@ async function readApiKey(fromStdin, input = process.stdin, output = process.std
1827
2518
 
1828
2519
  // src/help.ts
1829
2520
  var PROFILE_HELP = `Named endpoint profiles:
1830
- imagegen providers add <name> --api <openrouter|openai-images> [--base-url <url>]
1831
- [--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>]
1832
2525
  imagegen providers show <name>
1833
2526
  imagegen providers update <name> [--api <format>] [--base-url <url>]
1834
2527
  [--model <id>] [--auth <bearer|none>]
2528
+ [--start-command <text>] [--workflow <file>]
2529
+ [--edit-workflow <file>]
1835
2530
  imagegen providers remove <name>
1836
2531
  imagegen auth <name> login [--key-stdin]
1837
2532
  imagegen auth <name> status [--show-path]
@@ -1839,8 +2534,9 @@ var PROFILE_HELP = `Named endpoint profiles:
1839
2534
  imagegen models --provider <name>
1840
2535
 
1841
2536
  OpenRouter defaults to https://openrouter.ai/api/v1. openai-images requires
1842
- --base-url including the API prefix (e.g. https://api.openai.com/v1).
1843
- Keys use hidden terminal input or explicit --key-stdin and the existing vault/file
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
1844
2540
  store. --auth none supports endpoints without keys. Config is saved in providers.json
1845
2541
  alongside the credential file. Origin changes clear the saved key; removing a
1846
2542
  profile removes its credentials. Logout preserves profile configuration.
@@ -1849,13 +2545,25 @@ Select named profiles with --provider. Model precedence: --model > profile defau
1849
2545
  provide one if no default is saved. models explicitly discovers model metadata;
1850
2546
  OpenAI-compatible model listings do not establish image capabilities.
1851
2547
 
1852
- Supported protocols: OpenRouter dedicated Images API and OpenAI Images JSON /
1853
- 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.
1854
2551
  OpenRouter accepts aspect ratios; openai-images needs pixels or presets:
1855
2552
  square 1024x1024, portrait 1024x1536, landscape 1536x1024, wide 1536x864.
1856
2553
  Auto options are omitted. Native masks require openai-images.
1857
2554
  Outputs are measured and converted to PNG/JPEG/WebP without resizing. Count uses
1858
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.
1859
2567
  `;
1860
2568
  var IMAGE_OPTIONS = `Image options (generate, edit):
1861
2569
  --prompt <text> Prompt text (required)
@@ -1883,34 +2591,41 @@ Gemini provider:
1883
2591
  Model precedence: --model > constructor imageModel > IMAGEGEN_GEMINI_IMAGE_MODEL
1884
2592
  > gemini-nano-banana-2.1. Calls are independent; use prior output as --source.
1885
2593
 
1886
- Supported Codex models (account availability is determined by the provider):
1887
- gpt-image-2.5-flare Default; fast image generation and editing
1888
- gpt-image-2.5-sunburst Higher-precision image generation and editing
1889
- 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
1890
2600
 
1891
- 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.
1892
2604
  Model precedence: --model > constructor imageModel (library callers) >
1893
- IMAGEGEN_CODEX_IMAGE_MODEL > gpt-image-2.5-flare.
1894
- Codex may override the requested model, quality, and size; results report the
1895
- returned dimensions. Codex ignores --mask; describe the region in the prompt.
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.
1896
2612
  IMAGEGEN_USAGE_LOG=<file> appends one prompt-free JSON usage line per result.
1897
2613
 
1898
- Codex quality: auto (default), low, medium, high; Flare/Sunburst also support
1899
- xhigh and max. gpt-image-2 rejects xhigh/max.
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.
1900
2616
  `;
1901
2617
  var GENERATE_EXAMPLES = `Examples:
1902
2618
  imagegen generate --provider gemini --size 16:9 --prompt "a lighthouse at dawn" --out ./gemini.png
1903
2619
  imagegen generate --prompt "a lighthouse at dawn" --out ./lighthouse.png
1904
- imagegen generate --model gpt-image-2 --prompt "a lighthouse" --out ./classic.png
1905
- 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
1906
2621
  `;
1907
2622
  var EDIT_EXAMPLES = `Edit options:
1908
2623
  --source <file> Source image (required)
1909
- --mask <file> Optional edit mask (openai-images; Codex ignores it)
2624
+ --mask <file> Optional edit mask; transparent pixels mark the area to edit
1910
2625
 
1911
2626
  Examples:
1912
2627
  imagegen edit --format webp --source ./product.png --prompt "make the product blue" --out ./blue.webp
1913
- 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
1914
2629
  `;
1915
2630
  var USAGE = `imagegen - provider-agnostic image generation CLI
1916
2631
 
@@ -1928,6 +2643,7 @@ Usage:
1928
2643
  imagegen skill
1929
2644
  imagegen skill install --target <claude|codex|skills-dir> [--target <...>] [--force]
1930
2645
  imagegen help [command]
2646
+ imagegen --version
1931
2647
 
1932
2648
  ${PROFILE_HELP}
1933
2649
  ${IMAGE_OPTIONS}
@@ -1943,6 +2659,7 @@ Auth options:
1943
2659
 
1944
2660
  Global:
1945
2661
  -h, --help Show help and exit 0
2662
+ --version Print the CLI version and exit 0
1946
2663
  `;
1947
2664
  var COMMAND_HELP = {
1948
2665
  auth: `imagegen auth - manage provider credentials (one namespace per provider)
@@ -2049,7 +2766,7 @@ ${USAGE}`;
2049
2766
  }
2050
2767
 
2051
2768
  // src/skill.ts
2052
- import { mkdir as mkdir3, 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";
2053
2770
  import { homedir as homedir2 } from "node:os";
2054
2771
  import { dirname as dirname3, join as join4, resolve } from "node:path";
2055
2772
  var SKILL_NAME = "imagegen-cli";
@@ -2060,7 +2777,7 @@ var SkillExistsError = class extends Error {
2060
2777
  }
2061
2778
  };
2062
2779
  async function skillText() {
2063
- return readFile2(new URL("../skill/SKILL.md", import.meta.url), "utf8");
2780
+ return readFile3(new URL("../skill/SKILL.md", import.meta.url), "utf8");
2064
2781
  }
2065
2782
  function targetPath(target) {
2066
2783
  const root = target === "claude" ? join4(homedir2(), ".claude", "skills") : target === "codex" ? join4(homedir2(), ".agents", "skills") : resolve(target);
@@ -2068,7 +2785,7 @@ function targetPath(target) {
2068
2785
  }
2069
2786
  async function existingText(path) {
2070
2787
  try {
2071
- return await readFile2(path, "utf8");
2788
+ return await readFile3(path, "utf8");
2072
2789
  } catch (error2) {
2073
2790
  if (error2.code === "ENOENT") return void 0;
2074
2791
  throw error2;
@@ -2105,7 +2822,7 @@ function usageLogEntry(request, edit, result, paths) {
2105
2822
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2106
2823
  command: edit ? "edit" : "generate",
2107
2824
  provider: result.provider,
2108
- imageModel: result.imageModel,
2825
+ requestedModel: result.requestedModel,
2109
2826
  requested: {
2110
2827
  size: request.size ?? "auto",
2111
2828
  quality: request.quality ?? "auto",
@@ -2120,6 +2837,7 @@ function usageLogEntry(request, edit, result, paths) {
2120
2837
  height: image.height,
2121
2838
  bytes: image.data.byteLength,
2122
2839
  responseId: image.responseId,
2840
+ servedModel: image.servedModel ?? null,
2123
2841
  usage: image.usage
2124
2842
  }))
2125
2843
  };
@@ -2156,9 +2874,13 @@ function parseArgs(argv) {
2156
2874
  flags.set("help", true);
2157
2875
  continue;
2158
2876
  }
2877
+ if (arg === "--version") {
2878
+ flags.set("version", true);
2879
+ continue;
2880
+ }
2159
2881
  if (arg.startsWith("--")) {
2160
2882
  const key = arg.slice(2);
2161
- if (!["prompt", "out", "provider", "model", "reference", "size", "quality", "format", "count", "source", "mask", "timeout", "api", "base-url", "auth", "key-stdin", "no-open", "show-path", "target", "force"].includes(key)) {
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)) {
2162
2884
  fail("usage", `Unknown option --${key}. Run imagegen help.`, EXIT_USAGE);
2163
2885
  }
2164
2886
  if (key === "key-stdin" || key === "no-open" || key === "show-path" || key === "force") {
@@ -2192,6 +2914,10 @@ function imageModel(flags) {
2192
2914
  if (model !== void 0 && !model) fail("usage", "--model must not be blank.", EXIT_USAGE);
2193
2915
  return model;
2194
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
+ }
2195
2921
  function redirectUri() {
2196
2922
  return process.env["IMAGEGEN_CODEX_REDIRECT_URI"] ?? DEFAULT_REDIRECT_URI;
2197
2923
  }
@@ -2209,7 +2935,7 @@ function redirectPort(uri) {
2209
2935
  async function loadImageInput(path, label) {
2210
2936
  let data;
2211
2937
  try {
2212
- data = await readFile3(path);
2938
+ data = await readFile4(path);
2213
2939
  } catch {
2214
2940
  fail("io", `${label} not found: ${path}`, EXIT_IO);
2215
2941
  }
@@ -2286,14 +3012,16 @@ function printResult(providerName, result, paths) {
2286
3012
  process.stdout.write(
2287
3013
  `${JSON.stringify({
2288
3014
  provider: providerName,
2289
- imageModel: result.imageModel,
3015
+ requestedModel: result.requestedModel,
2290
3016
  images: result.images.map((image, i) => ({
2291
3017
  path: paths[i],
2292
3018
  mimeType: image.mimeType,
2293
3019
  width: image.width,
2294
3020
  height: image.height,
2295
3021
  prompt: image.prompt,
2296
- responseId: image.responseId
3022
+ responseId: image.responseId,
3023
+ servedModel: image.servedModel ?? null,
3024
+ served: image.usage?.["served"]
2297
3025
  }))
2298
3026
  })}
2299
3027
  `
@@ -2520,6 +3248,11 @@ async function runImageRequest(request, edit, providers) {
2520
3248
  }
2521
3249
  async function main() {
2522
3250
  const { flags, positionals } = parseArgs(process.argv.slice(2));
3251
+ if (flags.get("version")) {
3252
+ process.stdout.write(`${await cliVersion()}
3253
+ `);
3254
+ return;
3255
+ }
2523
3256
  if (flags.get("help") || positionals.length === 0) {
2524
3257
  process.stdout.write(helpFor(positionals[0]));
2525
3258
  return;
@@ -2563,10 +3296,14 @@ async function main() {
2563
3296
  `);
2564
3297
  } else if (sub === "add" || sub === "update") {
2565
3298
  const patch = {};
2566
- 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"]]) {
2567
3300
  const value = flagValue(flags, flag);
2568
3301
  if (value !== void 0) patch[key] = value;
2569
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
+ }
2570
3307
  const profile = sub === "add" ? await repository.add({ name: subSub, ...patch }) : await repository.update(subSub, patch);
2571
3308
  process.stdout.write(`${JSON.stringify({ provider: profile })}
2572
3309
  `);
@@ -2577,8 +3314,7 @@ async function main() {
2577
3314
  const selected = requestedProviders(flags);
2578
3315
  if (!selected || selected.length !== 1) fail("usage", "Model discovery requires exactly one --provider <profile-name>.", EXIT_USAGE);
2579
3316
  const profile = await new ProfileRepository().get(selected[0]);
2580
- const provider = new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore()));
2581
- process.stdout.write(`${JSON.stringify({ provider: profile.name, models: await provider.models() })}
3317
+ process.stdout.write(`${JSON.stringify({ provider: profile.name, models: await createProfileProvider(profile).models() })}
2582
3318
  `);
2583
3319
  return;
2584
3320
  }