@mrgoonie/multix 0.7.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ 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
+ ## [0.7.2](https://github.com/mrgoonie/multix-cli/compare/v0.7.1...v0.7.2) (2026-10-11)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * add AGENTS.md contributor and issue-label workflow ([#57](https://github.com/mrgoonie/multix-cli/issues/57)) ([2cffb46](https://github.com/mrgoonie/multix-cli/commit/2cffb46be060664539df62eaf262e8cb41bd367b))
14
+
15
+ ## [0.7.1](https://github.com/mrgoonie/multix-cli/compare/v0.7.0...v0.7.1) (2026-10-05)
16
+
17
+
18
+ ### Fixed
19
+
20
+ * **check:** probe ffmpeg with -version so ffmpeg 8.x is detected ([#51](https://github.com/mrgoonie/multix-cli/issues/51)) ([ca26541](https://github.com/mrgoonie/multix-cli/commit/ca265416075a0df6eecf21f3e359f7aab74bce43))
21
+
8
22
  ## [0.7.0](https://github.com/mrgoonie/multix-cli/compare/v0.6.0...v0.7.0) (2026-09-24)
9
23
 
10
24
 
package/dist/cli.js CHANGED
@@ -9,6 +9,28 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/core/redact.ts
13
+ function redact(key) {
14
+ if (key.length <= 10) return "***";
15
+ return `${key.slice(0, 6)}...${key.slice(-4)}`;
16
+ }
17
+ function redactUrl(url) {
18
+ try {
19
+ const u = new URL(url);
20
+ if (u.origin === "null") return `${u.protocol}[redacted]`;
21
+ const suffix = u.search || u.hash ? "?[redacted]" : "";
22
+ return `${u.origin}${u.pathname}${suffix}`;
23
+ } catch {
24
+ const cut = url.search(/[?#]/);
25
+ return cut === -1 ? url : `${url.slice(0, cut)}?[redacted]`;
26
+ }
27
+ }
28
+ var init_redact = __esm({
29
+ "src/core/redact.ts"() {
30
+ "use strict";
31
+ }
32
+ });
33
+
12
34
  // src/core/errors.ts
13
35
  var errors_exports = {};
14
36
  __export(errors_exports, {
@@ -22,6 +44,7 @@ var MultixError, ConfigError, ProviderError, ValidationError, HttpError;
22
44
  var init_errors = __esm({
23
45
  "src/core/errors.ts"() {
24
46
  "use strict";
47
+ init_redact();
25
48
  MultixError = class extends Error {
26
49
  constructor(message, code) {
27
50
  super(message);
@@ -54,33 +77,51 @@ var init_errors = __esm({
54
77
  }
55
78
  };
56
79
  HttpError = class extends MultixError {
57
- constructor(status, snippet, url) {
58
- super(`HTTP ${status} from ${url}: ${snippet}`, "HTTP_ERROR");
80
+ constructor(status, snippet, url, retryAfterMs) {
81
+ super(`HTTP ${status} from ${redactUrl(url)}: ${snippet}`, "HTTP_ERROR");
59
82
  this.status = status;
60
83
  this.snippet = snippet;
61
- this.url = url;
84
+ this.retryAfterMs = retryAfterMs;
85
+ this.url = redactUrl(url);
86
+ Object.defineProperty(this, "rawUrl", { value: url, enumerable: false });
62
87
  this.name = "HttpError";
63
88
  }
64
89
  status;
65
90
  snippet;
91
+ retryAfterMs;
92
+ /** Request URL with query string and fragment redacted — safe to log or serialize. */
66
93
  url;
67
94
  };
68
95
  }
69
96
  });
70
97
 
71
- // src/core/http-client.ts
72
- var http_client_exports = {};
73
- __export(http_client_exports, {
74
- downloadFile: () => downloadFile,
75
- fetchBytes: () => fetchBytes,
76
- httpJson: () => httpJson
77
- });
78
- import { once } from "events";
79
- import fs4 from "fs";
80
- import path4 from "path";
81
- import { pipeline } from "stream/promises";
82
- async function httpJson(opts) {
98
+ // src/core/fetch-json.ts
99
+ function parseRetryAfter(value, now = Date.now()) {
100
+ if (!value) return void 0;
101
+ const trimmed = value.trim();
102
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1e3);
103
+ const date = Date.parse(trimmed);
104
+ if (Number.isNaN(date)) return void 0;
105
+ return Math.max(0, date - now);
106
+ }
107
+ async function httpErrorFromResponse(response, url) {
108
+ let snippet = "";
109
+ try {
110
+ const text = await response.text();
111
+ snippet = text.slice(0, SNIPPET_MAX);
112
+ } catch {
113
+ snippet = "(could not read body)";
114
+ }
115
+ return new HttpError(
116
+ response.status,
117
+ snippet,
118
+ url,
119
+ parseRetryAfter(response.headers.get("retry-after"))
120
+ );
121
+ }
122
+ async function fetchJson(opts) {
83
123
  const { url, method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
124
+ const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
84
125
  const init = {
85
126
  method,
86
127
  headers: {
@@ -94,28 +135,64 @@ async function httpJson(opts) {
94
135
  }
95
136
  let response;
96
137
  try {
97
- response = await globalThis.fetch(url, init);
138
+ response = await doFetch(url, init);
98
139
  } catch (cause) {
99
140
  const msg = cause instanceof Error ? cause.message : String(cause);
100
141
  throw new HttpError(0, msg, url);
101
142
  }
102
143
  if (!response.ok) {
103
- let snippet = "";
104
- try {
105
- const text = await response.text();
106
- snippet = text.slice(0, SNIPPET_MAX);
107
- } catch {
108
- snippet = "(could not read body)";
109
- }
110
- throw new HttpError(response.status, snippet, url);
144
+ throw await httpErrorFromResponse(response, url);
145
+ }
146
+ let text;
147
+ try {
148
+ text = await response.text();
149
+ } catch (cause) {
150
+ const msg = cause instanceof Error ? cause.message : String(cause);
151
+ throw new HttpError(0, `Failed to read response body: ${msg}`, url);
152
+ }
153
+ if (text.trim() === "") return void 0;
154
+ try {
155
+ return JSON.parse(text);
156
+ } catch {
157
+ throw new HttpError(
158
+ response.status,
159
+ `Invalid JSON response: ${text.slice(0, SNIPPET_MAX)}`,
160
+ url
161
+ );
162
+ }
163
+ }
164
+ var DEFAULT_TIMEOUT_MS, SNIPPET_MAX;
165
+ var init_fetch_json = __esm({
166
+ "src/core/fetch-json.ts"() {
167
+ "use strict";
168
+ init_errors();
169
+ DEFAULT_TIMEOUT_MS = 12e4;
170
+ SNIPPET_MAX = 500;
111
171
  }
112
- return response.json();
172
+ });
173
+
174
+ // src/core/http-client.ts
175
+ var http_client_exports = {};
176
+ __export(http_client_exports, {
177
+ downloadFile: () => downloadFile,
178
+ fetchBytes: () => fetchBytes,
179
+ httpJson: () => httpJson
180
+ });
181
+ import { once } from "events";
182
+ import fs4 from "fs";
183
+ import path4 from "path";
184
+ import { pipeline } from "stream/promises";
185
+ async function httpJson(opts) {
186
+ return fetchJson(opts);
113
187
  }
114
- async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
188
+ async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers) {
115
189
  fs4.mkdirSync(path4.dirname(dest), { recursive: true });
116
190
  let response;
117
191
  try {
118
- response = await globalThis.fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
192
+ response = await globalThis.fetch(url, {
193
+ signal: AbortSignal.timeout(timeoutMs),
194
+ ...headers ? { headers } : {}
195
+ });
119
196
  } catch (cause) {
120
197
  const msg = cause instanceof Error ? cause.message : String(cause);
121
198
  throw new HttpError(0, msg, url);
@@ -155,13 +232,11 @@ async function fetchBytes(urlOrDataUri) {
155
232
  }
156
233
  return Buffer.from(await response.arrayBuffer());
157
234
  }
158
- var DEFAULT_TIMEOUT_MS, SNIPPET_MAX;
159
235
  var init_http_client = __esm({
160
236
  "src/core/http-client.ts"() {
161
237
  "use strict";
162
238
  init_errors();
163
- DEFAULT_TIMEOUT_MS = 12e4;
164
- SNIPPET_MAX = 500;
239
+ init_fetch_json();
165
240
  }
166
241
  });
167
242
 
@@ -346,6 +421,7 @@ async function poll(opts) {
346
421
  }
347
422
 
348
423
  // src/core/env-loader.ts
424
+ init_redact();
349
425
  import fs3 from "fs";
350
426
  import os from "os";
351
427
  import path3 from "path";
@@ -368,10 +444,6 @@ function loadEnv() {
368
444
  function resolveKey(name) {
369
445
  return process.env[name] || void 0;
370
446
  }
371
- function redact(key) {
372
- if (key.length <= 10) return "***";
373
- return `${key.slice(0, 6)}...${key.slice(-4)}`;
374
- }
375
447
 
376
448
  // src/providers/byteplus/client.ts
377
449
  init_errors();
@@ -2582,7 +2654,7 @@ function registerElevenLabsCommands(program2) {
2582
2654
  }
2583
2655
 
2584
2656
  // src/providers/fal/commands/image.ts
2585
- import fs16 from "fs";
2657
+ import fs17 from "fs";
2586
2658
  import path20 from "path";
2587
2659
 
2588
2660
  // src/providers/fal/models.ts
@@ -2597,15 +2669,45 @@ var FAL_DEFAULTS = {
2597
2669
  };
2598
2670
 
2599
2671
  // src/providers/fal/run-helpers.ts
2600
- init_errors();
2601
2672
  init_http_client();
2602
- import fs15 from "fs";
2673
+ import fs16 from "fs";
2603
2674
  import path19 from "path";
2604
2675
 
2676
+ // src/core/json-input.ts
2677
+ init_errors();
2678
+ import fs15 from "fs";
2679
+ function parseJsonInput(raw, flag = "--input") {
2680
+ let text;
2681
+ if (raw.startsWith("@")) {
2682
+ try {
2683
+ text = fs15.readFileSync(raw.slice(1), "utf8");
2684
+ } catch (e) {
2685
+ const msg = e instanceof Error ? e.message : String(e);
2686
+ throw new ValidationError(`Cannot read ${flag} file: ${msg}`);
2687
+ }
2688
+ } else {
2689
+ text = raw;
2690
+ }
2691
+ try {
2692
+ const parsed = JSON.parse(text);
2693
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2694
+ throw new Error("input must be a JSON object");
2695
+ }
2696
+ return parsed;
2697
+ } catch (e) {
2698
+ const msg = e instanceof Error ? e.message : String(e);
2699
+ throw new ValidationError(`Invalid ${flag} JSON: ${msg}`);
2700
+ }
2701
+ }
2702
+
2703
+ // src/providers/fal/run-helpers.ts
2704
+ init_redact();
2705
+
2605
2706
  // src/providers/fal/client.ts
2606
2707
  init_errors();
2607
2708
  init_errors();
2608
2709
  init_http_client();
2710
+ init_redact();
2609
2711
  function requireFalKey() {
2610
2712
  const key = resolveKey("FAL_KEY");
2611
2713
  if (!key) {
@@ -2631,7 +2733,7 @@ var FalClient = class {
2631
2733
  }
2632
2734
  /** Same auth/headers as `request`, but `url` is used verbatim (e.g. a status_url/response_url returned by submit). */
2633
2735
  async requestAbsolute(url, opts = {}) {
2634
- opts.logger?.debug(`${opts.method ?? "GET"} ${url}`);
2736
+ opts.logger?.debug(`${opts.method ?? "GET"} ${redactUrl(url)}`);
2635
2737
  return httpJson({
2636
2738
  url,
2637
2739
  method: opts.method ?? "GET",
@@ -2677,17 +2779,7 @@ function resultPath(model, requestId) {
2677
2779
 
2678
2780
  // src/providers/fal/run-helpers.ts
2679
2781
  function parseInputArg(raw) {
2680
- const text = raw.startsWith("@") ? fs15.readFileSync(raw.slice(1), "utf8") : raw;
2681
- try {
2682
- const parsed = JSON.parse(text);
2683
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2684
- throw new Error("input must be a JSON object");
2685
- }
2686
- return parsed;
2687
- } catch (e) {
2688
- const msg = e instanceof Error ? e.message : String(e);
2689
- throw new ValidationError(`Invalid --input JSON: ${msg}`);
2690
- }
2782
+ return parseJsonInput(raw);
2691
2783
  }
2692
2784
  function isTransientError(e) {
2693
2785
  if (e instanceof HttpError) return e.status === 0 || e.status >= 500;
@@ -2818,8 +2910,8 @@ async function downloadResultMedia(urls, outDir, requestId, logger) {
2818
2910
  saved.push(filepath);
2819
2911
  logger.success(`Saved ${filepath}`);
2820
2912
  } catch (e) {
2821
- fs15.rmSync(filepath, { force: true });
2822
- logger.warn(`Download failed for ${url}: ${e instanceof Error ? e.message : e}`);
2913
+ fs16.rmSync(filepath, { force: true });
2914
+ logger.warn(`Download failed for ${redactUrl(url)}: ${e instanceof Error ? e.message : e}`);
2823
2915
  }
2824
2916
  }
2825
2917
  return saved;
@@ -2864,8 +2956,8 @@ function registerFalImageCommand(parent) {
2864
2956
  }
2865
2957
  if (opts.output && saved[0]) {
2866
2958
  const dest = path20.resolve(opts.output);
2867
- fs16.mkdirSync(path20.dirname(dest), { recursive: true });
2868
- fs16.copyFileSync(saved[0], dest);
2959
+ fs17.mkdirSync(path20.dirname(dest), { recursive: true });
2960
+ fs17.copyFileSync(saved[0], dest);
2869
2961
  logger.success(`Copied to ${dest}`);
2870
2962
  }
2871
2963
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -3020,10 +3112,10 @@ function registerFalCommands(program2) {
3020
3112
 
3021
3113
  // src/providers/gemini/commands/analyze.ts
3022
3114
  init_errors();
3023
- import fs19 from "fs";
3115
+ import fs20 from "fs";
3024
3116
 
3025
3117
  // src/providers/gemini/client.ts
3026
- import fs17 from "fs";
3118
+ import fs18 from "fs";
3027
3119
  import path22 from "path";
3028
3120
  init_errors();
3029
3121
  init_http_client();
@@ -3096,7 +3188,7 @@ async function uploadFile(filePath, opts = {}) {
3096
3188
  const apiKey = requireApiKey();
3097
3189
  const { timeoutMs = 3e5, logger } = opts;
3098
3190
  const mimeType = getMimeType(filePath);
3099
- const fileBytes = fs17.readFileSync(filePath);
3191
+ const fileBytes = fs18.readFileSync(filePath);
3100
3192
  const displayName = path22.basename(filePath);
3101
3193
  const boundary = `multix_${Date.now()}`;
3102
3194
  const metaJson = JSON.stringify({ file: { displayName, mimeType } });
@@ -3213,12 +3305,12 @@ function extractImages(resp) {
3213
3305
  ).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
3214
3306
  }
3215
3307
  function inlinePart(filePath) {
3216
- const data = fs17.readFileSync(filePath).toString("base64");
3308
+ const data = fs18.readFileSync(filePath).toString("base64");
3217
3309
  const mimeType = getMimeType(filePath);
3218
3310
  return { inlineData: { mimeType, data } };
3219
3311
  }
3220
3312
  function shouldUseFileApi(filePath) {
3221
- const stat = fs17.statSync(filePath);
3313
+ const stat = fs18.statSync(filePath);
3222
3314
  return stat.size > 20 * 1024 * 1024;
3223
3315
  }
3224
3316
  function sleep2(ms) {
@@ -3272,13 +3364,13 @@ function getDefaultModel(task) {
3272
3364
  }
3273
3365
 
3274
3366
  // src/providers/gemini/result-formatter.ts
3275
- import fs18 from "fs";
3367
+ import fs19 from "fs";
3276
3368
  import path23 from "path";
3277
3369
  function formatResults(results, format, outputPath) {
3278
3370
  const content = renderFormat(results, format);
3279
3371
  if (outputPath) {
3280
- fs18.mkdirSync(path23.dirname(path23.resolve(outputPath)), { recursive: true });
3281
- fs18.writeFileSync(outputPath, content, "utf8");
3372
+ fs19.mkdirSync(path23.dirname(path23.resolve(outputPath)), { recursive: true });
3373
+ fs19.writeFileSync(outputPath, content, "utf8");
3282
3374
  } else {
3283
3375
  process.stdout.write(content);
3284
3376
  }
@@ -3345,7 +3437,7 @@ function registerAnalyzeCommand(parent) {
3345
3437
  const logger = createLogger({ verbose: opts.verbose ?? false });
3346
3438
  const model = opts.model ?? getDefaultModel("analyze");
3347
3439
  for (const f of opts.files) {
3348
- if (!fs19.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3440
+ if (!fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3349
3441
  }
3350
3442
  const results = [];
3351
3443
  for (let i = 0; i < opts.files.length; i++) {
@@ -3391,14 +3483,14 @@ function registerAnalyzeCommand(parent) {
3391
3483
 
3392
3484
  // src/providers/gemini/commands/extract.ts
3393
3485
  init_errors();
3394
- import fs20 from "fs";
3486
+ import fs21 from "fs";
3395
3487
  function registerExtractCommand(parent) {
3396
3488
  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(
3397
3489
  async (opts) => {
3398
3490
  const logger = createLogger({ verbose: opts.verbose ?? false });
3399
3491
  const model = opts.model ?? getDefaultModel("extract");
3400
3492
  for (const f of opts.files) {
3401
- if (!fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3493
+ if (!fs21.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3402
3494
  }
3403
3495
  const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
3404
3496
  const results = [];
@@ -3440,7 +3532,7 @@ function registerExtractCommand(parent) {
3440
3532
  init_errors();
3441
3533
 
3442
3534
  // src/providers/gemini/generators/speech.ts
3443
- import fs21 from "fs";
3535
+ import fs22 from "fs";
3444
3536
  import path24 from "path";
3445
3537
 
3446
3538
  // src/providers/gemini/voices.ts
@@ -3605,11 +3697,11 @@ function extractWavData(wav) {
3605
3697
  function saveSpeech(outBytes, outputFormat, output, logger) {
3606
3698
  const outDir = getOutputDir();
3607
3699
  const dest = path24.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
3608
- fs21.writeFileSync(dest, outBytes);
3700
+ fs22.writeFileSync(dest, outBytes);
3609
3701
  logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
3610
3702
  if (output) {
3611
- fs21.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
3612
- fs21.copyFileSync(dest, output);
3703
+ fs22.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
3704
+ fs22.copyFileSync(dest, output);
3613
3705
  logger?.success(`Copied to: ${output}`);
3614
3706
  }
3615
3707
  return dest;
@@ -3721,7 +3813,7 @@ function collectSpeakers(value, prev) {
3721
3813
  }
3722
3814
 
3723
3815
  // src/providers/gemini/commands/generate-video.ts
3724
- import fs22 from "fs";
3816
+ import fs23 from "fs";
3725
3817
  import path25 from "path";
3726
3818
  init_errors();
3727
3819
  init_http_client();
@@ -3756,9 +3848,9 @@ function registerGenerateVideoCommand(parent) {
3756
3848
  const contentParts = [{ text: opts.prompt }];
3757
3849
  if (opts.referenceImages?.length) {
3758
3850
  for (const imgPath of opts.referenceImages.slice(0, 2)) {
3759
- if (!fs22.existsSync(imgPath))
3851
+ if (!fs23.existsSync(imgPath))
3760
3852
  throw new ValidationError(`Reference image not found: ${imgPath}`);
3761
- const data = fs22.readFileSync(imgPath).toString("base64");
3853
+ const data = fs23.readFileSync(imgPath).toString("base64");
3762
3854
  const ext = path25.extname(imgPath).toLowerCase();
3763
3855
  const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
3764
3856
  contentParts.push({ inlineData: { mimeType, data } });
@@ -3813,8 +3905,8 @@ function registerGenerateVideoCommand(parent) {
3813
3905
  await downloadFile2(videoUri, dest);
3814
3906
  logger.success(`Saved video: ${dest}`);
3815
3907
  if (opts.output) {
3816
- fs22.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
3817
- fs22.copyFileSync(dest, opts.output);
3908
+ fs23.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
3909
+ fs23.copyFileSync(dest, opts.output);
3818
3910
  logger.success(`Copied to: ${opts.output}`);
3819
3911
  }
3820
3912
  await maybeDownloadThumb(finalOp.response, dest, {
@@ -3833,7 +3925,7 @@ function sleep3(ms) {
3833
3925
 
3834
3926
  // src/providers/gemini/commands/generate.ts
3835
3927
  init_errors();
3836
- import fs23 from "fs";
3928
+ import fs24 from "fs";
3837
3929
  import path26 from "path";
3838
3930
  function registerGenerateCommand(parent) {
3839
3931
  parent.command("generate").description("Generate images with Gemini (Nano Banana / Imagen 4)").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "Gemini image model to use").option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--num-images <n>", "Number of images to generate (1-4)", "1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Copy first generated image to this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
@@ -3877,7 +3969,7 @@ function registerGenerateCommand(parent) {
3877
3969
  const ext = img.mimeType.includes("png") ? "png" : "jpg";
3878
3970
  const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
3879
3971
  const dest = path26.join(outDir, fname);
3880
- fs23.writeFileSync(dest, Buffer.from(img.data, "base64"));
3972
+ fs24.writeFileSync(dest, Buffer.from(img.data, "base64"));
3881
3973
  savedFiles.push(dest);
3882
3974
  logger.success(`Saved: ${dest}`);
3883
3975
  }
@@ -3892,8 +3984,8 @@ function registerGenerateCommand(parent) {
3892
3984
  process.exit(1);
3893
3985
  }
3894
3986
  if (opts.output && savedFiles[0]) {
3895
- fs23.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
3896
- fs23.copyFileSync(savedFiles[0], opts.output);
3987
+ fs24.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
3988
+ fs24.copyFileSync(savedFiles[0], opts.output);
3897
3989
  logger.success(`Copied to: ${opts.output}`);
3898
3990
  }
3899
3991
  const finalImages = finalizeGeneratedImages(savedFiles, { ...opts, logger });
@@ -3905,7 +3997,7 @@ Generated ${finalImages.length} image(s):`);
3905
3997
  }
3906
3998
 
3907
3999
  // src/providers/gemini/commands/image-to-image.ts
3908
- import fs24 from "fs";
4000
+ import fs25 from "fs";
3909
4001
  import path27 from "path";
3910
4002
  var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
3911
4003
  var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
@@ -3968,13 +4060,13 @@ function registerGeminiImageToImageCommand(parent) {
3968
4060
  if (!img) continue;
3969
4061
  const ext = img.mimeType.includes("png") ? "png" : "jpg";
3970
4062
  const dest = path27.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
3971
- fs24.writeFileSync(dest, Buffer.from(img.data, "base64"));
4063
+ fs25.writeFileSync(dest, Buffer.from(img.data, "base64"));
3972
4064
  saved.push(dest);
3973
4065
  logger.success(`Saved: ${dest}`);
3974
4066
  }
3975
4067
  if (opts.output && saved[0]) {
3976
- fs24.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
3977
- fs24.copyFileSync(saved[0], opts.output);
4068
+ fs25.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
4069
+ fs25.copyFileSync(saved[0], opts.output);
3978
4070
  logger.success(`Copied to: ${opts.output}`);
3979
4071
  }
3980
4072
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -4010,16 +4102,16 @@ function sniffImageMime(buf, url) {
4010
4102
  }
4011
4103
 
4012
4104
  // src/providers/gemini/commands/image-to-video.ts
4013
- import fs25 from "fs";
4105
+ import fs26 from "fs";
4014
4106
  import path28 from "path";
4015
4107
  init_errors();
4016
4108
  init_http_client();
4017
4109
  var BASE3 = "https://generativelanguage.googleapis.com";
4018
4110
  function imagePart(imgPath) {
4019
- if (!fs25.existsSync(imgPath)) {
4111
+ if (!fs26.existsSync(imgPath)) {
4020
4112
  throw new ValidationError(`Image not found: ${imgPath}`);
4021
4113
  }
4022
- const data = fs25.readFileSync(imgPath).toString("base64");
4114
+ const data = fs26.readFileSync(imgPath).toString("base64");
4023
4115
  const ext = path28.extname(imgPath).toLowerCase();
4024
4116
  const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
4025
4117
  return { inlineData: { mimeType, data } };
@@ -4097,8 +4189,8 @@ function registerGeminiImageToVideoCommand(parent) {
4097
4189
  await downloadFile(videoUri, dest);
4098
4190
  logger.success(`Saved video: ${dest}`);
4099
4191
  if (opts.output) {
4100
- fs25.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
4101
- fs25.copyFileSync(dest, opts.output);
4192
+ fs26.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
4193
+ fs26.copyFileSync(dest, opts.output);
4102
4194
  logger.success(`Copied to: ${opts.output}`);
4103
4195
  }
4104
4196
  await maybeDownloadThumb(finalOp.response, dest, {
@@ -4114,14 +4206,14 @@ Generated video: ${dest}`);
4114
4206
 
4115
4207
  // src/providers/gemini/commands/transcribe.ts
4116
4208
  init_errors();
4117
- import fs26 from "fs";
4209
+ import fs27 from "fs";
4118
4210
  function registerTranscribeCommand2(parent) {
4119
4211
  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(
4120
4212
  async (opts) => {
4121
4213
  const logger = createLogger({ verbose: opts.verbose ?? false });
4122
4214
  const model = opts.model ?? getDefaultModel("transcribe");
4123
4215
  for (const f of opts.files) {
4124
- if (!fs26.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
4216
+ if (!fs27.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
4125
4217
  }
4126
4218
  const results = [];
4127
4219
  for (let i = 0; i < opts.files.length; i++) {
@@ -4179,7 +4271,7 @@ function registerGeminiCommands(program2) {
4179
4271
 
4180
4272
  // src/providers/leonardo/commands/generate.ts
4181
4273
  init_http_client();
4182
- import fs27 from "fs";
4274
+ import fs28 from "fs";
4183
4275
  import path29 from "path";
4184
4276
 
4185
4277
  // src/providers/leonardo/client.ts
@@ -4391,8 +4483,8 @@ function registerLeonardoGenerateCommand(parent) {
4391
4483
  }
4392
4484
  if (opts.output && saved[0]) {
4393
4485
  const dest = path29.resolve(opts.output);
4394
- fs27.mkdirSync(path29.dirname(dest), { recursive: true });
4395
- fs27.copyFileSync(saved[0], dest);
4486
+ fs28.mkdirSync(path29.dirname(dest), { recursive: true });
4487
+ fs28.copyFileSync(saved[0], dest);
4396
4488
  logger.success(`Copied to ${dest}`);
4397
4489
  }
4398
4490
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -4405,7 +4497,7 @@ Generated ${finalImages.length} image(s):`);
4405
4497
 
4406
4498
  // src/providers/leonardo/commands/image-to-image.ts
4407
4499
  init_http_client();
4408
- import fs28 from "fs";
4500
+ import fs29 from "fs";
4409
4501
  import path30 from "path";
4410
4502
  function extFromUrl3(url, fallback = ".png") {
4411
4503
  try {
@@ -4482,8 +4574,8 @@ function registerLeonardoImageToImageCommand(parent) {
4482
4574
  }
4483
4575
  if (opts.output && saved[0]) {
4484
4576
  const dest = path30.resolve(opts.output);
4485
- fs28.mkdirSync(path30.dirname(dest), { recursive: true });
4486
- fs28.copyFileSync(saved[0], dest);
4577
+ fs29.mkdirSync(path30.dirname(dest), { recursive: true });
4578
+ fs29.copyFileSync(saved[0], dest);
4487
4579
  logger.success(`Copied to ${dest}`);
4488
4580
  }
4489
4581
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -4951,7 +5043,7 @@ function sleep5(ms) {
4951
5043
 
4952
5044
  // src/providers/minimax/generators/music.ts
4953
5045
  init_http_client();
4954
- import fs29 from "fs";
5046
+ import fs30 from "fs";
4955
5047
  import path33 from "path";
4956
5048
  async function generateMinimaxMusic(opts) {
4957
5049
  const {
@@ -5001,10 +5093,10 @@ async function generateMinimaxMusic(opts) {
5001
5093
  try {
5002
5094
  if (audioData.startsWith("http")) {
5003
5095
  const bytes = await fetchBytes(audioData);
5004
- fs29.writeFileSync(dest, bytes);
5096
+ fs30.writeFileSync(dest, bytes);
5005
5097
  } else {
5006
5098
  const audioBytes = Buffer.from(audioData, "hex");
5007
- fs29.writeFileSync(dest, audioBytes);
5099
+ fs30.writeFileSync(dest, audioBytes);
5008
5100
  }
5009
5101
  } catch (e) {
5010
5102
  return {
@@ -5015,8 +5107,8 @@ async function generateMinimaxMusic(opts) {
5015
5107
  const durSec = durationMs / 1e3;
5016
5108
  logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
5017
5109
  if (output) {
5018
- fs29.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
5019
- fs29.copyFileSync(dest, output);
5110
+ fs30.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
5111
+ fs30.copyFileSync(dest, output);
5020
5112
  }
5021
5113
  return { status: "success", generatedAudio: dest, durationMs, model };
5022
5114
  }
@@ -5090,7 +5182,7 @@ Generated music: ${result.generatedAudio}`);
5090
5182
  init_errors();
5091
5183
 
5092
5184
  // src/providers/minimax/generators/speech.ts
5093
- import fs30 from "fs";
5185
+ import fs31 from "fs";
5094
5186
  import path34 from "path";
5095
5187
  async function generateMinimaxSpeech(opts) {
5096
5188
  const {
@@ -5139,11 +5231,11 @@ async function generateMinimaxSpeech(opts) {
5139
5231
  const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
5140
5232
  const outDir = getOutputDir();
5141
5233
  const dest = path34.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
5142
- fs30.writeFileSync(dest, audioBytes);
5234
+ fs31.writeFileSync(dest, audioBytes);
5143
5235
  logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
5144
5236
  if (output) {
5145
- fs30.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
5146
- fs30.copyFileSync(dest, output);
5237
+ fs31.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
5238
+ fs31.copyFileSync(dest, output);
5147
5239
  }
5148
5240
  return { status: "success", generatedAudio: dest, model };
5149
5241
  }
@@ -5190,7 +5282,7 @@ Generated audio: ${result.generatedAudio}`);
5190
5282
  init_errors();
5191
5283
 
5192
5284
  // src/providers/minimax/generators/video.ts
5193
- import fs31 from "fs";
5285
+ import fs32 from "fs";
5194
5286
  import path35 from "path";
5195
5287
  async function generateMinimaxVideo(opts) {
5196
5288
  const {
@@ -5244,11 +5336,11 @@ async function generateMinimaxVideo(opts) {
5244
5336
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
5245
5337
  }
5246
5338
  const elapsed = (Date.now() - start) / 1e3;
5247
- const stat = fs31.statSync(dest);
5339
+ const stat = fs32.statSync(dest);
5248
5340
  const fileSizeMb = stat.size / (1024 * 1024);
5249
5341
  if (output) {
5250
- fs31.mkdirSync(path35.dirname(path35.resolve(output)), { recursive: true });
5251
- fs31.copyFileSync(dest, output);
5342
+ fs32.mkdirSync(path35.dirname(path35.resolve(output)), { recursive: true });
5343
+ fs32.copyFileSync(dest, output);
5252
5344
  }
5253
5345
  await maybeDownloadThumb(pollResult, dest, {
5254
5346
  skip: thumb === false,
@@ -5311,7 +5403,7 @@ Generated video: ${result.generatedVideo}`);
5311
5403
  // src/providers/minimax/generators/image.ts
5312
5404
  init_errors();
5313
5405
  init_http_client();
5314
- import fs32 from "fs";
5406
+ import fs33 from "fs";
5315
5407
  import path36 from "path";
5316
5408
  async function generateMinimaxImage(opts) {
5317
5409
  const {
@@ -5351,7 +5443,7 @@ async function generateMinimaxImage(opts) {
5351
5443
  const bytes = await fetchBytes(url);
5352
5444
  const fname = `minimax_image_${Date.now()}_${i}.png`;
5353
5445
  const dest = path36.join(outDir, fname);
5354
- fs32.writeFileSync(dest, bytes);
5446
+ fs33.writeFileSync(dest, bytes);
5355
5447
  savedFiles.push(dest);
5356
5448
  logger?.success(`Saved: ${dest}`);
5357
5449
  } catch (e) {
@@ -5362,8 +5454,8 @@ async function generateMinimaxImage(opts) {
5362
5454
  }
5363
5455
  }
5364
5456
  if (output && savedFiles[0]) {
5365
- fs32.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
5366
- fs32.copyFileSync(savedFiles[0], output);
5457
+ fs33.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
5458
+ fs33.copyFileSync(savedFiles[0], output);
5367
5459
  }
5368
5460
  return { status: "success", generatedImages: savedFiles, model };
5369
5461
  }
@@ -5403,7 +5495,7 @@ Generated ${finalImages.length} image(s):`);
5403
5495
  // src/providers/minimax/commands/image-to-image.ts
5404
5496
  init_errors();
5405
5497
  init_http_client();
5406
- import fs33 from "fs";
5498
+ import fs34 from "fs";
5407
5499
  import path37 from "path";
5408
5500
  function registerMinimaxImageToImageCommand(parent) {
5409
5501
  parent.command("image-to-image").alias("i2i").description(
@@ -5460,7 +5552,7 @@ function registerMinimaxImageToImageCommand(parent) {
5460
5552
  try {
5461
5553
  const bytes = await fetchBytes(url);
5462
5554
  const dest = path37.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
5463
- fs33.writeFileSync(dest, bytes);
5555
+ fs34.writeFileSync(dest, bytes);
5464
5556
  saved.push(dest);
5465
5557
  logger.success(`Saved: ${dest}`);
5466
5558
  } catch (e) {
@@ -5471,8 +5563,8 @@ function registerMinimaxImageToImageCommand(parent) {
5471
5563
  }
5472
5564
  }
5473
5565
  if (opts.output && saved[0]) {
5474
- fs33.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
5475
- fs33.copyFileSync(saved[0], opts.output);
5566
+ fs34.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
5567
+ fs34.copyFileSync(saved[0], opts.output);
5476
5568
  logger.success(`Copied to: ${opts.output}`);
5477
5569
  }
5478
5570
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -5607,11 +5699,11 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
5607
5699
 
5608
5700
  // src/providers/openai/openai-audio.ts
5609
5701
  init_http_client();
5610
- import fs35 from "fs";
5702
+ import fs36 from "fs";
5611
5703
  import path39 from "path";
5612
5704
 
5613
5705
  // src/providers/openai/client.ts
5614
- import fs34 from "fs";
5706
+ import fs35 from "fs";
5615
5707
  import path38 from "path";
5616
5708
  init_errors();
5617
5709
  init_http_client();
@@ -5680,8 +5772,8 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
5680
5772
  }
5681
5773
  function fileFromPath(filePath) {
5682
5774
  const abs = path38.resolve(filePath);
5683
- if (!fs34.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
5684
- const bytes = fs34.readFileSync(abs);
5775
+ if (!fs35.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
5776
+ const bytes = fs35.readFileSync(abs);
5685
5777
  return new File([new Uint8Array(bytes)], path38.basename(abs));
5686
5778
  }
5687
5779
 
@@ -5741,11 +5833,11 @@ async function generateOpenAISpeech(opts) {
5741
5833
  const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
5742
5834
  const ext = extFromAudioFormat(opts.outputFormat);
5743
5835
  const dest = path39.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
5744
- fs35.writeFileSync(dest, bytes);
5836
+ fs36.writeFileSync(dest, bytes);
5745
5837
  opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
5746
5838
  if (opts.output) {
5747
- fs35.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
5748
- fs35.copyFileSync(dest, opts.output);
5839
+ fs36.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
5840
+ fs36.copyFileSync(dest, opts.output);
5749
5841
  }
5750
5842
  return dest;
5751
5843
  }
@@ -5768,8 +5860,8 @@ async function transcribeOpenAIAudio(opts) {
5768
5860
  const text = typeof data.text === "string" ? data.text : void 0;
5769
5861
  const ext = opts.format === "text" ? "txt" : "json";
5770
5862
  const dest = opts.output ? path39.resolve(opts.output) : path39.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
5771
- fs35.mkdirSync(path39.dirname(dest), { recursive: true });
5772
- fs35.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
5863
+ fs36.mkdirSync(path39.dirname(dest), { recursive: true });
5864
+ fs36.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
5773
5865
  opts.logger?.success(`Saved transcript: ${dest}`);
5774
5866
  return { path: dest, text };
5775
5867
  }
@@ -5796,7 +5888,7 @@ Generated audio: ${dest}`);
5796
5888
  }
5797
5889
 
5798
5890
  // src/providers/openai/codex-image-driver.ts
5799
- import fs36 from "fs";
5891
+ import fs37 from "fs";
5800
5892
  import os2 from "os";
5801
5893
  import path40 from "path";
5802
5894
  import { execa } from "execa";
@@ -5848,11 +5940,11 @@ async function runCodexImageGeneration(opts) {
5848
5940
  const runner = opts.runner ?? defaultRunner;
5849
5941
  const ext = extFromImageFormat(opts.outputFormat);
5850
5942
  const dest = opts.output ? path40.resolve(opts.output) : path40.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
5851
- fs36.mkdirSync(path40.dirname(dest), { recursive: true });
5852
- const tmp = fs36.mkdtempSync(path40.join(os2.tmpdir(), "multix-codex-image-"));
5943
+ fs37.mkdirSync(path40.dirname(dest), { recursive: true });
5944
+ const tmp = fs37.mkdtempSync(path40.join(os2.tmpdir(), "multix-codex-image-"));
5853
5945
  const schema = path40.join(tmp, "schema.json");
5854
5946
  const result = path40.join(tmp, "result.json");
5855
- fs36.writeFileSync(
5947
+ fs37.writeFileSync(
5856
5948
  schema,
5857
5949
  JSON.stringify({
5858
5950
  type: "object",
@@ -5882,7 +5974,7 @@ async function runCodexImageGeneration(opts) {
5882
5974
  if (res.exitCode !== 0) {
5883
5975
  throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
5884
5976
  }
5885
- if (!fs36.existsSync(dest) || fs36.statSync(dest).size === 0) {
5977
+ if (!fs37.existsSync(dest) || fs37.statSync(dest).size === 0) {
5886
5978
  throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
5887
5979
  }
5888
5980
  return { path: dest };
@@ -5900,7 +5992,7 @@ function buildCodexPrompt(userPrompt, outputPath) {
5900
5992
  // src/providers/openai/openai-image.ts
5901
5993
  init_errors();
5902
5994
  init_http_client();
5903
- import fs37 from "fs";
5995
+ import fs38 from "fs";
5904
5996
  import path41 from "path";
5905
5997
  function buildImageGenerationBody(opts) {
5906
5998
  return {
@@ -5935,13 +6027,13 @@ function saveOpenAIImages(opts) {
5935
6027
  const b64 = items[i]?.b64_json;
5936
6028
  if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
5937
6029
  const dest = path41.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
5938
- fs37.writeFileSync(dest, Buffer.from(b64, "base64"));
6030
+ fs38.writeFileSync(dest, Buffer.from(b64, "base64"));
5939
6031
  saved.push(dest);
5940
6032
  opts.logger?.success(`Saved: ${dest}`);
5941
6033
  }
5942
6034
  if (opts.output && saved[0]) {
5943
- fs37.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
5944
- fs37.copyFileSync(saved[0], opts.output);
6035
+ fs38.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
6036
+ fs38.copyFileSync(saved[0], opts.output);
5945
6037
  opts.logger?.success(`Copied to: ${opts.output}`);
5946
6038
  }
5947
6039
  return saved;
@@ -6139,7 +6231,7 @@ function registerOpenAICommands(program2) {
6139
6231
  }
6140
6232
 
6141
6233
  // src/providers/openrouter/client.ts
6142
- import fs38 from "fs";
6234
+ import fs39 from "fs";
6143
6235
  import path42 from "path";
6144
6236
  init_errors();
6145
6237
  init_http_client();
@@ -6306,7 +6398,7 @@ async function generateOpenRouterImage(opts) {
6306
6398
  try {
6307
6399
  const bytes = await fetchBytes(url);
6308
6400
  const dest = path42.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
6309
- fs38.writeFileSync(dest, bytes);
6401
+ fs39.writeFileSync(dest, bytes);
6310
6402
  savedFiles.push(dest);
6311
6403
  logger?.success(`Saved: ${dest}`);
6312
6404
  } catch (e) {
@@ -6317,8 +6409,8 @@ async function generateOpenRouterImage(opts) {
6317
6409
  }
6318
6410
  }
6319
6411
  if (output && savedFiles[0]) {
6320
- fs38.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
6321
- fs38.writeFileSync(output, fs38.readFileSync(savedFiles[0]));
6412
+ fs39.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
6413
+ fs39.writeFileSync(output, fs39.readFileSync(savedFiles[0]));
6322
6414
  }
6323
6415
  return { status: "success", generatedImages: savedFiles, model: usedModel };
6324
6416
  }
@@ -6359,7 +6451,7 @@ Generated ${finalImages.length} image(s):`);
6359
6451
  }
6360
6452
 
6361
6453
  // src/providers/openrouter/commands/image-to-image.ts
6362
- import fs39 from "fs";
6454
+ import fs40 from "fs";
6363
6455
  import path43 from "path";
6364
6456
  init_errors();
6365
6457
  init_http_client();
@@ -6454,13 +6546,13 @@ async function runImageToImage(opts) {
6454
6546
  if (!url) continue;
6455
6547
  const bytes = await fetchBytes(url);
6456
6548
  const dest = path43.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
6457
- fs39.writeFileSync(dest, bytes);
6549
+ fs40.writeFileSync(dest, bytes);
6458
6550
  saved.push(dest);
6459
6551
  opts.logger.success(`Saved: ${dest}`);
6460
6552
  }
6461
6553
  if (opts.output && saved[0]) {
6462
- fs39.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
6463
- fs39.copyFileSync(saved[0], opts.output);
6554
+ fs40.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
6555
+ fs40.copyFileSync(saved[0], opts.output);
6464
6556
  opts.logger.success(`Copied to: ${opts.output}`);
6465
6557
  }
6466
6558
  return saved;
@@ -6506,7 +6598,7 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
6506
6598
  }
6507
6599
 
6508
6600
  // src/providers/openrouter/commands/video-status.ts
6509
- import fs40 from "fs";
6601
+ import fs41 from "fs";
6510
6602
  import path44 from "path";
6511
6603
  async function downloadOpenRouterVideo(opts) {
6512
6604
  const { jobId, logger, source } = opts;
@@ -6522,14 +6614,14 @@ async function downloadOpenRouterVideo(opts) {
6522
6614
  logger.error("Empty response body");
6523
6615
  process.exit(1);
6524
6616
  }
6525
- fs40.mkdirSync(path44.dirname(dest), { recursive: true });
6526
- const ws = fs40.createWriteStream(dest);
6617
+ fs41.mkdirSync(path44.dirname(dest), { recursive: true });
6618
+ const ws = fs41.createWriteStream(dest);
6527
6619
  const { pipeline: pipeline2 } = await import("stream/promises");
6528
6620
  await pipeline2(r.body, ws);
6529
6621
  logger.success(`Saved ${dest}`);
6530
6622
  if (opts.output) {
6531
- fs40.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
6532
- fs40.copyFileSync(dest, opts.output);
6623
+ fs41.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
6624
+ fs41.copyFileSync(dest, opts.output);
6533
6625
  logger.success(`Copied to ${opts.output}`);
6534
6626
  }
6535
6627
  await maybeDownloadThumb(source, dest, {
@@ -6696,9 +6788,9 @@ function registerOpenRouterCommands(program2) {
6696
6788
 
6697
6789
  // src/commands/check-helpers/binary-check.ts
6698
6790
  import { execa as execa2 } from "execa";
6699
- async function checkBinary(binary) {
6791
+ async function checkBinary(binary, versionFlag = "--version") {
6700
6792
  try {
6701
- const result = await execa2(binary, ["--version"], { reject: true, all: true });
6793
+ const result = await execa2(binary, [versionFlag], { reject: true, all: true });
6702
6794
  const version = (result.stdout || result.stderr || "").split("\n")[0]?.trim();
6703
6795
  return { available: true, version, binary };
6704
6796
  } catch (e) {
@@ -6720,7 +6812,8 @@ var MODELS_URL = "https://generativelanguage.googleapis.com/v1beta/models";
6720
6812
  async function pingGemini(apiKey) {
6721
6813
  try {
6722
6814
  const resp = await httpJson({
6723
- url: `${MODELS_URL}?key=${encodeURIComponent(apiKey)}`,
6815
+ url: MODELS_URL,
6816
+ headers: { "x-goog-api-key": apiKey },
6724
6817
  timeoutMs: 1e4
6725
6818
  });
6726
6819
  const modelCount = Array.isArray(resp.models) ? resp.models.length : 0;
@@ -6846,7 +6939,7 @@ function registerCheckCommand(program2) {
6846
6939
  const logger = createLogger({ verbose: opts.verbose ?? false });
6847
6940
  let exitCode = 0;
6848
6941
  logger.header("Tooling");
6849
- const ffmpegStatus = await checkBinary("ffmpeg");
6942
+ const ffmpegStatus = await checkBinary("ffmpeg", "-version");
6850
6943
  const magickStatus = await checkBinary("magick");
6851
6944
  if (ffmpegStatus.available) {
6852
6945
  logger.success(`ffmpeg found${ffmpegStatus.version ? ` \u2014 ${ffmpegStatus.version}` : ""}`);
@@ -6947,7 +7040,7 @@ function registerCheckCommand(program2) {
6947
7040
 
6948
7041
  // src/commands/doc/convert.ts
6949
7042
  init_errors();
6950
- import fs41 from "fs";
7043
+ import fs42 from "fs";
6951
7044
  import path45 from "path";
6952
7045
 
6953
7046
  // src/commands/doc/default-prompt.ts
@@ -6969,7 +7062,7 @@ async function convertDocuments(opts) {
6969
7062
  const model = opts.model ?? DOC_MODEL_DEFAULT;
6970
7063
  const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
6971
7064
  for (const f of inputs) {
6972
- if (!fs41.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
7065
+ if (!fs42.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
6973
7066
  }
6974
7067
  let outputPath;
6975
7068
  if (output) {
@@ -6980,7 +7073,7 @@ async function convertDocuments(opts) {
6980
7073
  } else {
6981
7074
  outputPath = path45.join(getOutputDir(), "document-extraction.md");
6982
7075
  }
6983
- fs41.mkdirSync(path45.dirname(outputPath), { recursive: true });
7076
+ fs42.mkdirSync(path45.dirname(outputPath), { recursive: true });
6984
7077
  const results = [];
6985
7078
  for (let i = 0; i < inputs.length; i++) {
6986
7079
  const filePath = inputs[i];
@@ -7044,7 +7137,7 @@ async function convertDocuments(opts) {
7044
7137
  }
7045
7138
  lines.push("\n---\n");
7046
7139
  }
7047
- fs41.writeFileSync(outputPath, lines.join("\n"), "utf8");
7140
+ fs42.writeFileSync(outputPath, lines.join("\n"), "utf8");
7048
7141
  const succeeded = results.filter((r) => r.status === "success").length;
7049
7142
  console.log(`
7050
7143
  ${"=".repeat(50)}`);
@@ -7092,11 +7185,11 @@ function registerDocCommands(program2) {
7092
7185
 
7093
7186
  // src/commands/media/index.ts
7094
7187
  init_errors();
7095
- import fs44 from "fs";
7188
+ import fs45 from "fs";
7096
7189
  import path48 from "path";
7097
7190
 
7098
7191
  // src/commands/media/batch.ts
7099
- import fs42 from "fs";
7192
+ import fs43 from "fs";
7100
7193
  import path46 from "path";
7101
7194
 
7102
7195
  // src/commands/media/detect-type.ts
@@ -7293,8 +7386,8 @@ async function batchOptimize(opts) {
7293
7386
  verbose,
7294
7387
  logger
7295
7388
  } = opts;
7296
- fs42.mkdirSync(outputDir, { recursive: true });
7297
- const entries = fs42.readdirSync(inputDir);
7389
+ fs43.mkdirSync(outputDir, { recursive: true });
7390
+ const entries = fs43.readdirSync(inputDir);
7298
7391
  const mediaFiles = entries.filter((f) => {
7299
7392
  const ext = path46.extname(f).toLowerCase();
7300
7393
  return ALL_EXTS.has(ext);
@@ -7346,7 +7439,7 @@ async function batchOptimize(opts) {
7346
7439
 
7347
7440
  // src/commands/media/split-video.ts
7348
7441
  init_errors();
7349
- import fs43 from "fs";
7442
+ import fs44 from "fs";
7350
7443
  import path47 from "path";
7351
7444
  async function splitVideo(opts) {
7352
7445
  await assertFfmpeg();
@@ -7358,7 +7451,7 @@ async function splitVideo(opts) {
7358
7451
  logger?.info("Video is short enough \u2014 no splitting needed");
7359
7452
  return [input];
7360
7453
  }
7361
- fs43.mkdirSync(outputDir, { recursive: true });
7454
+ fs44.mkdirSync(outputDir, { recursive: true });
7362
7455
  const ext = path47.extname(input) || ".mp4";
7363
7456
  const baseName = path47.basename(input, ext);
7364
7457
  const outputPattern = path47.join(outputDir, `${baseName}_chunk_%03d${ext}`);
@@ -7381,7 +7474,7 @@ async function splitVideo(opts) {
7381
7474
  const files = [];
7382
7475
  for (let i = 0; i < numChunks + 1; i++) {
7383
7476
  const fname = path47.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
7384
- if (fs43.existsSync(fname)) files.push(fname);
7477
+ if (fs44.existsSync(fname)) files.push(fname);
7385
7478
  }
7386
7479
  logger?.success(`Created ${files.length} chunks in ${outputDir}`);
7387
7480
  return files;
@@ -7395,8 +7488,8 @@ function registerMediaCommands(program2) {
7395
7488
  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(
7396
7489
  async (opts) => {
7397
7490
  const logger = createLogger({ verbose: opts.verbose ?? false });
7398
- if (!fs44.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7399
- fs44.mkdirSync(path48.dirname(path48.resolve(opts.output)), { recursive: true });
7491
+ if (!fs45.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7492
+ fs45.mkdirSync(path48.dirname(path48.resolve(opts.output)), { recursive: true });
7400
7493
  const ext = path48.extname(opts.input).toLowerCase();
7401
7494
  const kind = extToKind(ext);
7402
7495
  if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
@@ -7437,7 +7530,7 @@ function registerMediaCommands(program2) {
7437
7530
  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(
7438
7531
  async (opts) => {
7439
7532
  const logger = createLogger({ verbose: opts.verbose ?? false });
7440
- if (!fs44.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7533
+ if (!fs45.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7441
7534
  const chunks = await splitVideo({
7442
7535
  input: opts.input,
7443
7536
  outputDir: opts.outputDir,
@@ -7453,7 +7546,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
7453
7546
  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(
7454
7547
  async (opts) => {
7455
7548
  const logger = createLogger({ verbose: opts.verbose ?? false });
7456
- if (!fs44.existsSync(opts.inputDir))
7549
+ if (!fs45.existsSync(opts.inputDir))
7457
7550
  throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
7458
7551
  const result = await batchOptimize({
7459
7552
  inputDir: opts.inputDir,
@@ -7640,7 +7733,12 @@ function registerCommands(program2) {
7640
7733
 
7641
7734
  // src/core/index.ts
7642
7735
  init_errors();
7736
+ init_redact();
7643
7737
  init_http_client();
7738
+ init_fetch_json();
7739
+
7740
+ // src/core/async-poll.ts
7741
+ init_errors();
7644
7742
 
7645
7743
  // src/cli.ts
7646
7744
  loadEnv();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrgoonie/multix",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "AI multimodal CLI — OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, 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), 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), 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, video generation, text-to-video, image-to-video, i2v, ElevenLabs voice cloning, updating/upgrading multix, or asks for batch media optimization.
4
- version: 0.7.0 # x-release-please-version
4
+ version: 0.7.2 # x-release-please-version
5
5
  ---
6
6
 
7
7
  # multix — AI Multimodal CLI