@mrgoonie/multix 0.0.2

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 ADDED
@@ -0,0 +1,2234 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/core/errors.ts
13
+ var MultixError, ConfigError, ProviderError, ValidationError, HttpError;
14
+ var init_errors = __esm({
15
+ "src/core/errors.ts"() {
16
+ "use strict";
17
+ MultixError = class extends Error {
18
+ constructor(message, code) {
19
+ super(message);
20
+ this.code = code;
21
+ this.name = "MultixError";
22
+ if (Error.captureStackTrace) {
23
+ Error.captureStackTrace(this, this.constructor);
24
+ }
25
+ }
26
+ code;
27
+ };
28
+ ConfigError = class extends MultixError {
29
+ constructor(message) {
30
+ super(message, "CONFIG_ERROR");
31
+ this.name = "ConfigError";
32
+ }
33
+ };
34
+ ProviderError = class extends MultixError {
35
+ constructor(message, provider) {
36
+ super(message, "PROVIDER_ERROR");
37
+ this.provider = provider;
38
+ this.name = "ProviderError";
39
+ }
40
+ provider;
41
+ };
42
+ ValidationError = class extends MultixError {
43
+ constructor(message) {
44
+ super(message, "VALIDATION_ERROR");
45
+ this.name = "ValidationError";
46
+ }
47
+ };
48
+ HttpError = class extends MultixError {
49
+ constructor(status, snippet, url) {
50
+ super(`HTTP ${status} from ${url}: ${snippet}`, "HTTP_ERROR");
51
+ this.status = status;
52
+ this.snippet = snippet;
53
+ this.url = url;
54
+ this.name = "HttpError";
55
+ }
56
+ status;
57
+ snippet;
58
+ url;
59
+ };
60
+ }
61
+ });
62
+
63
+ // src/core/http-client.ts
64
+ var http_client_exports = {};
65
+ __export(http_client_exports, {
66
+ downloadFile: () => downloadFile,
67
+ fetchBytes: () => fetchBytes,
68
+ httpJson: () => httpJson
69
+ });
70
+ import fs2 from "fs";
71
+ import path2 from "path";
72
+ import { pipeline } from "stream/promises";
73
+ async function httpJson(opts) {
74
+ const { url, method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
75
+ const init = {
76
+ method,
77
+ headers: {
78
+ "Content-Type": "application/json",
79
+ ...headers
80
+ },
81
+ signal: AbortSignal.timeout(timeoutMs)
82
+ };
83
+ if (body !== void 0) {
84
+ init.body = JSON.stringify(body);
85
+ }
86
+ let response;
87
+ try {
88
+ response = await globalThis.fetch(url, init);
89
+ } catch (cause) {
90
+ const msg = cause instanceof Error ? cause.message : String(cause);
91
+ throw new HttpError(0, msg, url);
92
+ }
93
+ if (!response.ok) {
94
+ let snippet = "";
95
+ try {
96
+ const text = await response.text();
97
+ snippet = text.slice(0, SNIPPET_MAX);
98
+ } catch {
99
+ snippet = "(could not read body)";
100
+ }
101
+ throw new HttpError(response.status, snippet, url);
102
+ }
103
+ return response.json();
104
+ }
105
+ async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
106
+ fs2.mkdirSync(path2.dirname(dest), { recursive: true });
107
+ let response;
108
+ try {
109
+ response = await globalThis.fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
110
+ } catch (cause) {
111
+ const msg = cause instanceof Error ? cause.message : String(cause);
112
+ throw new HttpError(0, msg, url);
113
+ }
114
+ if (!response.ok) {
115
+ throw new HttpError(response.status, "(download failed)", url);
116
+ }
117
+ if (!response.body) {
118
+ throw new HttpError(0, "Response body is null", url);
119
+ }
120
+ const fileStream = fs2.createWriteStream(dest);
121
+ await pipeline(response.body, fileStream);
122
+ }
123
+ async function fetchBytes(urlOrDataUri) {
124
+ if (urlOrDataUri.startsWith("data:")) {
125
+ const commaIdx = urlOrDataUri.indexOf(",");
126
+ if (commaIdx === -1) throw new Error("Malformed data URI: missing comma");
127
+ const encoded = urlOrDataUri.slice(commaIdx + 1);
128
+ return Buffer.from(encoded, "base64");
129
+ }
130
+ let response;
131
+ try {
132
+ response = await globalThis.fetch(urlOrDataUri, {
133
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
134
+ });
135
+ } catch (cause) {
136
+ const msg = cause instanceof Error ? cause.message : String(cause);
137
+ throw new HttpError(0, msg, urlOrDataUri);
138
+ }
139
+ if (!response.ok) {
140
+ throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
141
+ }
142
+ return Buffer.from(await response.arrayBuffer());
143
+ }
144
+ var DEFAULT_TIMEOUT_MS, SNIPPET_MAX;
145
+ var init_http_client = __esm({
146
+ "src/core/http-client.ts"() {
147
+ "use strict";
148
+ init_errors();
149
+ DEFAULT_TIMEOUT_MS = 12e4;
150
+ SNIPPET_MAX = 500;
151
+ }
152
+ });
153
+
154
+ // src/cli.ts
155
+ import { createRequire } from "module";
156
+ import { Command } from "commander";
157
+
158
+ // src/providers/gemini/commands/analyze.ts
159
+ init_errors();
160
+ import fs5 from "fs";
161
+
162
+ // src/core/logger.ts
163
+ var ANSI = {
164
+ reset: "\x1B[0m",
165
+ bold: "\x1B[1m",
166
+ green: "\x1B[32m",
167
+ yellow: "\x1B[33m",
168
+ red: "\x1B[31m",
169
+ blue: "\x1B[34m",
170
+ cyan: "\x1B[36m",
171
+ dim: "\x1B[2m"
172
+ };
173
+ function c(color, text) {
174
+ if (process.env.NO_COLOR || !process.stdout.isTTY) return text;
175
+ return `${color}${text}${ANSI.reset}`;
176
+ }
177
+ function createLogger(opts) {
178
+ const { verbose } = opts;
179
+ return {
180
+ info(msg) {
181
+ console.log(c(ANSI.blue, ` ${msg}`));
182
+ },
183
+ success(msg) {
184
+ console.log(c(ANSI.green, `\u2713 ${msg}`));
185
+ },
186
+ warn(msg) {
187
+ console.warn(c(ANSI.yellow, `\u26A0 ${msg}`));
188
+ },
189
+ error(msg) {
190
+ console.error(c(ANSI.red, `\u2717 ${msg}`));
191
+ },
192
+ debug(msg) {
193
+ if (verbose) console.log(c(ANSI.dim, ` [debug] ${msg}`));
194
+ },
195
+ header(msg) {
196
+ const line = "=".repeat(60);
197
+ console.log(c(ANSI.bold + ANSI.cyan, `
198
+ ${line}
199
+ ${msg}
200
+ ${line}
201
+ `));
202
+ }
203
+ };
204
+ }
205
+ var defaultLogger = createLogger({ verbose: false });
206
+
207
+ // src/providers/gemini/client.ts
208
+ import fs3 from "fs";
209
+ import path4 from "path";
210
+
211
+ // src/core/env-loader.ts
212
+ import fs from "fs";
213
+ import os from "os";
214
+ import path from "path";
215
+ import { config as dotenvConfig } from "dotenv";
216
+ var loaded = false;
217
+ function loadEnv() {
218
+ if (loaded) return;
219
+ loaded = true;
220
+ const cwdEnv = path.resolve(process.cwd(), ".env");
221
+ if (fs.existsSync(cwdEnv)) {
222
+ dotenvConfig({ path: cwdEnv, override: false });
223
+ }
224
+ if (!process.env.MULTIX_DISABLE_HOME_ENV) {
225
+ const homeEnv = path.join(os.homedir(), ".multix", ".env");
226
+ if (fs.existsSync(homeEnv)) {
227
+ dotenvConfig({ path: homeEnv, override: false });
228
+ }
229
+ }
230
+ }
231
+ function resolveKey(name) {
232
+ return process.env[name] || void 0;
233
+ }
234
+ function redact(key) {
235
+ if (key.length <= 10) return "***";
236
+ return `${key.slice(0, 6)}...${key.slice(-4)}`;
237
+ }
238
+
239
+ // src/providers/gemini/client.ts
240
+ init_errors();
241
+ init_http_client();
242
+
243
+ // src/providers/gemini/task-resolver.ts
244
+ import path3 from "path";
245
+ var MIME_MAP = {
246
+ // Audio
247
+ ".mp3": "audio/mp3",
248
+ ".wav": "audio/wav",
249
+ ".aac": "audio/aac",
250
+ ".flac": "audio/flac",
251
+ ".ogg": "audio/ogg",
252
+ ".aiff": "audio/aiff",
253
+ ".m4a": "audio/mp4",
254
+ // Image
255
+ ".jpg": "image/jpeg",
256
+ ".jpeg": "image/jpeg",
257
+ ".png": "image/png",
258
+ ".webp": "image/webp",
259
+ ".heic": "image/heic",
260
+ ".heif": "image/heif",
261
+ ".gif": "image/gif",
262
+ ".bmp": "image/bmp",
263
+ // Video
264
+ ".mp4": "video/mp4",
265
+ ".mpeg": "video/mpeg",
266
+ ".mov": "video/quicktime",
267
+ ".avi": "video/x-msvideo",
268
+ ".flv": "video/x-flv",
269
+ ".mpg": "video/mpeg",
270
+ ".webm": "video/webm",
271
+ ".wmv": "video/x-ms-wmv",
272
+ ".3gpp": "video/3gpp",
273
+ // Document
274
+ ".pdf": "application/pdf",
275
+ ".txt": "text/plain",
276
+ ".html": "text/html",
277
+ ".htm": "text/html",
278
+ ".md": "text/markdown",
279
+ ".csv": "text/csv",
280
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
281
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
282
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
283
+ };
284
+ function getMimeType(filePath) {
285
+ const ext = path3.extname(filePath).toLowerCase();
286
+ return MIME_MAP[ext] ?? "application/octet-stream";
287
+ }
288
+ function requiresProcessingWait(mimeType) {
289
+ return mimeType.startsWith("audio/") || mimeType.startsWith("video/");
290
+ }
291
+
292
+ // src/providers/gemini/client.ts
293
+ var BASE = "https://generativelanguage.googleapis.com";
294
+ var FILES_BASE = `${BASE}/upload/v1beta/files`;
295
+ var GENERATE_BASE = `${BASE}/v1beta/models`;
296
+ function requireApiKey() {
297
+ const key = resolveKey("GEMINI_API_KEY");
298
+ if (!key)
299
+ throw new ConfigError(
300
+ "GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
301
+ );
302
+ return key;
303
+ }
304
+ function authHeaders(apiKey) {
305
+ return { "x-goog-api-key": apiKey };
306
+ }
307
+ async function uploadFile(filePath, opts = {}) {
308
+ const apiKey = requireApiKey();
309
+ const { timeoutMs = 3e5, logger } = opts;
310
+ const mimeType = getMimeType(filePath);
311
+ const fileBytes = fs3.readFileSync(filePath);
312
+ const displayName = path4.basename(filePath);
313
+ const boundary = `multix_${Date.now()}`;
314
+ const metaJson = JSON.stringify({ file: { displayName, mimeType } });
315
+ const body = Buffer.concat([
316
+ Buffer.from(
317
+ `--${boundary}\r
318
+ Content-Type: application/json; charset=UTF-8\r
319
+ \r
320
+ ${metaJson}\r
321
+ --${boundary}\r
322
+ Content-Type: ${mimeType}\r
323
+ \r
324
+ `
325
+ ),
326
+ fileBytes,
327
+ Buffer.from(`\r
328
+ --${boundary}--`)
329
+ ]);
330
+ logger?.debug(`Uploading ${displayName} (${(fileBytes.length / 1024 / 1024).toFixed(2)} MB)`);
331
+ const resp = await globalThis.fetch(FILES_BASE, {
332
+ method: "POST",
333
+ headers: {
334
+ "x-goog-api-key": apiKey,
335
+ "Content-Type": `multipart/related; boundary=${boundary}`,
336
+ "Content-Length": String(body.length),
337
+ "X-Goog-Upload-Protocol": "multipart"
338
+ },
339
+ body,
340
+ signal: AbortSignal.timeout(timeoutMs)
341
+ });
342
+ if (!resp.ok) {
343
+ const txt = await resp.text();
344
+ throw new ProviderError(
345
+ `File upload failed (HTTP ${resp.status}): ${txt.slice(0, 500)}`,
346
+ "gemini"
347
+ );
348
+ }
349
+ const data = await resp.json();
350
+ let fileRef = data.file;
351
+ if (requiresProcessingWait(mimeType)) {
352
+ const deadline = Date.now() + timeoutMs;
353
+ while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
354
+ await sleep(2e3);
355
+ fileRef = await httpJson({
356
+ url: `${BASE}/v1beta/${fileRef.name}`,
357
+ headers: authHeaders(apiKey)
358
+ }).then((r) => r.file);
359
+ logger?.debug(`Processing... state=${fileRef.state}`);
360
+ }
361
+ if (fileRef.state === "FAILED")
362
+ throw new ProviderError(`File processing failed: ${filePath}`, "gemini");
363
+ if (fileRef.state === "PROCESSING")
364
+ throw new ProviderError(`Processing timeout: ${filePath}`, "gemini");
365
+ }
366
+ logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
367
+ return fileRef;
368
+ }
369
+ async function generateContent(req) {
370
+ const apiKey = requireApiKey();
371
+ return httpJson({
372
+ url: `${GENERATE_BASE}/${req.model}:generateContent`,
373
+ method: "POST",
374
+ headers: authHeaders(apiKey),
375
+ body: {
376
+ contents: req.contents,
377
+ ...req.generationConfig ? { generationConfig: req.generationConfig } : {}
378
+ }
379
+ });
380
+ }
381
+ function extractText(resp) {
382
+ const parts = resp.candidates?.[0]?.content?.parts ?? [];
383
+ return parts.filter((p) => p.text !== void 0).map((p) => p.text ?? "").join("");
384
+ }
385
+ function extractImages(resp) {
386
+ const parts = resp.candidates?.[0]?.content?.parts ?? [];
387
+ return parts.filter(
388
+ (p) => p.inlineData !== void 0
389
+ ).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
390
+ }
391
+ function inlinePart(filePath) {
392
+ const data = fs3.readFileSync(filePath).toString("base64");
393
+ const mimeType = getMimeType(filePath);
394
+ return { inlineData: { mimeType, data } };
395
+ }
396
+ function shouldUseFileApi(filePath) {
397
+ const stat = fs3.statSync(filePath);
398
+ return stat.size > 20 * 1024 * 1024;
399
+ }
400
+ function sleep(ms) {
401
+ return new Promise((resolve) => setTimeout(resolve, ms));
402
+ }
403
+
404
+ // src/providers/gemini/models.ts
405
+ var IMAGE_MODEL_DEFAULT = "gemini-3.1-flash-image-preview";
406
+ var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
407
+ var ANALYSIS_MODEL_DEFAULT = "gemini-2.5-flash";
408
+ var DOC_MODEL_DEFAULT = "gemini-2.5-flash";
409
+ var IMAGEN_MODELS = /* @__PURE__ */ new Set([
410
+ "imagen-4.0-generate-001",
411
+ "imagen-4.0-ultra-generate-001",
412
+ "imagen-4.0-fast-generate-001"
413
+ ]);
414
+ var GEMINI_IMAGE_MODELS = /* @__PURE__ */ new Set([
415
+ "gemini-3.1-flash-image-preview",
416
+ "gemini-3-pro-image-preview",
417
+ "gemini-2.5-flash-image",
418
+ "gemini-2.5-flash-image-preview",
419
+ ...IMAGEN_MODELS
420
+ ]);
421
+ var ASPECT_RATIOS = [
422
+ "1:1",
423
+ "2:3",
424
+ "3:2",
425
+ "3:4",
426
+ "4:3",
427
+ "4:5",
428
+ "5:4",
429
+ "9:16",
430
+ "16:9",
431
+ "21:9"
432
+ ];
433
+ var IMAGE_SIZES = ["1K", "2K", "4K"];
434
+ function getDefaultModel(task) {
435
+ if (task === "generate") {
436
+ return process.env.IMAGE_GEN_MODEL ?? process.env.GEMINI_IMAGE_GEN_MODEL ?? IMAGE_MODEL_DEFAULT;
437
+ }
438
+ if (task === "generate-video") {
439
+ return process.env.VIDEO_GEN_MODEL ?? VIDEO_MODEL_DEFAULT;
440
+ }
441
+ return process.env.MULTIMODAL_MODEL ?? process.env.GEMINI_MODEL ?? ANALYSIS_MODEL_DEFAULT;
442
+ }
443
+
444
+ // src/providers/gemini/result-formatter.ts
445
+ import fs4 from "fs";
446
+ import path5 from "path";
447
+ function formatResults(results, format, outputPath) {
448
+ const content = renderFormat(results, format);
449
+ if (outputPath) {
450
+ fs4.mkdirSync(path5.dirname(path5.resolve(outputPath)), { recursive: true });
451
+ fs4.writeFileSync(outputPath, content, "utf8");
452
+ } else {
453
+ process.stdout.write(content);
454
+ }
455
+ }
456
+ function renderFormat(results, format) {
457
+ switch (format) {
458
+ case "json":
459
+ return `${JSON.stringify(results, null, 2)}
460
+ `;
461
+ case "csv":
462
+ return renderCsv(results);
463
+ case "markdown":
464
+ return renderMarkdown(results);
465
+ default:
466
+ return renderText(results);
467
+ }
468
+ }
469
+ function renderText(results) {
470
+ return results.map((r) => {
471
+ const header = `[${r.file}]
472
+ Status: ${r.status}`;
473
+ if (r.status === "success" && r.response) return `${header}
474
+ Result:
475
+ ${r.response}
476
+ `;
477
+ if (r.status === "error") return `${header}
478
+ Error: ${r.error}
479
+ `;
480
+ return `${header}
481
+ `;
482
+ }).join("\n");
483
+ }
484
+ function renderCsv(results) {
485
+ const header = "file,status,response,error\n";
486
+ const rows = results.map((r) => {
487
+ const csvCell = (v) => `"${(v ?? "").replace(/"/g, '""')}"`;
488
+ return [csvCell(r.file), csvCell(r.status), csvCell(r.response), csvCell(r.error)].join(",");
489
+ });
490
+ return `${header + rows.join("\n")}
491
+ `;
492
+ }
493
+ function renderMarkdown(results) {
494
+ const lines = ["# Results\n"];
495
+ for (const r of results) {
496
+ lines.push(`## ${r.file}
497
+ `);
498
+ lines.push(`**Status**: ${r.status}
499
+ `);
500
+ if (r.status === "success" && r.response) lines.push(`**Response**:
501
+
502
+ ${r.response}
503
+ `);
504
+ if (r.status === "error") lines.push(`**Error**: ${r.error}
505
+ `);
506
+ lines.push("---\n");
507
+ }
508
+ return lines.join("\n");
509
+ }
510
+
511
+ // src/providers/gemini/commands/analyze.ts
512
+ function registerAnalyzeCommand(parent) {
513
+ parent.command("analyze").description("Analyze files with Gemini (images, video, audio, documents)").requiredOption("--files <paths...>", "Input file paths to analyze").option("--prompt <text>", "Analysis prompt", "Analyze this content").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file instead of stdout").option("-v, --verbose", "Verbose logging").action(
514
+ async (opts) => {
515
+ const logger = createLogger({ verbose: opts.verbose ?? false });
516
+ const model = opts.model ?? getDefaultModel("analyze");
517
+ for (const f of opts.files) {
518
+ if (!fs5.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
519
+ }
520
+ const results = [];
521
+ for (let i = 0; i < opts.files.length; i++) {
522
+ const filePath = opts.files[i];
523
+ logger.debug(`[${i + 1}/${opts.files.length}] Processing: ${filePath}`);
524
+ try {
525
+ const useApi = shouldUseFileApi(filePath);
526
+ let contentParts;
527
+ if (useApi) {
528
+ logger.debug("File >20MB \u2014 using Files API");
529
+ const ref = await uploadFile(filePath, { logger });
530
+ contentParts = [
531
+ { text: opts.prompt },
532
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
533
+ ];
534
+ } else {
535
+ contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
536
+ }
537
+ const resp = await generateContent({
538
+ model,
539
+ contents: [{ parts: contentParts }]
540
+ });
541
+ results.push({
542
+ file: filePath,
543
+ status: "success",
544
+ response: extractText(resp)
545
+ });
546
+ } catch (e) {
547
+ const msg = e instanceof Error ? e.message : String(e);
548
+ logger.error(`Failed: ${filePath} \u2014 ${msg}`);
549
+ results.push({ file: filePath, status: "error", error: msg });
550
+ }
551
+ }
552
+ formatResults(results, opts.format, opts.output);
553
+ logger.debug(
554
+ `Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
555
+ );
556
+ const failed = results.filter((r) => r.status === "error").length;
557
+ if (failed > 0) process.exit(1);
558
+ }
559
+ );
560
+ }
561
+
562
+ // src/providers/gemini/commands/extract.ts
563
+ init_errors();
564
+ import fs6 from "fs";
565
+ function registerExtractCommand(parent) {
566
+ parent.command("extract").description("Extract structured information from files with Gemini").requiredOption("--files <paths...>", "Input file paths").requiredOption("--prompt <text>", "Extraction prompt (required)").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(
567
+ async (opts) => {
568
+ const logger = createLogger({ verbose: opts.verbose ?? false });
569
+ const model = opts.model ?? getDefaultModel("extract");
570
+ for (const f of opts.files) {
571
+ if (!fs6.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
572
+ }
573
+ const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
574
+ const results = [];
575
+ for (let i = 0; i < opts.files.length; i++) {
576
+ const filePath = opts.files[i];
577
+ logger.debug(`[${i + 1}/${opts.files.length}] Extracting: ${filePath}`);
578
+ try {
579
+ const useApi = shouldUseFileApi(filePath);
580
+ let contentParts;
581
+ if (useApi) {
582
+ const ref = await uploadFile(filePath, { logger });
583
+ contentParts = [
584
+ { text: opts.prompt },
585
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
586
+ ];
587
+ } else {
588
+ contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
589
+ }
590
+ const resp = await generateContent({
591
+ model,
592
+ contents: [{ parts: contentParts }],
593
+ generationConfig
594
+ });
595
+ results.push({ file: filePath, status: "success", response: extractText(resp) });
596
+ } catch (e) {
597
+ const msg = e instanceof Error ? e.message : String(e);
598
+ logger.error(`Failed: ${filePath} \u2014 ${msg}`);
599
+ results.push({ file: filePath, status: "error", error: msg });
600
+ }
601
+ }
602
+ formatResults(results, opts.format, opts.output);
603
+ const failed = results.filter((r) => r.status === "error").length;
604
+ if (failed > 0) process.exit(1);
605
+ }
606
+ );
607
+ }
608
+
609
+ // src/providers/gemini/commands/generate-video.ts
610
+ import fs8 from "fs";
611
+ import path7 from "path";
612
+ init_errors();
613
+ init_http_client();
614
+
615
+ // src/core/output-dir.ts
616
+ import fs7 from "fs";
617
+ import path6 from "path";
618
+ var cachedOutputDir;
619
+ function getOutputDir() {
620
+ if (cachedOutputDir) return cachedOutputDir;
621
+ const dir = process.env.MULTIX_OUTPUT_DIR ? path6.resolve(process.env.MULTIX_OUTPUT_DIR) : path6.resolve(process.cwd(), "multix-output");
622
+ fs7.mkdirSync(dir, { recursive: true });
623
+ cachedOutputDir = dir;
624
+ return dir;
625
+ }
626
+
627
+ // src/providers/gemini/commands/generate-video.ts
628
+ var BASE2 = "https://generativelanguage.googleapis.com";
629
+ function registerGenerateVideoCommand(parent) {
630
+ 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(
631
+ "--reference-images <paths...>",
632
+ "Reference images (first=opening frame, second=closing)"
633
+ ).option("--upload-timeout <ms>", "Upload timeout in ms", "120000").option("--output <path>", "Copy generated video to this path").option("-v, --verbose", "Verbose logging").action(
634
+ async (opts) => {
635
+ const logger = createLogger({ verbose: opts.verbose ?? false });
636
+ const apiKey = resolveKey("GEMINI_API_KEY");
637
+ if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
638
+ const model = opts.model ?? getDefaultModel("generate-video");
639
+ if (!model.startsWith("veo-")) {
640
+ throw new ValidationError(
641
+ `Video generation requires a Veo model (got '${model}'). Use: veo-3.1-generate-preview, veo-3.0-generate-001, etc.`
642
+ );
643
+ }
644
+ if (!["720p", "1080p"].includes(opts.resolution)) {
645
+ throw new ValidationError("--resolution must be 720p or 1080p");
646
+ }
647
+ if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
648
+ throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
649
+ }
650
+ logger.debug(`Submitting video generation with ${model}...`);
651
+ logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
652
+ const config = {
653
+ aspectRatio: opts.aspectRatio,
654
+ resolution: opts.resolution
655
+ };
656
+ const contentParts = [{ text: opts.prompt }];
657
+ if (opts.referenceImages?.length) {
658
+ for (const imgPath of opts.referenceImages.slice(0, 2)) {
659
+ if (!fs8.existsSync(imgPath))
660
+ throw new ValidationError(`Reference image not found: ${imgPath}`);
661
+ const data = fs8.readFileSync(imgPath).toString("base64");
662
+ const ext = path7.extname(imgPath).toLowerCase();
663
+ const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
664
+ contentParts.push({ inlineData: { mimeType, data } });
665
+ }
666
+ }
667
+ const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
668
+ let operation;
669
+ try {
670
+ operation = await httpJson({
671
+ url: `${BASE2}/v1beta/models/${model}:generateVideos`,
672
+ method: "POST",
673
+ headers,
674
+ body: {
675
+ contents: [{ parts: contentParts }],
676
+ generationConfig: config
677
+ }
678
+ });
679
+ } catch (e) {
680
+ const msg = e instanceof Error ? e.message : String(e);
681
+ throw new ProviderError(`Video generation submit failed: ${msg}`, "gemini");
682
+ }
683
+ const operationName = operation.name;
684
+ logger.debug(`Operation: ${operationName}`);
685
+ const deadline = Date.now() + 10 * 60 * 1e3;
686
+ let done = false;
687
+ let finalOp = operation;
688
+ while (!done && Date.now() < deadline) {
689
+ await sleep2(1e4);
690
+ finalOp = await httpJson({
691
+ url: `${BASE2}/v1beta/${operationName}`,
692
+ headers
693
+ });
694
+ done = finalOp.done === true;
695
+ if (!done)
696
+ logger.debug(
697
+ `Still generating... (${Math.round((deadline - Date.now()) / 1e3)}s left)`
698
+ );
699
+ }
700
+ if (!done) throw new ProviderError("Video generation timed out after 10 minutes", "gemini");
701
+ if (finalOp.error)
702
+ throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
703
+ const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
704
+ if (!videoUri) {
705
+ throw new ProviderError(
706
+ `Unexpected Veo response structure: ${JSON.stringify(finalOp.response)}`,
707
+ "gemini"
708
+ );
709
+ }
710
+ const outDir = getOutputDir();
711
+ const dest = path7.join(outDir, `veo_generated_${Date.now()}.mp4`);
712
+ const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
713
+ await downloadFile2(videoUri, dest);
714
+ logger.success(`Saved video: ${dest}`);
715
+ if (opts.output) {
716
+ fs8.mkdirSync(path7.dirname(path7.resolve(opts.output)), { recursive: true });
717
+ fs8.copyFileSync(dest, opts.output);
718
+ logger.success(`Copied to: ${opts.output}`);
719
+ }
720
+ console.log(`
721
+ Generated video: ${dest}`);
722
+ }
723
+ );
724
+ }
725
+ function sleep2(ms) {
726
+ return new Promise((r) => setTimeout(r, ms));
727
+ }
728
+
729
+ // src/providers/gemini/commands/generate.ts
730
+ init_errors();
731
+ import fs9 from "fs";
732
+ import path8 from "path";
733
+ function registerGenerateCommand(parent) {
734
+ 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").option("-v, --verbose", "Verbose logging").action(
735
+ async (opts) => {
736
+ const logger = createLogger({ verbose: opts.verbose ?? false });
737
+ const model = opts.model ?? getDefaultModel("generate");
738
+ const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 4);
739
+ if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
740
+ throw new ValidationError(
741
+ `Invalid aspect ratio: ${opts.aspectRatio}. Valid: ${ASPECT_RATIOS.join(", ")}`
742
+ );
743
+ }
744
+ if (opts.size && !IMAGE_SIZES.includes(opts.size)) {
745
+ throw new ValidationError(`Invalid size: ${opts.size}. Valid: ${IMAGE_SIZES.join(", ")}`);
746
+ }
747
+ logger.debug(`Generating ${numImages} image(s) with model: ${model}`);
748
+ logger.debug(`Aspect ratio: ${opts.aspectRatio}${opts.size ? `, size: ${opts.size}` : ""}`);
749
+ const imageConfig = { aspectRatio: opts.aspectRatio };
750
+ if (opts.size) imageConfig.imageSize = opts.size;
751
+ const generationConfig = {
752
+ responseModalities: ["IMAGE"],
753
+ imageConfig
754
+ };
755
+ const savedFiles = [];
756
+ for (let i = 0; i < numImages; i++) {
757
+ try {
758
+ const resp = await generateContent({
759
+ model,
760
+ contents: [{ parts: [{ text: opts.prompt }] }],
761
+ generationConfig
762
+ });
763
+ const images = extractImages(resp);
764
+ if (images.length === 0) {
765
+ logger.warn(
766
+ `Request ${i + 1}: no images in response \u2014 model may not support image generation`
767
+ );
768
+ continue;
769
+ }
770
+ for (const img of images) {
771
+ const outDir = getOutputDir();
772
+ const ext = img.mimeType.includes("png") ? "png" : "jpg";
773
+ const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
774
+ const dest = path8.join(outDir, fname);
775
+ fs9.writeFileSync(dest, Buffer.from(img.data, "base64"));
776
+ savedFiles.push(dest);
777
+ logger.success(`Saved: ${dest}`);
778
+ }
779
+ } catch (e) {
780
+ const msg = e instanceof Error ? e.message : String(e);
781
+ logger.error(`Generation failed: ${msg}`);
782
+ process.exit(1);
783
+ }
784
+ }
785
+ if (savedFiles.length === 0) {
786
+ logger.error("No images were generated");
787
+ process.exit(1);
788
+ }
789
+ if (opts.output && savedFiles[0]) {
790
+ fs9.mkdirSync(path8.dirname(path8.resolve(opts.output)), { recursive: true });
791
+ fs9.copyFileSync(savedFiles[0], opts.output);
792
+ logger.success(`Copied to: ${opts.output}`);
793
+ }
794
+ console.log(`
795
+ Generated ${savedFiles.length} image(s):`);
796
+ for (const f of savedFiles) console.log(` ${f}`);
797
+ }
798
+ );
799
+ }
800
+
801
+ // src/providers/gemini/commands/transcribe.ts
802
+ init_errors();
803
+ import fs10 from "fs";
804
+ function registerTranscribeCommand(parent) {
805
+ 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(
806
+ async (opts) => {
807
+ const logger = createLogger({ verbose: opts.verbose ?? false });
808
+ const model = opts.model ?? getDefaultModel("transcribe");
809
+ for (const f of opts.files) {
810
+ if (!fs10.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
811
+ }
812
+ const results = [];
813
+ for (let i = 0; i < opts.files.length; i++) {
814
+ const filePath = opts.files[i];
815
+ logger.debug(`[${i + 1}/${opts.files.length}] Transcribing: ${filePath}`);
816
+ try {
817
+ const useApi = shouldUseFileApi(filePath);
818
+ let contentParts;
819
+ if (useApi) {
820
+ logger.debug("File >20MB \u2014 using Files API");
821
+ const ref = await uploadFile(filePath, { logger });
822
+ contentParts = [
823
+ { text: opts.prompt },
824
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
825
+ ];
826
+ } else {
827
+ contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
828
+ }
829
+ const resp = await generateContent({
830
+ model,
831
+ contents: [{ parts: contentParts }]
832
+ });
833
+ results.push({
834
+ file: filePath,
835
+ status: "success",
836
+ response: extractText(resp)
837
+ });
838
+ } catch (e) {
839
+ const msg = e instanceof Error ? e.message : String(e);
840
+ logger.error(`Failed: ${filePath} \u2014 ${msg}`);
841
+ results.push({ file: filePath, status: "error", error: msg });
842
+ }
843
+ }
844
+ formatResults(results, opts.format, opts.output);
845
+ const failed = results.filter((r) => r.status === "error").length;
846
+ if (failed > 0) process.exit(1);
847
+ }
848
+ );
849
+ }
850
+
851
+ // src/providers/gemini/commands/index.ts
852
+ function registerGeminiCommands(program2) {
853
+ const gemini = program2.command("gemini").description("Gemini AI operations: analyze, transcribe, extract, generate images/video");
854
+ registerAnalyzeCommand(gemini);
855
+ registerTranscribeCommand(gemini);
856
+ registerExtractCommand(gemini);
857
+ registerGenerateCommand(gemini);
858
+ registerGenerateVideoCommand(gemini);
859
+ }
860
+
861
+ // src/providers/minimax/commands/generate-music.ts
862
+ init_errors();
863
+
864
+ // src/providers/minimax/client.ts
865
+ init_errors();
866
+ init_http_client();
867
+ var MINIMAX_BASE_URL = "https://api.minimax.io/v1";
868
+ function requireMinimaxKey() {
869
+ const key = resolveKey("MINIMAX_API_KEY");
870
+ if (!key) {
871
+ throw new ConfigError(
872
+ "MINIMAX_API_KEY is not set. Get one at https://platform.minimax.io/user-center/basic-information/interface-key"
873
+ );
874
+ }
875
+ return key;
876
+ }
877
+ function authHeaders2(apiKey) {
878
+ return {
879
+ Authorization: `Bearer ${apiKey}`,
880
+ "Content-Type": "application/json"
881
+ };
882
+ }
883
+ async function apiPost(endpoint, payload, apiKey, opts = {}) {
884
+ const url = `${MINIMAX_BASE_URL}/${endpoint}`;
885
+ opts.logger?.debug(`POST ${url}`);
886
+ const data = await httpJson({
887
+ url,
888
+ method: "POST",
889
+ headers: authHeaders2(apiKey),
890
+ body: payload,
891
+ timeoutMs: opts.timeoutMs ?? 12e4
892
+ });
893
+ const baseResp = data.base_resp;
894
+ if (baseResp && baseResp.status_code !== 0) {
895
+ throw new ProviderError(
896
+ `MiniMax API error (code ${baseResp.status_code}): ${baseResp.status_msg ?? "unknown"}`,
897
+ "minimax"
898
+ );
899
+ }
900
+ return data;
901
+ }
902
+ async function apiGet(endpoint, params, apiKey) {
903
+ const url = new URL(`${MINIMAX_BASE_URL}/${endpoint}`);
904
+ for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
905
+ return httpJson({ url: url.toString(), headers: authHeaders2(apiKey) });
906
+ }
907
+ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
908
+ const { intervalMs = 1e4, timeoutMs = 6e5, logger } = opts;
909
+ const deadline = Date.now() + timeoutMs;
910
+ let elapsed = 0;
911
+ while (Date.now() < deadline) {
912
+ const result = await apiGet(`query/${taskType}`, { task_id: taskId }, apiKey);
913
+ const status = result.status ?? "Unknown";
914
+ if (elapsed > 0 && elapsed % 3e4 < intervalMs) {
915
+ logger?.debug(`Polling... ${Math.round(elapsed / 1e3)}s elapsed, status: ${status}`);
916
+ }
917
+ if (status === "Success") return result;
918
+ if (status === "Failed" || status === "Error") {
919
+ throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
920
+ }
921
+ await sleep3(intervalMs);
922
+ elapsed += intervalMs;
923
+ }
924
+ throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
925
+ }
926
+ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
927
+ const result = await apiGet(
928
+ "files/retrieve",
929
+ { file_id: fileId },
930
+ apiKey
931
+ );
932
+ const downloadUrl = result.file?.download_url;
933
+ if (!downloadUrl) {
934
+ throw new ProviderError(`No download URL for file ${fileId}`, "minimax");
935
+ }
936
+ logger?.debug(`Downloading to: ${dest}`);
937
+ await downloadFile(downloadUrl, dest);
938
+ return dest;
939
+ }
940
+ function sleep3(ms) {
941
+ return new Promise((r) => setTimeout(r, ms));
942
+ }
943
+
944
+ // src/providers/minimax/generators/music.ts
945
+ init_http_client();
946
+ import fs11 from "fs";
947
+ import path9 from "path";
948
+ async function generateMinimaxMusic(opts) {
949
+ const {
950
+ apiKey,
951
+ lyrics = "",
952
+ prompt = "",
953
+ model = "music-2.5",
954
+ outputFormat = "mp3",
955
+ output,
956
+ logger
957
+ } = opts;
958
+ if (!lyrics && !prompt) {
959
+ return {
960
+ status: "error",
961
+ error: "Either --lyrics or --prompt is required for music generation"
962
+ };
963
+ }
964
+ const payload = {
965
+ model,
966
+ output_format: "url",
967
+ audio_setting: {
968
+ sample_rate: 44100,
969
+ bitrate: 128e3,
970
+ format: outputFormat,
971
+ channel: 1
972
+ }
973
+ };
974
+ if (lyrics) payload.lyrics = lyrics.slice(0, 3500);
975
+ if (prompt) payload.prompt = prompt.slice(0, 2e3);
976
+ logger?.debug(`Generating music with ${model}...`);
977
+ let resp;
978
+ try {
979
+ resp = await apiPost("music_generation", payload, apiKey, {
980
+ logger,
981
+ timeoutMs: 3e5
982
+ });
983
+ } catch (e) {
984
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
985
+ }
986
+ const audioData = resp.data?.audio;
987
+ if (!audioData) {
988
+ return { status: "error", error: "No audio in response" };
989
+ }
990
+ const durationMs = resp.extra_info?.music_duration ?? 0;
991
+ const outDir = getOutputDir();
992
+ const dest = path9.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
993
+ try {
994
+ if (audioData.startsWith("http")) {
995
+ const bytes = await fetchBytes(audioData);
996
+ fs11.writeFileSync(dest, bytes);
997
+ } else {
998
+ const audioBytes = Buffer.from(audioData, "hex");
999
+ fs11.writeFileSync(dest, audioBytes);
1000
+ }
1001
+ } catch (e) {
1002
+ return {
1003
+ status: "error",
1004
+ error: `Failed to save audio: ${e instanceof Error ? e.message : String(e)}`
1005
+ };
1006
+ }
1007
+ const durSec = durationMs / 1e3;
1008
+ logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
1009
+ if (output) {
1010
+ fs11.mkdirSync(path9.dirname(path9.resolve(output)), { recursive: true });
1011
+ fs11.copyFileSync(dest, output);
1012
+ }
1013
+ return { status: "success", generatedAudio: dest, durationMs, model };
1014
+ }
1015
+
1016
+ // src/providers/minimax/models.ts
1017
+ var MINIMAX_IMAGE_MODELS = /* @__PURE__ */ new Set(["image-01", "image-01-live"]);
1018
+ var MINIMAX_VIDEO_MODELS = /* @__PURE__ */ new Set([
1019
+ "MiniMax-Hailuo-2.3",
1020
+ "MiniMax-Hailuo-2.3-Fast",
1021
+ "MiniMax-Hailuo-02",
1022
+ "S2V-01"
1023
+ ]);
1024
+ var MINIMAX_SPEECH_MODELS = /* @__PURE__ */ new Set([
1025
+ "speech-2.8-hd",
1026
+ "speech-2.8-turbo",
1027
+ "speech-2.6-hd",
1028
+ "speech-2.6-turbo",
1029
+ "speech-02-hd",
1030
+ "speech-02-turbo"
1031
+ ]);
1032
+ var MINIMAX_MUSIC_MODELS = /* @__PURE__ */ new Set(["music-2.5", "music-2.0"]);
1033
+ var ALL_MINIMAX_MODELS = /* @__PURE__ */ new Set([
1034
+ ...MINIMAX_IMAGE_MODELS,
1035
+ ...MINIMAX_VIDEO_MODELS,
1036
+ ...MINIMAX_SPEECH_MODELS,
1037
+ ...MINIMAX_MUSIC_MODELS
1038
+ ]);
1039
+ var TASK_DEFAULTS = {
1040
+ image: process.env.MINIMAX_IMAGE_MODEL ?? "image-01",
1041
+ video: process.env.MINIMAX_VIDEO_MODEL ?? "MiniMax-Hailuo-2.3",
1042
+ speech: process.env.MINIMAX_SPEECH_MODEL ?? "speech-2.8-hd",
1043
+ music: process.env.MINIMAX_MUSIC_MODEL ?? "music-2.5"
1044
+ };
1045
+ var VIDEO_DURATIONS = [6, 10];
1046
+ var VIDEO_RESOLUTIONS = ["720P", "1080P"];
1047
+ var SPEECH_FORMATS = ["mp3", "wav", "flac", "pcm"];
1048
+
1049
+ // src/providers/minimax/commands/generate-music.ts
1050
+ function registerMinimaxGenerateMusicCommand(parent) {
1051
+ parent.command("generate-music").description("Generate music with MiniMax").option("--lyrics <str>", "Song lyrics (use --lyrics or --prompt)").option("--prompt <str>", "Music style/description prompt").option("--model <id>", `Music model (${[...MINIMAX_MUSIC_MODELS].join("|")})`).option("--output-format <fmt>", "Audio format: mp3|wav|flac", "mp3").option("--output <path>", "Copy output audio to this path").option("-v, --verbose", "Verbose logging").action(
1052
+ async (opts) => {
1053
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1054
+ const apiKey = requireMinimaxKey();
1055
+ if (!opts.lyrics && !opts.prompt) {
1056
+ throw new ValidationError("Either --lyrics or --prompt is required");
1057
+ }
1058
+ const model = opts.model ?? TASK_DEFAULTS.music;
1059
+ const result = await generateMinimaxMusic({
1060
+ apiKey,
1061
+ lyrics: opts.lyrics,
1062
+ prompt: opts.prompt,
1063
+ model,
1064
+ outputFormat: opts.outputFormat,
1065
+ output: opts.output,
1066
+ logger
1067
+ });
1068
+ if (result.status === "error") {
1069
+ logger.error(result.error ?? "Unknown error");
1070
+ process.exit(1);
1071
+ }
1072
+ console.log(`
1073
+ Generated music: ${result.generatedAudio}`);
1074
+ if (result.durationMs) {
1075
+ console.log(`Duration: ${(result.durationMs / 1e3).toFixed(1)}s`);
1076
+ }
1077
+ }
1078
+ );
1079
+ }
1080
+
1081
+ // src/providers/minimax/commands/generate-speech.ts
1082
+ init_errors();
1083
+
1084
+ // src/providers/minimax/generators/speech.ts
1085
+ import fs12 from "fs";
1086
+ import path10 from "path";
1087
+ async function generateMinimaxSpeech(opts) {
1088
+ const {
1089
+ apiKey,
1090
+ text,
1091
+ model = "speech-2.8-hd",
1092
+ voice = "English_expressive_narrator",
1093
+ emotion = "neutral",
1094
+ outputFormat = "mp3",
1095
+ rate = 1,
1096
+ output,
1097
+ logger
1098
+ } = opts;
1099
+ const payload = {
1100
+ model,
1101
+ text: text.slice(0, 1e4),
1102
+ stream: false,
1103
+ language_boost: "auto",
1104
+ output_format: "hex",
1105
+ voice_setting: {
1106
+ voice_id: voice,
1107
+ speed: rate,
1108
+ vol: 1,
1109
+ pitch: 0,
1110
+ emotion
1111
+ },
1112
+ audio_setting: {
1113
+ sample_rate: 32e3,
1114
+ bitrate: 128e3,
1115
+ format: outputFormat,
1116
+ channel: 1
1117
+ }
1118
+ };
1119
+ logger?.debug(`Generating speech with ${model}, voice: ${voice}`);
1120
+ let resp;
1121
+ try {
1122
+ resp = await apiPost("t2a_v2", payload, apiKey, { logger });
1123
+ } catch (e) {
1124
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
1125
+ }
1126
+ const audioHex = resp.data?.audio;
1127
+ if (!audioHex) {
1128
+ return { status: "error", error: "No audio in response" };
1129
+ }
1130
+ const audioBytes = Buffer.from(audioHex, "hex");
1131
+ const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
1132
+ const outDir = getOutputDir();
1133
+ const dest = path10.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
1134
+ fs12.writeFileSync(dest, audioBytes);
1135
+ logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
1136
+ if (output) {
1137
+ fs12.mkdirSync(path10.dirname(path10.resolve(output)), { recursive: true });
1138
+ fs12.copyFileSync(dest, output);
1139
+ }
1140
+ return { status: "success", generatedAudio: dest, model };
1141
+ }
1142
+
1143
+ // src/providers/minimax/commands/generate-speech.ts
1144
+ function registerMinimaxGenerateSpeechCommand(parent) {
1145
+ parent.command("generate-speech").description("Generate speech (TTS) with MiniMax").option("--text <str>", "Text to speak (use --text or --prompt)").option("--prompt <str>", "Alias for --text").option("--model <id>", `Speech model (${[...MINIMAX_SPEECH_MODELS].join("|")})`).option("--voice <id>", "Voice ID", "English_expressive_narrator").option(
1146
+ "--emotion <e>",
1147
+ "Emotion (neutral|happy|sad|angry|fearful|disgusted|surprised)",
1148
+ "neutral"
1149
+ ).option("--output-format <fmt>", `Audio format (${SPEECH_FORMATS.join("|")})`, "mp3").option("--rate <n>", "Speech rate (0.5\u20132.0)", "1.0").option("--output <path>", "Copy output audio to this path").option("-v, --verbose", "Verbose logging").action(
1150
+ async (opts) => {
1151
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1152
+ const apiKey = requireMinimaxKey();
1153
+ const text = opts.text ?? opts.prompt;
1154
+ if (!text) throw new ValidationError("Either --text or --prompt is required");
1155
+ if (!SPEECH_FORMATS.includes(opts.outputFormat)) {
1156
+ throw new ValidationError(`--output-format must be one of: ${SPEECH_FORMATS.join(", ")}`);
1157
+ }
1158
+ const model = opts.model ?? TASK_DEFAULTS.speech;
1159
+ const rate = Number.parseFloat(opts.rate);
1160
+ const result = await generateMinimaxSpeech({
1161
+ apiKey,
1162
+ text,
1163
+ model,
1164
+ voice: opts.voice,
1165
+ emotion: opts.emotion,
1166
+ outputFormat: opts.outputFormat,
1167
+ rate,
1168
+ output: opts.output,
1169
+ logger
1170
+ });
1171
+ if (result.status === "error") {
1172
+ logger.error(result.error ?? "Unknown error");
1173
+ process.exit(1);
1174
+ }
1175
+ console.log(`
1176
+ Generated audio: ${result.generatedAudio}`);
1177
+ }
1178
+ );
1179
+ }
1180
+
1181
+ // src/providers/minimax/commands/generate-video.ts
1182
+ init_errors();
1183
+
1184
+ // src/providers/minimax/generators/video.ts
1185
+ import fs13 from "fs";
1186
+ import path11 from "path";
1187
+ async function generateMinimaxVideo(opts) {
1188
+ const {
1189
+ apiKey,
1190
+ prompt,
1191
+ model = "MiniMax-Hailuo-2.3",
1192
+ duration = 6,
1193
+ resolution = "1080P",
1194
+ firstFrame,
1195
+ output,
1196
+ logger
1197
+ } = opts;
1198
+ const payload = { prompt, model, duration, resolution };
1199
+ if (firstFrame) payload.first_frame_image = firstFrame;
1200
+ logger?.debug(`Submitting video generation with ${model}...`);
1201
+ let taskResp;
1202
+ try {
1203
+ taskResp = await apiPost("video_generation", payload, apiKey, {
1204
+ logger,
1205
+ timeoutMs: 6e4
1206
+ });
1207
+ } catch (e) {
1208
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
1209
+ }
1210
+ const taskId = taskResp.task_id;
1211
+ if (!taskId) {
1212
+ return { status: "error", error: `No task_id in response: ${JSON.stringify(taskResp)}` };
1213
+ }
1214
+ logger?.debug(`Task ID: ${taskId}, polling...`);
1215
+ const start = Date.now();
1216
+ let pollResult;
1217
+ try {
1218
+ pollResult = await pollAsyncTask(taskId, "video_generation", apiKey, {
1219
+ intervalMs: 1e4,
1220
+ timeoutMs: 6e5,
1221
+ logger
1222
+ });
1223
+ } catch (e) {
1224
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
1225
+ }
1226
+ const fileId = pollResult.file_id;
1227
+ if (!fileId) {
1228
+ return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
1229
+ }
1230
+ const outDir = getOutputDir();
1231
+ const dest = path11.join(outDir, `minimax_video_${Date.now()}.mp4`);
1232
+ try {
1233
+ await downloadMinimaxFile(fileId, apiKey, dest, logger);
1234
+ } catch (e) {
1235
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
1236
+ }
1237
+ const elapsed = (Date.now() - start) / 1e3;
1238
+ const stat = fs13.statSync(dest);
1239
+ const fileSizeMb = stat.size / (1024 * 1024);
1240
+ if (output) {
1241
+ fs13.mkdirSync(path11.dirname(path11.resolve(output)), { recursive: true });
1242
+ fs13.copyFileSync(dest, output);
1243
+ }
1244
+ logger?.debug(`Generated in ${elapsed.toFixed(1)}s, size: ${fileSizeMb.toFixed(2)} MB`);
1245
+ return {
1246
+ status: "success",
1247
+ generatedVideo: dest,
1248
+ generationTime: elapsed,
1249
+ fileSizeMb,
1250
+ model
1251
+ };
1252
+ }
1253
+
1254
+ // src/providers/minimax/commands/generate-video.ts
1255
+ function registerMinimaxGenerateVideoCommand(parent) {
1256
+ parent.command("generate-video").description("Generate video with MiniMax Hailuo models (async)").requiredOption("--prompt <text>", "Video generation prompt").option("--model <id>", `Model (${[...MINIMAX_VIDEO_MODELS].join("|")})`).option("--duration <n>", `Duration in seconds (${VIDEO_DURATIONS.join("|")})`, "6").option("--resolution <res>", `Resolution (${VIDEO_RESOLUTIONS.join("|")})`, "1080P").option("--first-frame <url>", "First frame image URL").option("--output <path>", "Copy generated video to this path").option("-v, --verbose", "Verbose logging").action(
1257
+ async (opts) => {
1258
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1259
+ const apiKey = requireMinimaxKey();
1260
+ const model = opts.model ?? TASK_DEFAULTS.video;
1261
+ const duration = Number.parseInt(opts.duration, 10);
1262
+ if (!VIDEO_DURATIONS.includes(duration)) {
1263
+ throw new ValidationError(`--duration must be one of: ${VIDEO_DURATIONS.join(", ")}`);
1264
+ }
1265
+ const resolution = opts.resolution.toUpperCase();
1266
+ if (!VIDEO_RESOLUTIONS.includes(resolution)) {
1267
+ throw new ValidationError(`--resolution must be one of: ${VIDEO_RESOLUTIONS.join(", ")}`);
1268
+ }
1269
+ if (opts.firstFrame && !opts.firstFrame.startsWith("http")) {
1270
+ throw new ValidationError("--first-frame must be an http(s) URL");
1271
+ }
1272
+ logger.info("Video generation is async and may take several minutes...");
1273
+ const result = await generateMinimaxVideo({
1274
+ apiKey,
1275
+ prompt: opts.prompt,
1276
+ model,
1277
+ duration,
1278
+ resolution,
1279
+ firstFrame: opts.firstFrame,
1280
+ output: opts.output,
1281
+ logger
1282
+ });
1283
+ if (result.status === "error") {
1284
+ logger.error(result.error ?? "Unknown error");
1285
+ process.exit(1);
1286
+ }
1287
+ console.log(`
1288
+ Generated video: ${result.generatedVideo}`);
1289
+ if (result.generationTime)
1290
+ console.log(`Generation time: ${result.generationTime.toFixed(1)}s`);
1291
+ if (result.fileSizeMb) console.log(`File size: ${result.fileSizeMb.toFixed(2)} MB`);
1292
+ }
1293
+ );
1294
+ }
1295
+
1296
+ // src/providers/minimax/generators/image.ts
1297
+ init_errors();
1298
+ init_http_client();
1299
+ import fs14 from "fs";
1300
+ import path12 from "path";
1301
+ async function generateMinimaxImage(opts) {
1302
+ const {
1303
+ apiKey,
1304
+ prompt,
1305
+ model = "image-01",
1306
+ aspectRatio = "1:1",
1307
+ numImages = 1,
1308
+ output,
1309
+ logger
1310
+ } = opts;
1311
+ const payload = {
1312
+ model,
1313
+ prompt,
1314
+ aspect_ratio: aspectRatio,
1315
+ n: Math.min(numImages, 9),
1316
+ response_format: "url",
1317
+ prompt_optimizer: true
1318
+ };
1319
+ logger?.debug(`Generating ${numImages} image(s) with ${model}`);
1320
+ let resp;
1321
+ try {
1322
+ resp = await apiPost("image_generation", payload, apiKey, { logger });
1323
+ } catch (e) {
1324
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
1325
+ }
1326
+ const imageUrls = resp.data?.image_urls ?? [];
1327
+ if (imageUrls.length === 0) {
1328
+ return { status: "error", error: "No images in response" };
1329
+ }
1330
+ const outDir = getOutputDir();
1331
+ const savedFiles = [];
1332
+ for (let i = 0; i < imageUrls.length; i++) {
1333
+ const url = imageUrls[i];
1334
+ if (!url) continue;
1335
+ try {
1336
+ const bytes = await fetchBytes(url);
1337
+ const fname = `minimax_image_${Date.now()}_${i}.png`;
1338
+ const dest = path12.join(outDir, fname);
1339
+ fs14.writeFileSync(dest, bytes);
1340
+ savedFiles.push(dest);
1341
+ logger?.success(`Saved: ${dest}`);
1342
+ } catch (e) {
1343
+ throw new ProviderError(
1344
+ `Failed to download image: ${e instanceof Error ? e.message : String(e)}`,
1345
+ "minimax"
1346
+ );
1347
+ }
1348
+ }
1349
+ if (output && savedFiles[0]) {
1350
+ fs14.mkdirSync(path12.dirname(path12.resolve(output)), { recursive: true });
1351
+ fs14.copyFileSync(savedFiles[0], output);
1352
+ }
1353
+ return { status: "success", generatedImages: savedFiles, model };
1354
+ }
1355
+
1356
+ // src/providers/minimax/commands/generate.ts
1357
+ function registerMinimaxGenerateCommand(parent) {
1358
+ parent.command("generate").description("Generate images with MiniMax").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", `Model (${[...MINIMAX_IMAGE_MODELS].join("|")})`).option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9)", "1:1").option("--num-images <n>", "Number of images (1-9)", "1").option("--output <path>", "Copy first image to this path").option("-v, --verbose", "Verbose logging").action(
1359
+ async (opts) => {
1360
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1361
+ const apiKey = requireMinimaxKey();
1362
+ const model = opts.model ?? TASK_DEFAULTS.image;
1363
+ const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 9);
1364
+ const result = await generateMinimaxImage({
1365
+ apiKey,
1366
+ prompt: opts.prompt,
1367
+ model,
1368
+ aspectRatio: opts.aspectRatio,
1369
+ numImages,
1370
+ output: opts.output,
1371
+ logger
1372
+ });
1373
+ if (result.status === "error") {
1374
+ logger.error(result.error ?? "Unknown error");
1375
+ process.exit(1);
1376
+ }
1377
+ console.log(`
1378
+ Generated ${result.generatedImages?.length ?? 0} image(s):`);
1379
+ for (const f of result.generatedImages ?? []) console.log(` ${f}`);
1380
+ }
1381
+ );
1382
+ }
1383
+
1384
+ // src/providers/minimax/commands/index.ts
1385
+ function registerMinimaxCommands(program2) {
1386
+ const minimax = program2.command("minimax").description("MiniMax generation: images, video, speech, and music");
1387
+ registerMinimaxGenerateCommand(minimax);
1388
+ registerMinimaxGenerateVideoCommand(minimax);
1389
+ registerMinimaxGenerateSpeechCommand(minimax);
1390
+ registerMinimaxGenerateMusicCommand(minimax);
1391
+ }
1392
+
1393
+ // src/providers/openrouter/client.ts
1394
+ import fs15 from "fs";
1395
+ import path13 from "path";
1396
+ init_errors();
1397
+ init_http_client();
1398
+ var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
1399
+ var DEFAULT_OPENROUTER_MODEL = "google/gemini-3.1-flash-image-preview";
1400
+ function requireOpenRouterKey() {
1401
+ const key = resolveKey("OPENROUTER_API_KEY");
1402
+ if (!key) {
1403
+ throw new ConfigError(
1404
+ "OPENROUTER_API_KEY is not set. Get one at https://openrouter.ai/settings/keys"
1405
+ );
1406
+ }
1407
+ return key;
1408
+ }
1409
+ function buildHeaders(apiKey) {
1410
+ const headers = {
1411
+ Authorization: `Bearer ${apiKey}`,
1412
+ "Content-Type": "application/json"
1413
+ };
1414
+ const referer = resolveKey("OPENROUTER_SITE_URL");
1415
+ const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
1416
+ if (referer) headers["HTTP-Referer"] = referer;
1417
+ if (title) headers["X-Title"] = title;
1418
+ return headers;
1419
+ }
1420
+ function buildPayload(opts) {
1421
+ const payload = {
1422
+ messages: [{ role: "user", content: opts.prompt }],
1423
+ modalities: opts.model.includes("gemini") ? ["image", "text"] : ["image"],
1424
+ image_config: { aspect_ratio: opts.aspectRatio }
1425
+ };
1426
+ if (opts.imageSize) {
1427
+ payload.image_config.image_size = opts.imageSize;
1428
+ }
1429
+ if (opts.fallbackModels.length > 0) {
1430
+ payload.models = [opts.model, ...opts.fallbackModels];
1431
+ } else {
1432
+ payload.model = opts.model;
1433
+ }
1434
+ return payload;
1435
+ }
1436
+ async function generateOpenRouterImage(opts) {
1437
+ const { prompt, model, aspectRatio = "1:1", imageSize, numImages = 1, output, logger } = opts;
1438
+ const apiKey = requireOpenRouterKey();
1439
+ const fallbackModels = (resolveKey("OPENROUTER_FALLBACK_MODELS") ?? "").split(",").map((m) => m.trim()).filter(Boolean);
1440
+ const imageUrls = [];
1441
+ let usedModel = model;
1442
+ for (let i = 0; i < Math.max(1, numImages); i++) {
1443
+ const payload = buildPayload({ prompt, model, aspectRatio, imageSize, fallbackModels });
1444
+ logger?.debug(
1445
+ `OpenRouter model: ${model}${fallbackModels.length ? ` (fallbacks: ${fallbackModels.join(", ")})` : ""}`
1446
+ );
1447
+ let data;
1448
+ try {
1449
+ data = await httpJson({
1450
+ url: OPENROUTER_API_URL,
1451
+ method: "POST",
1452
+ headers: buildHeaders(apiKey),
1453
+ body: payload,
1454
+ timeoutMs: 24e4
1455
+ });
1456
+ } catch (e) {
1457
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
1458
+ }
1459
+ const choices = data.choices ?? [];
1460
+ if (choices.length === 0) {
1461
+ return { status: "error", error: `No choices in response: ${JSON.stringify(data)}` };
1462
+ }
1463
+ const messageImages = choices[0]?.message?.images ?? [];
1464
+ if (messageImages.length === 0) {
1465
+ return {
1466
+ status: "error",
1467
+ error: `No images in response \u2014 model '${model}' may not support image generation`
1468
+ };
1469
+ }
1470
+ for (const img of messageImages) {
1471
+ const url = img.image_url?.url;
1472
+ if (url) imageUrls.push(url);
1473
+ }
1474
+ usedModel = data.model ?? usedModel;
1475
+ if (imageUrls.length >= numImages) break;
1476
+ }
1477
+ const outDir = getOutputDir();
1478
+ const savedFiles = [];
1479
+ for (let i = 0; i < Math.min(imageUrls.length, Math.max(1, numImages)); i++) {
1480
+ const url = imageUrls[i];
1481
+ if (!url) continue;
1482
+ try {
1483
+ const bytes = await fetchBytes(url);
1484
+ const dest = path13.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
1485
+ fs15.writeFileSync(dest, bytes);
1486
+ savedFiles.push(dest);
1487
+ logger?.success(`Saved: ${dest}`);
1488
+ } catch (e) {
1489
+ throw new ProviderError(
1490
+ `Failed to save image: ${e instanceof Error ? e.message : String(e)}`,
1491
+ "openrouter"
1492
+ );
1493
+ }
1494
+ }
1495
+ if (output && savedFiles[0]) {
1496
+ fs15.mkdirSync(path13.dirname(path13.resolve(output)), { recursive: true });
1497
+ fs15.writeFileSync(output, fs15.readFileSync(savedFiles[0]));
1498
+ }
1499
+ return { status: "success", generatedImages: savedFiles, model: usedModel };
1500
+ }
1501
+
1502
+ // src/providers/openrouter/commands/generate.ts
1503
+ function registerOpenRouterGenerateCommand(parent) {
1504
+ parent.command("generate").description("Generate images via OpenRouter (uses chat completions API)").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "OpenRouter model id (e.g. google/gemini-3.1-flash-image-preview)").option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9)", "1:1").option("--image-size <size>", "Image size hint (e.g. 1K, 2K)").option("--num-images <n>", "Number of images to generate", "1").option("--output <path>", "Copy first image to this path").option("-v, --verbose", "Verbose logging").action(
1505
+ async (opts) => {
1506
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1507
+ const model = opts.model ?? resolveKey("OPENROUTER_IMAGE_MODEL") ?? DEFAULT_OPENROUTER_MODEL;
1508
+ const numImages = Math.max(1, Number.parseInt(opts.numImages, 10) || 1);
1509
+ logger.debug(`Generating ${numImages} image(s) via OpenRouter: ${model}`);
1510
+ const result = await generateOpenRouterImage({
1511
+ prompt: opts.prompt,
1512
+ model,
1513
+ aspectRatio: opts.aspectRatio,
1514
+ imageSize: opts.imageSize,
1515
+ numImages,
1516
+ output: opts.output,
1517
+ logger
1518
+ });
1519
+ if (result.status === "error") {
1520
+ logger.error(result.error ?? "Unknown error");
1521
+ process.exit(1);
1522
+ }
1523
+ console.log(`
1524
+ Generated ${result.generatedImages?.length ?? 0} image(s):`);
1525
+ for (const f of result.generatedImages ?? []) console.log(` ${f}`);
1526
+ if (result.model) console.log(`Model used: ${result.model}`);
1527
+ }
1528
+ );
1529
+ }
1530
+
1531
+ // src/providers/openrouter/commands/index.ts
1532
+ function registerOpenRouterCommands(program2) {
1533
+ const openrouter = program2.command("openrouter").description("OpenRouter image generation via chat completions API");
1534
+ registerOpenRouterGenerateCommand(openrouter);
1535
+ }
1536
+
1537
+ // src/commands/check-helpers/binary-check.ts
1538
+ import { execa } from "execa";
1539
+ async function checkBinary(binary) {
1540
+ try {
1541
+ const result = await execa(binary, ["--version"], { reject: true, all: true });
1542
+ const version = (result.stdout || result.stderr || "").split("\n")[0]?.trim();
1543
+ return { available: true, version, binary };
1544
+ } catch (e) {
1545
+ const err2 = e;
1546
+ if (err2.code === "ENOENT") return { available: false, binary };
1547
+ const output = (err2.stderr ?? err2.all ?? "").toLowerCase();
1548
+ if (output.includes("not recognized") || output.includes("not found") || output.includes("no such file")) {
1549
+ return { available: false, binary };
1550
+ }
1551
+ if (err2.exitCode !== void 0) return { available: true, binary };
1552
+ return { available: false, binary };
1553
+ }
1554
+ }
1555
+
1556
+ // src/commands/check-helpers/gemini-ping.ts
1557
+ init_errors();
1558
+ init_http_client();
1559
+ var MODELS_URL = "https://generativelanguage.googleapis.com/v1beta/models";
1560
+ async function pingGemini(apiKey) {
1561
+ try {
1562
+ const resp = await httpJson({
1563
+ url: `${MODELS_URL}?key=${encodeURIComponent(apiKey)}`,
1564
+ timeoutMs: 1e4
1565
+ });
1566
+ const modelCount = Array.isArray(resp.models) ? resp.models.length : 0;
1567
+ return { status: "success", modelCount };
1568
+ } catch (e) {
1569
+ if (e instanceof HttpError) {
1570
+ if (e.status === 400 || e.status === 401 || e.status === 403) {
1571
+ return { status: "auth_error", error: `HTTP ${e.status}: ${e.snippet}` };
1572
+ }
1573
+ return { status: "network_error", error: `HTTP ${e.status}: ${e.snippet}` };
1574
+ }
1575
+ const msg = e instanceof Error ? e.message : String(e);
1576
+ return { status: "network_error", error: msg };
1577
+ }
1578
+ }
1579
+
1580
+ // src/commands/check-helpers/setup-hints.ts
1581
+ var SETUP_HINTS = `
1582
+ To configure multix, set at least one provider API key:
1583
+
1584
+ Provider URLs:
1585
+ Gemini / AI Studio : https://aistudio.google.com/apikey
1586
+ OpenRouter : https://openrouter.ai/settings/keys
1587
+ MiniMax : https://platform.minimax.io/user-center/basic-information/interface-key
1588
+
1589
+ Option A \u2014 user global config (recommended):
1590
+ echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
1591
+
1592
+ Option B \u2014 project .env:
1593
+ echo 'GEMINI_API_KEY=your-key' >> .env
1594
+
1595
+ Option C \u2014 shell environment:
1596
+ export GEMINI_API_KEY=your-key
1597
+ export OPENROUTER_API_KEY=your-openrouter-key # optional
1598
+ export MINIMAX_API_KEY=your-minimax-key # optional
1599
+
1600
+ After configuring, verify with:
1601
+ multix check
1602
+ `.trim();
1603
+ var FFMPEG_HINT = "ffmpeg not found \u2014 media optimize/split/batch commands unavailable.\n Linux: sudo apt-get install ffmpeg\n macOS: brew install ffmpeg\n Windows: https://ffmpeg.org/download.html";
1604
+ var MAGICK_HINT = "ImageMagick (magick) not found \u2014 image optimization unavailable.\n Linux: sudo apt-get install imagemagick\n macOS: brew install imagemagick\n Windows: https://imagemagick.org/script/download.php";
1605
+
1606
+ // src/commands/check.ts
1607
+ function registerCheckCommand(program2) {
1608
+ program2.command("check").description("Validate multix setup: tooling, API keys, and live Gemini connectivity").option("-v, --verbose", "Verbose output").action(async (opts) => {
1609
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1610
+ let exitCode = 0;
1611
+ logger.header("Tooling");
1612
+ const ffmpegStatus = await checkBinary("ffmpeg");
1613
+ const magickStatus = await checkBinary("magick");
1614
+ if (ffmpegStatus.available) {
1615
+ logger.success(`ffmpeg found${ffmpegStatus.version ? ` \u2014 ${ffmpegStatus.version}` : ""}`);
1616
+ } else {
1617
+ logger.warn("ffmpeg not found");
1618
+ if (opts.verbose) console.log(` ${FFMPEG_HINT}`);
1619
+ }
1620
+ if (magickStatus.available) {
1621
+ logger.success(`magick found${magickStatus.version ? ` \u2014 ${magickStatus.version}` : ""}`);
1622
+ } else {
1623
+ logger.warn("magick (ImageMagick) not found");
1624
+ if (opts.verbose) console.log(` ${MAGICK_HINT}`);
1625
+ }
1626
+ logger.header("API Keys");
1627
+ const geminiKey = resolveKey("GEMINI_API_KEY");
1628
+ const openrouterKey = resolveKey("OPENROUTER_API_KEY");
1629
+ const minimaxKey = resolveKey("MINIMAX_API_KEY");
1630
+ if (geminiKey) {
1631
+ logger.success(`GEMINI_API_KEY found \u2014 ${redact(geminiKey)}`);
1632
+ } else {
1633
+ logger.warn("GEMINI_API_KEY not set (Gemini analyze/transcribe/doc features unavailable)");
1634
+ }
1635
+ if (openrouterKey) {
1636
+ logger.success(`OPENROUTER_API_KEY found \u2014 ${redact(openrouterKey)}`);
1637
+ } else {
1638
+ logger.info("OPENROUTER_API_KEY not set (optional)");
1639
+ }
1640
+ if (minimaxKey) {
1641
+ logger.success(`MINIMAX_API_KEY found \u2014 ${redact(minimaxKey)}`);
1642
+ } else {
1643
+ logger.info("MINIMAX_API_KEY not set (optional)");
1644
+ }
1645
+ if (!geminiKey && !openrouterKey && !minimaxKey) {
1646
+ logger.header("No Provider Keys Configured");
1647
+ console.log(SETUP_HINTS);
1648
+ process.exit(1);
1649
+ }
1650
+ if (geminiKey) {
1651
+ logger.header("Gemini Connectivity");
1652
+ logger.info("Pinging Gemini models endpoint...");
1653
+ const ping = await pingGemini(geminiKey);
1654
+ if (ping.status === "success") {
1655
+ logger.success(`Gemini API reachable \u2014 ${ping.modelCount ?? 0} models available`);
1656
+ } else if (ping.status === "auth_error") {
1657
+ logger.error(`Gemini auth failed \u2014 check GEMINI_API_KEY: ${ping.error ?? ""}`);
1658
+ exitCode = 1;
1659
+ } else {
1660
+ logger.warn(`Gemini network error (API may be down): ${ping.error ?? ""}`);
1661
+ }
1662
+ }
1663
+ logger.header("Summary");
1664
+ if (exitCode === 0) {
1665
+ logger.success("multix is ready to use");
1666
+ if (geminiKey) {
1667
+ console.log(
1668
+ " Full Gemini multimodal setup (analyze, transcribe, generate, doc convert)"
1669
+ );
1670
+ } else {
1671
+ console.log(" Image generation via OpenRouter/MiniMax available");
1672
+ console.log(" Add GEMINI_API_KEY for analysis, transcription, and doc conversion");
1673
+ }
1674
+ } else {
1675
+ logger.error("One or more checks failed \u2014 see above");
1676
+ }
1677
+ process.exit(exitCode);
1678
+ });
1679
+ }
1680
+
1681
+ // src/commands/doc/convert.ts
1682
+ init_errors();
1683
+ import fs16 from "fs";
1684
+ import path14 from "path";
1685
+
1686
+ // src/commands/doc/default-prompt.ts
1687
+ var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
1688
+
1689
+ Requirements:
1690
+ - Preserve all content, structure, and formatting
1691
+ - Convert tables to markdown table format
1692
+ - Maintain heading hierarchy (# ## ### etc)
1693
+ - Preserve lists, code blocks, and quotes
1694
+ - Extract text from images if present
1695
+ - Keep formatting consistent and readable
1696
+
1697
+ Output only the markdown content without any preamble or explanation.`;
1698
+
1699
+ // src/commands/doc/convert.ts
1700
+ async function convertDocuments(opts) {
1701
+ const { inputs, output, autoName = false, verbose, logger } = opts;
1702
+ const model = opts.model ?? DOC_MODEL_DEFAULT;
1703
+ const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
1704
+ for (const f of inputs) {
1705
+ if (!fs16.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
1706
+ }
1707
+ let outputPath;
1708
+ if (output) {
1709
+ outputPath = path14.resolve(output);
1710
+ } else if (autoName && inputs.length === 1 && inputs[0]) {
1711
+ const base = path14.basename(inputs[0], path14.extname(inputs[0]));
1712
+ outputPath = path14.join(getOutputDir(), `${base}-extraction.md`);
1713
+ } else {
1714
+ outputPath = path14.join(getOutputDir(), "document-extraction.md");
1715
+ }
1716
+ fs16.mkdirSync(path14.dirname(outputPath), { recursive: true });
1717
+ const results = [];
1718
+ for (let i = 0; i < inputs.length; i++) {
1719
+ const filePath = inputs[i];
1720
+ logger?.debug(`[${i + 1}/${inputs.length}] Converting: ${filePath}`);
1721
+ let markdown;
1722
+ let errorMsg;
1723
+ for (let attempt = 0; attempt < 3; attempt++) {
1724
+ try {
1725
+ const useApi = shouldUseFileApi(filePath);
1726
+ let contentParts;
1727
+ if (useApi) {
1728
+ logger?.debug("File >20MB \u2014 using Files API");
1729
+ const ref = await uploadFile(filePath, { logger });
1730
+ contentParts = [
1731
+ { text: prompt },
1732
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
1733
+ ];
1734
+ } else {
1735
+ contentParts = [{ text: prompt }, inlinePart(filePath)];
1736
+ }
1737
+ const resp = await generateContent({
1738
+ model,
1739
+ contents: [{ parts: contentParts }]
1740
+ });
1741
+ markdown = extractText(resp);
1742
+ break;
1743
+ } catch (e) {
1744
+ errorMsg = e instanceof Error ? e.message : String(e);
1745
+ if (attempt < 2) {
1746
+ const wait = 2 ** attempt * 1e3;
1747
+ logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
1748
+ await sleep4(wait);
1749
+ }
1750
+ }
1751
+ }
1752
+ results.push({
1753
+ file: filePath,
1754
+ status: markdown !== void 0 ? "success" : "error",
1755
+ markdown,
1756
+ error: errorMsg
1757
+ });
1758
+ logger?.debug(`Status: ${results[results.length - 1]?.status}`);
1759
+ }
1760
+ const lines = ["# Document Extraction Results\n"];
1761
+ lines.push(`Converted ${inputs.length} document(s) to markdown.
1762
+
1763
+ ---
1764
+ `);
1765
+ for (const r of results) {
1766
+ lines.push(`
1767
+ ## ${path14.basename(r.file)}
1768
+ `);
1769
+ if (r.status === "success" && r.markdown) {
1770
+ lines.push(`${r.markdown}
1771
+ `);
1772
+ } else if (r.status === "success") {
1773
+ lines.push("**Note**: Conversion succeeded but no content was returned.\n");
1774
+ } else {
1775
+ lines.push(`**Error**: ${r.error ?? "Unknown error"}
1776
+ `);
1777
+ }
1778
+ lines.push("\n---\n");
1779
+ }
1780
+ fs16.writeFileSync(outputPath, lines.join("\n"), "utf8");
1781
+ const succeeded = results.filter((r) => r.status === "success").length;
1782
+ console.log(`
1783
+ ${"=".repeat(50)}`);
1784
+ console.log(`Converted: ${results.length} file(s)`);
1785
+ console.log(`Success: ${succeeded}`);
1786
+ console.log(`Failed: ${results.length - succeeded}`);
1787
+ console.log(`Output saved to: ${outputPath}`);
1788
+ if (verbose) {
1789
+ for (const r of results) {
1790
+ console.log(` [${r.status}] ${r.file}`);
1791
+ }
1792
+ }
1793
+ const failed = results.filter((r) => r.status === "error").length;
1794
+ if (failed > 0) process.exit(1);
1795
+ }
1796
+ function sleep4(ms) {
1797
+ return new Promise((r) => setTimeout(r, ms));
1798
+ }
1799
+
1800
+ // src/commands/doc/index.ts
1801
+ function registerDocCommands(program2) {
1802
+ const doc = program2.command("doc").description("Document operations: convert PDFs, images, Office docs to Markdown via Gemini");
1803
+ doc.command("convert").description("Convert documents to Markdown using Gemini Files API").requiredOption(
1804
+ "--input <files...>",
1805
+ "Input file(s) to convert",
1806
+ (val, prev) => [...prev, val],
1807
+ []
1808
+ ).alias("-i").option("--output <path>", "Output markdown file path").option("--auto-name", "Derive output filename from input basename (single file only)").option("--model <id>", `Gemini model to use (default: ${DOC_MODEL_DEFAULT})`).option("--prompt <text>", "Custom conversion prompt").option("-v, --verbose", "Verbose logging").action(
1809
+ async (opts) => {
1810
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1811
+ await convertDocuments({
1812
+ inputs: opts.input,
1813
+ output: opts.output,
1814
+ autoName: opts.autoName,
1815
+ model: opts.model,
1816
+ prompt: opts.prompt,
1817
+ verbose: opts.verbose,
1818
+ logger
1819
+ });
1820
+ }
1821
+ );
1822
+ }
1823
+
1824
+ // src/commands/media/index.ts
1825
+ init_errors();
1826
+ import fs19 from "fs";
1827
+ import path17 from "path";
1828
+
1829
+ // src/commands/media/batch.ts
1830
+ import fs17 from "fs";
1831
+ import path15 from "path";
1832
+
1833
+ // src/commands/media/detect-type.ts
1834
+ var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
1835
+ var AUDIO_EXTS = /* @__PURE__ */ new Set([".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg", ".aiff"]);
1836
+ var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".gif", ".bmp"]);
1837
+ function extToKind(ext) {
1838
+ const lower = ext.toLowerCase();
1839
+ if (VIDEO_EXTS.has(lower)) return "video";
1840
+ if (AUDIO_EXTS.has(lower)) return "audio";
1841
+ if (IMAGE_EXTS.has(lower)) return "image";
1842
+ return void 0;
1843
+ }
1844
+
1845
+ // src/commands/media/optimize-audio.ts
1846
+ init_errors();
1847
+
1848
+ // src/commands/media/ffmpeg-runner.ts
1849
+ init_errors();
1850
+ import { execa as execa2 } from "execa";
1851
+ async function assertFfmpeg() {
1852
+ try {
1853
+ await execa2("ffmpeg", ["-version"], { reject: true });
1854
+ } catch (e) {
1855
+ if (e.code === "ENOENT") {
1856
+ throw new ValidationError(
1857
+ "ffmpeg not found on PATH.\n Linux: sudo apt-get install ffmpeg\n macOS: brew install ffmpeg\n Windows: https://ffmpeg.org/download.html"
1858
+ );
1859
+ }
1860
+ }
1861
+ }
1862
+ async function runFfmpeg(args, opts = {}) {
1863
+ const { verbose, logger } = opts;
1864
+ logger?.debug(`ffmpeg ${args.join(" ")}`);
1865
+ try {
1866
+ const result = await execa2("ffmpeg", args, { reject: true, all: true });
1867
+ if (verbose && result.stderr) logger?.debug(result.stderr);
1868
+ } catch (e) {
1869
+ const err2 = e;
1870
+ const stderr = (err2.stderr ?? err2.all ?? "").toString().slice(0, 1e3);
1871
+ throw new ProviderError(`ffmpeg failed: ${stderr}`, "ffmpeg");
1872
+ }
1873
+ }
1874
+ async function getMediaInfo(filePath) {
1875
+ try {
1876
+ const result = await execa2(
1877
+ "ffprobe",
1878
+ ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath],
1879
+ { reject: true }
1880
+ );
1881
+ const data = JSON.parse(result.stdout);
1882
+ const fmt = data.format ?? {};
1883
+ const info = {
1884
+ sizeByes: Number.parseInt(fmt.size ?? "0", 10),
1885
+ duration: Number.parseFloat(fmt.duration ?? "0"),
1886
+ bitRate: Number.parseInt(fmt.bit_rate ?? "0", 10)
1887
+ };
1888
+ for (const stream of data.streams ?? []) {
1889
+ if (stream.codec_type === "video") {
1890
+ info.width = stream.width;
1891
+ info.height = stream.height;
1892
+ if (stream.r_frame_rate) {
1893
+ const [num, den] = stream.r_frame_rate.split("/").map(Number);
1894
+ info.fps = den ? (num ?? 0) / (den ?? 1) : num;
1895
+ }
1896
+ } else if (stream.codec_type === "audio") {
1897
+ info.sampleRate = Number.parseInt(stream.sample_rate ?? "0", 10);
1898
+ info.channels = stream.channels;
1899
+ }
1900
+ }
1901
+ return info;
1902
+ } catch {
1903
+ return {};
1904
+ }
1905
+ }
1906
+
1907
+ // src/commands/media/optimize-audio.ts
1908
+ async function optimizeAudio(opts) {
1909
+ await assertFfmpeg();
1910
+ const { input, output, bitrate = "64k", sampleRate = 16e3, verbose, logger } = opts;
1911
+ const info = await getMediaInfo(input);
1912
+ if (!info.duration) throw new ValidationError(`Cannot read media info from: ${input}`);
1913
+ if (verbose && logger) {
1914
+ logger.info(`Input: ${input}`);
1915
+ logger.info(` Size: ${((info.sizeByes ?? 0) / 1024 / 1024).toFixed(2)} MB`);
1916
+ logger.info(` Duration: ${(info.duration ?? 0).toFixed(2)}s`);
1917
+ }
1918
+ const args = [
1919
+ "-i",
1920
+ input,
1921
+ "-y",
1922
+ "-c:a",
1923
+ "aac",
1924
+ "-b:a",
1925
+ bitrate,
1926
+ "-ar",
1927
+ String(sampleRate),
1928
+ "-ac",
1929
+ "1",
1930
+ // mono — Gemini uses mono
1931
+ output
1932
+ ];
1933
+ await runFfmpeg(args, { verbose, logger });
1934
+ logger?.success(`Optimized: ${output}`);
1935
+ }
1936
+
1937
+ // src/commands/media/magick-runner.ts
1938
+ init_errors();
1939
+ import { execa as execa3 } from "execa";
1940
+ async function assertMagick() {
1941
+ try {
1942
+ await execa3("magick", ["-version"], { reject: true });
1943
+ } catch (e) {
1944
+ if (e.code === "ENOENT") {
1945
+ throw new ValidationError(
1946
+ "ImageMagick (magick) not found on PATH.\n Linux: sudo apt-get install imagemagick\n macOS: brew install imagemagick\n Windows: https://imagemagick.org/script/download.php"
1947
+ );
1948
+ }
1949
+ }
1950
+ }
1951
+ async function runMagick(args, opts = {}) {
1952
+ const { verbose, logger } = opts;
1953
+ logger?.debug(`magick ${args.join(" ")}`);
1954
+ try {
1955
+ const result = await execa3("magick", args, { reject: true, all: true });
1956
+ if (verbose && result.stderr) logger?.debug(result.stderr);
1957
+ } catch (e) {
1958
+ const err2 = e;
1959
+ const stderr = (err2.stderr ?? err2.all ?? "").toString().slice(0, 1e3);
1960
+ throw new ProviderError(`magick failed: ${stderr}`, "imagemagick");
1961
+ }
1962
+ }
1963
+
1964
+ // src/commands/media/optimize-image.ts
1965
+ async function optimizeImage(opts) {
1966
+ await assertMagick();
1967
+ const { input, output, maxWidth = 1920, quality = 85, verbose, logger } = opts;
1968
+ const args = [input, "-resize", `${maxWidth}x>`, "-quality", String(quality), output];
1969
+ await runMagick(args, { verbose, logger });
1970
+ logger?.success(`Optimized: ${output}`);
1971
+ }
1972
+
1973
+ // src/commands/media/optimize-video.ts
1974
+ init_errors();
1975
+ async function optimizeVideo(opts) {
1976
+ await assertFfmpeg();
1977
+ const {
1978
+ input,
1979
+ output,
1980
+ targetSizeMb,
1981
+ quality = 23,
1982
+ resolution,
1983
+ maxWidth = 1920,
1984
+ verbose,
1985
+ logger
1986
+ } = opts;
1987
+ const info = await getMediaInfo(input);
1988
+ if (!info.duration) throw new ValidationError(`Cannot read media info from: ${input}`);
1989
+ if (verbose && logger) {
1990
+ logger.info(`Input: ${input}`);
1991
+ logger.info(` Size: ${((info.sizeByes ?? 0) / 1024 / 1024).toFixed(2)} MB`);
1992
+ logger.info(` Duration: ${(info.duration ?? 0).toFixed(2)}s`);
1993
+ if (info.width) logger.info(` Resolution: ${info.width}x${info.height}`);
1994
+ logger.info(` Bitrate: ${((info.bitRate ?? 0) / 1e3).toFixed(0)} kbps`);
1995
+ }
1996
+ const args = ["-i", input, "-y"];
1997
+ args.push("-c:v", "libx264", "-crf", String(quality));
1998
+ if (resolution) {
1999
+ args.push("-vf", `scale=${resolution}`);
2000
+ } else if ((info.width ?? 0) > maxWidth) {
2001
+ args.push("-vf", "scale=1920:-2");
2002
+ }
2003
+ args.push("-c:a", "aac", "-b:a", "128k", "-ac", "2");
2004
+ if (targetSizeMb && info.duration) {
2005
+ const targetBits = targetSizeMb * 8 * 1024 * 1024;
2006
+ const targetBitrate = Math.floor(targetBits / info.duration);
2007
+ const videoBitrate = Math.max(targetBitrate - 128e3, 5e5);
2008
+ args.push("-b:v", String(videoBitrate));
2009
+ }
2010
+ args.push(output);
2011
+ await runFfmpeg(args, { verbose, logger });
2012
+ logger?.success(`Optimized: ${output}`);
2013
+ }
2014
+
2015
+ // src/commands/media/batch.ts
2016
+ var ALL_EXTS = /* @__PURE__ */ new Set([...VIDEO_EXTS, ...AUDIO_EXTS, ...IMAGE_EXTS]);
2017
+ async function batchOptimize(opts) {
2018
+ const {
2019
+ inputDir,
2020
+ outputDir,
2021
+ quality = 85,
2022
+ maxWidth = 1920,
2023
+ bitrate = "64k",
2024
+ verbose,
2025
+ logger
2026
+ } = opts;
2027
+ fs17.mkdirSync(outputDir, { recursive: true });
2028
+ const entries = fs17.readdirSync(inputDir);
2029
+ const mediaFiles = entries.filter((f) => {
2030
+ const ext = path15.extname(f).toLowerCase();
2031
+ return ALL_EXTS.has(ext);
2032
+ });
2033
+ if (mediaFiles.length === 0) {
2034
+ logger?.warn(`No supported media files found in ${inputDir}`);
2035
+ return { total: 0, succeeded: 0, failed: 0, errors: [] };
2036
+ }
2037
+ logger?.info(`Found ${mediaFiles.length} files to process`);
2038
+ let succeeded = 0;
2039
+ const errors = [];
2040
+ for (const file of mediaFiles) {
2041
+ const inputPath = path15.join(inputDir, file);
2042
+ const outputPath = path15.join(outputDir, file);
2043
+ const ext = path15.extname(file).toLowerCase();
2044
+ const kind = extToKind(ext);
2045
+ logger?.debug(`Processing: ${file}`);
2046
+ try {
2047
+ if (kind === "video") {
2048
+ await optimizeVideo({
2049
+ input: inputPath,
2050
+ output: outputPath,
2051
+ quality,
2052
+ maxWidth,
2053
+ verbose,
2054
+ logger
2055
+ });
2056
+ } else if (kind === "audio") {
2057
+ await optimizeAudio({ input: inputPath, output: outputPath, bitrate, verbose, logger });
2058
+ } else if (kind === "image") {
2059
+ await optimizeImage({
2060
+ input: inputPath,
2061
+ output: outputPath,
2062
+ maxWidth,
2063
+ quality,
2064
+ verbose,
2065
+ logger
2066
+ });
2067
+ }
2068
+ succeeded++;
2069
+ } catch (e) {
2070
+ const msg = e instanceof Error ? e.message : String(e);
2071
+ logger?.error(`Failed: ${file} \u2014 ${msg}`);
2072
+ errors.push({ file, error: msg });
2073
+ }
2074
+ }
2075
+ return { total: mediaFiles.length, succeeded, failed: errors.length, errors };
2076
+ }
2077
+
2078
+ // src/commands/media/split-video.ts
2079
+ init_errors();
2080
+ import fs18 from "fs";
2081
+ import path16 from "path";
2082
+ async function splitVideo(opts) {
2083
+ await assertFfmpeg();
2084
+ const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
2085
+ const info = await getMediaInfo(input);
2086
+ if (!info.duration) throw new ValidationError(`Cannot read media info from: ${input}`);
2087
+ const numChunks = Math.ceil(info.duration / chunkDuration);
2088
+ if (numChunks <= 1) {
2089
+ logger?.info("Video is short enough \u2014 no splitting needed");
2090
+ return [input];
2091
+ }
2092
+ fs18.mkdirSync(outputDir, { recursive: true });
2093
+ const ext = path16.extname(input) || ".mp4";
2094
+ const baseName = path16.basename(input, ext);
2095
+ const outputPattern = path16.join(outputDir, `${baseName}_chunk_%03d${ext}`);
2096
+ logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
2097
+ const args = [
2098
+ "-i",
2099
+ input,
2100
+ "-y",
2101
+ "-f",
2102
+ "segment",
2103
+ "-segment_time",
2104
+ String(chunkDuration),
2105
+ "-c",
2106
+ "copy",
2107
+ "-reset_timestamps",
2108
+ "1",
2109
+ outputPattern
2110
+ ];
2111
+ await runFfmpeg(args, { verbose, logger });
2112
+ const files = [];
2113
+ for (let i = 0; i < numChunks + 1; i++) {
2114
+ const fname = path16.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
2115
+ if (fs18.existsSync(fname)) files.push(fname);
2116
+ }
2117
+ logger?.success(`Created ${files.length} chunks in ${outputDir}`);
2118
+ return files;
2119
+ }
2120
+
2121
+ // src/commands/media/index.ts
2122
+ function registerMediaCommands(program2) {
2123
+ const media = program2.command("media").description(
2124
+ "Media optimization: compress video/audio/images, split long videos, batch process"
2125
+ );
2126
+ 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(
2127
+ async (opts) => {
2128
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2129
+ if (!fs19.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
2130
+ fs19.mkdirSync(path17.dirname(path17.resolve(opts.output)), { recursive: true });
2131
+ const ext = path17.extname(opts.input).toLowerCase();
2132
+ const kind = extToKind(ext);
2133
+ if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
2134
+ const quality = Number.parseInt(opts.quality, 10);
2135
+ const maxWidth = Number.parseInt(opts.maxWidth, 10);
2136
+ const targetSizeMb = opts.targetSize ? Number.parseInt(opts.targetSize, 10) : void 0;
2137
+ if (kind === "video") {
2138
+ await optimizeVideo({
2139
+ input: opts.input,
2140
+ output: opts.output,
2141
+ targetSizeMb,
2142
+ quality,
2143
+ maxWidth,
2144
+ resolution: opts.resolution,
2145
+ verbose: opts.verbose,
2146
+ logger
2147
+ });
2148
+ } else if (kind === "audio") {
2149
+ await optimizeAudio({
2150
+ input: opts.input,
2151
+ output: opts.output,
2152
+ bitrate: opts.bitrate,
2153
+ verbose: opts.verbose,
2154
+ logger
2155
+ });
2156
+ } else {
2157
+ await optimizeImage({
2158
+ input: opts.input,
2159
+ output: opts.output,
2160
+ maxWidth,
2161
+ quality,
2162
+ verbose: opts.verbose,
2163
+ logger
2164
+ });
2165
+ }
2166
+ }
2167
+ );
2168
+ 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(
2169
+ async (opts) => {
2170
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2171
+ if (!fs19.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
2172
+ const chunks = await splitVideo({
2173
+ input: opts.input,
2174
+ outputDir: opts.outputDir,
2175
+ chunkDuration: Number.parseInt(opts.chunkDuration, 10),
2176
+ verbose: opts.verbose,
2177
+ logger
2178
+ });
2179
+ console.log(`
2180
+ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
2181
+ for (const c2 of chunks) console.log(` ${c2}`);
2182
+ }
2183
+ );
2184
+ 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(
2185
+ async (opts) => {
2186
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2187
+ if (!fs19.existsSync(opts.inputDir))
2188
+ throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
2189
+ const result = await batchOptimize({
2190
+ inputDir: opts.inputDir,
2191
+ outputDir: opts.outputDir,
2192
+ quality: Number.parseInt(opts.quality, 10),
2193
+ maxWidth: Number.parseInt(opts.maxWidth, 10),
2194
+ bitrate: opts.bitrate,
2195
+ verbose: opts.verbose,
2196
+ logger
2197
+ });
2198
+ console.log(`
2199
+ Processed: ${result.succeeded}/${result.total} files`);
2200
+ if (result.failed > 0) {
2201
+ console.log(`Failed: ${result.failed}`);
2202
+ process.exit(1);
2203
+ }
2204
+ }
2205
+ );
2206
+ }
2207
+
2208
+ // src/commands/index.ts
2209
+ function registerCommands(program2) {
2210
+ registerCheckCommand(program2);
2211
+ registerGeminiCommands(program2);
2212
+ registerMinimaxCommands(program2);
2213
+ registerOpenRouterCommands(program2);
2214
+ registerMediaCommands(program2);
2215
+ registerDocCommands(program2);
2216
+ }
2217
+
2218
+ // src/core/index.ts
2219
+ init_errors();
2220
+ init_http_client();
2221
+
2222
+ // src/cli.ts
2223
+ loadEnv();
2224
+ var require2 = createRequire(import.meta.url);
2225
+ var pkg = require2("../package.json");
2226
+ var program = new Command().name("multix").description(
2227
+ "AI multimodal CLI \u2014 Gemini \xB7 MiniMax \xB7 OpenRouter \xB7 ffmpeg \xB7 ImageMagick \xB7 doc-to-md"
2228
+ ).version(pkg.version);
2229
+ registerCommands(program);
2230
+ program.parseAsync(process.argv).catch((err2) => {
2231
+ const msg = err2 instanceof Error ? err2.message : String(err2);
2232
+ console.error(`Error: ${msg}`);
2233
+ process.exit(1);
2234
+ });