@mrgoonie/multix 0.7.2 → 0.8.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/cli.js CHANGED
@@ -95,151 +95,6 @@ var init_errors = __esm({
95
95
  }
96
96
  });
97
97
 
98
- // src/core/fetch-json.ts
99
- function parseRetryAfter(value, now = Date.now()) {
100
- if (!value) return void 0;
101
- const trimmed = value.trim();
102
- if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1e3);
103
- const date = Date.parse(trimmed);
104
- if (Number.isNaN(date)) return void 0;
105
- return Math.max(0, date - now);
106
- }
107
- async function httpErrorFromResponse(response, url) {
108
- let snippet = "";
109
- try {
110
- const text = await response.text();
111
- snippet = text.slice(0, SNIPPET_MAX);
112
- } catch {
113
- snippet = "(could not read body)";
114
- }
115
- return new HttpError(
116
- response.status,
117
- snippet,
118
- url,
119
- parseRetryAfter(response.headers.get("retry-after"))
120
- );
121
- }
122
- async function fetchJson(opts) {
123
- const { url, method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
124
- const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
125
- const init = {
126
- method,
127
- headers: {
128
- "Content-Type": "application/json",
129
- ...headers
130
- },
131
- signal: AbortSignal.timeout(timeoutMs)
132
- };
133
- if (body !== void 0) {
134
- init.body = JSON.stringify(body);
135
- }
136
- let response;
137
- try {
138
- response = await doFetch(url, init);
139
- } catch (cause) {
140
- const msg = cause instanceof Error ? cause.message : String(cause);
141
- throw new HttpError(0, msg, url);
142
- }
143
- if (!response.ok) {
144
- throw await httpErrorFromResponse(response, url);
145
- }
146
- let text;
147
- try {
148
- text = await response.text();
149
- } catch (cause) {
150
- const msg = cause instanceof Error ? cause.message : String(cause);
151
- throw new HttpError(0, `Failed to read response body: ${msg}`, url);
152
- }
153
- if (text.trim() === "") return void 0;
154
- try {
155
- return JSON.parse(text);
156
- } catch {
157
- throw new HttpError(
158
- response.status,
159
- `Invalid JSON response: ${text.slice(0, SNIPPET_MAX)}`,
160
- url
161
- );
162
- }
163
- }
164
- var DEFAULT_TIMEOUT_MS, SNIPPET_MAX;
165
- var init_fetch_json = __esm({
166
- "src/core/fetch-json.ts"() {
167
- "use strict";
168
- init_errors();
169
- DEFAULT_TIMEOUT_MS = 12e4;
170
- SNIPPET_MAX = 500;
171
- }
172
- });
173
-
174
- // src/core/http-client.ts
175
- var http_client_exports = {};
176
- __export(http_client_exports, {
177
- downloadFile: () => downloadFile,
178
- fetchBytes: () => fetchBytes,
179
- httpJson: () => httpJson
180
- });
181
- import { once } from "events";
182
- import fs4 from "fs";
183
- import path4 from "path";
184
- import { pipeline } from "stream/promises";
185
- async function httpJson(opts) {
186
- return fetchJson(opts);
187
- }
188
- async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers) {
189
- fs4.mkdirSync(path4.dirname(dest), { recursive: true });
190
- let response;
191
- try {
192
- response = await globalThis.fetch(url, {
193
- signal: AbortSignal.timeout(timeoutMs),
194
- ...headers ? { headers } : {}
195
- });
196
- } catch (cause) {
197
- const msg = cause instanceof Error ? cause.message : String(cause);
198
- throw new HttpError(0, msg, url);
199
- }
200
- if (!response.ok) {
201
- throw new HttpError(response.status, "(download failed)", url);
202
- }
203
- if (!response.body) {
204
- throw new HttpError(0, "Response body is null", url);
205
- }
206
- const fileStream = fs4.createWriteStream(dest);
207
- try {
208
- await pipeline(response.body, fileStream);
209
- } catch (err2) {
210
- if (!fileStream.closed) await once(fileStream, "close");
211
- throw err2;
212
- }
213
- }
214
- async function fetchBytes(urlOrDataUri) {
215
- if (urlOrDataUri.startsWith("data:")) {
216
- const commaIdx = urlOrDataUri.indexOf(",");
217
- if (commaIdx === -1) throw new Error("Malformed data URI: missing comma");
218
- const encoded = urlOrDataUri.slice(commaIdx + 1);
219
- return Buffer.from(encoded, "base64");
220
- }
221
- let response;
222
- try {
223
- response = await globalThis.fetch(urlOrDataUri, {
224
- signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
225
- });
226
- } catch (cause) {
227
- const msg = cause instanceof Error ? cause.message : String(cause);
228
- throw new HttpError(0, msg, urlOrDataUri);
229
- }
230
- if (!response.ok) {
231
- throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
232
- }
233
- return Buffer.from(await response.arrayBuffer());
234
- }
235
- var init_http_client = __esm({
236
- "src/core/http-client.ts"() {
237
- "use strict";
238
- init_errors();
239
- init_fetch_json();
240
- }
241
- });
242
-
243
98
  // src/cli.ts
244
99
  import { createRequire as createRequire2 } from "module";
245
100
  import { Command } from "commander";
@@ -447,7 +302,135 @@ function resolveKey(name) {
447
302
 
448
303
  // src/providers/byteplus/client.ts
449
304
  init_errors();
450
- init_http_client();
305
+
306
+ // src/core/http-client.ts
307
+ init_errors();
308
+ import { once } from "events";
309
+ import fs4 from "fs";
310
+ import path4 from "path";
311
+ import { pipeline } from "stream/promises";
312
+
313
+ // src/core/fetch-json.ts
314
+ init_errors();
315
+ var DEFAULT_TIMEOUT_MS = 12e4;
316
+ var SNIPPET_MAX = 500;
317
+ function parseRetryAfter(value, now = Date.now()) {
318
+ if (!value) return void 0;
319
+ const trimmed = value.trim();
320
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1e3);
321
+ const date = Date.parse(trimmed);
322
+ if (Number.isNaN(date)) return void 0;
323
+ return Math.max(0, date - now);
324
+ }
325
+ async function httpErrorFromResponse(response, url) {
326
+ let snippet = "";
327
+ try {
328
+ const text = await response.text();
329
+ snippet = text.slice(0, SNIPPET_MAX);
330
+ } catch {
331
+ snippet = "(could not read body)";
332
+ }
333
+ return new HttpError(
334
+ response.status,
335
+ snippet,
336
+ url,
337
+ parseRetryAfter(response.headers.get("retry-after"))
338
+ );
339
+ }
340
+ async function fetchJson(opts) {
341
+ const { url, method = "GET", headers: headers2 = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
342
+ const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
343
+ const init = {
344
+ method,
345
+ headers: {
346
+ "Content-Type": "application/json",
347
+ ...headers2
348
+ },
349
+ signal: AbortSignal.timeout(timeoutMs)
350
+ };
351
+ if (body !== void 0) {
352
+ init.body = JSON.stringify(body);
353
+ }
354
+ let response;
355
+ try {
356
+ response = await doFetch(url, init);
357
+ } catch (cause) {
358
+ const msg = cause instanceof Error ? cause.message : String(cause);
359
+ throw new HttpError(0, msg, url);
360
+ }
361
+ if (!response.ok) {
362
+ throw await httpErrorFromResponse(response, url);
363
+ }
364
+ let text;
365
+ try {
366
+ text = await response.text();
367
+ } catch (cause) {
368
+ const msg = cause instanceof Error ? cause.message : String(cause);
369
+ throw new HttpError(0, `Failed to read response body: ${msg}`, url);
370
+ }
371
+ if (text.trim() === "") return void 0;
372
+ try {
373
+ return JSON.parse(text);
374
+ } catch {
375
+ throw new HttpError(
376
+ response.status,
377
+ `Invalid JSON response: ${text.slice(0, SNIPPET_MAX)}`,
378
+ url
379
+ );
380
+ }
381
+ }
382
+
383
+ // src/core/http-client.ts
384
+ async function httpJson(opts) {
385
+ return fetchJson(opts);
386
+ }
387
+ async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers2) {
388
+ fs4.mkdirSync(path4.dirname(dest), { recursive: true });
389
+ let response;
390
+ try {
391
+ response = await globalThis.fetch(url, {
392
+ signal: AbortSignal.timeout(timeoutMs),
393
+ ...headers2 ? { headers: headers2 } : {}
394
+ });
395
+ } catch (cause) {
396
+ const msg = cause instanceof Error ? cause.message : String(cause);
397
+ throw new HttpError(0, msg, url);
398
+ }
399
+ if (!response.ok) {
400
+ throw new HttpError(response.status, "(download failed)", url);
401
+ }
402
+ if (!response.body) {
403
+ throw new HttpError(0, "Response body is null", url);
404
+ }
405
+ const fileStream = fs4.createWriteStream(dest);
406
+ try {
407
+ await pipeline(response.body, fileStream);
408
+ } catch (err2) {
409
+ if (!fileStream.closed) await once(fileStream, "close");
410
+ throw err2;
411
+ }
412
+ }
413
+ async function fetchBytes(urlOrDataUri) {
414
+ if (urlOrDataUri.startsWith("data:")) {
415
+ const commaIdx = urlOrDataUri.indexOf(",");
416
+ if (commaIdx === -1) throw new Error("Malformed data URI: missing comma");
417
+ const encoded = urlOrDataUri.slice(commaIdx + 1);
418
+ return Buffer.from(encoded, "base64");
419
+ }
420
+ let response;
421
+ try {
422
+ response = await globalThis.fetch(urlOrDataUri, {
423
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
424
+ });
425
+ } catch (cause) {
426
+ const msg = cause instanceof Error ? cause.message : String(cause);
427
+ throw new HttpError(0, msg, urlOrDataUri);
428
+ }
429
+ if (!response.ok) {
430
+ throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
431
+ }
432
+ return Buffer.from(await response.arrayBuffer());
433
+ }
451
434
 
452
435
  // src/providers/byteplus/models.ts
453
436
  var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
@@ -527,7 +510,6 @@ function createBytePlusClient() {
527
510
  }
528
511
 
529
512
  // src/providers/byteplus/generators/three-d.ts
530
- init_http_client();
531
513
  function buildThreeDTaskBody(opts) {
532
514
  const promptText = (opts.prompt ?? "").trim();
533
515
  const flagPart = opts.flags?.trim() ?? "";
@@ -835,7 +817,6 @@ function finalizeGeneratedImages(files, opts) {
835
817
  }
836
818
 
837
819
  // src/providers/byteplus/generators/image.ts
838
- init_http_client();
839
820
  import fs7 from "fs";
840
821
  import path7 from "path";
841
822
  var ASPECT_TO_SIZE = {
@@ -999,7 +980,6 @@ function collect2(value, prev) {
999
980
  import path9 from "path";
1000
981
 
1001
982
  // src/core/video-thumb.ts
1002
- init_http_client();
1003
983
  import fs8 from "fs";
1004
984
  import path8 from "path";
1005
985
  var THUMB_KEYS = /* @__PURE__ */ new Set([
@@ -1084,7 +1064,6 @@ async function maybeDownloadThumb(source, videoPath, opts = {}) {
1084
1064
  }
1085
1065
 
1086
1066
  // src/providers/byteplus/generators/video.ts
1087
- init_http_client();
1088
1067
  function buildFlagSuffix(opts) {
1089
1068
  const parts = [];
1090
1069
  if (opts.resolution) parts.push(`--rs ${opts.resolution}`);
@@ -1895,7 +1874,6 @@ function registerCloudflareCommands(program2) {
1895
1874
  import fs10 from "fs";
1896
1875
  import path14 from "path";
1897
1876
  init_errors();
1898
- init_http_client();
1899
1877
 
1900
1878
  // src/providers/elevenlabs/models.ts
1901
1879
  var ELEVENLABS_BASE_URL = "https://api.elevenlabs.io/v1";
@@ -2004,19 +1982,19 @@ async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
2004
1982
  if (opts.query) {
2005
1983
  for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
2006
1984
  }
2007
- const headers = authHeaders(apiKey);
1985
+ const headers2 = authHeaders(apiKey);
2008
1986
  let init;
2009
1987
  if (body instanceof FormData) {
2010
1988
  init = {
2011
1989
  method: "POST",
2012
- headers,
1990
+ headers: headers2,
2013
1991
  body,
2014
1992
  signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
2015
1993
  };
2016
1994
  } else {
2017
1995
  init = {
2018
1996
  method: "POST",
2019
- headers: { ...headers, "Content-Type": "application/json" },
1997
+ headers: { ...headers2, "Content-Type": "application/json" },
2020
1998
  body: JSON.stringify(body),
2021
1999
  signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
2022
2000
  };
@@ -2185,7 +2163,6 @@ function registerCloneCommand(parent) {
2185
2163
 
2186
2164
  // src/providers/elevenlabs/commands/dub.ts
2187
2165
  init_errors();
2188
- init_http_client();
2189
2166
  import fs12 from "fs";
2190
2167
  import path16 from "path";
2191
2168
  function registerDubCommand(parent) {
@@ -2669,7 +2646,6 @@ var FAL_DEFAULTS = {
2669
2646
  };
2670
2647
 
2671
2648
  // src/providers/fal/run-helpers.ts
2672
- init_http_client();
2673
2649
  import fs16 from "fs";
2674
2650
  import path19 from "path";
2675
2651
 
@@ -2706,7 +2682,6 @@ init_redact();
2706
2682
  // src/providers/fal/client.ts
2707
2683
  init_errors();
2708
2684
  init_errors();
2709
- init_http_client();
2710
2685
  init_redact();
2711
2686
  function requireFalKey() {
2712
2687
  const key = resolveKey("FAL_KEY");
@@ -3115,10 +3090,72 @@ init_errors();
3115
3090
  import fs20 from "fs";
3116
3091
 
3117
3092
  // src/providers/gemini/client.ts
3093
+ init_errors();
3118
3094
  import fs18 from "fs";
3119
3095
  import path22 from "path";
3096
+
3097
+ // src/providers/gemini/key-pool.ts
3120
3098
  init_errors();
3121
- init_http_client();
3099
+ var PRIMARY_KEY_ENV = "GEMINI_API_KEY";
3100
+ var DEFAULT_COOLDOWN_MS = 6e4;
3101
+ var cooldownUntil = /* @__PURE__ */ new Map();
3102
+ function listGeminiKeys(env = process.env) {
3103
+ const numbered = Object.keys(env).map((name) => ({ name, match: /^GEMINI_API_KEY_(\d+)$/.exec(name) })).filter((e) => e.match !== null).sort((a, b) => Number(a.match[1]) - Number(b.match[1])).map((e) => e.name);
3104
+ const seen = /* @__PURE__ */ new Set();
3105
+ const keys = [];
3106
+ for (const name of [PRIMARY_KEY_ENV, ...numbered]) {
3107
+ const value = env[name]?.trim();
3108
+ if (!value || seen.has(value)) continue;
3109
+ seen.add(value);
3110
+ keys.push({ name, value });
3111
+ }
3112
+ return keys;
3113
+ }
3114
+ function isGeminiRateLimitError(error) {
3115
+ if (!(error instanceof HttpError)) return false;
3116
+ if (error.status === 429) return true;
3117
+ return (error.status === 400 || error.status === 403) && error.snippet.includes("RESOURCE_EXHAUSTED");
3118
+ }
3119
+ function cooldownMs(error) {
3120
+ if (!(error instanceof HttpError)) return DEFAULT_COOLDOWN_MS;
3121
+ if (error.retryAfterMs !== void 0 && error.retryAfterMs > 0) return error.retryAfterMs;
3122
+ const delay = /"retryDelay"\s*:\s*"(\d+(?:\.\d+)?)s"/.exec(error.snippet);
3123
+ if (delay?.[1]) return Math.ceil(Number(delay[1]) * 1e3);
3124
+ return DEFAULT_COOLDOWN_MS;
3125
+ }
3126
+ function keysToTry(keys, now) {
3127
+ const until = (k) => cooldownUntil.get(k.value) ?? 0;
3128
+ const ready = keys.filter((k) => until(k) <= now);
3129
+ if (ready.length > 0) return ready;
3130
+ const soonest = [...keys].sort((a, b) => until(a) - until(b))[0];
3131
+ return soonest ? [soonest] : [];
3132
+ }
3133
+ async function withGeminiKey(fn, opts = {}) {
3134
+ const now = opts.now ?? Date.now;
3135
+ const keys = listGeminiKeys(opts.env);
3136
+ if (keys.length === 0) {
3137
+ throw new ConfigError(
3138
+ "GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
3139
+ );
3140
+ }
3141
+ const ordered = keysToTry(keys, now());
3142
+ let lastError;
3143
+ for (const [i, key] of ordered.entries()) {
3144
+ try {
3145
+ return await fn(key.value, key.name);
3146
+ } catch (error) {
3147
+ if (!isGeminiRateLimitError(error)) throw error;
3148
+ lastError = error;
3149
+ cooldownUntil.set(key.value, now() + cooldownMs(error));
3150
+ const next = ordered[i + 1];
3151
+ if (next) opts.logger?.warn(`${key.name} is rate-limited; retrying with ${next.name}`);
3152
+ }
3153
+ }
3154
+ if (keys.length > 1) {
3155
+ opts.logger?.warn(`All ${keys.length} Gemini keys are rate-limited; try again later`);
3156
+ }
3157
+ throw lastError;
3158
+ }
3122
3159
 
3123
3160
  // src/providers/gemini/task-resolver.ts
3124
3161
  import path21 from "path";
@@ -3173,19 +3210,16 @@ function requiresProcessingWait(mimeType) {
3173
3210
  var BASE = "https://generativelanguage.googleapis.com";
3174
3211
  var FILES_BASE = `${BASE}/upload/v1beta/files`;
3175
3212
  var GENERATE_BASE = `${BASE}/v1beta/models`;
3176
- function requireApiKey() {
3177
- const key = resolveKey("GEMINI_API_KEY");
3178
- if (!key)
3179
- throw new ConfigError(
3180
- "GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
3181
- );
3182
- return key;
3213
+ function withKey(opts, fn) {
3214
+ return opts.apiKey ? fn(opts.apiKey) : withGeminiKey(fn, { logger: opts.logger });
3183
3215
  }
3184
3216
  function authHeaders2(apiKey) {
3185
3217
  return { "x-goog-api-key": apiKey };
3186
3218
  }
3187
3219
  async function uploadFile(filePath, opts = {}) {
3188
- const apiKey = requireApiKey();
3220
+ return withKey(opts, (apiKey) => uploadFileWithKey(filePath, apiKey, opts));
3221
+ }
3222
+ async function uploadFileWithKey(filePath, apiKey, opts) {
3189
3223
  const { timeoutMs = 3e5, logger } = opts;
3190
3224
  const mimeType = getMimeType(filePath);
3191
3225
  const fileBytes = fs18.readFileSync(filePath);
@@ -3221,10 +3255,7 @@ Content-Type: ${mimeType}\r
3221
3255
  });
3222
3256
  if (!resp.ok) {
3223
3257
  const txt = await resp.text();
3224
- throw new ProviderError(
3225
- `File upload failed (HTTP ${resp.status}): ${txt.slice(0, 500)}`,
3226
- "gemini"
3227
- );
3258
+ throw new HttpError(resp.status, `File upload failed: ${txt.slice(0, 500)}`, FILES_BASE);
3228
3259
  }
3229
3260
  const data = await resp.json();
3230
3261
  let fileRef = data.file;
@@ -3246,26 +3277,30 @@ Content-Type: ${mimeType}\r
3246
3277
  logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
3247
3278
  return fileRef;
3248
3279
  }
3249
- async function generateContent(req) {
3250
- const apiKey = requireApiKey();
3251
- return httpJson({
3252
- url: `${GENERATE_BASE}/${req.model}:generateContent`,
3253
- method: "POST",
3254
- headers: authHeaders2(apiKey),
3255
- body: {
3256
- contents: req.contents,
3257
- ...req.generationConfig ? { generationConfig: req.generationConfig } : {}
3258
- }
3259
- });
3280
+ async function generateContent(req, opts = {}) {
3281
+ return withKey(
3282
+ opts,
3283
+ (apiKey) => httpJson({
3284
+ url: `${GENERATE_BASE}/${req.model}:generateContent`,
3285
+ method: "POST",
3286
+ headers: authHeaders2(apiKey),
3287
+ body: {
3288
+ contents: req.contents,
3289
+ ...req.generationConfig ? { generationConfig: req.generationConfig } : {}
3290
+ }
3291
+ })
3292
+ );
3260
3293
  }
3261
- async function createInteraction(body) {
3262
- const apiKey = requireApiKey();
3263
- return httpJson({
3264
- url: `${BASE}/v1beta/interactions`,
3265
- method: "POST",
3266
- headers: authHeaders2(apiKey),
3267
- body
3268
- });
3294
+ async function createInteraction(body, opts = {}) {
3295
+ return withKey(
3296
+ opts,
3297
+ (apiKey) => httpJson({
3298
+ url: `${BASE}/v1beta/interactions`,
3299
+ method: "POST",
3300
+ headers: authHeaders2(apiKey),
3301
+ body
3302
+ })
3303
+ );
3269
3304
  }
3270
3305
  function extractInteractionAudio(resp) {
3271
3306
  let found = null;
@@ -3444,22 +3479,30 @@ function registerAnalyzeCommand(parent) {
3444
3479
  const filePath = opts.files[i];
3445
3480
  logger.debug(`[${i + 1}/${opts.files.length}] Processing: ${filePath}`);
3446
3481
  try {
3447
- const useApi = shouldUseFileApi(filePath);
3448
- let contentParts;
3449
- if (useApi) {
3450
- logger.debug("File >20MB \u2014 using Files API");
3451
- const ref = await uploadFile(filePath, { logger });
3452
- contentParts = [
3453
- { text: opts.prompt },
3454
- { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
3455
- ];
3456
- } else {
3457
- contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
3458
- }
3459
- const resp = await generateContent({
3460
- model,
3461
- contents: [{ parts: contentParts }]
3462
- });
3482
+ const resp = await withGeminiKey(
3483
+ async (apiKey) => {
3484
+ const useApi = shouldUseFileApi(filePath);
3485
+ let contentParts;
3486
+ if (useApi) {
3487
+ logger.debug("File >20MB \u2014 using Files API");
3488
+ const ref = await uploadFile(filePath, { logger, apiKey });
3489
+ contentParts = [
3490
+ { text: opts.prompt },
3491
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
3492
+ ];
3493
+ } else {
3494
+ contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
3495
+ }
3496
+ return generateContent(
3497
+ {
3498
+ model,
3499
+ contents: [{ parts: contentParts }]
3500
+ },
3501
+ { apiKey }
3502
+ );
3503
+ },
3504
+ { logger }
3505
+ );
3463
3506
  results.push({
3464
3507
  file: filePath,
3465
3508
  status: "success",
@@ -3498,22 +3541,30 @@ function registerExtractCommand(parent) {
3498
3541
  const filePath = opts.files[i];
3499
3542
  logger.debug(`[${i + 1}/${opts.files.length}] Extracting: ${filePath}`);
3500
3543
  try {
3501
- const useApi = shouldUseFileApi(filePath);
3502
- let contentParts;
3503
- if (useApi) {
3504
- const ref = await uploadFile(filePath, { logger });
3505
- contentParts = [
3506
- { text: opts.prompt },
3507
- { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
3508
- ];
3509
- } else {
3510
- contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
3511
- }
3512
- const resp = await generateContent({
3513
- model,
3514
- contents: [{ parts: contentParts }],
3515
- generationConfig
3516
- });
3544
+ const resp = await withGeminiKey(
3545
+ async (apiKey) => {
3546
+ const useApi = shouldUseFileApi(filePath);
3547
+ let contentParts;
3548
+ if (useApi) {
3549
+ const ref = await uploadFile(filePath, { logger, apiKey });
3550
+ contentParts = [
3551
+ { text: opts.prompt },
3552
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
3553
+ ];
3554
+ } else {
3555
+ contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
3556
+ }
3557
+ return generateContent(
3558
+ {
3559
+ model,
3560
+ contents: [{ parts: contentParts }],
3561
+ generationConfig
3562
+ },
3563
+ { apiKey }
3564
+ );
3565
+ },
3566
+ { logger }
3567
+ );
3517
3568
  results.push({ file: filePath, status: "success", response: extractText(resp) });
3518
3569
  } catch (e) {
3519
3570
  const msg = e instanceof Error ? e.message : String(e);
@@ -3617,11 +3668,14 @@ async function generateGeminiSpeech(opts) {
3617
3668
  logger?.debug(`Gemini TTS model=${model} mode=${speakers?.length ? "multi" : "single"}`);
3618
3669
  let resp;
3619
3670
  try {
3620
- resp = await generateContent({
3621
- model,
3622
- contents: [{ parts: [{ text }] }],
3623
- generationConfig
3624
- });
3671
+ resp = await generateContent(
3672
+ {
3673
+ model,
3674
+ contents: [{ parts: [{ text }] }],
3675
+ generationConfig
3676
+ },
3677
+ { logger }
3678
+ );
3625
3679
  } catch (e) {
3626
3680
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
3627
3681
  }
@@ -3657,7 +3711,7 @@ async function generateInteractionsSpeech(opts) {
3657
3711
  }
3658
3712
  let resp;
3659
3713
  try {
3660
- resp = await createInteraction(buildInteractionsSpeechBody(opts));
3714
+ resp = await createInteraction(buildInteractionsSpeechBody(opts), { logger });
3661
3715
  } catch (e) {
3662
3716
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
3663
3717
  }
@@ -3813,20 +3867,194 @@ function collectSpeakers(value, prev) {
3813
3867
  }
3814
3868
 
3815
3869
  // src/providers/gemini/commands/generate-video.ts
3816
- import fs23 from "fs";
3870
+ init_errors();
3871
+ import fs24 from "fs";
3817
3872
  import path25 from "path";
3873
+
3874
+ // src/providers/gemini/veo.ts
3875
+ import fs23 from "fs";
3876
+
3877
+ // src/core/async-poll.ts
3878
+ init_errors();
3879
+ var PollTimeoutError2 = class extends MultixError {
3880
+ constructor(message, lastStatus) {
3881
+ super(message, "POLL_TIMEOUT");
3882
+ this.lastStatus = lastStatus;
3883
+ this.name = "PollTimeoutError";
3884
+ }
3885
+ lastStatus;
3886
+ };
3887
+ function isTransientHttpError(error) {
3888
+ if (!(error instanceof HttpError)) return false;
3889
+ return error.status === 0 || error.status === 429 || error.status >= 500;
3890
+ }
3891
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3892
+ async function pollUntil2(opts) {
3893
+ const now = opts.now ?? Date.now;
3894
+ const sleep5 = opts.sleep ?? defaultSleep;
3895
+ const random = opts.random ?? Math.random;
3896
+ const isTransient = opts.isTransientError ?? isTransientHttpError;
3897
+ const maxInterval = opts.maxIntervalMs ?? 15e3;
3898
+ const minInterval = opts.minIntervalMs ?? 500;
3899
+ const factor = opts.backoffFactor ?? 1.5;
3900
+ const jitter = opts.jitter ?? true;
3901
+ const deadline = now() + opts.timeoutMs;
3902
+ let interval = opts.intervalMs ?? 3e3;
3903
+ let attempt = 0;
3904
+ let last;
3905
+ let lastError;
3906
+ for (; ; ) {
3907
+ attempt++;
3908
+ let retryAfterMs;
3909
+ try {
3910
+ last = await opts.fetchStatus({ remainingMs: Math.max(1, deadline - now()), attempt });
3911
+ lastError = void 0;
3912
+ opts.onPoll?.(last, attempt);
3913
+ if (opts.isTerminal(last)) return last;
3914
+ } catch (error) {
3915
+ if (!isTransient(error)) throw error;
3916
+ lastError = error;
3917
+ if (error instanceof HttpError) retryAfterMs = error.retryAfterMs;
3918
+ }
3919
+ const remaining = deadline - now();
3920
+ if (remaining <= 0) {
3921
+ const hint = opts.resumeHint ? ` ${opts.resumeHint}` : "";
3922
+ const cause = lastError instanceof Error ? ` Last error: ${lastError.message}.` : "";
3923
+ throw new PollTimeoutError2(
3924
+ `Timed out after ${opts.timeoutMs}ms waiting for job.${cause}${hint}`,
3925
+ last
3926
+ );
3927
+ }
3928
+ let delay = retryAfterMs ?? interval;
3929
+ if (jitter && retryAfterMs === void 0) delay *= 0.8 + random() * 0.4;
3930
+ await sleep5(Math.min(Math.max(delay, minInterval), remaining));
3931
+ interval = Math.min(interval * factor, maxInterval);
3932
+ }
3933
+ }
3934
+
3935
+ // src/providers/gemini/veo.ts
3818
3936
  init_errors();
3819
- init_http_client();
3820
- var BASE2 = "https://generativelanguage.googleapis.com";
3937
+ var API_HOST = "generativelanguage.googleapis.com";
3938
+ var BASE2 = `https://${API_HOST}/v1beta`;
3939
+ var VEO_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp"]);
3940
+ var VEO_TIMEOUT_MS = 10 * 60 * 1e3;
3941
+ function veoImageFromFile(imgPath) {
3942
+ if (!fs23.existsSync(imgPath)) throw new ValidationError(`Image not found: ${imgPath}`);
3943
+ const detected = getMimeType(imgPath);
3944
+ const mimeType = detected === "application/octet-stream" ? "image/jpeg" : detected;
3945
+ if (!VEO_IMAGE_TYPES.has(mimeType)) {
3946
+ throw new ValidationError(`Veo accepts JPEG, PNG or WebP images: ${imgPath} (${mimeType})`);
3947
+ }
3948
+ return { bytesBase64Encoded: fs23.readFileSync(imgPath).toString("base64"), mimeType };
3949
+ }
3950
+ function veoFramesFromPaths(paths = []) {
3951
+ if (paths.length > 2) {
3952
+ throw new ValidationError(
3953
+ "--reference-images accepts at most two images (opening frame, closing frame)"
3954
+ );
3955
+ }
3956
+ const [first, last] = paths;
3957
+ return {
3958
+ image: first ? veoImageFromFile(first) : void 0,
3959
+ lastFrame: last ? veoImageFromFile(last) : void 0
3960
+ };
3961
+ }
3962
+ function modelPath(model) {
3963
+ return model.replace(/^models\//, "");
3964
+ }
3965
+ function buildVeoRequest(opts) {
3966
+ const instance = { prompt: opts.prompt };
3967
+ if (opts.image) instance.image = opts.image;
3968
+ if (opts.lastFrame) instance.lastFrame = opts.lastFrame;
3969
+ return {
3970
+ instances: [instance],
3971
+ parameters: { aspectRatio: opts.aspectRatio, resolution: opts.resolution }
3972
+ };
3973
+ }
3974
+ function extractVeoVideoUri(op) {
3975
+ const r = op.response ?? {};
3976
+ return r.generateVideoResponse?.generatedSamples?.[0]?.video?.uri ?? r.generatedSamples?.[0]?.video?.uri ?? r.generatedVideos?.[0]?.video?.uri;
3977
+ }
3978
+ function headers(apiKey) {
3979
+ return { "x-goog-api-key": apiKey };
3980
+ }
3981
+ async function submitVeo(apiKey, model, body) {
3982
+ try {
3983
+ return await httpJson({
3984
+ url: `${BASE2}/models/${modelPath(model)}:predictLongRunning`,
3985
+ method: "POST",
3986
+ headers: headers(apiKey),
3987
+ body
3988
+ });
3989
+ } catch (e) {
3990
+ if (e instanceof HttpError) throw e;
3991
+ const msg = e instanceof Error ? e.message : String(e);
3992
+ throw new ProviderError(`Veo submit failed: ${msg}`, "gemini");
3993
+ }
3994
+ }
3995
+ async function waitForVeo(apiKey, operation, logger, opts = {}) {
3996
+ if (operation.done) return checkVeoResult(operation);
3997
+ const finalOp = await pollUntil2({
3998
+ fetchStatus: ({ remainingMs }) => httpJson({
3999
+ url: `${BASE2}/${operation.name}`,
4000
+ headers: headers(apiKey),
4001
+ timeoutMs: Math.min(remainingMs, 6e4)
4002
+ }),
4003
+ isTerminal: (op) => op.done === true,
4004
+ timeoutMs: opts.timeoutMs ?? VEO_TIMEOUT_MS,
4005
+ intervalMs: opts.intervalMs ?? 1e4,
4006
+ maxIntervalMs: 2e4,
4007
+ sleep: opts.sleep,
4008
+ onPoll: (op, attempt) => {
4009
+ if (!op.done) logger.debug(`Still generating... (poll ${attempt})`);
4010
+ },
4011
+ resumeHint: `Operation (still running server-side): ${operation.name}`
4012
+ });
4013
+ return checkVeoResult(finalOp);
4014
+ }
4015
+ function checkVeoResult(op) {
4016
+ if (op.error) throw new ProviderError(`Veo error: ${JSON.stringify(op.error)}`, "gemini");
4017
+ return op;
4018
+ }
4019
+ async function downloadVeoVideo(apiKey, uri, dest) {
4020
+ let host = "";
4021
+ try {
4022
+ host = new URL(uri).hostname;
4023
+ } catch {
4024
+ throw new ProviderError("Veo returned an invalid video URI", "gemini");
4025
+ }
4026
+ await downloadFile(uri, dest, 3e5, host === API_HOST ? headers(apiKey) : void 0);
4027
+ }
4028
+ async function runVeo(model, request, dest, logger) {
4029
+ const body = buildVeoRequest(request);
4030
+ const { apiKey, operation } = await withGeminiKey(
4031
+ async (key) => ({ apiKey: key, operation: await submitVeo(key, model, body) }),
4032
+ { logger }
4033
+ );
4034
+ logger.debug(`Operation: ${operation.name}`);
4035
+ const finalOp = await waitForVeo(apiKey, operation, logger);
4036
+ const uri = extractVeoVideoUri(finalOp);
4037
+ if (!uri) {
4038
+ throw new ProviderError(
4039
+ `Unexpected Veo response structure: ${JSON.stringify(finalOp.response)}`,
4040
+ "gemini"
4041
+ );
4042
+ }
4043
+ await downloadVeoVideo(apiKey, uri, dest);
4044
+ return finalOp;
4045
+ }
4046
+
4047
+ // src/providers/gemini/commands/generate-video.ts
3821
4048
  function registerGenerateVideoCommand(parent) {
3822
4049
  parent.command("generate-video").description("Generate video with Veo via Gemini API [EXPERIMENTAL]").requiredOption("--prompt <text>", "Video generation prompt").option("--model <id>", "Veo model to use").option("--resolution <res>", "Video resolution: 720p|1080p", "1080p").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option(
3823
4050
  "--reference-images <paths...>",
3824
4051
  "Reference images (first=opening frame, second=closing)"
3825
- ).option("--upload-timeout <ms>", "Upload timeout in ms", "120000").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
4052
+ ).option("--upload-timeout <ms>", "Deprecated and ignored (images are sent inline)").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
3826
4053
  async (opts) => {
3827
4054
  const logger = createLogger({ verbose: opts.verbose ?? false });
3828
- const apiKey = resolveKey("GEMINI_API_KEY");
3829
- if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
4055
+ if (opts.uploadTimeout !== void 0) {
4056
+ logger.warn("--upload-timeout is deprecated and ignored (images are sent inline)");
4057
+ }
3830
4058
  const model = opts.model ?? getDefaultModel("generate-video");
3831
4059
  if (!model.startsWith("veo-")) {
3832
4060
  throw new ValidationError(
@@ -3841,72 +4069,23 @@ function registerGenerateVideoCommand(parent) {
3841
4069
  }
3842
4070
  logger.debug(`Submitting video generation with ${model}...`);
3843
4071
  logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
3844
- const config = {
3845
- aspectRatio: opts.aspectRatio,
3846
- resolution: opts.resolution
3847
- };
3848
- const contentParts = [{ text: opts.prompt }];
3849
- if (opts.referenceImages?.length) {
3850
- for (const imgPath of opts.referenceImages.slice(0, 2)) {
3851
- if (!fs23.existsSync(imgPath))
3852
- throw new ValidationError(`Reference image not found: ${imgPath}`);
3853
- const data = fs23.readFileSync(imgPath).toString("base64");
3854
- const ext = path25.extname(imgPath).toLowerCase();
3855
- const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
3856
- contentParts.push({ inlineData: { mimeType, data } });
3857
- }
3858
- }
3859
- const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
3860
- let operation;
3861
- try {
3862
- operation = await httpJson({
3863
- url: `${BASE2}/v1beta/models/${model}:generateVideos`,
3864
- method: "POST",
3865
- headers,
3866
- body: {
3867
- contents: [{ parts: contentParts }],
3868
- generationConfig: config
3869
- }
3870
- });
3871
- } catch (e) {
3872
- const msg = e instanceof Error ? e.message : String(e);
3873
- throw new ProviderError(`Video generation submit failed: ${msg}`, "gemini");
3874
- }
3875
- const operationName = operation.name;
3876
- logger.debug(`Operation: ${operationName}`);
3877
- const deadline = Date.now() + 10 * 60 * 1e3;
3878
- let done = false;
3879
- let finalOp = operation;
3880
- while (!done && Date.now() < deadline) {
3881
- await sleep3(1e4);
3882
- finalOp = await httpJson({
3883
- url: `${BASE2}/v1beta/${operationName}`,
3884
- headers
3885
- });
3886
- done = finalOp.done === true;
3887
- if (!done)
3888
- logger.debug(
3889
- `Still generating... (${Math.round((deadline - Date.now()) / 1e3)}s left)`
3890
- );
3891
- }
3892
- if (!done) throw new ProviderError("Video generation timed out after 10 minutes", "gemini");
3893
- if (finalOp.error)
3894
- throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
3895
- const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
3896
- if (!videoUri) {
3897
- throw new ProviderError(
3898
- `Unexpected Veo response structure: ${JSON.stringify(finalOp.response)}`,
3899
- "gemini"
3900
- );
3901
- }
3902
- const outDir = getOutputDir();
3903
- const dest = path25.join(outDir, `veo_generated_${Date.now()}.mp4`);
3904
- const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
3905
- await downloadFile2(videoUri, dest);
4072
+ const frames = veoFramesFromPaths(opts.referenceImages);
4073
+ const dest = path25.join(getOutputDir(), `veo_generated_${Date.now()}.mp4`);
4074
+ const finalOp = await runVeo(
4075
+ model,
4076
+ {
4077
+ prompt: opts.prompt,
4078
+ ...frames,
4079
+ aspectRatio: opts.aspectRatio,
4080
+ resolution: opts.resolution
4081
+ },
4082
+ dest,
4083
+ logger
4084
+ );
3906
4085
  logger.success(`Saved video: ${dest}`);
3907
4086
  if (opts.output) {
3908
- fs23.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
3909
- fs23.copyFileSync(dest, opts.output);
4087
+ fs24.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
4088
+ fs24.copyFileSync(dest, opts.output);
3910
4089
  logger.success(`Copied to: ${opts.output}`);
3911
4090
  }
3912
4091
  await maybeDownloadThumb(finalOp.response, dest, {
@@ -3919,13 +4098,10 @@ Generated video: ${dest}`);
3919
4098
  }
3920
4099
  );
3921
4100
  }
3922
- function sleep3(ms) {
3923
- return new Promise((r) => setTimeout(r, ms));
3924
- }
3925
4101
 
3926
4102
  // src/providers/gemini/commands/generate.ts
3927
4103
  init_errors();
3928
- import fs24 from "fs";
4104
+ import fs25 from "fs";
3929
4105
  import path26 from "path";
3930
4106
  function registerGenerateCommand(parent) {
3931
4107
  parent.command("generate").description("Generate images with Gemini (Nano Banana / Imagen 4)").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "Gemini image model to use").option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--num-images <n>", "Number of images to generate (1-4)", "1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Copy first generated image to this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
@@ -3952,11 +4128,14 @@ function registerGenerateCommand(parent) {
3952
4128
  const savedFiles = [];
3953
4129
  for (let i = 0; i < numImages; i++) {
3954
4130
  try {
3955
- const resp = await generateContent({
3956
- model,
3957
- contents: [{ parts: [{ text: opts.prompt }] }],
3958
- generationConfig
3959
- });
4131
+ const resp = await generateContent(
4132
+ {
4133
+ model,
4134
+ contents: [{ parts: [{ text: opts.prompt }] }],
4135
+ generationConfig
4136
+ },
4137
+ { logger }
4138
+ );
3960
4139
  const images = extractImages(resp);
3961
4140
  if (images.length === 0) {
3962
4141
  logger.warn(
@@ -3969,7 +4148,7 @@ function registerGenerateCommand(parent) {
3969
4148
  const ext = img.mimeType.includes("png") ? "png" : "jpg";
3970
4149
  const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
3971
4150
  const dest = path26.join(outDir, fname);
3972
- fs24.writeFileSync(dest, Buffer.from(img.data, "base64"));
4151
+ fs25.writeFileSync(dest, Buffer.from(img.data, "base64"));
3973
4152
  savedFiles.push(dest);
3974
4153
  logger.success(`Saved: ${dest}`);
3975
4154
  }
@@ -3984,8 +4163,8 @@ function registerGenerateCommand(parent) {
3984
4163
  process.exit(1);
3985
4164
  }
3986
4165
  if (opts.output && savedFiles[0]) {
3987
- fs24.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
3988
- fs24.copyFileSync(savedFiles[0], opts.output);
4166
+ fs25.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
4167
+ fs25.copyFileSync(savedFiles[0], opts.output);
3989
4168
  logger.success(`Copied to: ${opts.output}`);
3990
4169
  }
3991
4170
  const finalImages = finalizeGeneratedImages(savedFiles, { ...opts, logger });
@@ -3997,7 +4176,7 @@ Generated ${finalImages.length} image(s):`);
3997
4176
  }
3998
4177
 
3999
4178
  // src/providers/gemini/commands/image-to-image.ts
4000
- import fs25 from "fs";
4179
+ import fs26 from "fs";
4001
4180
  import path27 from "path";
4002
4181
  var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
4003
4182
  var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
@@ -4044,7 +4223,7 @@ function registerGeminiImageToImageCommand(parent) {
4044
4223
  contents: [{ parts }],
4045
4224
  generationConfig: { responseModalities: ["IMAGE"] }
4046
4225
  };
4047
- const resp = await generateContent(req);
4226
+ const resp = await generateContent(req, { logger });
4048
4227
  const images = extractImages(resp);
4049
4228
  if (images.length === 0) {
4050
4229
  logger.error(
@@ -4060,13 +4239,13 @@ function registerGeminiImageToImageCommand(parent) {
4060
4239
  if (!img) continue;
4061
4240
  const ext = img.mimeType.includes("png") ? "png" : "jpg";
4062
4241
  const dest = path27.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
4063
- fs25.writeFileSync(dest, Buffer.from(img.data, "base64"));
4242
+ fs26.writeFileSync(dest, Buffer.from(img.data, "base64"));
4064
4243
  saved.push(dest);
4065
4244
  logger.success(`Saved: ${dest}`);
4066
4245
  }
4067
4246
  if (opts.output && saved[0]) {
4068
- fs25.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
4069
- fs25.copyFileSync(saved[0], opts.output);
4247
+ fs26.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
4248
+ fs26.copyFileSync(saved[0], opts.output);
4070
4249
  logger.success(`Copied to: ${opts.output}`);
4071
4250
  }
4072
4251
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -4102,29 +4281,13 @@ function sniffImageMime(buf, url) {
4102
4281
  }
4103
4282
 
4104
4283
  // src/providers/gemini/commands/image-to-video.ts
4105
- import fs26 from "fs";
4106
- import path28 from "path";
4107
4284
  init_errors();
4108
- init_http_client();
4109
- var BASE3 = "https://generativelanguage.googleapis.com";
4110
- function imagePart(imgPath) {
4111
- if (!fs26.existsSync(imgPath)) {
4112
- throw new ValidationError(`Image not found: ${imgPath}`);
4113
- }
4114
- const data = fs26.readFileSync(imgPath).toString("base64");
4115
- const ext = path28.extname(imgPath).toLowerCase();
4116
- const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
4117
- return { inlineData: { mimeType, data } };
4118
- }
4119
- function sleep4(ms) {
4120
- return new Promise((r) => setTimeout(r, ms));
4121
- }
4285
+ import fs27 from "fs";
4286
+ import path28 from "path";
4122
4287
  function registerGeminiImageToVideoCommand(parent) {
4123
4288
  parent.command("image-to-video <imagePath>").alias("i2v").description("Generate video from a starting image with Veo (image-to-video) [EXPERIMENTAL]").requiredOption("--prompt <text>", "Motion/scene prompt").option("--last-frame <path>", "Optional closing frame image (Veo 3.1)").option("--model <id>", "Veo model id").option("--resolution <res>", "720p|1080p", "1080p").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
4124
4289
  async (imagePath, opts) => {
4125
4290
  const logger = createLogger({ verbose: opts.verbose ?? false });
4126
- const apiKey = resolveKey("GEMINI_API_KEY");
4127
- if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
4128
4291
  const model = opts.model ?? getDefaultModel("generate-video");
4129
4292
  if (!model.startsWith("veo-")) {
4130
4293
  throw new ValidationError(
@@ -4137,60 +4300,26 @@ function registerGeminiImageToVideoCommand(parent) {
4137
4300
  if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
4138
4301
  throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
4139
4302
  }
4140
- const contentParts = [
4141
- { text: opts.prompt },
4142
- imagePart(imagePath)
4143
- ];
4144
- if (opts.lastFrame) contentParts.push(imagePart(opts.lastFrame));
4145
- const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
4146
4303
  logger.info(
4147
4304
  "Submitting image-to-video. This may take 30s\u20136 minutes. Veo requires billing."
4148
4305
  );
4149
- let operation;
4150
- try {
4151
- operation = await httpJson({
4152
- url: `${BASE3}/v1beta/models/${model}:generateVideos`,
4153
- method: "POST",
4154
- headers,
4155
- body: {
4156
- contents: [{ parts: contentParts }],
4157
- generationConfig: { aspectRatio: opts.aspectRatio, resolution: opts.resolution }
4158
- }
4159
- });
4160
- } catch (e) {
4161
- throw new ProviderError(
4162
- `Submit failed: ${e instanceof Error ? e.message : String(e)}`,
4163
- "gemini"
4164
- );
4165
- }
4166
- const operationName = operation.name;
4167
- logger.debug(`Operation: ${operationName}`);
4168
- const deadline = Date.now() + 10 * 60 * 1e3;
4169
- let finalOp = operation;
4170
- while (!finalOp.done && Date.now() < deadline) {
4171
- await sleep4(1e4);
4172
- finalOp = await httpJson({ url: `${BASE3}/v1beta/${operationName}`, headers });
4173
- if (!finalOp.done) {
4174
- logger.debug(`Still generating... ${Math.round((deadline - Date.now()) / 1e3)}s left`);
4175
- }
4176
- }
4177
- if (!finalOp.done) throw new ProviderError("Veo timed out after 10 minutes", "gemini");
4178
- if (finalOp.error) {
4179
- throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
4180
- }
4181
- const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
4182
- if (!videoUri) {
4183
- throw new ProviderError(
4184
- `Unexpected Veo response: ${JSON.stringify(finalOp.response)}`,
4185
- "gemini"
4186
- );
4187
- }
4188
4306
  const dest = path28.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
4189
- await downloadFile(videoUri, dest);
4307
+ const finalOp = await runVeo(
4308
+ model,
4309
+ {
4310
+ prompt: opts.prompt,
4311
+ image: veoImageFromFile(imagePath),
4312
+ lastFrame: opts.lastFrame ? veoImageFromFile(opts.lastFrame) : void 0,
4313
+ aspectRatio: opts.aspectRatio,
4314
+ resolution: opts.resolution
4315
+ },
4316
+ dest,
4317
+ logger
4318
+ );
4190
4319
  logger.success(`Saved video: ${dest}`);
4191
4320
  if (opts.output) {
4192
- fs26.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
4193
- fs26.copyFileSync(dest, opts.output);
4321
+ fs27.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
4322
+ fs27.copyFileSync(dest, opts.output);
4194
4323
  logger.success(`Copied to: ${opts.output}`);
4195
4324
  }
4196
4325
  await maybeDownloadThumb(finalOp.response, dest, {
@@ -4206,36 +4335,44 @@ Generated video: ${dest}`);
4206
4335
 
4207
4336
  // src/providers/gemini/commands/transcribe.ts
4208
4337
  init_errors();
4209
- import fs27 from "fs";
4338
+ import fs28 from "fs";
4210
4339
  function registerTranscribeCommand2(parent) {
4211
4340
  parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption("--files <paths...>", "Audio/video file paths to transcribe").option("--prompt <text>", "Transcription prompt", "Generate a transcript with timestamps").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
4212
4341
  async (opts) => {
4213
4342
  const logger = createLogger({ verbose: opts.verbose ?? false });
4214
4343
  const model = opts.model ?? getDefaultModel("transcribe");
4215
4344
  for (const f of opts.files) {
4216
- if (!fs27.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
4345
+ if (!fs28.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
4217
4346
  }
4218
4347
  const results = [];
4219
4348
  for (let i = 0; i < opts.files.length; i++) {
4220
4349
  const filePath = opts.files[i];
4221
4350
  logger.debug(`[${i + 1}/${opts.files.length}] Transcribing: ${filePath}`);
4222
4351
  try {
4223
- const useApi = shouldUseFileApi(filePath);
4224
- let contentParts;
4225
- if (useApi) {
4226
- logger.debug("File >20MB \u2014 using Files API");
4227
- const ref = await uploadFile(filePath, { logger });
4228
- contentParts = [
4229
- { text: opts.prompt },
4230
- { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
4231
- ];
4232
- } else {
4233
- contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
4234
- }
4235
- const resp = await generateContent({
4236
- model,
4237
- contents: [{ parts: contentParts }]
4238
- });
4352
+ const resp = await withGeminiKey(
4353
+ async (apiKey) => {
4354
+ const useApi = shouldUseFileApi(filePath);
4355
+ let contentParts;
4356
+ if (useApi) {
4357
+ logger.debug("File >20MB \u2014 using Files API");
4358
+ const ref = await uploadFile(filePath, { logger, apiKey });
4359
+ contentParts = [
4360
+ { text: opts.prompt },
4361
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
4362
+ ];
4363
+ } else {
4364
+ contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
4365
+ }
4366
+ return generateContent(
4367
+ {
4368
+ model,
4369
+ contents: [{ parts: contentParts }]
4370
+ },
4371
+ { apiKey }
4372
+ );
4373
+ },
4374
+ { logger }
4375
+ );
4239
4376
  results.push({
4240
4377
  file: filePath,
4241
4378
  status: "success",
@@ -4270,13 +4407,11 @@ function registerGeminiCommands(program2) {
4270
4407
  }
4271
4408
 
4272
4409
  // src/providers/leonardo/commands/generate.ts
4273
- init_http_client();
4274
- import fs28 from "fs";
4410
+ import fs29 from "fs";
4275
4411
  import path29 from "path";
4276
4412
 
4277
4413
  // src/providers/leonardo/client.ts
4278
4414
  init_errors();
4279
- init_http_client();
4280
4415
 
4281
4416
  // src/providers/leonardo/models.ts
4282
4417
  var DEFAULT_LEONARDO_BASE_URL = "https://cloud.leonardo.ai/api/rest/v1";
@@ -4483,8 +4618,8 @@ function registerLeonardoGenerateCommand(parent) {
4483
4618
  }
4484
4619
  if (opts.output && saved[0]) {
4485
4620
  const dest = path29.resolve(opts.output);
4486
- fs28.mkdirSync(path29.dirname(dest), { recursive: true });
4487
- fs28.copyFileSync(saved[0], dest);
4621
+ fs29.mkdirSync(path29.dirname(dest), { recursive: true });
4622
+ fs29.copyFileSync(saved[0], dest);
4488
4623
  logger.success(`Copied to ${dest}`);
4489
4624
  }
4490
4625
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -4496,8 +4631,7 @@ Generated ${finalImages.length} image(s):`);
4496
4631
  }
4497
4632
 
4498
4633
  // src/providers/leonardo/commands/image-to-image.ts
4499
- init_http_client();
4500
- import fs29 from "fs";
4634
+ import fs30 from "fs";
4501
4635
  import path30 from "path";
4502
4636
  function extFromUrl3(url, fallback = ".png") {
4503
4637
  try {
@@ -4574,8 +4708,8 @@ function registerLeonardoImageToImageCommand(parent) {
4574
4708
  }
4575
4709
  if (opts.output && saved[0]) {
4576
4710
  const dest = path30.resolve(opts.output);
4577
- fs29.mkdirSync(path30.dirname(dest), { recursive: true });
4578
- fs29.copyFileSync(saved[0], dest);
4711
+ fs30.mkdirSync(path30.dirname(dest), { recursive: true });
4712
+ fs30.copyFileSync(saved[0], dest);
4579
4713
  logger.success(`Copied to ${dest}`);
4580
4714
  }
4581
4715
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -4587,7 +4721,6 @@ Generated ${finalImages.length} image(s):`);
4587
4721
  }
4588
4722
 
4589
4723
  // src/providers/leonardo/commands/video.ts
4590
- init_http_client();
4591
4724
  import path31 from "path";
4592
4725
  function v2DimsFor(resolution) {
4593
4726
  if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
@@ -4805,7 +4938,6 @@ function registerLeonardoModelsCommand(parent) {
4805
4938
  }
4806
4939
 
4807
4940
  // src/providers/leonardo/commands/status.ts
4808
- init_http_client();
4809
4941
  import path32 from "path";
4810
4942
  var VIDEO_KEYS = ["motionMP4URL", "url"];
4811
4943
  var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
@@ -4963,7 +5095,6 @@ init_errors();
4963
5095
 
4964
5096
  // src/providers/minimax/client.ts
4965
5097
  init_errors();
4966
- init_http_client();
4967
5098
  var MINIMAX_BASE_URL = "https://api.minimax.io/v1";
4968
5099
  function requireMinimaxKey() {
4969
5100
  const key = resolveKey("MINIMAX_API_KEY");
@@ -5018,7 +5149,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
5018
5149
  if (status === "Failed" || status === "Error") {
5019
5150
  throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
5020
5151
  }
5021
- await sleep5(intervalMs);
5152
+ await sleep3(intervalMs);
5022
5153
  elapsed += intervalMs;
5023
5154
  }
5024
5155
  throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
@@ -5037,13 +5168,12 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
5037
5168
  await downloadFile(downloadUrl, dest);
5038
5169
  return dest;
5039
5170
  }
5040
- function sleep5(ms) {
5171
+ function sleep3(ms) {
5041
5172
  return new Promise((r) => setTimeout(r, ms));
5042
5173
  }
5043
5174
 
5044
5175
  // src/providers/minimax/generators/music.ts
5045
- init_http_client();
5046
- import fs30 from "fs";
5176
+ import fs31 from "fs";
5047
5177
  import path33 from "path";
5048
5178
  async function generateMinimaxMusic(opts) {
5049
5179
  const {
@@ -5093,10 +5223,10 @@ async function generateMinimaxMusic(opts) {
5093
5223
  try {
5094
5224
  if (audioData.startsWith("http")) {
5095
5225
  const bytes = await fetchBytes(audioData);
5096
- fs30.writeFileSync(dest, bytes);
5226
+ fs31.writeFileSync(dest, bytes);
5097
5227
  } else {
5098
5228
  const audioBytes = Buffer.from(audioData, "hex");
5099
- fs30.writeFileSync(dest, audioBytes);
5229
+ fs31.writeFileSync(dest, audioBytes);
5100
5230
  }
5101
5231
  } catch (e) {
5102
5232
  return {
@@ -5107,8 +5237,8 @@ async function generateMinimaxMusic(opts) {
5107
5237
  const durSec = durationMs / 1e3;
5108
5238
  logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
5109
5239
  if (output) {
5110
- fs30.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
5111
- fs30.copyFileSync(dest, output);
5240
+ fs31.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
5241
+ fs31.copyFileSync(dest, output);
5112
5242
  }
5113
5243
  return { status: "success", generatedAudio: dest, durationMs, model };
5114
5244
  }
@@ -5182,7 +5312,7 @@ Generated music: ${result.generatedAudio}`);
5182
5312
  init_errors();
5183
5313
 
5184
5314
  // src/providers/minimax/generators/speech.ts
5185
- import fs31 from "fs";
5315
+ import fs32 from "fs";
5186
5316
  import path34 from "path";
5187
5317
  async function generateMinimaxSpeech(opts) {
5188
5318
  const {
@@ -5231,11 +5361,11 @@ async function generateMinimaxSpeech(opts) {
5231
5361
  const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
5232
5362
  const outDir = getOutputDir();
5233
5363
  const dest = path34.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
5234
- fs31.writeFileSync(dest, audioBytes);
5364
+ fs32.writeFileSync(dest, audioBytes);
5235
5365
  logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
5236
5366
  if (output) {
5237
- fs31.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
5238
- fs31.copyFileSync(dest, output);
5367
+ fs32.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
5368
+ fs32.copyFileSync(dest, output);
5239
5369
  }
5240
5370
  return { status: "success", generatedAudio: dest, model };
5241
5371
  }
@@ -5282,7 +5412,7 @@ Generated audio: ${result.generatedAudio}`);
5282
5412
  init_errors();
5283
5413
 
5284
5414
  // src/providers/minimax/generators/video.ts
5285
- import fs32 from "fs";
5415
+ import fs33 from "fs";
5286
5416
  import path35 from "path";
5287
5417
  async function generateMinimaxVideo(opts) {
5288
5418
  const {
@@ -5336,11 +5466,11 @@ async function generateMinimaxVideo(opts) {
5336
5466
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
5337
5467
  }
5338
5468
  const elapsed = (Date.now() - start) / 1e3;
5339
- const stat = fs32.statSync(dest);
5469
+ const stat = fs33.statSync(dest);
5340
5470
  const fileSizeMb = stat.size / (1024 * 1024);
5341
5471
  if (output) {
5342
- fs32.mkdirSync(path35.dirname(path35.resolve(output)), { recursive: true });
5343
- fs32.copyFileSync(dest, output);
5472
+ fs33.mkdirSync(path35.dirname(path35.resolve(output)), { recursive: true });
5473
+ fs33.copyFileSync(dest, output);
5344
5474
  }
5345
5475
  await maybeDownloadThumb(pollResult, dest, {
5346
5476
  skip: thumb === false,
@@ -5402,8 +5532,7 @@ Generated video: ${result.generatedVideo}`);
5402
5532
 
5403
5533
  // src/providers/minimax/generators/image.ts
5404
5534
  init_errors();
5405
- init_http_client();
5406
- import fs33 from "fs";
5535
+ import fs34 from "fs";
5407
5536
  import path36 from "path";
5408
5537
  async function generateMinimaxImage(opts) {
5409
5538
  const {
@@ -5443,7 +5572,7 @@ async function generateMinimaxImage(opts) {
5443
5572
  const bytes = await fetchBytes(url);
5444
5573
  const fname = `minimax_image_${Date.now()}_${i}.png`;
5445
5574
  const dest = path36.join(outDir, fname);
5446
- fs33.writeFileSync(dest, bytes);
5575
+ fs34.writeFileSync(dest, bytes);
5447
5576
  savedFiles.push(dest);
5448
5577
  logger?.success(`Saved: ${dest}`);
5449
5578
  } catch (e) {
@@ -5454,8 +5583,8 @@ async function generateMinimaxImage(opts) {
5454
5583
  }
5455
5584
  }
5456
5585
  if (output && savedFiles[0]) {
5457
- fs33.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
5458
- fs33.copyFileSync(savedFiles[0], output);
5586
+ fs34.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
5587
+ fs34.copyFileSync(savedFiles[0], output);
5459
5588
  }
5460
5589
  return { status: "success", generatedImages: savedFiles, model };
5461
5590
  }
@@ -5494,8 +5623,7 @@ Generated ${finalImages.length} image(s):`);
5494
5623
 
5495
5624
  // src/providers/minimax/commands/image-to-image.ts
5496
5625
  init_errors();
5497
- init_http_client();
5498
- import fs34 from "fs";
5626
+ import fs35 from "fs";
5499
5627
  import path37 from "path";
5500
5628
  function registerMinimaxImageToImageCommand(parent) {
5501
5629
  parent.command("image-to-image").alias("i2i").description(
@@ -5552,7 +5680,7 @@ function registerMinimaxImageToImageCommand(parent) {
5552
5680
  try {
5553
5681
  const bytes = await fetchBytes(url);
5554
5682
  const dest = path37.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
5555
- fs34.writeFileSync(dest, bytes);
5683
+ fs35.writeFileSync(dest, bytes);
5556
5684
  saved.push(dest);
5557
5685
  logger.success(`Saved: ${dest}`);
5558
5686
  } catch (e) {
@@ -5563,8 +5691,8 @@ function registerMinimaxImageToImageCommand(parent) {
5563
5691
  }
5564
5692
  }
5565
5693
  if (opts.output && saved[0]) {
5566
- fs34.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
5567
- fs34.copyFileSync(saved[0], opts.output);
5694
+ fs35.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
5695
+ fs35.copyFileSync(saved[0], opts.output);
5568
5696
  logger.success(`Copied to: ${opts.output}`);
5569
5697
  }
5570
5698
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -5698,15 +5826,13 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
5698
5826
  }
5699
5827
 
5700
5828
  // src/providers/openai/openai-audio.ts
5701
- init_http_client();
5702
- import fs36 from "fs";
5829
+ import fs37 from "fs";
5703
5830
  import path39 from "path";
5704
5831
 
5705
5832
  // src/providers/openai/client.ts
5706
- import fs35 from "fs";
5833
+ import fs36 from "fs";
5707
5834
  import path38 from "path";
5708
5835
  init_errors();
5709
- init_http_client();
5710
5836
  var DEFAULT_TIMEOUT_MS3 = 24e4;
5711
5837
  function requireOpenAIKey() {
5712
5838
  const key = resolveKey("OPENAI_API_KEY");
@@ -5772,8 +5898,8 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
5772
5898
  }
5773
5899
  function fileFromPath(filePath) {
5774
5900
  const abs = path38.resolve(filePath);
5775
- if (!fs35.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
5776
- const bytes = fs35.readFileSync(abs);
5901
+ if (!fs36.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
5902
+ const bytes = fs36.readFileSync(abs);
5777
5903
  return new File([new Uint8Array(bytes)], path38.basename(abs));
5778
5904
  }
5779
5905
 
@@ -5833,11 +5959,11 @@ async function generateOpenAISpeech(opts) {
5833
5959
  const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
5834
5960
  const ext = extFromAudioFormat(opts.outputFormat);
5835
5961
  const dest = path39.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
5836
- fs36.writeFileSync(dest, bytes);
5962
+ fs37.writeFileSync(dest, bytes);
5837
5963
  opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
5838
5964
  if (opts.output) {
5839
- fs36.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
5840
- fs36.copyFileSync(dest, opts.output);
5965
+ fs37.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
5966
+ fs37.copyFileSync(dest, opts.output);
5841
5967
  }
5842
5968
  return dest;
5843
5969
  }
@@ -5860,8 +5986,8 @@ async function transcribeOpenAIAudio(opts) {
5860
5986
  const text = typeof data.text === "string" ? data.text : void 0;
5861
5987
  const ext = opts.format === "text" ? "txt" : "json";
5862
5988
  const dest = opts.output ? path39.resolve(opts.output) : path39.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
5863
- fs36.mkdirSync(path39.dirname(dest), { recursive: true });
5864
- fs36.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
5989
+ fs37.mkdirSync(path39.dirname(dest), { recursive: true });
5990
+ fs37.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
5865
5991
  opts.logger?.success(`Saved transcript: ${dest}`);
5866
5992
  return { path: dest, text };
5867
5993
  }
@@ -5888,7 +6014,7 @@ Generated audio: ${dest}`);
5888
6014
  }
5889
6015
 
5890
6016
  // src/providers/openai/codex-image-driver.ts
5891
- import fs37 from "fs";
6017
+ import fs38 from "fs";
5892
6018
  import os2 from "os";
5893
6019
  import path40 from "path";
5894
6020
  import { execa } from "execa";
@@ -5940,11 +6066,11 @@ async function runCodexImageGeneration(opts) {
5940
6066
  const runner = opts.runner ?? defaultRunner;
5941
6067
  const ext = extFromImageFormat(opts.outputFormat);
5942
6068
  const dest = opts.output ? path40.resolve(opts.output) : path40.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
5943
- fs37.mkdirSync(path40.dirname(dest), { recursive: true });
5944
- const tmp = fs37.mkdtempSync(path40.join(os2.tmpdir(), "multix-codex-image-"));
6069
+ fs38.mkdirSync(path40.dirname(dest), { recursive: true });
6070
+ const tmp = fs38.mkdtempSync(path40.join(os2.tmpdir(), "multix-codex-image-"));
5945
6071
  const schema = path40.join(tmp, "schema.json");
5946
6072
  const result = path40.join(tmp, "result.json");
5947
- fs37.writeFileSync(
6073
+ fs38.writeFileSync(
5948
6074
  schema,
5949
6075
  JSON.stringify({
5950
6076
  type: "object",
@@ -5974,7 +6100,7 @@ async function runCodexImageGeneration(opts) {
5974
6100
  if (res.exitCode !== 0) {
5975
6101
  throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
5976
6102
  }
5977
- if (!fs37.existsSync(dest) || fs37.statSync(dest).size === 0) {
6103
+ if (!fs38.existsSync(dest) || fs38.statSync(dest).size === 0) {
5978
6104
  throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
5979
6105
  }
5980
6106
  return { path: dest };
@@ -5991,8 +6117,7 @@ function buildCodexPrompt(userPrompt, outputPath) {
5991
6117
 
5992
6118
  // src/providers/openai/openai-image.ts
5993
6119
  init_errors();
5994
- init_http_client();
5995
- import fs38 from "fs";
6120
+ import fs39 from "fs";
5996
6121
  import path41 from "path";
5997
6122
  function buildImageGenerationBody(opts) {
5998
6123
  return {
@@ -6027,13 +6152,13 @@ function saveOpenAIImages(opts) {
6027
6152
  const b64 = items[i]?.b64_json;
6028
6153
  if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
6029
6154
  const dest = path41.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
6030
- fs38.writeFileSync(dest, Buffer.from(b64, "base64"));
6155
+ fs39.writeFileSync(dest, Buffer.from(b64, "base64"));
6031
6156
  saved.push(dest);
6032
6157
  opts.logger?.success(`Saved: ${dest}`);
6033
6158
  }
6034
6159
  if (opts.output && saved[0]) {
6035
- fs38.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
6036
- fs38.copyFileSync(saved[0], opts.output);
6160
+ fs39.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
6161
+ fs39.copyFileSync(saved[0], opts.output);
6037
6162
  opts.logger?.success(`Copied to: ${opts.output}`);
6038
6163
  }
6039
6164
  return saved;
@@ -6231,10 +6356,9 @@ function registerOpenAICommands(program2) {
6231
6356
  }
6232
6357
 
6233
6358
  // src/providers/openrouter/client.ts
6234
- import fs39 from "fs";
6359
+ import fs40 from "fs";
6235
6360
  import path42 from "path";
6236
6361
  init_errors();
6237
- init_http_client();
6238
6362
 
6239
6363
  // src/providers/openrouter/payload.ts
6240
6364
  var IMAGE_ONLY_MODEL_PREFIXES = [
@@ -6247,15 +6371,15 @@ function resolveModalities(model) {
6247
6371
  return IMAGE_ONLY_MODEL_PREFIXES.some((p) => lower.startsWith(p)) ? ["image"] : ["image", "text"];
6248
6372
  }
6249
6373
  function buildOpenRouterHeaders(apiKey) {
6250
- const headers = {
6374
+ const headers2 = {
6251
6375
  Authorization: `Bearer ${apiKey}`,
6252
6376
  "Content-Type": "application/json"
6253
6377
  };
6254
6378
  const referer = resolveKey("OPENROUTER_SITE_URL");
6255
6379
  const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
6256
- if (referer) headers["HTTP-Referer"] = referer;
6257
- if (title) headers["X-Title"] = title;
6258
- return headers;
6380
+ if (referer) headers2["HTTP-Referer"] = referer;
6381
+ if (title) headers2["X-Title"] = title;
6382
+ return headers2;
6259
6383
  }
6260
6384
  function extractImagesFromResponse(data) {
6261
6385
  const out = { urls: [] };
@@ -6398,7 +6522,7 @@ async function generateOpenRouterImage(opts) {
6398
6522
  try {
6399
6523
  const bytes = await fetchBytes(url);
6400
6524
  const dest = path42.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
6401
- fs39.writeFileSync(dest, bytes);
6525
+ fs40.writeFileSync(dest, bytes);
6402
6526
  savedFiles.push(dest);
6403
6527
  logger?.success(`Saved: ${dest}`);
6404
6528
  } catch (e) {
@@ -6409,8 +6533,8 @@ async function generateOpenRouterImage(opts) {
6409
6533
  }
6410
6534
  }
6411
6535
  if (output && savedFiles[0]) {
6412
- fs39.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
6413
- fs39.writeFileSync(output, fs39.readFileSync(savedFiles[0]));
6536
+ fs40.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
6537
+ fs40.writeFileSync(output, fs40.readFileSync(savedFiles[0]));
6414
6538
  }
6415
6539
  return { status: "success", generatedImages: savedFiles, model: usedModel };
6416
6540
  }
@@ -6451,10 +6575,9 @@ Generated ${finalImages.length} image(s):`);
6451
6575
  }
6452
6576
 
6453
6577
  // src/providers/openrouter/commands/image-to-image.ts
6454
- import fs40 from "fs";
6578
+ import fs41 from "fs";
6455
6579
  import path43 from "path";
6456
6580
  init_errors();
6457
- init_http_client();
6458
6581
  var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
6459
6582
  var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
6460
6583
  var HARD_LIMIT_BYTES2 = 8 * 1024 * 1024;
@@ -6519,7 +6642,7 @@ function parseStrength(raw) {
6519
6642
  }
6520
6643
  async function runImageToImage(opts) {
6521
6644
  const apiKey = requireOpenRouterKey();
6522
- const headers = buildOpenRouterHeaders(apiKey);
6645
+ const headers2 = buildOpenRouterHeaders(apiKey);
6523
6646
  const payload = buildI2IPayload({
6524
6647
  prompt: opts.prompt,
6525
6648
  model: opts.model,
@@ -6530,7 +6653,7 @@ async function runImageToImage(opts) {
6530
6653
  const data = await httpJson({
6531
6654
  url: OPENROUTER_API_URL,
6532
6655
  method: "POST",
6533
- headers,
6656
+ headers: headers2,
6534
6657
  body: payload,
6535
6658
  timeoutMs: 24e4
6536
6659
  });
@@ -6546,31 +6669,30 @@ async function runImageToImage(opts) {
6546
6669
  if (!url) continue;
6547
6670
  const bytes = await fetchBytes(url);
6548
6671
  const dest = path43.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
6549
- fs40.writeFileSync(dest, bytes);
6672
+ fs41.writeFileSync(dest, bytes);
6550
6673
  saved.push(dest);
6551
6674
  opts.logger.success(`Saved: ${dest}`);
6552
6675
  }
6553
6676
  if (opts.output && saved[0]) {
6554
- fs40.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
6555
- fs40.copyFileSync(saved[0], opts.output);
6677
+ fs41.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
6678
+ fs41.copyFileSync(saved[0], opts.output);
6556
6679
  opts.logger.success(`Copied to: ${opts.output}`);
6557
6680
  }
6558
6681
  return saved;
6559
6682
  }
6560
6683
 
6561
6684
  // src/providers/openrouter/video-client.ts
6562
- init_http_client();
6563
6685
  var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
6564
6686
  function authHeaders5(apiKey) {
6565
- const headers = {
6687
+ const headers2 = {
6566
6688
  Authorization: `Bearer ${apiKey}`,
6567
6689
  "Content-Type": "application/json"
6568
6690
  };
6569
6691
  const referer = resolveKey("OPENROUTER_SITE_URL");
6570
6692
  const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
6571
- if (referer) headers["HTTP-Referer"] = referer;
6572
- if (title) headers["X-Title"] = title;
6573
- return headers;
6693
+ if (referer) headers2["HTTP-Referer"] = referer;
6694
+ if (title) headers2["X-Title"] = title;
6695
+ return headers2;
6574
6696
  }
6575
6697
  async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
6576
6698
  return httpJson({
@@ -6598,7 +6720,7 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
6598
6720
  }
6599
6721
 
6600
6722
  // src/providers/openrouter/commands/video-status.ts
6601
- import fs41 from "fs";
6723
+ import fs42 from "fs";
6602
6724
  import path44 from "path";
6603
6725
  async function downloadOpenRouterVideo(opts) {
6604
6726
  const { jobId, logger, source } = opts;
@@ -6614,14 +6736,14 @@ async function downloadOpenRouterVideo(opts) {
6614
6736
  logger.error("Empty response body");
6615
6737
  process.exit(1);
6616
6738
  }
6617
- fs41.mkdirSync(path44.dirname(dest), { recursive: true });
6618
- const ws = fs41.createWriteStream(dest);
6739
+ fs42.mkdirSync(path44.dirname(dest), { recursive: true });
6740
+ const ws = fs42.createWriteStream(dest);
6619
6741
  const { pipeline: pipeline2 } = await import("stream/promises");
6620
6742
  await pipeline2(r.body, ws);
6621
6743
  logger.success(`Saved ${dest}`);
6622
6744
  if (opts.output) {
6623
- fs41.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
6624
- fs41.copyFileSync(dest, opts.output);
6745
+ fs42.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
6746
+ fs42.copyFileSync(dest, opts.output);
6625
6747
  logger.success(`Copied to ${opts.output}`);
6626
6748
  }
6627
6749
  await maybeDownloadThumb(source, dest, {
@@ -6807,7 +6929,6 @@ async function checkBinary(binary, versionFlag = "--version") {
6807
6929
 
6808
6930
  // src/commands/check-helpers/gemini-ping.ts
6809
6931
  init_errors();
6810
- init_http_client();
6811
6932
  var MODELS_URL = "https://generativelanguage.googleapis.com/v1beta/models";
6812
6933
  async function pingGemini(apiKey) {
6813
6934
  try {
@@ -6912,6 +7033,10 @@ var PROVIDERS = [
6912
7033
  }
6913
7034
  ];
6914
7035
  function resolveProviderKey(p) {
7036
+ if (p.envPrimary === "GEMINI_API_KEY") {
7037
+ const first = listGeminiKeys()[0];
7038
+ return first ? { key: first.value, envUsed: first.name } : {};
7039
+ }
6915
7040
  const primary = resolveKey(p.envPrimary);
6916
7041
  if (primary) return { key: primary, envUsed: p.envPrimary };
6917
7042
  if (p.envFallback) {
@@ -6963,6 +7088,14 @@ function registerCheckCommand(program2) {
6963
7088
  const envLabel = r.envUsed ?? p.envPrimary;
6964
7089
  if (r.key) {
6965
7090
  logger.success(`${envLabel} found \u2014 ${redact(r.key)}`);
7091
+ if (p.name === "Gemini") {
7092
+ const pool = listGeminiKeys();
7093
+ if (pool.length > 1) {
7094
+ logger.info(
7095
+ `Gemini key rotation: ${pool.length} keys (${pool.map((k) => k.name).join(", ")})`
7096
+ );
7097
+ }
7098
+ }
6966
7099
  } else if (p.optional) {
6967
7100
  const fbNote = p.envFallback ? ` (or ${p.envFallback})` : "";
6968
7101
  logger.info(`${p.envPrimary}${fbNote} not set (optional)`);
@@ -7008,14 +7141,18 @@ function registerCheckCommand(program2) {
7008
7141
  if (geminiKey) {
7009
7142
  logger.header("Gemini Connectivity");
7010
7143
  logger.info("Pinging Gemini models endpoint...");
7011
- const ping = await pingGemini(geminiKey);
7012
- if (ping.status === "success") {
7013
- logger.success(`Gemini API reachable \u2014 ${ping.modelCount ?? 0} models available`);
7014
- } else if (ping.status === "auth_error") {
7015
- logger.error(`Gemini auth failed \u2014 check GEMINI_API_KEY: ${ping.error ?? ""}`);
7016
- exitCode = 1;
7017
- } else {
7018
- logger.warn(`Gemini network error (API may be down): ${ping.error ?? ""}`);
7144
+ for (const key of listGeminiKeys()) {
7145
+ const ping = await pingGemini(key.value);
7146
+ if (ping.status === "success") {
7147
+ logger.success(
7148
+ `${key.name}: Gemini API reachable \u2014 ${ping.modelCount ?? 0} models available`
7149
+ );
7150
+ } else if (ping.status === "auth_error") {
7151
+ logger.error(`Gemini auth failed \u2014 check ${key.name}: ${ping.error ?? ""}`);
7152
+ exitCode = 1;
7153
+ } else {
7154
+ logger.warn(`${key.name}: Gemini network error (API may be down): ${ping.error ?? ""}`);
7155
+ }
7019
7156
  }
7020
7157
  }
7021
7158
  logger.header("Summary");
@@ -7040,7 +7177,7 @@ function registerCheckCommand(program2) {
7040
7177
 
7041
7178
  // src/commands/doc/convert.ts
7042
7179
  init_errors();
7043
- import fs42 from "fs";
7180
+ import fs43 from "fs";
7044
7181
  import path45 from "path";
7045
7182
 
7046
7183
  // src/commands/doc/default-prompt.ts
@@ -7062,7 +7199,7 @@ async function convertDocuments(opts) {
7062
7199
  const model = opts.model ?? DOC_MODEL_DEFAULT;
7063
7200
  const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
7064
7201
  for (const f of inputs) {
7065
- if (!fs42.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
7202
+ if (!fs43.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
7066
7203
  }
7067
7204
  let outputPath;
7068
7205
  if (output) {
@@ -7073,7 +7210,7 @@ async function convertDocuments(opts) {
7073
7210
  } else {
7074
7211
  outputPath = path45.join(getOutputDir(), "document-extraction.md");
7075
7212
  }
7076
- fs42.mkdirSync(path45.dirname(outputPath), { recursive: true });
7213
+ fs43.mkdirSync(path45.dirname(outputPath), { recursive: true });
7077
7214
  const results = [];
7078
7215
  for (let i = 0; i < inputs.length; i++) {
7079
7216
  const filePath = inputs[i];
@@ -7082,30 +7219,39 @@ async function convertDocuments(opts) {
7082
7219
  let errorMsg;
7083
7220
  for (let attempt = 0; attempt < 3; attempt++) {
7084
7221
  try {
7085
- const useApi = shouldUseFileApi(filePath);
7086
- let contentParts;
7087
- if (useApi) {
7088
- logger?.debug("File >20MB \u2014 using Files API");
7089
- const ref = await uploadFile(filePath, { logger });
7090
- contentParts = [
7091
- { text: prompt },
7092
- { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
7093
- ];
7094
- } else {
7095
- contentParts = [{ text: prompt }, inlinePart(filePath)];
7096
- }
7097
- const resp = await generateContent({
7098
- model,
7099
- contents: [{ parts: contentParts }]
7100
- });
7222
+ const resp = await withGeminiKey(
7223
+ async (apiKey) => {
7224
+ const useApi = shouldUseFileApi(filePath);
7225
+ let contentParts;
7226
+ if (useApi) {
7227
+ logger?.debug("File >20MB \u2014 using Files API");
7228
+ const ref = await uploadFile(filePath, { logger, apiKey });
7229
+ contentParts = [
7230
+ { text: prompt },
7231
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
7232
+ ];
7233
+ } else {
7234
+ contentParts = [{ text: prompt }, inlinePart(filePath)];
7235
+ }
7236
+ return generateContent(
7237
+ {
7238
+ model,
7239
+ contents: [{ parts: contentParts }]
7240
+ },
7241
+ { apiKey }
7242
+ );
7243
+ },
7244
+ { logger }
7245
+ );
7101
7246
  markdown = extractText(resp);
7102
7247
  break;
7103
7248
  } catch (e) {
7104
7249
  errorMsg = e instanceof Error ? e.message : String(e);
7250
+ if (isGeminiRateLimitError(e)) break;
7105
7251
  if (attempt < 2) {
7106
7252
  const wait = 2 ** attempt * 1e3;
7107
7253
  logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
7108
- await sleep6(wait);
7254
+ await sleep4(wait);
7109
7255
  }
7110
7256
  }
7111
7257
  }
@@ -7137,7 +7283,7 @@ async function convertDocuments(opts) {
7137
7283
  }
7138
7284
  lines.push("\n---\n");
7139
7285
  }
7140
- fs42.writeFileSync(outputPath, lines.join("\n"), "utf8");
7286
+ fs43.writeFileSync(outputPath, lines.join("\n"), "utf8");
7141
7287
  const succeeded = results.filter((r) => r.status === "success").length;
7142
7288
  console.log(`
7143
7289
  ${"=".repeat(50)}`);
@@ -7153,7 +7299,7 @@ ${"=".repeat(50)}`);
7153
7299
  const failed = results.filter((r) => r.status === "error").length;
7154
7300
  if (failed > 0) process.exit(1);
7155
7301
  }
7156
- function sleep6(ms) {
7302
+ function sleep4(ms) {
7157
7303
  return new Promise((r) => setTimeout(r, ms));
7158
7304
  }
7159
7305
 
@@ -7185,11 +7331,11 @@ function registerDocCommands(program2) {
7185
7331
 
7186
7332
  // src/commands/media/index.ts
7187
7333
  init_errors();
7188
- import fs45 from "fs";
7334
+ import fs46 from "fs";
7189
7335
  import path48 from "path";
7190
7336
 
7191
7337
  // src/commands/media/batch.ts
7192
- import fs43 from "fs";
7338
+ import fs44 from "fs";
7193
7339
  import path46 from "path";
7194
7340
 
7195
7341
  // src/commands/media/detect-type.ts
@@ -7386,8 +7532,8 @@ async function batchOptimize(opts) {
7386
7532
  verbose,
7387
7533
  logger
7388
7534
  } = opts;
7389
- fs43.mkdirSync(outputDir, { recursive: true });
7390
- const entries = fs43.readdirSync(inputDir);
7535
+ fs44.mkdirSync(outputDir, { recursive: true });
7536
+ const entries = fs44.readdirSync(inputDir);
7391
7537
  const mediaFiles = entries.filter((f) => {
7392
7538
  const ext = path46.extname(f).toLowerCase();
7393
7539
  return ALL_EXTS.has(ext);
@@ -7439,7 +7585,7 @@ async function batchOptimize(opts) {
7439
7585
 
7440
7586
  // src/commands/media/split-video.ts
7441
7587
  init_errors();
7442
- import fs44 from "fs";
7588
+ import fs45 from "fs";
7443
7589
  import path47 from "path";
7444
7590
  async function splitVideo(opts) {
7445
7591
  await assertFfmpeg();
@@ -7451,7 +7597,7 @@ async function splitVideo(opts) {
7451
7597
  logger?.info("Video is short enough \u2014 no splitting needed");
7452
7598
  return [input];
7453
7599
  }
7454
- fs44.mkdirSync(outputDir, { recursive: true });
7600
+ fs45.mkdirSync(outputDir, { recursive: true });
7455
7601
  const ext = path47.extname(input) || ".mp4";
7456
7602
  const baseName = path47.basename(input, ext);
7457
7603
  const outputPattern = path47.join(outputDir, `${baseName}_chunk_%03d${ext}`);
@@ -7474,7 +7620,7 @@ async function splitVideo(opts) {
7474
7620
  const files = [];
7475
7621
  for (let i = 0; i < numChunks + 1; i++) {
7476
7622
  const fname = path47.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
7477
- if (fs44.existsSync(fname)) files.push(fname);
7623
+ if (fs45.existsSync(fname)) files.push(fname);
7478
7624
  }
7479
7625
  logger?.success(`Created ${files.length} chunks in ${outputDir}`);
7480
7626
  return files;
@@ -7488,8 +7634,8 @@ function registerMediaCommands(program2) {
7488
7634
  media.command("optimize").description("Optimize a single media file (video, audio, or image)").requiredOption("--input <file>", "Input media file").requiredOption("--output <file>", "Output file path").option("--target-size <MB>", "Target file size in MB").option("--quality <n>", "Quality: video CRF (0-51) or image (1-100)", "85").option("--max-width <px>", "Max image/video width in pixels", "1920").option("--bitrate <rate>", "Audio bitrate (e.g. 64k)", "64k").option("--resolution <WxH>", "Force video resolution (e.g. 1920x1080)").option("-v, --verbose", "Verbose logging").action(
7489
7635
  async (opts) => {
7490
7636
  const logger = createLogger({ verbose: opts.verbose ?? false });
7491
- if (!fs45.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7492
- fs45.mkdirSync(path48.dirname(path48.resolve(opts.output)), { recursive: true });
7637
+ if (!fs46.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7638
+ fs46.mkdirSync(path48.dirname(path48.resolve(opts.output)), { recursive: true });
7493
7639
  const ext = path48.extname(opts.input).toLowerCase();
7494
7640
  const kind = extToKind(ext);
7495
7641
  if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
@@ -7530,7 +7676,7 @@ function registerMediaCommands(program2) {
7530
7676
  media.command("split").description("Split a long video into fixed-duration chunks").requiredOption("--input <file>", "Input video file").option("--output-dir <dir>", "Output directory for chunks", "./chunks").option("--chunk-duration <sec>", "Chunk duration in seconds", "3600").option("-v, --verbose", "Verbose logging").action(
7531
7677
  async (opts) => {
7532
7678
  const logger = createLogger({ verbose: opts.verbose ?? false });
7533
- if (!fs45.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7679
+ if (!fs46.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7534
7680
  const chunks = await splitVideo({
7535
7681
  input: opts.input,
7536
7682
  outputDir: opts.outputDir,
@@ -7546,7 +7692,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
7546
7692
  media.command("batch").description("Batch optimize all media files in a directory").requiredOption("--input-dir <dir>", "Input directory").requiredOption("--output-dir <dir>", "Output directory").option("--quality <n>", "Quality setting", "85").option("--max-width <px>", "Max image/video width", "1920").option("--bitrate <rate>", "Audio bitrate", "64k").option("-v, --verbose", "Verbose logging").action(
7547
7693
  async (opts) => {
7548
7694
  const logger = createLogger({ verbose: opts.verbose ?? false });
7549
- if (!fs45.existsSync(opts.inputDir))
7695
+ if (!fs46.existsSync(opts.inputDir))
7550
7696
  throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
7551
7697
  const result = await batchOptimize({
7552
7698
  inputDir: opts.inputDir,
@@ -7569,7 +7715,6 @@ Processed: ${result.succeeded}/${result.total} files`);
7569
7715
 
7570
7716
  // src/commands/update.ts
7571
7717
  init_errors();
7572
- init_http_client();
7573
7718
  import { createRequire } from "module";
7574
7719
  import { execa as execa5 } from "execa";
7575
7720
 
@@ -7734,11 +7879,6 @@ function registerCommands(program2) {
7734
7879
  // src/core/index.ts
7735
7880
  init_errors();
7736
7881
  init_redact();
7737
- init_http_client();
7738
- init_fetch_json();
7739
-
7740
- // src/core/async-poll.ts
7741
- init_errors();
7742
7882
 
7743
7883
  // src/cli.ts
7744
7884
  loadEnv();