@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 +7 -0
- package/dist/cli.js +205 -184
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
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/
|
|
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(
|
|
5298
|
-
const 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: ${
|
|
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
|
|
5305
|
-
return
|
|
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(
|
|
5230
|
+
async function uploadFileWithKey(source, apiKey, opts) {
|
|
5355
5231
|
const { timeoutMs = DEFAULT_TIMEOUT_MS3, logger, bytesPerSec } = opts;
|
|
5356
|
-
const mimeType =
|
|
5357
|
-
const
|
|
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
|
|
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: ${
|
|
5282
|
+
throw new ProviderError(`File processing failed: ${label}`, "gemini");
|
|
5408
5283
|
if (fileRef.state === "PROCESSING")
|
|
5409
|
-
throw new ProviderError(`Processing timeout: ${
|
|
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
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
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
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
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 >
|
|
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
|
|
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
|
|
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
|
|
5812
|
-
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
|
|
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
|
|
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
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
4
|
+
version: 1.0.1 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|