@mrgoonie/multix 1.0.0 → 1.0.1

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/CHANGELOG.md CHANGED
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.1](https://github.com/mrgoonie/multix-cli/compare/v1.0.0...v1.0.1) (2026-10-11)
9
+
10
+
11
+ ### Changed
12
+
13
+ * **gemini:** move file-system work out of the Gemini client ([#109](https://github.com/mrgoonie/multix-cli/issues/109)) ([7df49b2](https://github.com/mrgoonie/multix-cli/commit/7df49b24994bf169cb5709adbada61663a66c786))
14
+
8
15
  ## [1.0.0](https://github.com/mrgoonie/multix-cli/compare/v0.15.3...v1.0.0) (2026-10-11)
9
16
 
10
17
 
package/dist/cli.js CHANGED
@@ -5150,118 +5150,9 @@ async function resolveFileInputs(files, opts = {}) {
5150
5150
  }
5151
5151
 
5152
5152
  // src/providers/gemini/client.ts
5153
- import fs21 from "fs";
5154
- import path25 from "path";
5155
- init_errors();
5156
-
5157
- // src/providers/gemini/key-pool.ts
5158
5153
  init_errors();
5159
- var PRIMARY_KEY_ENV = "GEMINI_API_KEY";
5160
- var DEFAULT_COOLDOWN_MS = 6e4;
5161
- var cooldownUntil = /* @__PURE__ */ new Map();
5162
- function listGeminiKeys(env = process.env) {
5163
- 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);
5164
- const seen = /* @__PURE__ */ new Set();
5165
- const keys = [];
5166
- for (const name of [PRIMARY_KEY_ENV, ...numbered]) {
5167
- const value = env[name]?.trim();
5168
- if (!value || seen.has(value)) continue;
5169
- seen.add(value);
5170
- keys.push({ name, value });
5171
- }
5172
- return keys;
5173
- }
5174
- function isGeminiRateLimitError(error) {
5175
- if (!(error instanceof HttpError)) return false;
5176
- if (error.status === 429) return true;
5177
- return (error.status === 400 || error.status === 403) && error.snippet.includes("RESOURCE_EXHAUSTED");
5178
- }
5179
- function cooldownMs(error) {
5180
- if (!(error instanceof HttpError)) return DEFAULT_COOLDOWN_MS;
5181
- if (error.retryAfterMs !== void 0 && error.retryAfterMs > 0) return error.retryAfterMs;
5182
- const delay = /"retryDelay"\s*:\s*"(\d+(?:\.\d+)?)s"/.exec(error.snippet);
5183
- if (delay?.[1]) return Math.ceil(Number(delay[1]) * 1e3);
5184
- return DEFAULT_COOLDOWN_MS;
5185
- }
5186
- function keysToTry(keys, now) {
5187
- const until = (k) => cooldownUntil.get(k.value) ?? 0;
5188
- const ready = keys.filter((k) => until(k) <= now);
5189
- if (ready.length > 0) return ready;
5190
- const soonest = [...keys].sort((a, b) => until(a) - until(b))[0];
5191
- return soonest ? [soonest] : [];
5192
- }
5193
- async function withGeminiKey(fn, opts = {}) {
5194
- const now = opts.now ?? Date.now;
5195
- const keys = listGeminiKeys(opts.env);
5196
- if (keys.length === 0) {
5197
- throw new ConfigError(
5198
- "GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
5199
- );
5200
- }
5201
- const ordered = keysToTry(keys, now());
5202
- let lastError;
5203
- for (const [i, key] of ordered.entries()) {
5204
- try {
5205
- return await fn(key.value, key.name);
5206
- } catch (error) {
5207
- if (!isGeminiRateLimitError(error)) throw error;
5208
- lastError = error;
5209
- cooldownUntil.set(key.value, now() + cooldownMs(error));
5210
- const next = ordered[i + 1];
5211
- if (next) opts.logger?.warn(`${key.name} is rate-limited; retrying with ${next.name}`);
5212
- }
5213
- }
5214
- if (keys.length > 1) {
5215
- opts.logger?.warn(`All ${keys.length} Gemini keys are rate-limited; try again later`);
5216
- }
5217
- throw lastError;
5218
- }
5219
5154
 
5220
- // src/providers/gemini/task-resolver.ts
5221
- import path24 from "path";
5222
- var MIME_MAP = {
5223
- // Audio
5224
- ".mp3": "audio/mp3",
5225
- ".wav": "audio/wav",
5226
- ".aac": "audio/aac",
5227
- ".flac": "audio/flac",
5228
- ".ogg": "audio/ogg",
5229
- ".aiff": "audio/aiff",
5230
- ".m4a": "audio/mp4",
5231
- // Image
5232
- ".jpg": "image/jpeg",
5233
- ".jpeg": "image/jpeg",
5234
- ".png": "image/png",
5235
- ".webp": "image/webp",
5236
- ".heic": "image/heic",
5237
- ".heif": "image/heif",
5238
- ".gif": "image/gif",
5239
- ".bmp": "image/bmp",
5240
- // Video
5241
- ".mp4": "video/mp4",
5242
- ".mpeg": "video/mpeg",
5243
- ".mov": "video/quicktime",
5244
- ".avi": "video/x-msvideo",
5245
- ".flv": "video/x-flv",
5246
- ".mpg": "video/mpeg",
5247
- ".webm": "video/webm",
5248
- ".wmv": "video/x-ms-wmv",
5249
- ".3gpp": "video/3gpp",
5250
- // Document
5251
- ".pdf": "application/pdf",
5252
- ".txt": "text/plain",
5253
- ".html": "text/html",
5254
- ".htm": "text/html",
5255
- ".md": "text/markdown",
5256
- ".csv": "text/csv",
5257
- ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
5258
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
5259
- ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
5260
- };
5261
- function getMimeType(filePath) {
5262
- const ext = path24.extname(filePath).toLowerCase();
5263
- return MIME_MAP[ext] ?? "application/octet-stream";
5264
- }
5155
+ // src/providers/gemini/media-types.ts
5265
5156
  function requiresProcessingWait(mimeType) {
5266
5157
  return mimeType.startsWith("audio/") || mimeType.startsWith("video/");
5267
5158
  }
@@ -5270,39 +5161,24 @@ function requiresProcessingWait(mimeType) {
5270
5161
  var BASE = "https://generativelanguage.googleapis.com";
5271
5162
  var FILES_BASE = `${BASE}/upload/v1beta/files`;
5272
5163
  var GENERATE_BASE = `${BASE}/v1beta/models`;
5273
- function withKey(opts, fn) {
5274
- return opts.apiKey ? fn(opts.apiKey) : withGeminiKey(fn, { logger: opts.logger });
5275
- }
5276
5164
  function authHeaders2(apiKey) {
5277
5165
  return { "x-goog-api-key": apiKey };
5278
5166
  }
5279
5167
  var DEFAULT_TIMEOUT_MS3 = 3e5;
5280
- var UPLOAD_MIN_KBPS_ENV = "MULTIX_UPLOAD_MIN_KBPS";
5281
5168
  var DEFAULT_MIN_UPLOAD_BYTES_PER_SEC = 1e6;
5282
5169
  var UPLOAD_TIMEOUT_SLACK_MS = 6e4;
5283
- function minUploadBytesPerSec() {
5284
- const raw = resolveKey(UPLOAD_MIN_KBPS_ENV)?.trim();
5285
- if (!raw) return DEFAULT_MIN_UPLOAD_BYTES_PER_SEC;
5286
- const kbps = Number(raw);
5287
- if (!Number.isFinite(kbps) || kbps < 1) {
5288
- throw new ConfigError(
5289
- `${UPLOAD_MIN_KBPS_ENV} must be a number of kilobytes per second, at least 1, got "${raw}"`
5290
- );
5291
- }
5292
- return kbps * 1e3;
5293
- }
5294
5170
  function uploadTimeoutMs(size, timeoutMs = DEFAULT_TIMEOUT_MS3, bytesPerSec = DEFAULT_MIN_UPLOAD_BYTES_PER_SEC) {
5295
5171
  return Math.max(timeoutMs, UPLOAD_TIMEOUT_SLACK_MS + Math.ceil(size / bytesPerSec * 1e3));
5296
5172
  }
5297
- async function uploadFile(filePath, opts = {}) {
5298
- const size = fs21.statSync(filePath).size;
5173
+ async function uploadFile(source, opts) {
5174
+ const size = source.blob.size;
5299
5175
  if (size > FILES_API_MAX_BYTES) {
5300
5176
  throw new ValidationError(
5301
- `File too large for the Gemini Files API: ${path25.basename(filePath)} is ${formatMb3(size)} MB (limit 2 GB = ${FILES_API_MAX_BYTES} bytes)`
5177
+ `File too large for the Gemini Files API: ${source.displayName} is ${formatMb3(size)} MB (limit 2 GB = ${FILES_API_MAX_BYTES} bytes)`
5302
5178
  );
5303
5179
  }
5304
- const bytesPerSec = minUploadBytesPerSec();
5305
- return withKey(opts, (apiKey) => uploadFileWithKey(filePath, apiKey, { ...opts, bytesPerSec }));
5180
+ const { apiKey, minUploadBytesPerSec: minUploadBytesPerSec2 = DEFAULT_MIN_UPLOAD_BYTES_PER_SEC } = opts;
5181
+ return uploadFileWithKey(source, apiKey, { ...opts, bytesPerSec: minUploadBytesPerSec2 });
5306
5182
  }
5307
5183
  function formatMb3(bytes) {
5308
5184
  return (bytes / 1024 / 1024).toFixed(2);
@@ -5351,11 +5227,10 @@ async function readFinalizedFile(resp) {
5351
5227
  }
5352
5228
  return file;
5353
5229
  }
5354
- async function uploadFileWithKey(filePath, apiKey, opts) {
5230
+ async function uploadFileWithKey(source, apiKey, opts) {
5355
5231
  const { timeoutMs = DEFAULT_TIMEOUT_MS3, logger, bytesPerSec } = opts;
5356
- const mimeType = getMimeType(filePath);
5357
- const displayName = path25.basename(filePath);
5358
- const file = await fs21.openAsBlob(filePath);
5232
+ const { mimeType, displayName, label = source.displayName } = source;
5233
+ const file = source.blob.type ? source.blob.slice(0, source.blob.size, "") : source.blob;
5359
5234
  logger?.debug(`Uploading ${displayName} (${formatMb3(file.size)} MB)`);
5360
5235
  const start = await uploadFetch(
5361
5236
  FILES_BASE,
@@ -5397,44 +5272,38 @@ async function uploadFileWithKey(filePath, apiKey, opts) {
5397
5272
  const deadline = Date.now() + timeoutMs;
5398
5273
  while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
5399
5274
  await sleep2(2e3);
5400
- fileRef = await httpJson({
5275
+ fileRef = await fetchJson({
5401
5276
  url: `${BASE}/v1beta/${fileRef.name}`,
5402
5277
  headers: authHeaders2(apiKey)
5403
5278
  }).then((r) => r.file);
5404
5279
  logger?.debug(`Processing... state=${fileRef.state}`);
5405
5280
  }
5406
5281
  if (fileRef.state === "FAILED")
5407
- throw new ProviderError(`File processing failed: ${filePath}`, "gemini");
5282
+ throw new ProviderError(`File processing failed: ${label}`, "gemini");
5408
5283
  if (fileRef.state === "PROCESSING")
5409
- throw new ProviderError(`Processing timeout: ${filePath}`, "gemini");
5284
+ throw new ProviderError(`Processing timeout: ${label}`, "gemini");
5410
5285
  }
5411
5286
  logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
5412
5287
  return fileRef;
5413
5288
  }
5414
- async function generateContent(req, opts = {}) {
5415
- return withKey(
5416
- opts,
5417
- (apiKey) => httpJson({
5418
- url: `${GENERATE_BASE}/${req.model}:generateContent`,
5419
- method: "POST",
5420
- headers: authHeaders2(apiKey),
5421
- body: {
5422
- contents: req.contents,
5423
- ...req.generationConfig ? { generationConfig: req.generationConfig } : {}
5424
- }
5425
- })
5426
- );
5289
+ async function generateContent(req, opts) {
5290
+ return fetchJson({
5291
+ url: `${GENERATE_BASE}/${req.model}:generateContent`,
5292
+ method: "POST",
5293
+ headers: authHeaders2(opts.apiKey),
5294
+ body: {
5295
+ contents: req.contents,
5296
+ ...req.generationConfig ? { generationConfig: req.generationConfig } : {}
5297
+ }
5298
+ });
5427
5299
  }
5428
- async function createInteraction(body, opts = {}) {
5429
- return withKey(
5430
- opts,
5431
- (apiKey) => httpJson({
5432
- url: `${BASE}/v1beta/interactions`,
5433
- method: "POST",
5434
- headers: authHeaders2(apiKey),
5435
- body
5436
- })
5437
- );
5300
+ async function createInteraction(body, opts) {
5301
+ return fetchJson({
5302
+ url: `${BASE}/v1beta/interactions`,
5303
+ method: "POST",
5304
+ headers: authHeaders2(opts.apiKey),
5305
+ body
5306
+ });
5438
5307
  }
5439
5308
  function extractInteractionAudio(resp) {
5440
5309
  let found = null;
@@ -5473,6 +5342,150 @@ function extractImages(resp) {
5473
5342
  (p) => p.inlineData !== void 0
5474
5343
  ).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
5475
5344
  }
5345
+ function sleep2(ms) {
5346
+ return new Promise((resolve) => setTimeout(resolve, ms));
5347
+ }
5348
+
5349
+ // src/providers/gemini/key-pool.ts
5350
+ init_errors();
5351
+ var PRIMARY_KEY_ENV = "GEMINI_API_KEY";
5352
+ var DEFAULT_COOLDOWN_MS = 6e4;
5353
+ var cooldownUntil = /* @__PURE__ */ new Map();
5354
+ function listGeminiKeys(env = process.env) {
5355
+ 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);
5356
+ const seen = /* @__PURE__ */ new Set();
5357
+ const keys = [];
5358
+ for (const name of [PRIMARY_KEY_ENV, ...numbered]) {
5359
+ const value = env[name]?.trim();
5360
+ if (!value || seen.has(value)) continue;
5361
+ seen.add(value);
5362
+ keys.push({ name, value });
5363
+ }
5364
+ return keys;
5365
+ }
5366
+ function isGeminiRateLimitError(error) {
5367
+ if (!(error instanceof HttpError)) return false;
5368
+ if (error.status === 429) return true;
5369
+ return (error.status === 400 || error.status === 403) && error.snippet.includes("RESOURCE_EXHAUSTED");
5370
+ }
5371
+ function cooldownMs(error) {
5372
+ if (!(error instanceof HttpError)) return DEFAULT_COOLDOWN_MS;
5373
+ if (error.retryAfterMs !== void 0 && error.retryAfterMs > 0) return error.retryAfterMs;
5374
+ const delay = /"retryDelay"\s*:\s*"(\d+(?:\.\d+)?)s"/.exec(error.snippet);
5375
+ if (delay?.[1]) return Math.ceil(Number(delay[1]) * 1e3);
5376
+ return DEFAULT_COOLDOWN_MS;
5377
+ }
5378
+ function keysToTry(keys, now) {
5379
+ const until = (k) => cooldownUntil.get(k.value) ?? 0;
5380
+ const ready = keys.filter((k) => until(k) <= now);
5381
+ if (ready.length > 0) return ready;
5382
+ const soonest = [...keys].sort((a, b) => until(a) - until(b))[0];
5383
+ return soonest ? [soonest] : [];
5384
+ }
5385
+ async function withGeminiKey(fn, opts = {}) {
5386
+ const now = opts.now ?? Date.now;
5387
+ const keys = listGeminiKeys(opts.env);
5388
+ if (keys.length === 0) {
5389
+ throw new ConfigError(
5390
+ "GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
5391
+ );
5392
+ }
5393
+ const ordered = keysToTry(keys, now());
5394
+ let lastError;
5395
+ for (const [i, key] of ordered.entries()) {
5396
+ try {
5397
+ return await fn(key.value, key.name);
5398
+ } catch (error) {
5399
+ if (!isGeminiRateLimitError(error)) throw error;
5400
+ lastError = error;
5401
+ cooldownUntil.set(key.value, now() + cooldownMs(error));
5402
+ const next = ordered[i + 1];
5403
+ if (next) opts.logger?.warn(`${key.name} is rate-limited; retrying with ${next.name}`);
5404
+ }
5405
+ }
5406
+ if (keys.length > 1) {
5407
+ opts.logger?.warn(`All ${keys.length} Gemini keys are rate-limited; try again later`);
5408
+ }
5409
+ throw lastError;
5410
+ }
5411
+
5412
+ // src/providers/gemini/local-files.ts
5413
+ import fs21 from "fs";
5414
+ import path25 from "path";
5415
+ init_errors();
5416
+
5417
+ // src/providers/gemini/task-resolver.ts
5418
+ import path24 from "path";
5419
+ var MIME_MAP = {
5420
+ // Audio
5421
+ ".mp3": "audio/mp3",
5422
+ ".wav": "audio/wav",
5423
+ ".aac": "audio/aac",
5424
+ ".flac": "audio/flac",
5425
+ ".ogg": "audio/ogg",
5426
+ ".aiff": "audio/aiff",
5427
+ ".m4a": "audio/mp4",
5428
+ // Image
5429
+ ".jpg": "image/jpeg",
5430
+ ".jpeg": "image/jpeg",
5431
+ ".png": "image/png",
5432
+ ".webp": "image/webp",
5433
+ ".heic": "image/heic",
5434
+ ".heif": "image/heif",
5435
+ ".gif": "image/gif",
5436
+ ".bmp": "image/bmp",
5437
+ // Video
5438
+ ".mp4": "video/mp4",
5439
+ ".mpeg": "video/mpeg",
5440
+ ".mov": "video/quicktime",
5441
+ ".avi": "video/x-msvideo",
5442
+ ".flv": "video/x-flv",
5443
+ ".mpg": "video/mpeg",
5444
+ ".webm": "video/webm",
5445
+ ".wmv": "video/x-ms-wmv",
5446
+ ".3gpp": "video/3gpp",
5447
+ // Document
5448
+ ".pdf": "application/pdf",
5449
+ ".txt": "text/plain",
5450
+ ".html": "text/html",
5451
+ ".htm": "text/html",
5452
+ ".md": "text/markdown",
5453
+ ".csv": "text/csv",
5454
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
5455
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
5456
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
5457
+ };
5458
+ function getMimeType(filePath) {
5459
+ const ext = path24.extname(filePath).toLowerCase();
5460
+ return MIME_MAP[ext] ?? "application/octet-stream";
5461
+ }
5462
+
5463
+ // src/providers/gemini/local-files.ts
5464
+ var INLINE_MAX_BYTES = 20 * 1024 * 1024;
5465
+ var UPLOAD_MIN_KBPS_ENV = "MULTIX_UPLOAD_MIN_KBPS";
5466
+ function minUploadBytesPerSec() {
5467
+ const raw = resolveKey(UPLOAD_MIN_KBPS_ENV)?.trim();
5468
+ if (!raw) return DEFAULT_MIN_UPLOAD_BYTES_PER_SEC;
5469
+ const kbps = Number(raw);
5470
+ if (!Number.isFinite(kbps) || kbps < 1) {
5471
+ throw new ConfigError(
5472
+ `${UPLOAD_MIN_KBPS_ENV} must be a number of kilobytes per second, at least 1, got "${raw}"`
5473
+ );
5474
+ }
5475
+ return kbps * 1e3;
5476
+ }
5477
+ async function uploadLocalFile(filePath, opts = {}) {
5478
+ const bytesPerSec = minUploadBytesPerSec();
5479
+ const blob = await fs21.openAsBlob(filePath);
5480
+ const source = {
5481
+ blob,
5482
+ displayName: path25.basename(filePath),
5483
+ mimeType: getMimeType(filePath),
5484
+ label: filePath
5485
+ };
5486
+ const upload = (apiKey) => uploadFile(source, { ...opts, apiKey, minUploadBytesPerSec: bytesPerSec });
5487
+ return opts.apiKey ? upload(opts.apiKey) : withGeminiKey(upload, { logger: opts.logger });
5488
+ }
5476
5489
  function inlinePart(filePath) {
5477
5490
  const data = fs21.readFileSync(filePath).toString("base64");
5478
5491
  const mimeType = getMimeType(filePath);
@@ -5480,10 +5493,7 @@ function inlinePart(filePath) {
5480
5493
  }
5481
5494
  function shouldUseFileApi(filePath) {
5482
5495
  const stat = fs21.statSync(filePath);
5483
- return stat.size > 20 * 1024 * 1024;
5484
- }
5485
- function sleep2(ms) {
5486
- return new Promise((resolve) => setTimeout(resolve, ms));
5496
+ return stat.size > INLINE_MAX_BYTES;
5487
5497
  }
5488
5498
 
5489
5499
  // src/providers/gemini/models.ts
@@ -5619,7 +5629,7 @@ function registerAnalyzeCommand(parent) {
5619
5629
  let contentParts;
5620
5630
  if (useApi) {
5621
5631
  logger.debug("File >20MB \u2014 using Files API");
5622
- const ref = await uploadFile(filePath, { logger, apiKey });
5632
+ const ref = await uploadLocalFile(filePath, { logger, apiKey });
5623
5633
  contentParts = [
5624
5634
  { text: opts.prompt },
5625
5635
  { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
@@ -5682,7 +5692,7 @@ function registerExtractCommand(parent) {
5682
5692
  const useApi = shouldUseFileApi(filePath);
5683
5693
  let contentParts;
5684
5694
  if (useApi) {
5685
- const ref = await uploadFile(filePath, { logger, apiKey });
5695
+ const ref = await uploadLocalFile(filePath, { logger, apiKey });
5686
5696
  contentParts = [
5687
5697
  { text: opts.prompt },
5688
5698
  { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
@@ -5808,12 +5818,15 @@ async function generateGeminiSpeech(opts) {
5808
5818
  logger?.debug(`Gemini TTS model=${model} mode=${speakers?.length ? "multi" : "single"}`);
5809
5819
  let resp;
5810
5820
  try {
5811
- resp = await generateContent(
5812
- {
5813
- model,
5814
- contents: [{ parts: [{ text }] }],
5815
- generationConfig
5816
- },
5821
+ resp = await withGeminiKey(
5822
+ (apiKey) => generateContent(
5823
+ {
5824
+ model,
5825
+ contents: [{ parts: [{ text }] }],
5826
+ generationConfig
5827
+ },
5828
+ { apiKey, logger }
5829
+ ),
5817
5830
  { logger }
5818
5831
  );
5819
5832
  } catch (e) {
@@ -5851,7 +5864,10 @@ async function generateInteractionsSpeech(opts) {
5851
5864
  }
5852
5865
  let resp;
5853
5866
  try {
5854
- resp = await createInteraction(buildInteractionsSpeechBody(opts), { logger });
5867
+ resp = await withGeminiKey(
5868
+ (apiKey) => createInteraction(buildInteractionsSpeechBody(opts), { apiKey, logger }),
5869
+ { logger }
5870
+ );
5855
5871
  } catch (e) {
5856
5872
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
5857
5873
  }
@@ -6209,12 +6225,15 @@ function registerGenerateCommand(parent) {
6209
6225
  const savedFiles = [];
6210
6226
  for (let i = 0; i < numImages; i++) {
6211
6227
  try {
6212
- const resp = await generateContent(
6213
- {
6214
- model,
6215
- contents: [{ parts: [{ text: opts.prompt }] }],
6216
- generationConfig
6217
- },
6228
+ const resp = await withGeminiKey(
6229
+ (apiKey) => generateContent(
6230
+ {
6231
+ model,
6232
+ contents: [{ parts: [{ text: opts.prompt }] }],
6233
+ generationConfig
6234
+ },
6235
+ { apiKey, logger }
6236
+ ),
6218
6237
  { logger }
6219
6238
  );
6220
6239
  const images = extractImages(resp);
@@ -6304,7 +6323,9 @@ function registerGeminiImageToImageCommand(parent) {
6304
6323
  contents: [{ parts }],
6305
6324
  generationConfig: { responseModalities: ["IMAGE"] }
6306
6325
  };
6307
- const resp = await generateContent(req, { logger });
6326
+ const resp = await withGeminiKey((apiKey) => generateContent(req, { apiKey, logger }), {
6327
+ logger
6328
+ });
6308
6329
  const images = extractImages(resp);
6309
6330
  if (images.length === 0) {
6310
6331
  logger.error(
@@ -6437,7 +6458,7 @@ function registerTranscribeCommand2(parent) {
6437
6458
  let contentParts;
6438
6459
  if (useApi) {
6439
6460
  logger.debug("File >20MB \u2014 using Files API");
6440
- const ref = await uploadFile(filePath, { logger, apiKey });
6461
+ const ref = await uploadLocalFile(filePath, { logger, apiKey });
6441
6462
  contentParts = [
6442
6463
  { text: opts.prompt },
6443
6464
  { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
@@ -7533,7 +7554,7 @@ function createHeygenClientFromEnv() {
7533
7554
  const baseUrl = resolveProviderBaseUrl(HEYGEN_ROUTING);
7534
7555
  return new HeygenClient({ apiKey, baseUrl: checkBaseUrl(baseUrl, "HEYGEN_BASE_URL") });
7535
7556
  }
7536
- async function uploadLocalFile(client, filePath, logger) {
7557
+ async function uploadLocalFile2(client, filePath, logger) {
7537
7558
  const abs = path32.resolve(filePath);
7538
7559
  let stat;
7539
7560
  try {
@@ -7569,7 +7590,7 @@ async function resolveMediaInput(client, value, logger) {
7569
7590
  const ref = classifyMediaRef(value);
7570
7591
  if (ref.kind === "url") return { type: "url", url: ref.url };
7571
7592
  if (ref.kind === "asset") return { type: "asset_id", asset_id: ref.assetId };
7572
- return uploadLocalFile(client, ref.path, logger);
7593
+ return uploadLocalFile2(client, ref.path, logger);
7573
7594
  }
7574
7595
  async function resolveMediaInputs(client, values, logger) {
7575
7596
  const out = [];
@@ -12375,7 +12396,7 @@ async function convertResolved(inputs, ctx) {
12375
12396
  let contentParts;
12376
12397
  if (useApi) {
12377
12398
  logger?.debug("File >20MB \u2014 using Files API");
12378
- const ref = await uploadFile(filePath, { logger, apiKey });
12399
+ const ref = await uploadLocalFile(filePath, { logger, apiKey });
12379
12400
  contentParts = [
12380
12401
  { text: prompt },
12381
12402
  { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrgoonie/multix",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "AI multimodal CLI — OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, Replicate, HeyGen, ffmpeg, ImageMagick, doc-to-md",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: multix
3
3
  description: AI multimodal CLI — generate and edit images (OpenAI, Codex driver, Gemini Nano Banana, Imagen, MiniMax, OpenRouter, Leonardo, BytePlus Seedream, Cloudflare Workers AI FLUX.1 Schnell, fal.ai FLUX, Replicate FLUX), generate video (Gemini Veo, MiniMax Hailuo, OpenRouter via Veo/Seedance/Kling, Leonardo via Motion/Veo/Kling/Hailuo/Seedance/LTX, BytePlus Seedance, Cloudflare AI Gateway via Replicate, fal.ai queue models, Replicate direct API models incl. P-Video, HeyGen Video Agent and scripted avatar videos), video translation and lipsync (HeyGen), TTS/STT/music (OpenAI, Gemini Flash TTS incl. Gemini 3.8 with delivery-style control, MiniMax, Cloudflare Workers AI MeloTTS, ElevenLabs), 3D models (BytePlus Hyper3D/Hitem3d), analyze/transcribe media, convert documents to Markdown, optimize media via ffmpeg/ImageMagick, and self-update the CLI. Use whenever the user wants to create, edit, or transform images/video/audio/3D/documents from a CLI, mentions any of: OpenAI, Codex image driver, image-to-image, i2i, image edit, watercolor/cyberpunk style transfer, reference image, OpenRouter, Nano Banana, Flux, Seedream, Veo, Hailuo, Seedance, Kling, Cloudflare Workers AI, AI Gateway, fal.ai, Atlas Cloud, Replicate, replicate.com models, Replicate predictions, HeyGen, avatar video, talking head, Video Agent, video translation, dubbing, lipsync, video generation, text-to-video, image-to-video, i2v, ElevenLabs voice cloning, updating/upgrading multix, or asks for batch media optimization.
4
- version: 1.0.0 # x-release-please-version
4
+ version: 1.0.1 # x-release-please-version
5
5
  ---
6
6
 
7
7
  # multix — AI Multimodal CLI