@dorely13/ai-imagegen-cli 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 readFile3, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
5
+ import { basename, dirname as dirname5, extname, join as join5, resolve as resolve3 } from "node:path";
6
6
 
7
7
  // ../../packages/core/dist/types.js
8
8
  var ProviderError = class extends Error {
@@ -226,20 +226,24 @@ var AUTH_ENDPOINT = "https://auth.openai.com/oauth/authorize";
226
226
  var TOKEN_ENDPOINT = "https://auth.openai.com/oauth/token";
227
227
  var CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
228
228
  var SCOPE = "openid profile email offline_access";
229
- var RESPONSES_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses";
229
+ var IMAGES_ENDPOINT = "https://chatgpt.com/backend-api/codex/images";
230
230
  var DEFAULT_REDIRECT_URI = "http://localhost:1455/auth/callback";
231
231
  var LOOPBACK_PORT = 1455;
232
232
  var CALLBACK_PATH = "/auth/callback";
233
233
  var DEFAULT_IMAGE_MODEL = "gpt-image-2.5-flare";
234
- var DEFAULT_ORCHESTRATION_MODEL = "gpt-5.6-luna";
235
234
  var DEFAULT_QUALITY = "auto";
236
235
  var REQUEST_TIMEOUT_SECONDS = 600;
237
- var PARTIAL_IMAGES = 2;
238
236
  var MAX_REFERENCE_IMAGES2 = 4;
239
237
  var MAX_IMAGE_COUNT2 = 4;
240
238
  var MIN_IMAGE_COUNT2 = 1;
241
239
  var FLOW_TIMEOUT_MINUTES = 10;
242
240
  var USER_AGENT = "ImageGenCli";
241
+ var TOKEN_REFRESH_MARGIN_MS = 5 * 60 * 1e3;
242
+ var TOKEN_REQUEST_TIMEOUT_MS = 30 * 1e3;
243
+ var REFRESH_LOCK_FILE = "codex-refresh.lock";
244
+ var REFRESH_LOCK_STALE_MS = 60 * 1e3;
245
+ var REFRESH_LOCK_WAIT_MS = 90 * 1e3;
246
+ var REFRESH_LOCK_POLL_MS = 100;
243
247
 
244
248
  // ../../packages/providers-codex/dist/pkce.js
245
249
  import { createHash, randomBytes } from "node:crypto";
@@ -282,7 +286,7 @@ function callbackPage(title, message) {
282
286
  return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head><body style="font-family:sans-serif;padding:2rem"><h1>${title}</h1><p>${message}</p></body></html>`;
283
287
  }
284
288
  async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3, port = LOOPBACK_PORT) {
285
- return new Promise((resolve3, reject) => {
289
+ return new Promise((resolve4, reject) => {
286
290
  let server;
287
291
  const timer = setTimeout(() => {
288
292
  closeServer();
@@ -322,7 +326,7 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
322
326
  res.writeHead(200, { "content-type": "text/html" });
323
327
  res.end(callbackPage("Connected", "ImageGenCli is connected. You can close this tab."));
324
328
  closeServer();
325
- resolve3({ code, state });
329
+ resolve4({ code, state });
326
330
  } catch (error2) {
327
331
  fail2(error2 instanceof Error ? error2 : new LoopbackError("OAuth callback failed.", "callback_error"));
328
332
  }
@@ -340,7 +344,7 @@ async function waitForOAuthCallback(timeoutMs = FLOW_TIMEOUT_MINUTES * 60 * 1e3,
340
344
 
341
345
  // ../../packages/providers-codex/dist/codex-auth.js
342
346
  var ReauthRequiredError = class extends Error {
343
- constructor(message = "Codex credentials are missing or expired. Run `imagegen auth login` to reconnect.") {
347
+ constructor(message = "Codex credentials are missing or expired. Run `imagegen auth codex login` to reconnect.") {
344
348
  super(message);
345
349
  this.name = "ReauthRequiredError";
346
350
  }
@@ -353,10 +357,24 @@ function toTokenSet(body, fallbackRefreshToken) {
353
357
  scope: body.scope
354
358
  };
355
359
  }
360
+ var REJECTED_GRANT_CODES = /* @__PURE__ */ new Set([
361
+ "invalid_grant",
362
+ "invalid_token",
363
+ "refresh_token_expired",
364
+ "refresh_token_reused",
365
+ "refresh_token_invalidated"
366
+ ]);
356
367
  async function readOAuthErrorCode(response) {
357
368
  try {
358
369
  const body = await response.json();
359
- return typeof body.error === "string" ? body.error : null;
370
+ if (typeof body.error === "string")
371
+ return body.error;
372
+ if (typeof body.error === "object" && body.error !== null) {
373
+ const code = body.error["code"];
374
+ if (typeof code === "string")
375
+ return code;
376
+ }
377
+ return null;
360
378
  } catch {
361
379
  return null;
362
380
  }
@@ -365,30 +383,31 @@ async function postTokenRequest(params) {
365
383
  const response = await fetch(TOKEN_ENDPOINT, {
366
384
  method: "POST",
367
385
  headers: { "content-type": "application/x-www-form-urlencoded" },
368
- body: new URLSearchParams(params)
386
+ body: new URLSearchParams(params),
387
+ signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)
369
388
  });
370
389
  if (!response.ok) {
371
390
  const errorCode = await readOAuthErrorCode(response.clone());
372
- if (response.status === 401 || response.status === 400 && (errorCode === "invalid_grant" || errorCode === "invalid_token")) {
373
- return null;
391
+ if (response.status === 401 || errorCode !== null && REJECTED_GRANT_CODES.has(errorCode)) {
392
+ return { ok: false, reason: errorCode ?? `status ${response.status}` };
374
393
  }
375
- throw new Error(`Token request failed with status ${response.status}.`);
394
+ throw new Error(`Token request failed with status ${response.status}${errorCode ? ` (${errorCode})` : ""}.`);
376
395
  }
377
396
  const body = await response.json();
378
- return toTokenSet(body, params["refresh_token"]);
397
+ return { ok: true, tokens: toTokenSet(body, params["refresh_token"]) };
379
398
  }
380
399
  async function exchangeCodeForTokens(args) {
381
- const tokens = await postTokenRequest({
400
+ const outcome = await postTokenRequest({
382
401
  grant_type: "authorization_code",
383
402
  client_id: CLIENT_ID,
384
403
  code: args.code,
385
404
  redirect_uri: args.redirectUri,
386
405
  code_verifier: args.codeVerifier
387
406
  });
388
- if (!tokens) {
389
- throw new ReauthRequiredError("The authorization code was rejected. Run `imagegen auth login` again.");
407
+ if (!outcome.ok) {
408
+ throw new ReauthRequiredError(`The authorization code was rejected (${outcome.reason}). Run \`imagegen auth codex login\` again.`);
390
409
  }
391
- return tokens;
410
+ return outcome.tokens;
392
411
  }
393
412
  async function refreshTokens(refreshToken) {
394
413
  return postTokenRequest({
@@ -397,8 +416,8 @@ async function refreshTokens(refreshToken) {
397
416
  refresh_token: refreshToken
398
417
  });
399
418
  }
400
- function isTokenExpired(tokens, now = Date.now()) {
401
- return tokens.expiresAt <= now;
419
+ function needsRefresh(tokens, now = Date.now()) {
420
+ return tokens.expiresAt - TOKEN_REFRESH_MARGIN_MS <= now;
402
421
  }
403
422
  var pendingVerifiers = /* @__PURE__ */ new Map();
404
423
  function startPkceFlow(redirectUri2) {
@@ -424,65 +443,9 @@ function prunePendingVerifiers() {
424
443
  }
425
444
  }
426
445
 
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
- };
446
+ // ../../packages/providers-codex/dist/refresh-lock.js
447
+ import { mkdir, open, rm, stat } from "node:fs/promises";
448
+ import { dirname, join as join2 } from "node:path";
486
449
 
487
450
  // ../../packages/credentials/dist/index.js
488
451
  import { homedir } from "node:os";
@@ -490,8 +453,8 @@ import { join } from "node:path";
490
453
  function asKeytar(module) {
491
454
  if (typeof module !== "object" || module === null)
492
455
  return null;
493
- const record2 = module;
494
- const candidate = record2["default"] ?? module;
456
+ const record3 = module;
457
+ const candidate = record3["default"] ?? module;
495
458
  if (typeof candidate.getPassword === "function" && typeof candidate.setPassword === "function" && typeof candidate.deletePassword === "function") {
496
459
  return candidate;
497
460
  }
@@ -582,9 +545,9 @@ var FileCredentialStore = class {
582
545
  }
583
546
  }
584
547
  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 });
548
+ const { mkdir: mkdir6, writeFile: writeFile4, chmod } = await import("node:fs/promises");
549
+ const { dirname: dirname6 } = await import("node:path");
550
+ await mkdir6(dirname6(this.filePath), { recursive: true });
588
551
  await writeFile4(this.filePath, JSON.stringify(all, null, 2), {
589
552
  mode: 384
590
553
  });
@@ -602,6 +565,133 @@ function defaultCredentialFilePath() {
602
565
  return join(base, "ImageGenCli", "tokens.json");
603
566
  }
604
567
 
568
+ // ../../packages/providers-codex/dist/refresh-lock.js
569
+ function defaultRefreshLockPath() {
570
+ return join2(dirname(defaultCredentialFilePath()), REFRESH_LOCK_FILE);
571
+ }
572
+ function createRefreshLock(options = {}) {
573
+ const path = options.path ?? defaultRefreshLockPath();
574
+ const staleMs = options.staleMs ?? REFRESH_LOCK_STALE_MS;
575
+ const waitMs = options.waitMs ?? REFRESH_LOCK_WAIT_MS;
576
+ const pollMs = options.pollMs ?? REFRESH_LOCK_POLL_MS;
577
+ return async (work) => {
578
+ await acquire(path, staleMs, waitMs, pollMs);
579
+ try {
580
+ return await work();
581
+ } finally {
582
+ await rm(path, { force: true });
583
+ }
584
+ };
585
+ }
586
+ async function acquire(path, staleMs, waitMs, pollMs) {
587
+ await mkdir(dirname(path), { recursive: true });
588
+ const deadline = Date.now() + waitMs;
589
+ for (; ; ) {
590
+ try {
591
+ const handle = await open(path, "wx");
592
+ try {
593
+ await handle.writeFile(`${process.pid}
594
+ `);
595
+ } finally {
596
+ await handle.close();
597
+ }
598
+ return;
599
+ } catch (error2) {
600
+ if (!isContention(error2))
601
+ throw error2;
602
+ }
603
+ if (await isStale(path, staleMs)) {
604
+ await rm(path, { force: true });
605
+ continue;
606
+ }
607
+ if (Date.now() >= deadline) {
608
+ throw new Error("Timed out waiting for another imagegen process to refresh Codex credentials.");
609
+ }
610
+ await new Promise((resolve4) => setTimeout(resolve4, pollMs + Math.random() * pollMs));
611
+ }
612
+ }
613
+ function isContention(error2) {
614
+ const code = error2.code;
615
+ return code === "EEXIST" || process.platform === "win32" && (code === "EPERM" || code === "EACCES");
616
+ }
617
+ async function isStale(path, staleMs) {
618
+ try {
619
+ return Date.now() - (await stat(path)).mtimeMs > staleMs;
620
+ } catch {
621
+ return false;
622
+ }
623
+ }
624
+
625
+ // ../../packages/providers-codex/dist/codex-token-manager.js
626
+ var CodexTokenManager = class {
627
+ store;
628
+ pendingRefresh = null;
629
+ lock;
630
+ refresh;
631
+ constructor(store, options = {}) {
632
+ this.store = store;
633
+ this.lock = options.lock ?? createRefreshLock();
634
+ this.refresh = options.refresh ?? refreshTokens;
635
+ }
636
+ async exchangeAndSave(args) {
637
+ const tokens = await exchangeCodeForTokens(args);
638
+ await this.store.save(PROVIDER_NAME, tokens);
639
+ return tokens;
640
+ }
641
+ async status() {
642
+ if (!await this.store.load(PROVIDER_NAME))
643
+ return { connected: false };
644
+ try {
645
+ await this.getValidAccessToken();
646
+ const current = await this.store.load(PROVIDER_NAME);
647
+ return { connected: true, expiresAt: current?.expiresAt };
648
+ } catch {
649
+ return { connected: false };
650
+ }
651
+ }
652
+ async logout() {
653
+ await this.store.clear(PROVIDER_NAME);
654
+ }
655
+ async getValidAccessToken() {
656
+ const tokens = await this.store.load(PROVIDER_NAME);
657
+ if (!tokens)
658
+ throw new ReauthRequiredError();
659
+ if (!needsRefresh(tokens))
660
+ return tokens.accessToken;
661
+ return this.refreshFrom(tokens.accessToken);
662
+ }
663
+ // The Codex image endpoint rejected this access token before its recorded expiry.
664
+ async recoverFromUnauthorized(rejectedAccessToken) {
665
+ return this.refreshFrom(rejectedAccessToken);
666
+ }
667
+ refreshFrom(staleAccessToken) {
668
+ if (!this.pendingRefresh) {
669
+ this.pendingRefresh = this.lock(() => this.refreshLocked(staleAccessToken)).finally(() => {
670
+ this.pendingRefresh = null;
671
+ });
672
+ }
673
+ return this.pendingRefresh;
674
+ }
675
+ async refreshLocked(staleAccessToken) {
676
+ const current = await this.store.load(PROVIDER_NAME);
677
+ if (!current)
678
+ throw new ReauthRequiredError();
679
+ if (current.accessToken !== staleAccessToken && !needsRefresh(current))
680
+ return current.accessToken;
681
+ if (!current.refreshToken) {
682
+ await this.store.clear(PROVIDER_NAME);
683
+ throw new ReauthRequiredError();
684
+ }
685
+ const outcome = await this.refresh(current.refreshToken);
686
+ if (!outcome.ok) {
687
+ await this.store.clear(PROVIDER_NAME);
688
+ throw new ReauthRequiredError(`Codex rejected the stored refresh token (${outcome.reason}). Run \`imagegen auth codex login\` to reconnect.`);
689
+ }
690
+ await this.store.save(PROVIDER_NAME, outcome.tokens);
691
+ return outcome.tokens.accessToken;
692
+ }
693
+ };
694
+
605
695
  // ../../packages/providers-codex/dist/token-store.js
606
696
  async function resolveTokenStore() {
607
697
  const store = await resolveCredentialStore();
@@ -642,13 +732,18 @@ function extractAccountId(token) {
642
732
  return accountId;
643
733
  }
644
734
 
645
- // ../../packages/providers-codex/dist/sse.js
735
+ // ../../packages/providers-codex/dist/codex-client.js
736
+ import sharp from "sharp";
737
+
738
+ // ../../packages/providers-codex/dist/errors.js
646
739
  function classifyImageError(message) {
647
740
  if (!message.trim())
648
741
  return "unknown";
649
742
  const lowerRate = message.includes("input-images") && message.includes("per min") && message.toLowerCase().includes("gpt-image");
650
743
  if (lowerRate)
651
744
  return "codex_image_input_rate_limit";
745
+ if (/usage limit/i.test(message))
746
+ return "usage_limit";
652
747
  if (/rate limit|429|too many requests|please try again/i.test(message))
653
748
  return "rate_limit";
654
749
  if (/403|forbidden|cloudflare/i.test(message))
@@ -657,107 +752,26 @@ function classifyImageError(message) {
657
752
  return "image_model_invalid";
658
753
  return "api_error";
659
754
  }
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)
755
+ function readErrorMessage(body) {
756
+ if (!body.trim())
693
757
  return null;
694
- let data;
695
758
  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
- }
759
+ const parsed = JSON.parse(body);
760
+ if (typeof parsed === "object" && parsed !== null) {
761
+ const record3 = parsed;
762
+ if (typeof record3["message"] === "string")
763
+ return record3["message"];
764
+ if (typeof record3["detail"] === "string")
765
+ return record3["detail"];
766
+ const error2 = record3["error"];
767
+ if (typeof error2 === "object" && error2 !== null && typeof error2["message"] === "string") {
768
+ return error2["message"];
757
769
  }
758
770
  }
771
+ } catch {
772
+ return body.slice(0, 500);
759
773
  }
760
- throw new Error(`stream_completed_without_image: Codex image generation completed without returning an image. responseId=${responseId ?? "unknown"}, events=${eventCount}, lastEvent=${lastEventType ?? "none"}.`);
774
+ return null;
761
775
  }
762
776
 
763
777
  // ../../packages/providers-codex/dist/codex-client.js
@@ -779,112 +793,39 @@ function resolveSize(size) {
779
793
  });
780
794
  }
781
795
  }
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,
789
- background: "auto"
790
- };
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();
795
- }
796
- if (args.outputFormat === "jpeg" && typeof args.outputCompression === "number") {
797
- tool["output_compression"] = Math.min(100, Math.max(0, Math.trunc(args.outputCompression)));
798
- }
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" });
805
- }
806
- return {
796
+ function buildBody(args) {
797
+ const body = {
798
+ prompt: args.prompt,
807
799
  model: args.model,
808
- input: [{ role: "user", content }],
809
- tools: [args.tool],
810
- tool_choice: { type: "image_generation" },
811
- stream: true,
812
- store: false
800
+ size: args.size.size,
801
+ background: "auto"
813
802
  };
803
+ const quality = args.quality.trim();
804
+ if (quality && quality.toLowerCase() !== "auto")
805
+ body["quality"] = quality;
806
+ if (args.images.length > 0)
807
+ body["images"] = args.images.map((image) => ({ image_url: toDataUrl(image) }));
808
+ return body;
814
809
  }
815
810
  var CodexImageProvider = class {
816
811
  tokens;
817
812
  name = PROVIDER_NAME;
818
813
  sizePolicy = { resolveSize: (size) => resolveSize(size) };
819
- orchestrationModel;
820
814
  imageModel;
821
815
  timeoutSeconds;
822
- partialImages;
823
816
  fetchImpl;
824
817
  constructor(tokens, options = {}) {
825
818
  this.tokens = tokens;
826
- this.orchestrationModel = options.model ?? process.env["IMAGEGEN_CODEX_MODEL"] ?? DEFAULT_ORCHESTRATION_MODEL;
827
819
  this.imageModel = options.imageModel ?? process.env["IMAGEGEN_CODEX_IMAGE_MODEL"] ?? DEFAULT_IMAGE_MODEL;
828
820
  this.timeoutSeconds = options.timeoutSeconds ?? REQUEST_TIMEOUT_SECONDS;
829
- this.partialImages = options.partialImages ?? PARTIAL_IMAGES;
830
821
  this.fetchImpl = options.fetchImpl ?? fetch;
831
822
  }
832
823
  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
- });
824
+ const references = (request.referenceImages ?? []).slice(0, MAX_REFERENCE_IMAGES2);
825
+ return this.runAll(request, references.length > 0 ? "edits" : "generations", references);
856
826
  }
857
827
  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
- });
828
+ return this.runAll(request, "edits", [request.sourceImage, ...request.referenceImages ?? []].slice(0, MAX_REFERENCE_IMAGES2));
888
829
  }
889
830
  resolveImageModel(request) {
890
831
  const model = (request.model ?? this.imageModel).trim();
@@ -896,116 +837,120 @@ var CodexImageProvider = class {
896
837
  }
897
838
  return model;
898
839
  }
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));
840
+ async runAll(request, endpoint, images) {
841
+ const format = normalizeOutputFormat(request.format);
842
+ const size = this.sizePolicy.resolveSize(request.size);
843
+ const imageModel2 = this.resolveImageModel(request);
844
+ const body = JSON.stringify(buildBody({ prompt: request.prompt, model: imageModel2, size, quality: request.quality ?? DEFAULT_QUALITY, images }));
845
+ const results = [];
846
+ for (let i = 0, count = normalizeCount(request.count); i < count; i += 1) {
847
+ results.push(await this.sendOnce(endpoint, body, format, request.prompt));
848
+ }
849
+ return { provider: this.name, imageModel: imageModel2, images: results };
850
+ }
851
+ async sendOnce(endpoint, body, format, prompt) {
852
+ let accessToken = await this.tokens.getValidAccessToken();
853
+ let response = await this.post(endpoint, accessToken, body);
854
+ if (response.status === 401) {
855
+ await response.body?.cancel();
856
+ accessToken = await this.tokens.recoverFromUnauthorized(accessToken);
857
+ response = await this.post(endpoint, accessToken, body);
858
+ }
859
+ const requestId = response.headers.get("x-codex-imagegen-request-id") ?? response.headers.get("x-oai-request-id") ?? response.headers.get("x-request-id") ?? void 0;
860
+ if (!response.ok) {
861
+ const errorText = await response.text().catch(() => "");
862
+ const kind = classifyImageError(errorText);
863
+ const detail = readErrorMessage(errorText);
864
+ const retryAfter = Number(response.headers.get("retry-after"));
865
+ throw new ProviderError({
866
+ kind,
867
+ message: detail ? `Codex image request returned ${response.status} (${kind}): ${detail}` : `Codex image request returned ${response.status} (${kind}).`,
868
+ statusCode: response.status,
869
+ requestId,
870
+ ...Number.isFinite(retryAfter) && retryAfter > 0 ? { retryAfterSeconds: retryAfter } : {}
871
+ });
872
+ }
873
+ const payload = record(await response.json().catch(() => null));
874
+ const item = Array.isArray(payload["data"]) ? record(payload["data"][0]) : {};
875
+ if (typeof item["b64_json"] !== "string" || !item["b64_json"]) {
876
+ throw new ProviderError({ kind: "no_image", message: "Codex image request returned no image data.", requestId });
877
+ }
878
+ let data = Buffer.from(item["b64_json"], "base64");
879
+ let width;
880
+ let height;
881
+ try {
882
+ const metadata = await sharp(data).metadata();
883
+ if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? ""))
884
+ throw new Error();
885
+ await sharp(data).stats();
886
+ if (metadata.format !== format)
887
+ data = await sharp(data).toFormat(format).toBuffer();
888
+ width = metadata.width;
889
+ height = metadata.height;
890
+ } catch {
891
+ throw new ProviderError({ kind: "malformed", message: "Codex returned an invalid raster image.", requestId });
903
892
  }
904
- return { provider: this.name, imageModel: args.imageModel, images };
893
+ return {
894
+ data,
895
+ mimeType: `image/${format}`,
896
+ width,
897
+ height,
898
+ prompt,
899
+ responseId: typeof item["generation_id"] === "string" ? item["generation_id"] : void 0,
900
+ usage: {
901
+ requestId,
902
+ imageGeneration: payload["usage"],
903
+ // What the backend actually rendered; it may override requested size and quality.
904
+ served: { size: payload["size"], quality: payload["quality"], background: payload["background"] },
905
+ planLimits: readPlanLimits(response.headers)
906
+ }
907
+ };
905
908
  }
906
- async sendOnce(payload, format, prompt, size) {
907
- const accessToken = await this.tokens.getValidAccessToken();
908
- const accountId = extractAccountId(accessToken);
909
+ async post(endpoint, accessToken, body) {
909
910
  const controller = new AbortController();
910
911
  const timer = setTimeout(() => controller.abort(), this.timeoutSeconds * 1e3);
911
- let response;
912
912
  try {
913
- response = await this.fetchImpl(RESPONSES_ENDPOINT, {
913
+ return await this.fetchImpl(`${IMAGES_ENDPOINT}/${endpoint}`, {
914
914
  method: "POST",
915
915
  headers: {
916
916
  authorization: `Bearer ${accessToken}`,
917
- "chatgpt-account-id": accountId,
918
- "OpenAI-Beta": "responses=experimental",
917
+ "chatgpt-account-id": extractAccountId(accessToken),
919
918
  originator: "pi",
920
919
  "user-agent": USER_AGENT,
921
- accept: "text/event-stream",
920
+ accept: "application/json",
922
921
  "content-type": "application/json"
923
922
  },
924
- body: JSON.stringify(payload),
923
+ body,
925
924
  signal: controller.signal
926
925
  });
927
926
  } finally {
928
927
  clearTimeout(timer);
929
928
  }
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
929
  }
959
930
  };
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 });
931
+ function record(value) {
932
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
933
+ }
934
+ function readPlanLimits(headers) {
935
+ const number = (name) => {
936
+ const value = headers.get(`x-codex-${name}`);
937
+ const parsed = value === null ? Number.NaN : Number(value);
938
+ return Number.isFinite(parsed) ? parsed : void 0;
939
+ };
940
+ const window = (name) => {
941
+ const usedPercent = number(`${name}-used-percent`);
942
+ const windowMinutes = number(`${name}-window-minutes`);
943
+ if (usedPercent === void 0 || !windowMinutes)
944
+ return void 0;
945
+ return { usedPercent, windowMinutes, resetAt: number(`${name}-reset-at`) };
946
+ };
947
+ const limits = {
948
+ planType: headers.get("x-codex-plan-type") ?? void 0,
949
+ activeLimit: headers.get("x-codex-active-limit") ?? void 0,
950
+ primary: window("primary"),
951
+ secondary: window("secondary")
952
+ };
953
+ return Object.values(limits).some((value) => value !== void 0) ? limits : void 0;
1009
954
  }
1010
955
 
1011
956
  // ../../packages/providers-codex/dist/codex-entry.js
@@ -1049,7 +994,7 @@ function createCodexEntry() {
1049
994
  }
1050
995
 
1051
996
  // ../../packages/providers-google/dist/gemini-client.js
1052
- import sharp from "sharp";
997
+ import sharp2 from "sharp";
1053
998
 
1054
999
  // ../../packages/providers-google/dist/image-size.js
1055
1000
  var DEFAULT_IMAGE_MODEL2 = "gemini-nano-banana-2.1";
@@ -1244,12 +1189,12 @@ var GeminiImageProvider = class {
1244
1189
  if (typeof image.data !== "string" || !image.data || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.data) || image.data.length % 4 !== 0)
1245
1190
  throw error("malformed", "Gemini returned invalid inline image data.");
1246
1191
  const original = Buffer.from(image.data, "base64");
1247
- const metadata = await sharp(original).metadata().catch(() => {
1192
+ const metadata = await sharp2(original).metadata().catch(() => {
1248
1193
  throw error("malformed", "Gemini returned undecodable image bytes.");
1249
1194
  });
1250
1195
  if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || image.mime_type !== `image/${metadata.format}`)
1251
1196
  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();
1197
+ const data = metadata.format === format ? original : await sharp2(original).toFormat(format).toBuffer();
1253
1198
  return {
1254
1199
  data,
1255
1200
  mimeType: `image/${format}`,
@@ -1315,8 +1260,8 @@ function createGeminiEntry() {
1315
1260
  }
1316
1261
 
1317
1262
  // ../../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";
1263
+ import { readFile, mkdir as mkdir2, writeFile, rename, rm as rm2 } from "node:fs/promises";
1264
+ import { dirname as dirname2, join as join3 } from "node:path";
1320
1265
  import { randomUUID } from "node:crypto";
1321
1266
  var usage = (message) => new ProviderError({ kind: "usage", message });
1322
1267
  var profileStore = () => resolveCredentialStore();
@@ -1366,7 +1311,7 @@ function validateProfile(value) {
1366
1311
  var ProfileRepository = class {
1367
1312
  path;
1368
1313
  credentials;
1369
- constructor(path = join2(dirname(defaultCredentialFilePath()), "providers.json"), credentials = profileStore) {
1314
+ constructor(path = join3(dirname2(defaultCredentialFilePath()), "providers.json"), credentials = profileStore) {
1370
1315
  this.path = path;
1371
1316
  this.credentials = credentials;
1372
1317
  }
@@ -1431,7 +1376,7 @@ var ProfileRepository = class {
1431
1376
  async save(profiles) {
1432
1377
  const temporary = `${this.path}.${randomUUID()}.tmp`;
1433
1378
  try {
1434
- await mkdir(dirname(this.path), { recursive: true });
1379
+ await mkdir2(dirname2(this.path), { recursive: true });
1435
1380
  await writeFile(temporary, JSON.stringify({ version: 1, profiles }, null, 2) + "\n", { mode: 384 });
1436
1381
  await rename(temporary, this.path);
1437
1382
  } catch {
@@ -1440,7 +1385,7 @@ var ProfileRepository = class {
1440
1385
  message: "Could not save endpoint configuration."
1441
1386
  });
1442
1387
  } finally {
1443
- await rm(temporary, { force: true }).catch(() => {
1388
+ await rm2(temporary, { force: true }).catch(() => {
1444
1389
  });
1445
1390
  }
1446
1391
  }
@@ -1449,9 +1394,9 @@ async function profileKey(profile, store) {
1449
1394
  if (profile.auth === "none")
1450
1395
  return void 0;
1451
1396
  try {
1452
- const record2 = await store.load(profile.name);
1453
- if (record2 && typeof record2.apiKey === "string")
1454
- return validateApiKey(record2.apiKey);
1397
+ const record3 = await store.load(profile.name);
1398
+ if (record3 && typeof record3.apiKey === "string")
1399
+ return validateApiKey(record3.apiKey);
1455
1400
  } catch {
1456
1401
  }
1457
1402
  throw new ProviderError({
@@ -1461,7 +1406,7 @@ async function profileKey(profile, store) {
1461
1406
  }
1462
1407
 
1463
1408
  // ../../packages/providers-configurable/dist/provider.js
1464
- import sharp2 from "sharp";
1409
+ import sharp3 from "sharp";
1465
1410
 
1466
1411
  // ../../packages/providers-configurable/dist/image-size.js
1467
1412
  function imageSize(value, api) {
@@ -1487,7 +1432,7 @@ function imageSize(value, api) {
1487
1432
  }
1488
1433
 
1489
1434
  // ../../packages/providers-configurable/dist/http.js
1490
- var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
1435
+ var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
1491
1436
  var EndpointHttp = class {
1492
1437
  fetchImpl;
1493
1438
  timeoutSeconds;
@@ -1511,9 +1456,9 @@ var EndpointHttp = class {
1511
1456
  });
1512
1457
  if (!response.ok) {
1513
1458
  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;
1459
+ const failure2 = record2(await response.json().catch(() => null));
1460
+ const detail = record2(failure2.error);
1461
+ const upstream = record2(detail.metadata).provider_name;
1517
1462
  const token = new Headers(init.headers).get("authorization")?.replace(/^Bearer\s+/i, "");
1518
1463
  const upstreamProvider = typeof upstream === "string" && /^[A-Za-z0-9][A-Za-z0-9 ._()-]{0,63}$/.test(upstream) && !(token && upstream.includes(token)) && !/(?:sk-|AIza)/.test(upstream) ? upstream : void 0;
1519
1464
  const retryHeader = response.headers.get("retry-after");
@@ -1537,7 +1482,7 @@ var EndpointHttp = class {
1537
1482
  const value = await response.json();
1538
1483
  if (!value || typeof value !== "object" || Array.isArray(value))
1539
1484
  throw new Error();
1540
- return record(value);
1485
+ return record2(value);
1541
1486
  } catch {
1542
1487
  throw new ProviderError({
1543
1488
  kind: "malformed",
@@ -1672,14 +1617,14 @@ var ConfigurableImageProvider = class {
1672
1617
  message: "Model discovery response must contain a data array."
1673
1618
  });
1674
1619
  return body.data.map((value) => {
1675
- const model = record(value);
1620
+ const model = record2(value);
1676
1621
  if (typeof model.id !== "string")
1677
1622
  throw new ProviderError({
1678
1623
  kind: "malformed",
1679
1624
  message: "Discovered model has no string ID."
1680
1625
  });
1681
- const architecture = record(model.architecture);
1682
- const supported = record(model.supported_parameters);
1626
+ const architecture = record2(model.architecture);
1627
+ const supported = record2(model.supported_parameters);
1683
1628
  const strings = (v) => Array.isArray(v) && v.every((s) => typeof s === "string") ? v : void 0;
1684
1629
  return {
1685
1630
  id: model.id,
@@ -1737,7 +1682,7 @@ var ConfigurableImageProvider = class {
1737
1682
  kind: "malformed",
1738
1683
  message: "Endpoint must return exactly one image for a single-image request."
1739
1684
  });
1740
- const image = record(body.data[0]);
1685
+ const image = record2(body.data[0]);
1741
1686
  let data;
1742
1687
  if (typeof image.b64_json === "string") {
1743
1688
  if (!image.b64_json || image.b64_json.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(image.b64_json))
@@ -1768,12 +1713,12 @@ var ConfigurableImageProvider = class {
1768
1713
  message: "Endpoint returned no image bytes or download URL."
1769
1714
  });
1770
1715
  try {
1771
- const metadata = await sharp2(data).metadata();
1716
+ const metadata = await sharp3(data).metadata();
1772
1717
  if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || typeof image.media_type === "string" && image.media_type !== `image/${metadata.format}`)
1773
1718
  throw new Error();
1774
- await sharp2(data).stats();
1719
+ await sharp3(data).stats();
1775
1720
  if (metadata.format !== format)
1776
- data = await sharp2(data).toFormat(format).toBuffer();
1721
+ data = await sharp3(data).toFormat(format).toBuffer();
1777
1722
  return {
1778
1723
  data,
1779
1724
  width: metadata.width,
@@ -1833,7 +1778,7 @@ async function readApiKey(fromStdin, input = process.stdin, output = process.std
1833
1778
  const wasRaw = terminal.isRaw;
1834
1779
  const wasFlowing = input.readableFlowing === true;
1835
1780
  emitKeypressEvents(input);
1836
- return new Promise((resolve3, reject) => {
1781
+ return new Promise((resolve4, reject) => {
1837
1782
  let text = "";
1838
1783
  const cleanup = () => {
1839
1784
  input.removeListener("keypress", onKey);
@@ -1858,7 +1803,7 @@ async function readApiKey(fromStdin, input = process.stdin, output = process.std
1858
1803
  }
1859
1804
  if (key.name === "return" || key.name === "enter") {
1860
1805
  cleanup();
1861
- resolve3(text);
1806
+ resolve4(text);
1862
1807
  return;
1863
1808
  }
1864
1809
  if (key.name === "backspace") {
@@ -1908,7 +1853,7 @@ Supported protocols: OpenRouter dedicated Images API and OpenAI Images JSON /
1908
1853
  multipart endpoints, not arbitrary HTTP templates. Options vary by model.
1909
1854
  OpenRouter accepts aspect ratios; openai-images needs pixels or presets:
1910
1855
  square 1024x1024, portrait 1024x1536, landscape 1536x1024, wide 1536x864.
1911
- Auto options are omitted. Native masks require openai-images or Codex.
1856
+ Auto options are omitted. Native masks require openai-images.
1912
1857
  Outputs are measured and converted to PNG/JPEG/WebP without resizing. Count uses
1913
1858
  separate single-image requests without automatic retries or model substitution.
1914
1859
  `;
@@ -1946,7 +1891,9 @@ Supported Codex models (account availability is determined by the provider):
1946
1891
  Use full model IDs; other provider-supported IDs may also be supplied.
1947
1892
  Model precedence: --model > constructor imageModel (library callers) >
1948
1893
  IMAGEGEN_CODEX_IMAGE_MODEL > gpt-image-2.5-flare.
1949
- IMAGEGEN_CODEX_MODEL configures orchestration separately; --model selects images.
1894
+ Codex may override the requested model, quality, and size; results report the
1895
+ returned dimensions. Codex ignores --mask; describe the region in the prompt.
1896
+ IMAGEGEN_USAGE_LOG=<file> appends one prompt-free JSON usage line per result.
1950
1897
 
1951
1898
  Codex quality: auto (default), low, medium, high; Flare/Sunburst also support
1952
1899
  xhigh and max. gpt-image-2 rejects xhigh/max.
@@ -1959,7 +1906,7 @@ var GENERATE_EXAMPLES = `Examples:
1959
1906
  `;
1960
1907
  var EDIT_EXAMPLES = `Edit options:
1961
1908
  --source <file> Source image (required)
1962
- --mask <file> Optional edit mask (Codex or openai-images)
1909
+ --mask <file> Optional edit mask (openai-images; Codex ignores it)
1963
1910
 
1964
1911
  Examples:
1965
1912
  imagegen edit --format webp --source ./product.png --prompt "make the product blue" --out ./blue.webp
@@ -2102,9 +2049,9 @@ ${USAGE}`;
2102
2049
  }
2103
2050
 
2104
2051
  // src/skill.ts
2105
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2052
+ import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
2106
2053
  import { homedir as homedir2 } from "node:os";
2107
- import { dirname as dirname2, join as join3, resolve } from "node:path";
2054
+ import { dirname as dirname3, join as join4, resolve } from "node:path";
2108
2055
  var SKILL_NAME = "imagegen-cli";
2109
2056
  var SkillExistsError = class extends Error {
2110
2057
  constructor(path) {
@@ -2116,8 +2063,8 @@ async function skillText() {
2116
2063
  return readFile2(new URL("../skill/SKILL.md", import.meta.url), "utf8");
2117
2064
  }
2118
2065
  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");
2066
+ const root = target === "claude" ? join4(homedir2(), ".claude", "skills") : target === "codex" ? join4(homedir2(), ".agents", "skills") : resolve(target);
2067
+ return join4(root, SKILL_NAME, "SKILL.md");
2121
2068
  }
2122
2069
  async function existingText(path) {
2123
2070
  try {
@@ -2143,12 +2090,50 @@ async function installSkill(targets, force) {
2143
2090
  }
2144
2091
  for (const item of planned.values()) {
2145
2092
  if (item.status === "up-to-date") continue;
2146
- await mkdir2(dirname2(item.path), { recursive: true });
2093
+ await mkdir3(dirname3(item.path), { recursive: true });
2147
2094
  await writeFile2(item.path, content, { encoding: "utf8", flag: item.status === "installed" ? "wx" : "w" });
2148
2095
  }
2149
2096
  return [...planned.values()];
2150
2097
  }
2151
2098
 
2099
+ // src/usage-log.ts
2100
+ import { appendFile, mkdir as mkdir4 } from "node:fs/promises";
2101
+ import { dirname as dirname4, resolve as resolve2 } from "node:path";
2102
+ var USAGE_LOG_ENV = "IMAGEGEN_USAGE_LOG";
2103
+ function usageLogEntry(request, edit, result, paths) {
2104
+ return {
2105
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2106
+ command: edit ? "edit" : "generate",
2107
+ provider: result.provider,
2108
+ imageModel: result.imageModel,
2109
+ requested: {
2110
+ size: request.size ?? "auto",
2111
+ quality: request.quality ?? "auto",
2112
+ format: request.format ?? "png",
2113
+ count: request.count ?? 1,
2114
+ references: request.referenceImages?.length ?? 0,
2115
+ mask: edit ? edit.mask !== void 0 : void 0
2116
+ },
2117
+ images: result.images.map((image, i) => ({
2118
+ path: paths[i],
2119
+ width: image.width,
2120
+ height: image.height,
2121
+ bytes: image.data.byteLength,
2122
+ responseId: image.responseId,
2123
+ usage: image.usage
2124
+ }))
2125
+ };
2126
+ }
2127
+ async function appendUsageLog(entry) {
2128
+ const target = process.env[USAGE_LOG_ENV]?.trim();
2129
+ if (!target) return void 0;
2130
+ const path = resolve2(target);
2131
+ await mkdir4(dirname4(path), { recursive: true });
2132
+ await appendFile(path, `${JSON.stringify(entry)}
2133
+ `, "utf8");
2134
+ return path;
2135
+ }
2136
+
2152
2137
  // src/index.ts
2153
2138
  var EXIT_USAGE = 2;
2154
2139
  var EXIT_AUTH = 3;
@@ -2277,10 +2262,10 @@ function requestedProviders(flags) {
2277
2262
  return raw;
2278
2263
  }
2279
2264
  async function resolveOutPaths(out, providerName, multiProvider, count, format) {
2280
- const resolved = resolve2(out);
2265
+ const resolved = resolve3(out);
2281
2266
  let isDir = false;
2282
2267
  try {
2283
- const info = await stat(resolved);
2268
+ const info = await stat2(resolved);
2284
2269
  isDir = info.isDirectory();
2285
2270
  } catch {
2286
2271
  isDir = extname(resolved) === "";
@@ -2288,12 +2273,12 @@ async function resolveOutPaths(out, providerName, multiProvider, count, format)
2288
2273
  const extension = `.${normalizeOutputFormat(format)}`;
2289
2274
  const prefix = multiProvider ? `${providerName}-` : "";
2290
2275
  if (isDir) {
2291
- return Array.from({ length: count }, (_, i) => join4(resolved, `${prefix}image-${i + 1}${extension}`));
2276
+ return Array.from({ length: count }, (_, i) => join5(resolved, `${prefix}image-${i + 1}${extension}`));
2292
2277
  }
2293
2278
  if (count > 1 || multiProvider) {
2294
- const dir = dirname3(resolved);
2279
+ const dir = dirname5(resolved);
2295
2280
  const base = basename(resolved, extname(resolved));
2296
- return Array.from({ length: count }, (_, i) => join4(dir, `${prefix}${base}-${i + 1}${extension}`));
2281
+ return Array.from({ length: count }, (_, i) => join5(dir, `${prefix}${base}-${i + 1}${extension}`));
2297
2282
  }
2298
2283
  return [resolved.endsWith(extension) ? resolved : `${resolved}${extension}`];
2299
2284
  }
@@ -2318,7 +2303,7 @@ async function saveImages(result, paths) {
2318
2303
  for (let i = 0; i < result.images.length; i += 1) {
2319
2304
  const path = paths[i];
2320
2305
  try {
2321
- await mkdir3(dirname3(path), { recursive: true });
2306
+ await mkdir5(dirname5(path), { recursive: true });
2322
2307
  await writeFile3(path, Buffer.from(result.images[i].data));
2323
2308
  } catch {
2324
2309
  fail("io", `Could not write output file: ${path}`, EXIT_IO);
@@ -2391,8 +2376,8 @@ async function runAuthLogin(providerName, flags) {
2391
2376
  const waiter = waitForOAuthCallback(timeoutMinutes * 60 * 1e3, port);
2392
2377
  if (!flags.get("no-open")) {
2393
2378
  try {
2394
- const { default: open } = await import("open");
2395
- await open(url);
2379
+ const { default: open2 } = await import("open");
2380
+ await open2(url);
2396
2381
  } catch {
2397
2382
  process.stderr.write("Could not open a browser automatically. Open this URL manually:\n");
2398
2383
  }
@@ -2525,6 +2510,12 @@ async function runImageRequest(request, edit, providers) {
2525
2510
  const paths = await resolveOutPaths(request.out, entry.provider.name, multiProvider, result.images.length, request.format ?? "png");
2526
2511
  await saveImages(result, paths);
2527
2512
  printResult(entry.provider.name, result, paths);
2513
+ try {
2514
+ await appendUsageLog(usageLogEntry(request, edit, result, paths));
2515
+ } catch {
2516
+ process.stderr.write(`${JSON.stringify({ warning: { kind: "io", message: `Could not append to ${USAGE_LOG_ENV} file.` } })}
2517
+ `);
2518
+ }
2528
2519
  }
2529
2520
  }
2530
2521
  async function main() {