@mrgoonie/multix 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -75,6 +75,7 @@ __export(http_client_exports, {
75
75
  fetchBytes: () => fetchBytes,
76
76
  httpJson: () => httpJson
77
77
  });
78
+ import { once } from "events";
78
79
  import fs4 from "fs";
79
80
  import path4 from "path";
80
81
  import { pipeline } from "stream/promises";
@@ -126,7 +127,12 @@ async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
126
127
  throw new HttpError(0, "Response body is null", url);
127
128
  }
128
129
  const fileStream = fs4.createWriteStream(dest);
129
- await pipeline(response.body, fileStream);
130
+ try {
131
+ await pipeline(response.body, fileStream);
132
+ } catch (err2) {
133
+ if (!fileStream.closed) await once(fileStream, "close");
134
+ throw err2;
135
+ }
130
136
  }
131
137
  async function fetchBytes(urlOrDataUri) {
132
138
  if (urlOrDataUri.startsWith("data:")) {
@@ -160,7 +166,7 @@ var init_http_client = __esm({
160
166
  });
161
167
 
162
168
  // src/cli.ts
163
- import { createRequire } from "module";
169
+ import { createRequire as createRequire2 } from "module";
164
170
  import { Command } from "commander";
165
171
 
166
172
  // src/providers/byteplus/commands/generate-3d.ts
@@ -414,9 +420,9 @@ var BytePlusClient = class {
414
420
  }
415
421
  apiKey;
416
422
  baseUrl;
417
- async request(path47, opts = {}) {
423
+ async request(path49, opts = {}) {
418
424
  const base = this.baseUrl.replace(/\/$/, "");
419
- const url = new URL(base + path47);
425
+ const url = new URL(base + path49);
420
426
  if (opts.query) {
421
427
  for (const [k, v] of Object.entries(opts.query)) {
422
428
  if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
@@ -434,14 +440,14 @@ var BytePlusClient = class {
434
440
  timeoutMs: opts.timeoutMs ?? 12e4
435
441
  });
436
442
  }
437
- get(path47, query, logger) {
438
- return this.request(path47, { method: "GET", query, logger });
443
+ get(path49, query, logger) {
444
+ return this.request(path49, { method: "GET", query, logger });
439
445
  }
440
- post(path47, body, logger) {
441
- return this.request(path47, { method: "POST", body, logger });
446
+ post(path49, body, logger) {
447
+ return this.request(path49, { method: "POST", body, logger });
442
448
  }
443
- delete(path47, logger) {
444
- return this.request(path47, { method: "DELETE", logger });
449
+ delete(path49, logger) {
450
+ return this.request(path49, { method: "DELETE", logger });
445
451
  }
446
452
  };
447
453
  function createBytePlusClient() {
@@ -1185,10 +1191,10 @@ function toRef(kind, resolved, role) {
1185
1191
  return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
1186
1192
  }
1187
1193
  async function resolveRefSpec(kind, spec, logger) {
1188
- const { path: path47, role } = parseRefSpec(spec);
1189
- if (kind === "image") return toRef(kind, await resolveImageInput(path47, logger), role);
1190
- if (kind === "video") return toRef(kind, await resolveVideoInput(path47, logger), role);
1191
- return toRef(kind, await resolveAudioInput(path47, logger), role);
1194
+ const { path: path49, role } = parseRefSpec(spec);
1195
+ if (kind === "image") return toRef(kind, await resolveImageInput(path49, logger), role);
1196
+ if (kind === "video") return toRef(kind, await resolveVideoInput(path49, logger), role);
1197
+ return toRef(kind, await resolveAudioInput(path49, logger), role);
1192
1198
  }
1193
1199
  async function resolveRefSpecs(specs, logger) {
1194
1200
  const out = [];
@@ -2575,18 +2581,455 @@ function registerElevenLabsCommands(program2) {
2575
2581
  registerAccountCommands(el);
2576
2582
  }
2577
2583
 
2584
+ // src/providers/fal/commands/image.ts
2585
+ import fs16 from "fs";
2586
+ import path20 from "path";
2587
+
2588
+ // src/providers/fal/models.ts
2589
+ var DEFAULT_FAL_BASE_URL = "https://queue.fal.run";
2590
+ var FAL_DEFAULTS = {
2591
+ // FLUX.1 [schnell] — fast, low-cost default text-to-image model.
2592
+ imageModel: process.env.FAL_IMAGE_MODEL ?? "fal-ai/flux/schnell",
2593
+ // Stable, widely available default text-to-video model.
2594
+ videoModel: process.env.FAL_VIDEO_MODEL ?? "fal-ai/kling-video/v1.6/standard/text-to-video",
2595
+ // Matching image-to-video model — used when --image-url is passed without -m.
2596
+ videoImageModel: process.env.FAL_VIDEO_IMAGE_MODEL ?? "fal-ai/kling-video/v1.6/standard/image-to-video"
2597
+ };
2598
+
2599
+ // src/providers/fal/run-helpers.ts
2600
+ init_errors();
2601
+ init_http_client();
2602
+ import fs15 from "fs";
2603
+ import path19 from "path";
2604
+
2605
+ // src/providers/fal/client.ts
2606
+ init_errors();
2607
+ init_errors();
2608
+ init_http_client();
2609
+ function requireFalKey() {
2610
+ const key = resolveKey("FAL_KEY");
2611
+ if (!key) {
2612
+ throw new ConfigError("FAL_KEY is not set. Get one at https://fal.ai/dashboard/keys");
2613
+ }
2614
+ return key;
2615
+ }
2616
+ function falBaseUrl() {
2617
+ return resolveKey("FAL_BASE_URL") ?? DEFAULT_FAL_BASE_URL;
2618
+ }
2619
+ var FalClient = class {
2620
+ constructor(apiKey, baseUrl = falBaseUrl()) {
2621
+ this.apiKey = apiKey;
2622
+ this.baseUrl = baseUrl;
2623
+ }
2624
+ apiKey;
2625
+ baseUrl;
2626
+ /** `path` is appended verbatim to the base queue URL, e.g. "/fal-ai/flux/schnell". */
2627
+ async request(path49, opts = {}) {
2628
+ const base = this.baseUrl.replace(/\/$/, "");
2629
+ const suffix = path49.startsWith("/") ? path49 : `/${path49}`;
2630
+ return this.requestAbsolute(base + suffix, opts);
2631
+ }
2632
+ /** Same auth/headers as `request`, but `url` is used verbatim (e.g. a status_url/response_url returned by submit). */
2633
+ async requestAbsolute(url, opts = {}) {
2634
+ opts.logger?.debug(`${opts.method ?? "GET"} ${url}`);
2635
+ return httpJson({
2636
+ url,
2637
+ method: opts.method ?? "GET",
2638
+ headers: {
2639
+ accept: "application/json",
2640
+ authorization: `Key ${this.apiKey}`
2641
+ },
2642
+ body: opts.body,
2643
+ timeoutMs: opts.timeoutMs ?? 12e4
2644
+ });
2645
+ }
2646
+ get(path49, logger) {
2647
+ return this.request(path49, { method: "GET", logger });
2648
+ }
2649
+ post(path49, body, logger) {
2650
+ return this.request(path49, { method: "POST", body, logger });
2651
+ }
2652
+ getAbsolute(url, logger) {
2653
+ return this.requestAbsolute(url, { method: "GET", logger });
2654
+ }
2655
+ };
2656
+ function createFalClient() {
2657
+ return new FalClient(requireFalKey(), falBaseUrl());
2658
+ }
2659
+ function submitPath(model) {
2660
+ return `/${model.replace(/^\/+/, "")}`;
2661
+ }
2662
+ function appIdFromModel(model) {
2663
+ const segments = model.replace(/^\/+|\/+$/g, "").split("/");
2664
+ if (segments.length < 2 || !segments[0] || !segments[1]) {
2665
+ throw new ValidationError(
2666
+ `Invalid fal model id "${model}" \u2014 expected at least "owner/alias" (e.g. fal-ai/flux)`
2667
+ );
2668
+ }
2669
+ return `${segments[0]}/${segments[1]}`;
2670
+ }
2671
+ function statusPath(model, requestId) {
2672
+ return `/${appIdFromModel(model)}/requests/${requestId}/status`;
2673
+ }
2674
+ function resultPath(model, requestId) {
2675
+ return `/${appIdFromModel(model)}/requests/${requestId}`;
2676
+ }
2677
+
2678
+ // src/providers/fal/run-helpers.ts
2679
+ 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
+ }
2691
+ }
2692
+ function isTransientError(e) {
2693
+ if (e instanceof HttpError) return e.status === 0 || e.status >= 500;
2694
+ return false;
2695
+ }
2696
+ function sleep(ms, signal) {
2697
+ return new Promise((resolve, reject) => {
2698
+ const t = setTimeout(resolve, ms);
2699
+ signal?.addEventListener("abort", () => {
2700
+ clearTimeout(t);
2701
+ reject(new Error("Aborted"));
2702
+ });
2703
+ });
2704
+ }
2705
+ async function submitAndWait(opts) {
2706
+ const { model, input, logger } = opts;
2707
+ const client = createFalClient();
2708
+ const submitRes = await client.post(submitPath(model), input, logger);
2709
+ const requestId = submitRes.request_id;
2710
+ logger.debug(`request_id: ${requestId}`);
2711
+ logger.info("Polling for completion...");
2712
+ const waitTimeoutMs = opts.waitTimeoutMs ?? 48e4;
2713
+ const intervalMs = opts.pollIntervalMs ?? 3e3;
2714
+ const deadline = Date.now() + waitTimeoutMs;
2715
+ const resumeHint = `Resume with: multix fal status ${model} ${requestId} or multix fal result ${model} ${requestId}`;
2716
+ let attempt = 0;
2717
+ for (; ; ) {
2718
+ if (Date.now() >= deadline) {
2719
+ throw new Error(
2720
+ `Timed out waiting for fal request ${requestId} after ${waitTimeoutMs}ms. ${resumeHint}`
2721
+ );
2722
+ }
2723
+ attempt++;
2724
+ let statusRes;
2725
+ try {
2726
+ statusRes = await client.getAbsolute(submitRes.status_url, logger);
2727
+ } catch (e) {
2728
+ if (isTransientError(e)) {
2729
+ const msg2 = e instanceof Error ? e.message : String(e);
2730
+ logger.debug(`attempt ${attempt} \u2014 transient error, retrying: ${msg2}`);
2731
+ await sleep(intervalMs);
2732
+ continue;
2733
+ }
2734
+ const msg = e instanceof Error ? e.message : String(e);
2735
+ throw new Error(`Failed to poll status for fal request ${requestId}: ${msg}. ${resumeHint}`);
2736
+ }
2737
+ logger.debug(`attempt ${attempt} \u2014 status: ${statusRes.status}`);
2738
+ if (statusRes.status === "COMPLETED") break;
2739
+ await sleep(intervalMs);
2740
+ }
2741
+ try {
2742
+ const result = await client.getAbsolute(submitRes.response_url, logger);
2743
+ return { requestId, result };
2744
+ } catch (e) {
2745
+ const msg = e instanceof Error ? e.message : String(e);
2746
+ throw new Error(`Failed to fetch result for fal request ${requestId}: ${msg}. ${resumeHint}`);
2747
+ }
2748
+ }
2749
+ var MEDIA_URL_RE = /^https?:\/\//i;
2750
+ function isHttpUrl2(v) {
2751
+ return typeof v === "string" && MEDIA_URL_RE.test(v);
2752
+ }
2753
+ function urlOf(item) {
2754
+ if (isHttpUrl2(item)) return item;
2755
+ if (item !== null && typeof item === "object" && "url" in item) {
2756
+ const u = item.url;
2757
+ if (isHttpUrl2(u)) return u;
2758
+ }
2759
+ return void 0;
2760
+ }
2761
+ function extractKnownMediaUrls(result) {
2762
+ const urls = [];
2763
+ const images = result.images;
2764
+ if (Array.isArray(images)) {
2765
+ for (const img of images) {
2766
+ const u = urlOf(img);
2767
+ if (u) urls.push(u);
2768
+ }
2769
+ }
2770
+ for (const key of ["video", "image", "audio"]) {
2771
+ const u = urlOf(result[key]);
2772
+ if (u) urls.push(u);
2773
+ }
2774
+ return urls;
2775
+ }
2776
+ function extractMediaUrlsGeneric(value, acc = [], seen = /* @__PURE__ */ new Set()) {
2777
+ if (value === null || typeof value !== "object") return acc;
2778
+ if (seen.has(value)) return acc;
2779
+ seen.add(value);
2780
+ if (Array.isArray(value)) {
2781
+ for (const item of value) extractMediaUrlsGeneric(item, acc, seen);
2782
+ return acc;
2783
+ }
2784
+ for (const [key, val] of Object.entries(value)) {
2785
+ if (key === "url" && isHttpUrl2(val)) {
2786
+ acc.push(val);
2787
+ } else if (typeof val === "object" && val !== null) {
2788
+ extractMediaUrlsGeneric(val, acc, seen);
2789
+ }
2790
+ }
2791
+ return acc;
2792
+ }
2793
+ function extractMediaUrls(result) {
2794
+ if (result === null || typeof result !== "object") return [];
2795
+ const known = extractKnownMediaUrls(result);
2796
+ if (known.length > 0) return known;
2797
+ return extractMediaUrlsGeneric(result);
2798
+ }
2799
+ function extFromUrl(url, fallback = ".bin") {
2800
+ try {
2801
+ const u = new URL(url);
2802
+ const e = path19.extname(u.pathname);
2803
+ return e.length > 0 ? e : fallback;
2804
+ } catch {
2805
+ return fallback;
2806
+ }
2807
+ }
2808
+ async function downloadResultMedia(urls, outDir, requestId, logger) {
2809
+ const saved = [];
2810
+ for (let i = 0; i < urls.length; i++) {
2811
+ const url = urls[i];
2812
+ if (!url) continue;
2813
+ const ext = extFromUrl(url);
2814
+ const filename = `fal-${requestId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
2815
+ const filepath = path19.join(outDir, filename);
2816
+ try {
2817
+ await downloadFile(url, filepath);
2818
+ saved.push(filepath);
2819
+ logger.success(`Saved ${filepath}`);
2820
+ } catch (e) {
2821
+ fs15.rmSync(filepath, { force: true });
2822
+ logger.warn(`Download failed for ${url}: ${e instanceof Error ? e.message : e}`);
2823
+ }
2824
+ }
2825
+ return saved;
2826
+ }
2827
+
2828
+ // src/providers/fal/commands/image.ts
2829
+ function registerFalImageCommand(parent) {
2830
+ parent.command("image <prompt>").description(
2831
+ `Generate images from a text prompt via fal.ai (default: ${FAL_DEFAULTS.imageModel})`
2832
+ ).option("-m, --model <id>", "fal model id", FAL_DEFAULTS.imageModel).option("--image-size <size>", "e.g. square_hd, portrait_16_9, landscape_16_9, or WxH").option("-n, --num-images <n>", "Number of images", "1").option("--seed <n>", "Seed").option("--negative-prompt <text>", "Negative prompt").option("--output <path>", "Copy first image to this path (single-image only)").addOption(imageFormatOption()).addOption(noWebpOption()).option("--no-download", "Skip downloading; print URLs only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "480000").option("-v, --verbose", "Verbose logging").action(
2833
+ async (prompt, opts) => {
2834
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2835
+ const input = {
2836
+ prompt,
2837
+ num_images: Number.parseInt(opts.numImages, 10) || 1,
2838
+ ...opts.imageSize ? { image_size: opts.imageSize } : {},
2839
+ ...opts.seed ? { seed: Number.parseInt(opts.seed, 10) } : {},
2840
+ ...opts.negativePrompt ? { negative_prompt: opts.negativePrompt } : {}
2841
+ };
2842
+ logger.info(`Submitting to ${opts.model}...`);
2843
+ const { requestId, result } = await submitAndWait({
2844
+ model: opts.model,
2845
+ input,
2846
+ logger,
2847
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 48e4
2848
+ });
2849
+ const urls = extractMediaUrls(result);
2850
+ if (urls.length === 0) {
2851
+ logger.warn("No image URLs found in result.");
2852
+ console.log(JSON.stringify(result, null, 2));
2853
+ return;
2854
+ }
2855
+ if (opts.download === false) {
2856
+ for (const u of urls) console.log(u);
2857
+ return;
2858
+ }
2859
+ const outDir = getOutputDir();
2860
+ const saved = await downloadResultMedia(urls, outDir, requestId, logger);
2861
+ if (saved.length === 0) {
2862
+ logger.error(`Found ${urls.length} image URL(s) but none downloaded successfully.`);
2863
+ process.exit(1);
2864
+ }
2865
+ if (opts.output && saved[0]) {
2866
+ const dest = path20.resolve(opts.output);
2867
+ fs16.mkdirSync(path20.dirname(dest), { recursive: true });
2868
+ fs16.copyFileSync(saved[0], dest);
2869
+ logger.success(`Copied to ${dest}`);
2870
+ }
2871
+ const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
2872
+ console.log(`
2873
+ Generated ${finalImages.length} image(s):`);
2874
+ for (const f of finalImages) console.log(` ${f}`);
2875
+ }
2876
+ );
2877
+ }
2878
+
2879
+ // src/providers/fal/commands/result.ts
2880
+ function registerFalResultCommand(parent) {
2881
+ parent.command("result <model> <requestId>").description("Fetch a completed fal.ai request's result").option("--download", "Download media URLs found in the result to the output dir").option("-v, --verbose", "Verbose logging").action(
2882
+ async (model, requestId, opts) => {
2883
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2884
+ const client = createFalClient();
2885
+ let result;
2886
+ try {
2887
+ result = await client.get(resultPath(model, requestId), logger);
2888
+ } catch (e) {
2889
+ const msg = e instanceof Error ? e.message : String(e);
2890
+ logger.error(`Failed to fetch result for fal request ${requestId}: ${msg}`);
2891
+ process.exit(1);
2892
+ }
2893
+ if (!opts.download) {
2894
+ console.log(JSON.stringify(result, null, 2));
2895
+ return;
2896
+ }
2897
+ const urls = extractMediaUrls(result);
2898
+ if (urls.length === 0) {
2899
+ logger.warn("No media URLs found in result.");
2900
+ console.log(JSON.stringify(result, null, 2));
2901
+ return;
2902
+ }
2903
+ const outDir = getOutputDir();
2904
+ const saved = await downloadResultMedia(urls, outDir, requestId, logger);
2905
+ if (saved.length === 0) {
2906
+ logger.error(`Found ${urls.length} media URL(s) but none downloaded successfully.`);
2907
+ process.exit(1);
2908
+ }
2909
+ console.log(`
2910
+ Downloaded ${saved.length} file(s):`);
2911
+ for (const f of saved) console.log(` ${f}`);
2912
+ }
2913
+ );
2914
+ }
2915
+
2916
+ // src/providers/fal/commands/run.ts
2917
+ function registerFalRunCommand(parent) {
2918
+ parent.command("run <model>").description("Run any fal.ai model by id (e.g. fal-ai/flux/schnell)").requiredOption("--input <json>", "JSON input, or @path/to/file.json").option("--no-download", "Skip downloading media; print result JSON only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "480000").option("-v, --verbose", "Verbose logging").action(
2919
+ async (model, opts) => {
2920
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2921
+ const input = parseInputArg(opts.input);
2922
+ logger.info(`Submitting to ${model}...`);
2923
+ const { requestId, result } = await submitAndWait({
2924
+ model,
2925
+ input,
2926
+ logger,
2927
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 48e4
2928
+ });
2929
+ if (opts.download === false) {
2930
+ console.log(JSON.stringify(result, null, 2));
2931
+ return;
2932
+ }
2933
+ const urls = extractMediaUrls(result);
2934
+ if (urls.length === 0) {
2935
+ logger.warn("No media URLs found in result; printing raw JSON.");
2936
+ console.log(JSON.stringify(result, null, 2));
2937
+ return;
2938
+ }
2939
+ const outDir = getOutputDir();
2940
+ const saved = await downloadResultMedia(urls, outDir, requestId, logger);
2941
+ if (saved.length === 0) {
2942
+ logger.error(`Found ${urls.length} media URL(s) but none downloaded successfully.`);
2943
+ process.exit(1);
2944
+ }
2945
+ console.log(`
2946
+ Downloaded ${saved.length} file(s):`);
2947
+ for (const f of saved) console.log(` ${f}`);
2948
+ }
2949
+ );
2950
+ }
2951
+
2952
+ // src/providers/fal/commands/status.ts
2953
+ function registerFalStatusCommand(parent) {
2954
+ parent.command("status <model> <requestId>").description("Get a fal.ai request's queue status").option("-v, --verbose", "Verbose logging").action(async (model, requestId, opts) => {
2955
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2956
+ const client = createFalClient();
2957
+ const res = await client.get(statusPath(model, requestId), logger);
2958
+ console.log(JSON.stringify(res, null, 2));
2959
+ });
2960
+ }
2961
+
2962
+ // src/providers/fal/commands/video.ts
2963
+ function registerFalVideoCommand(parent) {
2964
+ parent.command("video <prompt>").description(
2965
+ `Generate video via fal.ai (default text-to-video: ${FAL_DEFAULTS.videoModel}; default image-to-video: ${FAL_DEFAULTS.videoImageModel})`
2966
+ ).option(
2967
+ "-m, --model <id>",
2968
+ "fal model id (default depends on --image-url: text-to-video vs. image-to-video default)"
2969
+ ).option("--image-url <url>", "First-frame image URL \u2014 switches to image-to-video input").option("--duration <n>", "Duration in seconds (model-dependent)").option("--aspect-ratio <r>", "Aspect ratio hint (e.g. 16:9, 9:16)").option("--seed <n>", "Seed").option("--no-download", "Skip downloading; print URLs only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "900000").option("-v, --verbose", "Verbose logging").action(
2970
+ async (prompt, opts) => {
2971
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2972
+ const model = opts.model ?? (opts.imageUrl ? FAL_DEFAULTS.videoImageModel : FAL_DEFAULTS.videoModel);
2973
+ appIdFromModel(model);
2974
+ const input = {
2975
+ prompt,
2976
+ ...opts.imageUrl ? { image_url: opts.imageUrl } : {},
2977
+ ...opts.duration ? { duration: opts.duration } : {},
2978
+ ...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
2979
+ ...opts.seed ? { seed: Number.parseInt(opts.seed, 10) } : {}
2980
+ };
2981
+ logger.info(`Submitting to ${model}...`);
2982
+ const { requestId, result } = await submitAndWait({
2983
+ model,
2984
+ input,
2985
+ logger,
2986
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 9e5
2987
+ });
2988
+ const urls = extractMediaUrls(result);
2989
+ if (urls.length === 0) {
2990
+ logger.warn("No video URLs found in result.");
2991
+ console.log(JSON.stringify(result, null, 2));
2992
+ return;
2993
+ }
2994
+ if (opts.download === false) {
2995
+ for (const u of urls) console.log(u);
2996
+ return;
2997
+ }
2998
+ const outDir = getOutputDir();
2999
+ const saved = await downloadResultMedia(urls, outDir, requestId, logger);
3000
+ if (saved.length === 0) {
3001
+ logger.error(`Found ${urls.length} video URL(s) but none downloaded successfully.`);
3002
+ process.exit(1);
3003
+ }
3004
+ console.log(`
3005
+ Generated ${saved.length} file(s):`);
3006
+ for (const f of saved) console.log(` ${f}`);
3007
+ }
3008
+ );
3009
+ }
3010
+
3011
+ // src/providers/fal/commands/index.ts
3012
+ function registerFalCommands(program2) {
3013
+ const fal = program2.command("fal").description("fal.ai: generic model runner, image, video, and queue status/result");
3014
+ registerFalRunCommand(fal);
3015
+ registerFalImageCommand(fal);
3016
+ registerFalVideoCommand(fal);
3017
+ registerFalStatusCommand(fal);
3018
+ registerFalResultCommand(fal);
3019
+ }
3020
+
2578
3021
  // src/providers/gemini/commands/analyze.ts
2579
3022
  init_errors();
2580
- import fs17 from "fs";
3023
+ import fs19 from "fs";
2581
3024
 
2582
3025
  // src/providers/gemini/client.ts
2583
- import fs15 from "fs";
2584
- import path20 from "path";
3026
+ import fs17 from "fs";
3027
+ import path22 from "path";
2585
3028
  init_errors();
2586
3029
  init_http_client();
2587
3030
 
2588
3031
  // src/providers/gemini/task-resolver.ts
2589
- import path19 from "path";
3032
+ import path21 from "path";
2590
3033
  var MIME_MAP = {
2591
3034
  // Audio
2592
3035
  ".mp3": "audio/mp3",
@@ -2627,7 +3070,7 @@ var MIME_MAP = {
2627
3070
  ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
2628
3071
  };
2629
3072
  function getMimeType(filePath) {
2630
- const ext = path19.extname(filePath).toLowerCase();
3073
+ const ext = path21.extname(filePath).toLowerCase();
2631
3074
  return MIME_MAP[ext] ?? "application/octet-stream";
2632
3075
  }
2633
3076
  function requiresProcessingWait(mimeType) {
@@ -2653,8 +3096,8 @@ async function uploadFile(filePath, opts = {}) {
2653
3096
  const apiKey = requireApiKey();
2654
3097
  const { timeoutMs = 3e5, logger } = opts;
2655
3098
  const mimeType = getMimeType(filePath);
2656
- const fileBytes = fs15.readFileSync(filePath);
2657
- const displayName = path20.basename(filePath);
3099
+ const fileBytes = fs17.readFileSync(filePath);
3100
+ const displayName = path22.basename(filePath);
2658
3101
  const boundary = `multix_${Date.now()}`;
2659
3102
  const metaJson = JSON.stringify({ file: { displayName, mimeType } });
2660
3103
  const body = Buffer.concat([
@@ -2696,7 +3139,7 @@ Content-Type: ${mimeType}\r
2696
3139
  if (requiresProcessingWait(mimeType)) {
2697
3140
  const deadline = Date.now() + timeoutMs;
2698
3141
  while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
2699
- await sleep(2e3);
3142
+ await sleep2(2e3);
2700
3143
  fileRef = await httpJson({
2701
3144
  url: `${BASE}/v1beta/${fileRef.name}`,
2702
3145
  headers: authHeaders2(apiKey)
@@ -2723,6 +3166,33 @@ async function generateContent(req) {
2723
3166
  }
2724
3167
  });
2725
3168
  }
3169
+ async function createInteraction(body) {
3170
+ const apiKey = requireApiKey();
3171
+ return httpJson({
3172
+ url: `${BASE}/v1beta/interactions`,
3173
+ method: "POST",
3174
+ headers: authHeaders2(apiKey),
3175
+ body
3176
+ });
3177
+ }
3178
+ function extractInteractionAudio(resp) {
3179
+ let found = null;
3180
+ const walk = (node) => {
3181
+ if (Array.isArray(node)) {
3182
+ for (const n of node) walk(n);
3183
+ return;
3184
+ }
3185
+ if (!node || typeof node !== "object") return;
3186
+ const o = node;
3187
+ if (o.type === "audio" && typeof o.data === "string") {
3188
+ const mime = o.mime_type ?? o.mimeType ?? "audio/wav";
3189
+ found = { mimeType: mime, data: o.data };
3190
+ }
3191
+ for (const v of Object.values(o)) walk(v);
3192
+ };
3193
+ walk(resp);
3194
+ return found;
3195
+ }
2726
3196
  function extractText(resp) {
2727
3197
  const parts = resp.candidates?.[0]?.content?.parts ?? [];
2728
3198
  return parts.filter((p) => p.text !== void 0).map((p) => p.text ?? "").join("");
@@ -2743,15 +3213,15 @@ function extractImages(resp) {
2743
3213
  ).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
2744
3214
  }
2745
3215
  function inlinePart(filePath) {
2746
- const data = fs15.readFileSync(filePath).toString("base64");
3216
+ const data = fs17.readFileSync(filePath).toString("base64");
2747
3217
  const mimeType = getMimeType(filePath);
2748
3218
  return { inlineData: { mimeType, data } };
2749
3219
  }
2750
3220
  function shouldUseFileApi(filePath) {
2751
- const stat = fs15.statSync(filePath);
3221
+ const stat = fs17.statSync(filePath);
2752
3222
  return stat.size > 20 * 1024 * 1024;
2753
3223
  }
2754
- function sleep(ms) {
3224
+ function sleep2(ms) {
2755
3225
  return new Promise((resolve) => setTimeout(resolve, ms));
2756
3226
  }
2757
3227
 
@@ -2761,7 +3231,7 @@ var IMAGE_MODEL_FALLBACK = "gemini-2.5-flash-image";
2761
3231
  var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
2762
3232
  var TEXT_MODEL_DEFAULT = "gemini-3.6-flash";
2763
3233
  var ANALYSIS_MODEL_DEFAULT = TEXT_MODEL_DEFAULT;
2764
- var TTS_MODEL_DEFAULT = "gemini-3.1-flash-tts-preview";
3234
+ var TTS_MODEL_DEFAULT = "gemini-3.8-flash-lite-tts";
2765
3235
  var DOC_MODEL_DEFAULT = TEXT_MODEL_DEFAULT;
2766
3236
  var IMAGEN_MODELS = /* @__PURE__ */ new Set([
2767
3237
  "imagen-4.0-generate-001",
@@ -2802,13 +3272,13 @@ function getDefaultModel(task) {
2802
3272
  }
2803
3273
 
2804
3274
  // src/providers/gemini/result-formatter.ts
2805
- import fs16 from "fs";
2806
- import path21 from "path";
3275
+ import fs18 from "fs";
3276
+ import path23 from "path";
2807
3277
  function formatResults(results, format, outputPath) {
2808
3278
  const content = renderFormat(results, format);
2809
3279
  if (outputPath) {
2810
- fs16.mkdirSync(path21.dirname(path21.resolve(outputPath)), { recursive: true });
2811
- fs16.writeFileSync(outputPath, content, "utf8");
3280
+ fs18.mkdirSync(path23.dirname(path23.resolve(outputPath)), { recursive: true });
3281
+ fs18.writeFileSync(outputPath, content, "utf8");
2812
3282
  } else {
2813
3283
  process.stdout.write(content);
2814
3284
  }
@@ -2875,7 +3345,7 @@ function registerAnalyzeCommand(parent) {
2875
3345
  const logger = createLogger({ verbose: opts.verbose ?? false });
2876
3346
  const model = opts.model ?? getDefaultModel("analyze");
2877
3347
  for (const f of opts.files) {
2878
- if (!fs17.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3348
+ if (!fs19.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
2879
3349
  }
2880
3350
  const results = [];
2881
3351
  for (let i = 0; i < opts.files.length; i++) {
@@ -2921,14 +3391,14 @@ function registerAnalyzeCommand(parent) {
2921
3391
 
2922
3392
  // src/providers/gemini/commands/extract.ts
2923
3393
  init_errors();
2924
- import fs18 from "fs";
3394
+ import fs20 from "fs";
2925
3395
  function registerExtractCommand(parent) {
2926
3396
  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(
2927
3397
  async (opts) => {
2928
3398
  const logger = createLogger({ verbose: opts.verbose ?? false });
2929
3399
  const model = opts.model ?? getDefaultModel("extract");
2930
3400
  for (const f of opts.files) {
2931
- if (!fs18.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3401
+ if (!fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
2932
3402
  }
2933
3403
  const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
2934
3404
  const results = [];
@@ -2970,8 +3440,8 @@ function registerExtractCommand(parent) {
2970
3440
  init_errors();
2971
3441
 
2972
3442
  // src/providers/gemini/generators/speech.ts
2973
- import fs19 from "fs";
2974
- import path22 from "path";
3443
+ import fs21 from "fs";
3444
+ import path24 from "path";
2975
3445
 
2976
3446
  // src/providers/gemini/voices.ts
2977
3447
  var GEMINI_TTS_VOICES = [
@@ -3007,10 +3477,16 @@ var GEMINI_TTS_VOICES = [
3007
3477
  "Sulafat"
3008
3478
  ];
3009
3479
  var GEMINI_TTS_MODELS = /* @__PURE__ */ new Set([
3480
+ "gemini-3.8-flash-lite-tts",
3481
+ "gemini-3.8-flash-tts",
3010
3482
  "gemini-3.1-flash-tts-preview",
3011
3483
  "gemini-2.5-flash-preview-tts",
3012
3484
  "gemini-2.5-pro-preview-tts"
3013
3485
  ]);
3486
+ var GEMINI_INTERACTIONS_TTS_MODELS = /* @__PURE__ */ new Set([
3487
+ "gemini-3.8-flash-lite-tts",
3488
+ "gemini-3.8-flash-tts"
3489
+ ]);
3014
3490
  var TTS_VOICE_DEFAULT = "Kore";
3015
3491
  var TTS_PCM_SAMPLE_RATE = 24e3;
3016
3492
  var TTS_PCM_CHANNELS = 1;
@@ -3019,10 +3495,19 @@ var TTS_OUTPUT_FORMATS = ["wav", "pcm"];
3019
3495
  function isValidGeminiVoice(name) {
3020
3496
  return GEMINI_TTS_VOICES.includes(name);
3021
3497
  }
3498
+ function isCustomGeminiVoiceId(name) {
3499
+ return /^(voice|voicekey)_[A-Za-z0-9_-]+$/.test(name);
3500
+ }
3501
+ function isVoiceAllowedForModel(voice, model) {
3502
+ return isValidGeminiVoice(voice) || GEMINI_INTERACTIONS_TTS_MODELS.has(model) && isCustomGeminiVoiceId(voice);
3503
+ }
3022
3504
 
3023
3505
  // src/providers/gemini/generators/speech.ts
3024
3506
  async function generateGeminiSpeech(opts) {
3025
3507
  const { text, model, voice, speakers, outputFormat, output, logger } = opts;
3508
+ if (GEMINI_INTERACTIONS_TTS_MODELS.has(model)) {
3509
+ return generateInteractionsSpeech(opts);
3510
+ }
3026
3511
  const speechConfig = speakers && speakers.length > 0 ? {
3027
3512
  multiSpeakerVoiceConfig: {
3028
3513
  speakerVoiceConfigs: speakers.map((s) => ({
@@ -3053,16 +3538,81 @@ async function generateGeminiSpeech(opts) {
3053
3538
  const pcmBytes = Buffer.from(audio.data, "base64");
3054
3539
  const sampleRate = parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE;
3055
3540
  const outBytes = outputFormat === "wav" ? wrapPcmInWav(pcmBytes, sampleRate, TTS_PCM_CHANNELS, TTS_PCM_BITS_PER_SAMPLE) : pcmBytes;
3541
+ const dest = saveSpeech(outBytes, outputFormat, output, logger);
3542
+ return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
3543
+ }
3544
+ function buildInteractionsSpeechBody(opts) {
3545
+ const { text, model, voice, speakers, style } = opts;
3546
+ const speechConfig = speakers && speakers.length > 0 ? { mode: "conversational", speakers: speakers.map((s) => ({ ...s })) } : [{ voice }];
3547
+ const content = { type: "text", text };
3548
+ if (style) content.annotations = [{ type: "speech_metadata", style }];
3549
+ return {
3550
+ model,
3551
+ input: [{ type: "user_input", content: [content] }],
3552
+ response_format: { type: "audio", mime_type: "audio/wav" },
3553
+ generation_config: { speech_config: speechConfig }
3554
+ };
3555
+ }
3556
+ async function generateInteractionsSpeech(opts) {
3557
+ const { model, speakers, outputFormat, output, logger, text, style } = opts;
3558
+ logger?.debug(
3559
+ `Gemini TTS (interactions) model=${model} mode=${speakers?.length ? "multi" : "single"}`
3560
+ );
3561
+ if (!style && /^\s*(say|read|speak)\b[^:\n]{0,80}:/i.test(text)) {
3562
+ logger?.warn(
3563
+ `${model} reads text verbatim; move delivery directions like "Say cheerfully:" into --style`
3564
+ );
3565
+ }
3566
+ let resp;
3567
+ try {
3568
+ resp = await createInteraction(buildInteractionsSpeechBody(opts));
3569
+ } catch (e) {
3570
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
3571
+ }
3572
+ const interaction = resp;
3573
+ if (typeof interaction?.status === "string" && interaction.status !== "completed") {
3574
+ const detail = typeof interaction.error?.message === "string" ? `: ${interaction.error.message}` : "";
3575
+ return { status: "error", error: `Interaction ${interaction.status}${detail}` };
3576
+ }
3577
+ const audio = extractInteractionAudio(resp);
3578
+ if (!audio) return { status: "error", error: "No audio in response" };
3579
+ const bytes = Buffer.from(audio.data, "base64");
3580
+ const isWav = bytes.subarray(0, 4).toString("ascii") === "RIFF";
3581
+ let outBytes;
3582
+ if (outputFormat === "wav") {
3583
+ outBytes = isWav ? bytes : wrapPcmInWav(
3584
+ bytes,
3585
+ parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE,
3586
+ TTS_PCM_CHANNELS,
3587
+ TTS_PCM_BITS_PER_SAMPLE
3588
+ );
3589
+ } else {
3590
+ outBytes = isWav ? extractWavData(bytes) : bytes;
3591
+ }
3592
+ const dest = saveSpeech(outBytes, outputFormat, output, logger);
3593
+ return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
3594
+ }
3595
+ function extractWavData(wav) {
3596
+ let offset = 12;
3597
+ while (offset + 8 <= wav.length) {
3598
+ const id = wav.subarray(offset, offset + 4).toString("ascii");
3599
+ const size = wav.readUInt32LE(offset + 4);
3600
+ if (id === "data") return wav.subarray(offset + 8, Math.min(offset + 8 + size, wav.length));
3601
+ offset += 8 + size + size % 2;
3602
+ }
3603
+ throw new Error("WAV response has no data chunk");
3604
+ }
3605
+ function saveSpeech(outBytes, outputFormat, output, logger) {
3056
3606
  const outDir = getOutputDir();
3057
- const dest = path22.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
3058
- fs19.writeFileSync(dest, outBytes);
3607
+ const dest = path24.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
3608
+ fs21.writeFileSync(dest, outBytes);
3059
3609
  logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
3060
3610
  if (output) {
3061
- fs19.mkdirSync(path22.dirname(path22.resolve(output)), { recursive: true });
3062
- fs19.copyFileSync(dest, output);
3611
+ fs21.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
3612
+ fs21.copyFileSync(dest, output);
3063
3613
  logger?.success(`Copied to: ${output}`);
3064
3614
  }
3065
- return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
3615
+ return dest;
3066
3616
  }
3067
3617
  function parseSampleRate(mimeType) {
3068
3618
  const m = mimeType.match(/rate=(\d+)/i);
@@ -3091,7 +3641,14 @@ function wrapPcmInWav(pcm, sampleRate, channels, bitsPerSample) {
3091
3641
 
3092
3642
  // src/providers/gemini/commands/generate-speech.ts
3093
3643
  function registerGeminiGenerateSpeechCommand(parent) {
3094
- parent.command("generate-speech").description("Generate speech (TTS) with Gemini Flash TTS").option("--text <str>", "Text to speak (use --text or --prompt)").option("--prompt <str>", "Alias for --text").option("--model <id>", `TTS model (${[...GEMINI_TTS_MODELS].join("|")})`).option("--voice <name>", "Prebuilt voice for single-speaker mode", TTS_VOICE_DEFAULT).option(
3644
+ parent.command("generate-speech").description("Generate speech (TTS) with Gemini Flash TTS").option("--text <str>", "Text to speak (use --text or --prompt)").option("--prompt <str>", "Alias for --text").option("--model <id>", `TTS model (${[...GEMINI_TTS_MODELS].join("|")})`).option(
3645
+ "--voice <name>",
3646
+ "Prebuilt voice for single-speaker mode (3.8 models also accept voice_/voicekey_ ids)",
3647
+ TTS_VOICE_DEFAULT
3648
+ ).option(
3649
+ "--style <text>",
3650
+ 'Delivery direction for Gemini 3.8 models, e.g. "cheerful and friendly"'
3651
+ ).option(
3095
3652
  "--speaker <name:voice>",
3096
3653
  "Speaker mapping (repeatable, max 2 \u2014 switches to multi-speaker mode)",
3097
3654
  collectSpeakers,
@@ -3114,13 +3671,18 @@ function registerGeminiGenerateSpeechCommand(parent) {
3114
3671
  if (speakers.length > 2) {
3115
3672
  throw new ValidationError("Gemini TTS supports max 2 speakers in multi-speaker mode");
3116
3673
  }
3117
- if (speakers.length === 0 && !isValidGeminiVoice(opts.voice)) {
3674
+ if (opts.style && !GEMINI_INTERACTIONS_TTS_MODELS.has(model)) {
3675
+ throw new ValidationError(
3676
+ `--style requires a Gemini 3.8 TTS model (${[...GEMINI_INTERACTIONS_TTS_MODELS].join(", ")}); put directions in the text for ${model}`
3677
+ );
3678
+ }
3679
+ if (speakers.length === 0 && !isVoiceAllowedForModel(opts.voice, model)) {
3118
3680
  throw new ValidationError(
3119
3681
  `Invalid --voice "${opts.voice}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
3120
3682
  );
3121
3683
  }
3122
3684
  for (const sv of speakers) {
3123
- if (!isValidGeminiVoice(sv.voice)) {
3685
+ if (!isVoiceAllowedForModel(sv.voice, model)) {
3124
3686
  throw new ValidationError(
3125
3687
  `Invalid voice "${sv.voice}" for speaker "${sv.speaker}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
3126
3688
  );
@@ -3131,6 +3693,7 @@ function registerGeminiGenerateSpeechCommand(parent) {
3131
3693
  model,
3132
3694
  voice: speakers.length === 0 ? opts.voice : void 0,
3133
3695
  speakers: speakers.length > 0 ? speakers : void 0,
3696
+ style: opts.style,
3134
3697
  outputFormat: opts.outputFormat,
3135
3698
  output: opts.output,
3136
3699
  logger
@@ -3158,8 +3721,8 @@ function collectSpeakers(value, prev) {
3158
3721
  }
3159
3722
 
3160
3723
  // src/providers/gemini/commands/generate-video.ts
3161
- import fs20 from "fs";
3162
- import path23 from "path";
3724
+ import fs22 from "fs";
3725
+ import path25 from "path";
3163
3726
  init_errors();
3164
3727
  init_http_client();
3165
3728
  var BASE2 = "https://generativelanguage.googleapis.com";
@@ -3193,10 +3756,10 @@ function registerGenerateVideoCommand(parent) {
3193
3756
  const contentParts = [{ text: opts.prompt }];
3194
3757
  if (opts.referenceImages?.length) {
3195
3758
  for (const imgPath of opts.referenceImages.slice(0, 2)) {
3196
- if (!fs20.existsSync(imgPath))
3759
+ if (!fs22.existsSync(imgPath))
3197
3760
  throw new ValidationError(`Reference image not found: ${imgPath}`);
3198
- const data = fs20.readFileSync(imgPath).toString("base64");
3199
- const ext = path23.extname(imgPath).toLowerCase();
3761
+ const data = fs22.readFileSync(imgPath).toString("base64");
3762
+ const ext = path25.extname(imgPath).toLowerCase();
3200
3763
  const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
3201
3764
  contentParts.push({ inlineData: { mimeType, data } });
3202
3765
  }
@@ -3223,7 +3786,7 @@ function registerGenerateVideoCommand(parent) {
3223
3786
  let done = false;
3224
3787
  let finalOp = operation;
3225
3788
  while (!done && Date.now() < deadline) {
3226
- await sleep2(1e4);
3789
+ await sleep3(1e4);
3227
3790
  finalOp = await httpJson({
3228
3791
  url: `${BASE2}/v1beta/${operationName}`,
3229
3792
  headers
@@ -3245,13 +3808,13 @@ function registerGenerateVideoCommand(parent) {
3245
3808
  );
3246
3809
  }
3247
3810
  const outDir = getOutputDir();
3248
- const dest = path23.join(outDir, `veo_generated_${Date.now()}.mp4`);
3811
+ const dest = path25.join(outDir, `veo_generated_${Date.now()}.mp4`);
3249
3812
  const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
3250
3813
  await downloadFile2(videoUri, dest);
3251
3814
  logger.success(`Saved video: ${dest}`);
3252
3815
  if (opts.output) {
3253
- fs20.mkdirSync(path23.dirname(path23.resolve(opts.output)), { recursive: true });
3254
- fs20.copyFileSync(dest, opts.output);
3816
+ fs22.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
3817
+ fs22.copyFileSync(dest, opts.output);
3255
3818
  logger.success(`Copied to: ${opts.output}`);
3256
3819
  }
3257
3820
  await maybeDownloadThumb(finalOp.response, dest, {
@@ -3264,14 +3827,14 @@ Generated video: ${dest}`);
3264
3827
  }
3265
3828
  );
3266
3829
  }
3267
- function sleep2(ms) {
3830
+ function sleep3(ms) {
3268
3831
  return new Promise((r) => setTimeout(r, ms));
3269
3832
  }
3270
3833
 
3271
3834
  // src/providers/gemini/commands/generate.ts
3272
3835
  init_errors();
3273
- import fs21 from "fs";
3274
- import path24 from "path";
3836
+ import fs23 from "fs";
3837
+ import path26 from "path";
3275
3838
  function registerGenerateCommand(parent) {
3276
3839
  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(
3277
3840
  async (opts) => {
@@ -3313,8 +3876,8 @@ function registerGenerateCommand(parent) {
3313
3876
  const outDir = getOutputDir();
3314
3877
  const ext = img.mimeType.includes("png") ? "png" : "jpg";
3315
3878
  const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
3316
- const dest = path24.join(outDir, fname);
3317
- fs21.writeFileSync(dest, Buffer.from(img.data, "base64"));
3879
+ const dest = path26.join(outDir, fname);
3880
+ fs23.writeFileSync(dest, Buffer.from(img.data, "base64"));
3318
3881
  savedFiles.push(dest);
3319
3882
  logger.success(`Saved: ${dest}`);
3320
3883
  }
@@ -3329,8 +3892,8 @@ function registerGenerateCommand(parent) {
3329
3892
  process.exit(1);
3330
3893
  }
3331
3894
  if (opts.output && savedFiles[0]) {
3332
- fs21.mkdirSync(path24.dirname(path24.resolve(opts.output)), { recursive: true });
3333
- fs21.copyFileSync(savedFiles[0], opts.output);
3895
+ fs23.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
3896
+ fs23.copyFileSync(savedFiles[0], opts.output);
3334
3897
  logger.success(`Copied to: ${opts.output}`);
3335
3898
  }
3336
3899
  const finalImages = finalizeGeneratedImages(savedFiles, { ...opts, logger });
@@ -3342,8 +3905,8 @@ Generated ${finalImages.length} image(s):`);
3342
3905
  }
3343
3906
 
3344
3907
  // src/providers/gemini/commands/image-to-image.ts
3345
- import fs22 from "fs";
3346
- import path25 from "path";
3908
+ import fs24 from "fs";
3909
+ import path27 from "path";
3347
3910
  var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
3348
3911
  var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
3349
3912
  function registerGeminiImageToImageCommand(parent) {
@@ -3404,14 +3967,14 @@ function registerGeminiImageToImageCommand(parent) {
3404
3967
  const img = images[i];
3405
3968
  if (!img) continue;
3406
3969
  const ext = img.mimeType.includes("png") ? "png" : "jpg";
3407
- const dest = path25.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
3408
- fs22.writeFileSync(dest, Buffer.from(img.data, "base64"));
3970
+ const dest = path27.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
3971
+ fs24.writeFileSync(dest, Buffer.from(img.data, "base64"));
3409
3972
  saved.push(dest);
3410
3973
  logger.success(`Saved: ${dest}`);
3411
3974
  }
3412
3975
  if (opts.output && saved[0]) {
3413
- fs22.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
3414
- fs22.copyFileSync(saved[0], opts.output);
3976
+ fs24.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
3977
+ fs24.copyFileSync(saved[0], opts.output);
3415
3978
  logger.success(`Copied to: ${opts.output}`);
3416
3979
  }
3417
3980
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -3439,7 +4002,7 @@ function sniffImageMime(buf, url) {
3439
4002
  if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
3440
4003
  return "image/webp";
3441
4004
  }
3442
- const ext = path25.extname(new URL(url).pathname).toLowerCase();
4005
+ const ext = path27.extname(new URL(url).pathname).toLowerCase();
3443
4006
  if (ext === ".png") return "image/png";
3444
4007
  if (ext === ".webp") return "image/webp";
3445
4008
  if (ext === ".gif") return "image/gif";
@@ -3447,21 +4010,21 @@ function sniffImageMime(buf, url) {
3447
4010
  }
3448
4011
 
3449
4012
  // src/providers/gemini/commands/image-to-video.ts
3450
- import fs23 from "fs";
3451
- import path26 from "path";
4013
+ import fs25 from "fs";
4014
+ import path28 from "path";
3452
4015
  init_errors();
3453
4016
  init_http_client();
3454
4017
  var BASE3 = "https://generativelanguage.googleapis.com";
3455
4018
  function imagePart(imgPath) {
3456
- if (!fs23.existsSync(imgPath)) {
4019
+ if (!fs25.existsSync(imgPath)) {
3457
4020
  throw new ValidationError(`Image not found: ${imgPath}`);
3458
4021
  }
3459
- const data = fs23.readFileSync(imgPath).toString("base64");
3460
- const ext = path26.extname(imgPath).toLowerCase();
4022
+ const data = fs25.readFileSync(imgPath).toString("base64");
4023
+ const ext = path28.extname(imgPath).toLowerCase();
3461
4024
  const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
3462
4025
  return { inlineData: { mimeType, data } };
3463
4026
  }
3464
- function sleep3(ms) {
4027
+ function sleep4(ms) {
3465
4028
  return new Promise((r) => setTimeout(r, ms));
3466
4029
  }
3467
4030
  function registerGeminiImageToVideoCommand(parent) {
@@ -3513,7 +4076,7 @@ function registerGeminiImageToVideoCommand(parent) {
3513
4076
  const deadline = Date.now() + 10 * 60 * 1e3;
3514
4077
  let finalOp = operation;
3515
4078
  while (!finalOp.done && Date.now() < deadline) {
3516
- await sleep3(1e4);
4079
+ await sleep4(1e4);
3517
4080
  finalOp = await httpJson({ url: `${BASE3}/v1beta/${operationName}`, headers });
3518
4081
  if (!finalOp.done) {
3519
4082
  logger.debug(`Still generating... ${Math.round((deadline - Date.now()) / 1e3)}s left`);
@@ -3530,12 +4093,12 @@ function registerGeminiImageToVideoCommand(parent) {
3530
4093
  "gemini"
3531
4094
  );
3532
4095
  }
3533
- const dest = path26.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
4096
+ const dest = path28.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
3534
4097
  await downloadFile(videoUri, dest);
3535
4098
  logger.success(`Saved video: ${dest}`);
3536
4099
  if (opts.output) {
3537
- fs23.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
3538
- fs23.copyFileSync(dest, opts.output);
4100
+ fs25.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
4101
+ fs25.copyFileSync(dest, opts.output);
3539
4102
  logger.success(`Copied to: ${opts.output}`);
3540
4103
  }
3541
4104
  await maybeDownloadThumb(finalOp.response, dest, {
@@ -3551,14 +4114,14 @@ Generated video: ${dest}`);
3551
4114
 
3552
4115
  // src/providers/gemini/commands/transcribe.ts
3553
4116
  init_errors();
3554
- import fs24 from "fs";
4117
+ import fs26 from "fs";
3555
4118
  function registerTranscribeCommand2(parent) {
3556
4119
  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(
3557
4120
  async (opts) => {
3558
4121
  const logger = createLogger({ verbose: opts.verbose ?? false });
3559
4122
  const model = opts.model ?? getDefaultModel("transcribe");
3560
4123
  for (const f of opts.files) {
3561
- if (!fs24.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
4124
+ if (!fs26.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3562
4125
  }
3563
4126
  const results = [];
3564
4127
  for (let i = 0; i < opts.files.length; i++) {
@@ -3616,8 +4179,8 @@ function registerGeminiCommands(program2) {
3616
4179
 
3617
4180
  // src/providers/leonardo/commands/generate.ts
3618
4181
  init_http_client();
3619
- import fs25 from "fs";
3620
- import path27 from "path";
4182
+ import fs27 from "fs";
4183
+ import path29 from "path";
3621
4184
 
3622
4185
  // src/providers/leonardo/client.ts
3623
4186
  init_errors();
@@ -3691,11 +4254,11 @@ var LeonardoClient = class {
3691
4254
  }
3692
4255
  apiKey;
3693
4256
  baseUrl;
3694
- async request(path47, opts = {}) {
4257
+ async request(path49, opts = {}) {
3695
4258
  const base = this.baseUrl.replace(/\/$/, "");
3696
- const v2Override = path47.startsWith("v2:");
4259
+ const v2Override = path49.startsWith("v2:");
3697
4260
  const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
3698
- const finalPath = v2Override ? path47.slice(3) : path47;
4261
+ const finalPath = v2Override ? path49.slice(3) : path49;
3699
4262
  const url = new URL(finalBase + finalPath);
3700
4263
  if (opts.query) {
3701
4264
  for (const [k, v] of Object.entries(opts.query)) {
@@ -3714,14 +4277,14 @@ var LeonardoClient = class {
3714
4277
  timeoutMs: opts.timeoutMs ?? 12e4
3715
4278
  });
3716
4279
  }
3717
- get(path47, query, logger) {
3718
- return this.request(path47, { method: "GET", query, logger });
4280
+ get(path49, query, logger) {
4281
+ return this.request(path49, { method: "GET", query, logger });
3719
4282
  }
3720
- post(path47, body, logger) {
3721
- return this.request(path47, { method: "POST", body, logger });
4283
+ post(path49, body, logger) {
4284
+ return this.request(path49, { method: "POST", body, logger });
3722
4285
  }
3723
- delete(path47, logger) {
3724
- return this.request(path47, { method: "DELETE", logger });
4286
+ delete(path49, logger) {
4287
+ return this.request(path49, { method: "DELETE", logger });
3725
4288
  }
3726
4289
  };
3727
4290
  function createLeonardoClient() {
@@ -3729,10 +4292,10 @@ function createLeonardoClient() {
3729
4292
  }
3730
4293
 
3731
4294
  // src/providers/leonardo/commands/generate.ts
3732
- function extFromUrl(url, fallback = ".png") {
4295
+ function extFromUrl2(url, fallback = ".png") {
3733
4296
  try {
3734
4297
  const u = new URL(url);
3735
- const e = path27.extname(u.pathname);
4298
+ const e = path29.extname(u.pathname);
3736
4299
  return e.length > 0 ? e : fallback;
3737
4300
  } catch {
3738
4301
  return fallback;
@@ -3815,9 +4378,9 @@ function registerLeonardoGenerateCommand(parent) {
3815
4378
  for (let i = 0; i < images.length; i++) {
3816
4379
  const img = images[i];
3817
4380
  if (!img) continue;
3818
- const ext = extFromUrl(img.url);
4381
+ const ext = extFromUrl2(img.url);
3819
4382
  const filename = autoFilename(generationId, i + 1, ext);
3820
- const filepath = path27.join(outDir, filename);
4383
+ const filepath = path29.join(outDir, filename);
3821
4384
  try {
3822
4385
  await downloadFile(img.url, filepath);
3823
4386
  saved.push(filepath);
@@ -3827,9 +4390,9 @@ function registerLeonardoGenerateCommand(parent) {
3827
4390
  }
3828
4391
  }
3829
4392
  if (opts.output && saved[0]) {
3830
- const dest = path27.resolve(opts.output);
3831
- fs25.mkdirSync(path27.dirname(dest), { recursive: true });
3832
- fs25.copyFileSync(saved[0], dest);
4393
+ const dest = path29.resolve(opts.output);
4394
+ fs27.mkdirSync(path29.dirname(dest), { recursive: true });
4395
+ fs27.copyFileSync(saved[0], dest);
3833
4396
  logger.success(`Copied to ${dest}`);
3834
4397
  }
3835
4398
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -3842,11 +4405,11 @@ Generated ${finalImages.length} image(s):`);
3842
4405
 
3843
4406
  // src/providers/leonardo/commands/image-to-image.ts
3844
4407
  init_http_client();
3845
- import fs26 from "fs";
3846
- import path28 from "path";
3847
- function extFromUrl2(url, fallback = ".png") {
4408
+ import fs28 from "fs";
4409
+ import path30 from "path";
4410
+ function extFromUrl3(url, fallback = ".png") {
3848
4411
  try {
3849
- return path28.extname(new URL(url).pathname) || fallback;
4412
+ return path30.extname(new URL(url).pathname) || fallback;
3850
4413
  } catch {
3851
4414
  return fallback;
3852
4415
  }
@@ -3906,9 +4469,9 @@ function registerLeonardoImageToImageCommand(parent) {
3906
4469
  for (let i = 0; i < images.length; i++) {
3907
4470
  const img = images[i];
3908
4471
  if (!img) continue;
3909
- const ext = extFromUrl2(img.url);
4472
+ const ext = extFromUrl3(img.url);
3910
4473
  const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
3911
- const filepath = path28.join(outDir, filename);
4474
+ const filepath = path30.join(outDir, filename);
3912
4475
  try {
3913
4476
  await downloadFile(img.url, filepath);
3914
4477
  saved.push(filepath);
@@ -3918,9 +4481,9 @@ function registerLeonardoImageToImageCommand(parent) {
3918
4481
  }
3919
4482
  }
3920
4483
  if (opts.output && saved[0]) {
3921
- const dest = path28.resolve(opts.output);
3922
- fs26.mkdirSync(path28.dirname(dest), { recursive: true });
3923
- fs26.copyFileSync(saved[0], dest);
4484
+ const dest = path30.resolve(opts.output);
4485
+ fs28.mkdirSync(path30.dirname(dest), { recursive: true });
4486
+ fs28.copyFileSync(saved[0], dest);
3924
4487
  logger.success(`Copied to ${dest}`);
3925
4488
  }
3926
4489
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -3933,7 +4496,7 @@ Generated ${finalImages.length} image(s):`);
3933
4496
 
3934
4497
  // src/providers/leonardo/commands/video.ts
3935
4498
  init_http_client();
3936
- import path29 from "path";
4499
+ import path31 from "path";
3937
4500
  function v2DimsFor(resolution) {
3938
4501
  if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
3939
4502
  if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
@@ -4044,7 +4607,7 @@ async function pollAndDownload(client, jobId, opts) {
4044
4607
  logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
4045
4608
  process.exit(1);
4046
4609
  }
4047
- const outPath = opts.output ?? path29.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
4610
+ const outPath = opts.output ?? path31.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
4048
4611
  await downloadFile(videoUrl2, outPath);
4049
4612
  logger.success(`Saved ${outPath}`);
4050
4613
  await maybeDownloadThumb(gen, outPath, {
@@ -4151,7 +4714,7 @@ function registerLeonardoModelsCommand(parent) {
4151
4714
 
4152
4715
  // src/providers/leonardo/commands/status.ts
4153
4716
  init_http_client();
4154
- import path30 from "path";
4717
+ import path32 from "path";
4155
4718
  var VIDEO_KEYS = ["motionMP4URL", "url"];
4156
4719
  var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
4157
4720
  var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
@@ -4216,7 +4779,7 @@ function registerLeonardoStatusCommand(parent) {
4216
4779
  const outDir = getOutputDir();
4217
4780
  let idx = 0;
4218
4781
  for (const url of videos) {
4219
- const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path30.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
4782
+ const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path32.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
4220
4783
  await downloadFile(url, dest);
4221
4784
  logger.success(`Saved ${dest}`);
4222
4785
  await maybeDownloadThumb(gen, dest, {
@@ -4229,7 +4792,7 @@ function registerLeonardoStatusCommand(parent) {
4229
4792
  for (const url of images) {
4230
4793
  const m = url.match(IMAGE_EXT_RE2);
4231
4794
  const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
4232
- const dest = path30.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
4795
+ const dest = path32.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
4233
4796
  await downloadFile(url, dest);
4234
4797
  logger.success(`Saved ${dest}`);
4235
4798
  idx++;
@@ -4363,7 +4926,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
4363
4926
  if (status === "Failed" || status === "Error") {
4364
4927
  throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
4365
4928
  }
4366
- await sleep4(intervalMs);
4929
+ await sleep5(intervalMs);
4367
4930
  elapsed += intervalMs;
4368
4931
  }
4369
4932
  throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
@@ -4382,14 +4945,14 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
4382
4945
  await downloadFile(downloadUrl, dest);
4383
4946
  return dest;
4384
4947
  }
4385
- function sleep4(ms) {
4948
+ function sleep5(ms) {
4386
4949
  return new Promise((r) => setTimeout(r, ms));
4387
4950
  }
4388
4951
 
4389
4952
  // src/providers/minimax/generators/music.ts
4390
4953
  init_http_client();
4391
- import fs27 from "fs";
4392
- import path31 from "path";
4954
+ import fs29 from "fs";
4955
+ import path33 from "path";
4393
4956
  async function generateMinimaxMusic(opts) {
4394
4957
  const {
4395
4958
  apiKey,
@@ -4434,14 +4997,14 @@ async function generateMinimaxMusic(opts) {
4434
4997
  }
4435
4998
  const durationMs = resp.extra_info?.music_duration ?? 0;
4436
4999
  const outDir = getOutputDir();
4437
- const dest = path31.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
5000
+ const dest = path33.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
4438
5001
  try {
4439
5002
  if (audioData.startsWith("http")) {
4440
5003
  const bytes = await fetchBytes(audioData);
4441
- fs27.writeFileSync(dest, bytes);
5004
+ fs29.writeFileSync(dest, bytes);
4442
5005
  } else {
4443
5006
  const audioBytes = Buffer.from(audioData, "hex");
4444
- fs27.writeFileSync(dest, audioBytes);
5007
+ fs29.writeFileSync(dest, audioBytes);
4445
5008
  }
4446
5009
  } catch (e) {
4447
5010
  return {
@@ -4452,8 +5015,8 @@ async function generateMinimaxMusic(opts) {
4452
5015
  const durSec = durationMs / 1e3;
4453
5016
  logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
4454
5017
  if (output) {
4455
- fs27.mkdirSync(path31.dirname(path31.resolve(output)), { recursive: true });
4456
- fs27.copyFileSync(dest, output);
5018
+ fs29.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
5019
+ fs29.copyFileSync(dest, output);
4457
5020
  }
4458
5021
  return { status: "success", generatedAudio: dest, durationMs, model };
4459
5022
  }
@@ -4527,8 +5090,8 @@ Generated music: ${result.generatedAudio}`);
4527
5090
  init_errors();
4528
5091
 
4529
5092
  // src/providers/minimax/generators/speech.ts
4530
- import fs28 from "fs";
4531
- import path32 from "path";
5093
+ import fs30 from "fs";
5094
+ import path34 from "path";
4532
5095
  async function generateMinimaxSpeech(opts) {
4533
5096
  const {
4534
5097
  apiKey,
@@ -4575,12 +5138,12 @@ async function generateMinimaxSpeech(opts) {
4575
5138
  const audioBytes = Buffer.from(audioHex, "hex");
4576
5139
  const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
4577
5140
  const outDir = getOutputDir();
4578
- const dest = path32.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
4579
- fs28.writeFileSync(dest, audioBytes);
5141
+ const dest = path34.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
5142
+ fs30.writeFileSync(dest, audioBytes);
4580
5143
  logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
4581
5144
  if (output) {
4582
- fs28.mkdirSync(path32.dirname(path32.resolve(output)), { recursive: true });
4583
- fs28.copyFileSync(dest, output);
5145
+ fs30.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
5146
+ fs30.copyFileSync(dest, output);
4584
5147
  }
4585
5148
  return { status: "success", generatedAudio: dest, model };
4586
5149
  }
@@ -4627,8 +5190,8 @@ Generated audio: ${result.generatedAudio}`);
4627
5190
  init_errors();
4628
5191
 
4629
5192
  // src/providers/minimax/generators/video.ts
4630
- import fs29 from "fs";
4631
- import path33 from "path";
5193
+ import fs31 from "fs";
5194
+ import path35 from "path";
4632
5195
  async function generateMinimaxVideo(opts) {
4633
5196
  const {
4634
5197
  apiKey,
@@ -4674,18 +5237,18 @@ async function generateMinimaxVideo(opts) {
4674
5237
  return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
4675
5238
  }
4676
5239
  const outDir = getOutputDir();
4677
- const dest = path33.join(outDir, `minimax_video_${Date.now()}.mp4`);
5240
+ const dest = path35.join(outDir, `minimax_video_${Date.now()}.mp4`);
4678
5241
  try {
4679
5242
  await downloadMinimaxFile(fileId, apiKey, dest, logger);
4680
5243
  } catch (e) {
4681
5244
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
4682
5245
  }
4683
5246
  const elapsed = (Date.now() - start) / 1e3;
4684
- const stat = fs29.statSync(dest);
5247
+ const stat = fs31.statSync(dest);
4685
5248
  const fileSizeMb = stat.size / (1024 * 1024);
4686
5249
  if (output) {
4687
- fs29.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
4688
- fs29.copyFileSync(dest, output);
5250
+ fs31.mkdirSync(path35.dirname(path35.resolve(output)), { recursive: true });
5251
+ fs31.copyFileSync(dest, output);
4689
5252
  }
4690
5253
  await maybeDownloadThumb(pollResult, dest, {
4691
5254
  skip: thumb === false,
@@ -4748,8 +5311,8 @@ Generated video: ${result.generatedVideo}`);
4748
5311
  // src/providers/minimax/generators/image.ts
4749
5312
  init_errors();
4750
5313
  init_http_client();
4751
- import fs30 from "fs";
4752
- import path34 from "path";
5314
+ import fs32 from "fs";
5315
+ import path36 from "path";
4753
5316
  async function generateMinimaxImage(opts) {
4754
5317
  const {
4755
5318
  apiKey,
@@ -4787,8 +5350,8 @@ async function generateMinimaxImage(opts) {
4787
5350
  try {
4788
5351
  const bytes = await fetchBytes(url);
4789
5352
  const fname = `minimax_image_${Date.now()}_${i}.png`;
4790
- const dest = path34.join(outDir, fname);
4791
- fs30.writeFileSync(dest, bytes);
5353
+ const dest = path36.join(outDir, fname);
5354
+ fs32.writeFileSync(dest, bytes);
4792
5355
  savedFiles.push(dest);
4793
5356
  logger?.success(`Saved: ${dest}`);
4794
5357
  } catch (e) {
@@ -4799,8 +5362,8 @@ async function generateMinimaxImage(opts) {
4799
5362
  }
4800
5363
  }
4801
5364
  if (output && savedFiles[0]) {
4802
- fs30.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
4803
- fs30.copyFileSync(savedFiles[0], output);
5365
+ fs32.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
5366
+ fs32.copyFileSync(savedFiles[0], output);
4804
5367
  }
4805
5368
  return { status: "success", generatedImages: savedFiles, model };
4806
5369
  }
@@ -4840,8 +5403,8 @@ Generated ${finalImages.length} image(s):`);
4840
5403
  // src/providers/minimax/commands/image-to-image.ts
4841
5404
  init_errors();
4842
5405
  init_http_client();
4843
- import fs31 from "fs";
4844
- import path35 from "path";
5406
+ import fs33 from "fs";
5407
+ import path37 from "path";
4845
5408
  function registerMinimaxImageToImageCommand(parent) {
4846
5409
  parent.command("image-to-image").alias("i2i").description(
4847
5410
  "MiniMax character/subject preservation. CAVEAT: NOT free-form image editing \u2014 the --ref image is used only as a character/face reference for a NEW scene described by --prompt."
@@ -4896,8 +5459,8 @@ function registerMinimaxImageToImageCommand(parent) {
4896
5459
  if (!url) continue;
4897
5460
  try {
4898
5461
  const bytes = await fetchBytes(url);
4899
- const dest = path35.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
4900
- fs31.writeFileSync(dest, bytes);
5462
+ const dest = path37.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
5463
+ fs33.writeFileSync(dest, bytes);
4901
5464
  saved.push(dest);
4902
5465
  logger.success(`Saved: ${dest}`);
4903
5466
  } catch (e) {
@@ -4908,8 +5471,8 @@ function registerMinimaxImageToImageCommand(parent) {
4908
5471
  }
4909
5472
  }
4910
5473
  if (opts.output && saved[0]) {
4911
- fs31.mkdirSync(path35.dirname(path35.resolve(opts.output)), { recursive: true });
4912
- fs31.copyFileSync(saved[0], opts.output);
5474
+ fs33.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
5475
+ fs33.copyFileSync(saved[0], opts.output);
4913
5476
  logger.success(`Copied to: ${opts.output}`);
4914
5477
  }
4915
5478
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -5044,12 +5607,12 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
5044
5607
 
5045
5608
  // src/providers/openai/openai-audio.ts
5046
5609
  init_http_client();
5047
- import fs33 from "fs";
5048
- import path37 from "path";
5610
+ import fs35 from "fs";
5611
+ import path39 from "path";
5049
5612
 
5050
5613
  // src/providers/openai/client.ts
5051
- import fs32 from "fs";
5052
- import path36 from "path";
5614
+ import fs34 from "fs";
5615
+ import path38 from "path";
5053
5616
  init_errors();
5054
5617
  init_http_client();
5055
5618
  var DEFAULT_TIMEOUT_MS3 = 24e4;
@@ -5116,10 +5679,10 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
5116
5679
  return Buffer.from(await res.arrayBuffer());
5117
5680
  }
5118
5681
  function fileFromPath(filePath) {
5119
- const abs = path36.resolve(filePath);
5120
- if (!fs32.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
5121
- const bytes = fs32.readFileSync(abs);
5122
- return new File([new Uint8Array(bytes)], path36.basename(abs));
5682
+ const abs = path38.resolve(filePath);
5683
+ if (!fs34.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
5684
+ const bytes = fs34.readFileSync(abs);
5685
+ return new File([new Uint8Array(bytes)], path38.basename(abs));
5123
5686
  }
5124
5687
 
5125
5688
  // src/providers/openai/openai-audio.ts
@@ -5158,7 +5721,7 @@ async function resolveAudioReferenceDataUrl(ref) {
5158
5721
  }
5159
5722
  function audioMimeFromReference(ref) {
5160
5723
  const pathname = new URL(ref).pathname;
5161
- const ext = path37.extname(pathname).toLowerCase();
5724
+ const ext = path39.extname(pathname).toLowerCase();
5162
5725
  switch (ext) {
5163
5726
  case ".mp3":
5164
5727
  return "audio/mpeg";
@@ -5177,12 +5740,12 @@ function audioMimeFromReference(ref) {
5177
5740
  async function generateOpenAISpeech(opts) {
5178
5741
  const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
5179
5742
  const ext = extFromAudioFormat(opts.outputFormat);
5180
- const dest = path37.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
5181
- fs33.writeFileSync(dest, bytes);
5743
+ const dest = path39.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
5744
+ fs35.writeFileSync(dest, bytes);
5182
5745
  opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
5183
5746
  if (opts.output) {
5184
- fs33.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
5185
- fs33.copyFileSync(dest, opts.output);
5747
+ fs35.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
5748
+ fs35.copyFileSync(dest, opts.output);
5186
5749
  }
5187
5750
  return dest;
5188
5751
  }
@@ -5204,9 +5767,9 @@ async function transcribeOpenAIAudio(opts) {
5204
5767
  );
5205
5768
  const text = typeof data.text === "string" ? data.text : void 0;
5206
5769
  const ext = opts.format === "text" ? "txt" : "json";
5207
- const dest = opts.output ? path37.resolve(opts.output) : path37.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
5208
- fs33.mkdirSync(path37.dirname(dest), { recursive: true });
5209
- fs33.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
5770
+ 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));
5210
5773
  opts.logger?.success(`Saved transcript: ${dest}`);
5211
5774
  return { path: dest, text };
5212
5775
  }
@@ -5233,9 +5796,9 @@ Generated audio: ${dest}`);
5233
5796
  }
5234
5797
 
5235
5798
  // src/providers/openai/codex-image-driver.ts
5236
- import fs34 from "fs";
5799
+ import fs36 from "fs";
5237
5800
  import os2 from "os";
5238
- import path38 from "path";
5801
+ import path40 from "path";
5239
5802
  import { execa } from "execa";
5240
5803
  init_errors();
5241
5804
  var defaultRunner = async (cmd, args, opts) => {
@@ -5284,12 +5847,12 @@ function sanitizeCodexEnv() {
5284
5847
  async function runCodexImageGeneration(opts) {
5285
5848
  const runner = opts.runner ?? defaultRunner;
5286
5849
  const ext = extFromImageFormat(opts.outputFormat);
5287
- const dest = opts.output ? path38.resolve(opts.output) : path38.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
5288
- fs34.mkdirSync(path38.dirname(dest), { recursive: true });
5289
- const tmp = fs34.mkdtempSync(path38.join(os2.tmpdir(), "multix-codex-image-"));
5290
- const schema = path38.join(tmp, "schema.json");
5291
- const result = path38.join(tmp, "result.json");
5292
- fs34.writeFileSync(
5850
+ 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-"));
5853
+ const schema = path40.join(tmp, "schema.json");
5854
+ const result = path40.join(tmp, "result.json");
5855
+ fs36.writeFileSync(
5293
5856
  schema,
5294
5857
  JSON.stringify({
5295
5858
  type: "object",
@@ -5319,7 +5882,7 @@ async function runCodexImageGeneration(opts) {
5319
5882
  if (res.exitCode !== 0) {
5320
5883
  throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
5321
5884
  }
5322
- if (!fs34.existsSync(dest) || fs34.statSync(dest).size === 0) {
5885
+ if (!fs36.existsSync(dest) || fs36.statSync(dest).size === 0) {
5323
5886
  throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
5324
5887
  }
5325
5888
  return { path: dest };
@@ -5337,8 +5900,8 @@ function buildCodexPrompt(userPrompt, outputPath) {
5337
5900
  // src/providers/openai/openai-image.ts
5338
5901
  init_errors();
5339
5902
  init_http_client();
5340
- import fs35 from "fs";
5341
- import path39 from "path";
5903
+ import fs37 from "fs";
5904
+ import path41 from "path";
5342
5905
  function buildImageGenerationBody(opts) {
5343
5906
  return {
5344
5907
  model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
@@ -5371,14 +5934,14 @@ function saveOpenAIImages(opts) {
5371
5934
  for (let i = 0; i < items.length; i++) {
5372
5935
  const b64 = items[i]?.b64_json;
5373
5936
  if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
5374
- const dest = path39.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
5375
- fs35.writeFileSync(dest, Buffer.from(b64, "base64"));
5937
+ const dest = path41.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
5938
+ fs37.writeFileSync(dest, Buffer.from(b64, "base64"));
5376
5939
  saved.push(dest);
5377
5940
  opts.logger?.success(`Saved: ${dest}`);
5378
5941
  }
5379
5942
  if (opts.output && saved[0]) {
5380
- fs35.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
5381
- fs35.copyFileSync(saved[0], opts.output);
5943
+ fs37.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
5944
+ fs37.copyFileSync(saved[0], opts.output);
5382
5945
  opts.logger?.success(`Copied to: ${opts.output}`);
5383
5946
  }
5384
5947
  return saved;
@@ -5387,7 +5950,7 @@ async function resolveImageRefBytes(ref) {
5387
5950
  if (isHttpUrl(ref)) {
5388
5951
  const bytes2 = await fetchBytes(ref);
5389
5952
  const pathname = new URL(ref).pathname;
5390
- const filename = path39.basename(pathname) || "reference.png";
5953
+ const filename = path41.basename(pathname) || "reference.png";
5391
5954
  return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
5392
5955
  }
5393
5956
  const file = fileFromPath(ref);
@@ -5432,7 +5995,7 @@ function sniffImageMime2(buf, name) {
5432
5995
  if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
5433
5996
  if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
5434
5997
  return "image/webp";
5435
- const ext = path39.extname(name).toLowerCase();
5998
+ const ext = path41.extname(name).toLowerCase();
5436
5999
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
5437
6000
  if (ext === ".webp") return "image/webp";
5438
6001
  return "image/png";
@@ -5576,8 +6139,8 @@ function registerOpenAICommands(program2) {
5576
6139
  }
5577
6140
 
5578
6141
  // src/providers/openrouter/client.ts
5579
- import fs36 from "fs";
5580
- import path40 from "path";
6142
+ import fs38 from "fs";
6143
+ import path42 from "path";
5581
6144
  init_errors();
5582
6145
  init_http_client();
5583
6146
 
@@ -5742,8 +6305,8 @@ async function generateOpenRouterImage(opts) {
5742
6305
  if (!url) continue;
5743
6306
  try {
5744
6307
  const bytes = await fetchBytes(url);
5745
- const dest = path40.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
5746
- fs36.writeFileSync(dest, bytes);
6308
+ const dest = path42.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
6309
+ fs38.writeFileSync(dest, bytes);
5747
6310
  savedFiles.push(dest);
5748
6311
  logger?.success(`Saved: ${dest}`);
5749
6312
  } catch (e) {
@@ -5754,8 +6317,8 @@ async function generateOpenRouterImage(opts) {
5754
6317
  }
5755
6318
  }
5756
6319
  if (output && savedFiles[0]) {
5757
- fs36.mkdirSync(path40.dirname(path40.resolve(output)), { recursive: true });
5758
- fs36.writeFileSync(output, fs36.readFileSync(savedFiles[0]));
6320
+ fs38.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
6321
+ fs38.writeFileSync(output, fs38.readFileSync(savedFiles[0]));
5759
6322
  }
5760
6323
  return { status: "success", generatedImages: savedFiles, model: usedModel };
5761
6324
  }
@@ -5796,8 +6359,8 @@ Generated ${finalImages.length} image(s):`);
5796
6359
  }
5797
6360
 
5798
6361
  // src/providers/openrouter/commands/image-to-image.ts
5799
- import fs37 from "fs";
5800
- import path41 from "path";
6362
+ import fs39 from "fs";
6363
+ import path43 from "path";
5801
6364
  init_errors();
5802
6365
  init_http_client();
5803
6366
  var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
@@ -5890,14 +6453,14 @@ async function runImageToImage(opts) {
5890
6453
  const url = parsed.urls[i];
5891
6454
  if (!url) continue;
5892
6455
  const bytes = await fetchBytes(url);
5893
- const dest = path41.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
5894
- fs37.writeFileSync(dest, bytes);
6456
+ const dest = path43.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
6457
+ fs39.writeFileSync(dest, bytes);
5895
6458
  saved.push(dest);
5896
6459
  opts.logger.success(`Saved: ${dest}`);
5897
6460
  }
5898
6461
  if (opts.output && saved[0]) {
5899
- fs37.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
5900
- fs37.copyFileSync(saved[0], opts.output);
6462
+ fs39.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
6463
+ fs39.copyFileSync(saved[0], opts.output);
5901
6464
  opts.logger.success(`Copied to: ${opts.output}`);
5902
6465
  }
5903
6466
  return saved;
@@ -5943,13 +6506,13 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
5943
6506
  }
5944
6507
 
5945
6508
  // src/providers/openrouter/commands/video-status.ts
5946
- import fs38 from "fs";
5947
- import path42 from "path";
6509
+ import fs40 from "fs";
6510
+ import path44 from "path";
5948
6511
  async function downloadOpenRouterVideo(opts) {
5949
6512
  const { jobId, logger, source } = opts;
5950
6513
  const apiKey = requireOpenRouterKey();
5951
6514
  const url = videoContentUrl(jobId);
5952
- const dest = path42.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
6515
+ const dest = path44.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
5953
6516
  const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
5954
6517
  if (!r.ok) {
5955
6518
  logger.error(`Download HTTP ${r.status}`);
@@ -5959,14 +6522,14 @@ async function downloadOpenRouterVideo(opts) {
5959
6522
  logger.error("Empty response body");
5960
6523
  process.exit(1);
5961
6524
  }
5962
- fs38.mkdirSync(path42.dirname(dest), { recursive: true });
5963
- const ws = fs38.createWriteStream(dest);
6525
+ fs40.mkdirSync(path44.dirname(dest), { recursive: true });
6526
+ const ws = fs40.createWriteStream(dest);
5964
6527
  const { pipeline: pipeline2 } = await import("stream/promises");
5965
6528
  await pipeline2(r.body, ws);
5966
6529
  logger.success(`Saved ${dest}`);
5967
6530
  if (opts.output) {
5968
- fs38.mkdirSync(path42.dirname(path42.resolve(opts.output)), { recursive: true });
5969
- fs38.copyFileSync(dest, opts.output);
6531
+ fs40.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
6532
+ fs40.copyFileSync(dest, opts.output);
5970
6533
  logger.success(`Copied to ${opts.output}`);
5971
6534
  }
5972
6535
  await maybeDownloadThumb(source, dest, {
@@ -6133,9 +6696,9 @@ function registerOpenRouterCommands(program2) {
6133
6696
 
6134
6697
  // src/commands/check-helpers/binary-check.ts
6135
6698
  import { execa as execa2 } from "execa";
6136
- async function checkBinary(binary) {
6699
+ async function checkBinary(binary, versionFlag = "--version") {
6137
6700
  try {
6138
- const result = await execa2(binary, ["--version"], { reject: true, all: true });
6701
+ const result = await execa2(binary, [versionFlag], { reject: true, all: true });
6139
6702
  const version = (result.stdout || result.stderr || "").split("\n")[0]?.trim();
6140
6703
  return { available: true, version, binary };
6141
6704
  } catch (e) {
@@ -6184,6 +6747,7 @@ To configure multix, set at least one provider API key:
6184
6747
  MiniMax : https://platform.minimax.io/user-center/basic-information/interface-key
6185
6748
  Leonardo.Ai : https://app.leonardo.ai/settings/api-keys
6186
6749
  BytePlus : https://console.byteplus.com/auth/api-keys
6750
+ fal.ai : https://fal.ai/dashboard/keys
6187
6751
 
6188
6752
  Option A \u2014 user global config (recommended):
6189
6753
  echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
@@ -6246,6 +6810,12 @@ var PROVIDERS = [
6246
6810
  envPrimary: "ELEVENLABS_API_KEY",
6247
6811
  optional: true,
6248
6812
  link: "https://elevenlabs.io/app/settings/api-keys"
6813
+ },
6814
+ {
6815
+ name: "fal.ai",
6816
+ envPrimary: "FAL_KEY",
6817
+ optional: true,
6818
+ link: "https://fal.ai/dashboard/keys"
6249
6819
  }
6250
6820
  ];
6251
6821
  function resolveProviderKey(p) {
@@ -6276,7 +6846,7 @@ function registerCheckCommand(program2) {
6276
6846
  const logger = createLogger({ verbose: opts.verbose ?? false });
6277
6847
  let exitCode = 0;
6278
6848
  logger.header("Tooling");
6279
- const ffmpegStatus = await checkBinary("ffmpeg");
6849
+ const ffmpegStatus = await checkBinary("ffmpeg", "-version");
6280
6850
  const magickStatus = await checkBinary("magick");
6281
6851
  if (ffmpegStatus.available) {
6282
6852
  logger.success(`ffmpeg found${ffmpegStatus.version ? ` \u2014 ${ffmpegStatus.version}` : ""}`);
@@ -6377,8 +6947,8 @@ function registerCheckCommand(program2) {
6377
6947
 
6378
6948
  // src/commands/doc/convert.ts
6379
6949
  init_errors();
6380
- import fs39 from "fs";
6381
- import path43 from "path";
6950
+ import fs41 from "fs";
6951
+ import path45 from "path";
6382
6952
 
6383
6953
  // src/commands/doc/default-prompt.ts
6384
6954
  var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
@@ -6399,18 +6969,18 @@ async function convertDocuments(opts) {
6399
6969
  const model = opts.model ?? DOC_MODEL_DEFAULT;
6400
6970
  const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
6401
6971
  for (const f of inputs) {
6402
- if (!fs39.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
6972
+ if (!fs41.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
6403
6973
  }
6404
6974
  let outputPath;
6405
6975
  if (output) {
6406
- outputPath = path43.resolve(output);
6976
+ outputPath = path45.resolve(output);
6407
6977
  } else if (autoName && inputs.length === 1 && inputs[0]) {
6408
- const base = path43.basename(inputs[0], path43.extname(inputs[0]));
6409
- outputPath = path43.join(getOutputDir(), `${base}-extraction.md`);
6978
+ const base = path45.basename(inputs[0], path45.extname(inputs[0]));
6979
+ outputPath = path45.join(getOutputDir(), `${base}-extraction.md`);
6410
6980
  } else {
6411
- outputPath = path43.join(getOutputDir(), "document-extraction.md");
6981
+ outputPath = path45.join(getOutputDir(), "document-extraction.md");
6412
6982
  }
6413
- fs39.mkdirSync(path43.dirname(outputPath), { recursive: true });
6983
+ fs41.mkdirSync(path45.dirname(outputPath), { recursive: true });
6414
6984
  const results = [];
6415
6985
  for (let i = 0; i < inputs.length; i++) {
6416
6986
  const filePath = inputs[i];
@@ -6442,7 +7012,7 @@ async function convertDocuments(opts) {
6442
7012
  if (attempt < 2) {
6443
7013
  const wait = 2 ** attempt * 1e3;
6444
7014
  logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
6445
- await sleep5(wait);
7015
+ await sleep6(wait);
6446
7016
  }
6447
7017
  }
6448
7018
  }
@@ -6461,7 +7031,7 @@ async function convertDocuments(opts) {
6461
7031
  `);
6462
7032
  for (const r of results) {
6463
7033
  lines.push(`
6464
- ## ${path43.basename(r.file)}
7034
+ ## ${path45.basename(r.file)}
6465
7035
  `);
6466
7036
  if (r.status === "success" && r.markdown) {
6467
7037
  lines.push(`${r.markdown}
@@ -6474,7 +7044,7 @@ async function convertDocuments(opts) {
6474
7044
  }
6475
7045
  lines.push("\n---\n");
6476
7046
  }
6477
- fs39.writeFileSync(outputPath, lines.join("\n"), "utf8");
7047
+ fs41.writeFileSync(outputPath, lines.join("\n"), "utf8");
6478
7048
  const succeeded = results.filter((r) => r.status === "success").length;
6479
7049
  console.log(`
6480
7050
  ${"=".repeat(50)}`);
@@ -6490,7 +7060,7 @@ ${"=".repeat(50)}`);
6490
7060
  const failed = results.filter((r) => r.status === "error").length;
6491
7061
  if (failed > 0) process.exit(1);
6492
7062
  }
6493
- function sleep5(ms) {
7063
+ function sleep6(ms) {
6494
7064
  return new Promise((r) => setTimeout(r, ms));
6495
7065
  }
6496
7066
 
@@ -6522,12 +7092,12 @@ function registerDocCommands(program2) {
6522
7092
 
6523
7093
  // src/commands/media/index.ts
6524
7094
  init_errors();
6525
- import fs42 from "fs";
6526
- import path46 from "path";
7095
+ import fs44 from "fs";
7096
+ import path48 from "path";
6527
7097
 
6528
7098
  // src/commands/media/batch.ts
6529
- import fs40 from "fs";
6530
- import path44 from "path";
7099
+ import fs42 from "fs";
7100
+ import path46 from "path";
6531
7101
 
6532
7102
  // src/commands/media/detect-type.ts
6533
7103
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
@@ -6723,10 +7293,10 @@ async function batchOptimize(opts) {
6723
7293
  verbose,
6724
7294
  logger
6725
7295
  } = opts;
6726
- fs40.mkdirSync(outputDir, { recursive: true });
6727
- const entries = fs40.readdirSync(inputDir);
7296
+ fs42.mkdirSync(outputDir, { recursive: true });
7297
+ const entries = fs42.readdirSync(inputDir);
6728
7298
  const mediaFiles = entries.filter((f) => {
6729
- const ext = path44.extname(f).toLowerCase();
7299
+ const ext = path46.extname(f).toLowerCase();
6730
7300
  return ALL_EXTS.has(ext);
6731
7301
  });
6732
7302
  if (mediaFiles.length === 0) {
@@ -6737,9 +7307,9 @@ async function batchOptimize(opts) {
6737
7307
  let succeeded = 0;
6738
7308
  const errors = [];
6739
7309
  for (const file of mediaFiles) {
6740
- const inputPath = path44.join(inputDir, file);
6741
- const outputPath = path44.join(outputDir, file);
6742
- const ext = path44.extname(file).toLowerCase();
7310
+ const inputPath = path46.join(inputDir, file);
7311
+ const outputPath = path46.join(outputDir, file);
7312
+ const ext = path46.extname(file).toLowerCase();
6743
7313
  const kind = extToKind(ext);
6744
7314
  logger?.debug(`Processing: ${file}`);
6745
7315
  try {
@@ -6776,8 +7346,8 @@ async function batchOptimize(opts) {
6776
7346
 
6777
7347
  // src/commands/media/split-video.ts
6778
7348
  init_errors();
6779
- import fs41 from "fs";
6780
- import path45 from "path";
7349
+ import fs43 from "fs";
7350
+ import path47 from "path";
6781
7351
  async function splitVideo(opts) {
6782
7352
  await assertFfmpeg();
6783
7353
  const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
@@ -6788,10 +7358,10 @@ async function splitVideo(opts) {
6788
7358
  logger?.info("Video is short enough \u2014 no splitting needed");
6789
7359
  return [input];
6790
7360
  }
6791
- fs41.mkdirSync(outputDir, { recursive: true });
6792
- const ext = path45.extname(input) || ".mp4";
6793
- const baseName = path45.basename(input, ext);
6794
- const outputPattern = path45.join(outputDir, `${baseName}_chunk_%03d${ext}`);
7361
+ fs43.mkdirSync(outputDir, { recursive: true });
7362
+ const ext = path47.extname(input) || ".mp4";
7363
+ const baseName = path47.basename(input, ext);
7364
+ const outputPattern = path47.join(outputDir, `${baseName}_chunk_%03d${ext}`);
6795
7365
  logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
6796
7366
  const args = [
6797
7367
  "-i",
@@ -6810,8 +7380,8 @@ async function splitVideo(opts) {
6810
7380
  await runFfmpeg(args, { verbose, logger });
6811
7381
  const files = [];
6812
7382
  for (let i = 0; i < numChunks + 1; i++) {
6813
- const fname = path45.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
6814
- if (fs41.existsSync(fname)) files.push(fname);
7383
+ const fname = path47.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
7384
+ if (fs43.existsSync(fname)) files.push(fname);
6815
7385
  }
6816
7386
  logger?.success(`Created ${files.length} chunks in ${outputDir}`);
6817
7387
  return files;
@@ -6825,9 +7395,9 @@ function registerMediaCommands(program2) {
6825
7395
  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(
6826
7396
  async (opts) => {
6827
7397
  const logger = createLogger({ verbose: opts.verbose ?? false });
6828
- if (!fs42.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
6829
- fs42.mkdirSync(path46.dirname(path46.resolve(opts.output)), { recursive: true });
6830
- const ext = path46.extname(opts.input).toLowerCase();
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 });
7400
+ const ext = path48.extname(opts.input).toLowerCase();
6831
7401
  const kind = extToKind(ext);
6832
7402
  if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
6833
7403
  const quality = Number.parseInt(opts.quality, 10);
@@ -6867,7 +7437,7 @@ function registerMediaCommands(program2) {
6867
7437
  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(
6868
7438
  async (opts) => {
6869
7439
  const logger = createLogger({ verbose: opts.verbose ?? false });
6870
- if (!fs42.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7440
+ if (!fs44.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
6871
7441
  const chunks = await splitVideo({
6872
7442
  input: opts.input,
6873
7443
  outputDir: opts.outputDir,
@@ -6883,7 +7453,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
6883
7453
  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(
6884
7454
  async (opts) => {
6885
7455
  const logger = createLogger({ verbose: opts.verbose ?? false });
6886
- if (!fs42.existsSync(opts.inputDir))
7456
+ if (!fs44.existsSync(opts.inputDir))
6887
7457
  throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
6888
7458
  const result = await batchOptimize({
6889
7459
  inputDir: opts.inputDir,
@@ -6904,9 +7474,157 @@ Processed: ${result.succeeded}/${result.total} files`);
6904
7474
  );
6905
7475
  }
6906
7476
 
7477
+ // src/commands/update.ts
7478
+ init_errors();
7479
+ init_http_client();
7480
+ import { createRequire } from "module";
7481
+ import { execa as execa5 } from "execa";
7482
+
7483
+ // src/core/package-manager.ts
7484
+ var PACKAGE_NAME = "@mrgoonie/multix";
7485
+ function detectPackageManager(execPath, env = process.env) {
7486
+ const normalized = execPath.replace(/\\/g, "/").toLowerCase();
7487
+ if (normalized.includes("/.bun/") || normalized.includes("/bun/install/")) return "bun";
7488
+ if (normalized.includes("/.pnpm/") || normalized.includes("/pnpm/") || normalized.includes("/pnpm-global/"))
7489
+ return "pnpm";
7490
+ if (normalized.includes("/yarn/") || normalized.includes("/.yarn/")) return "yarn";
7491
+ if (normalized.includes("/.npm/") || normalized.includes("/npm/") || normalized.includes("/node_modules/"))
7492
+ return "npm";
7493
+ const userAgent = env.npm_config_user_agent ?? "";
7494
+ if (userAgent.startsWith("bun")) return "bun";
7495
+ if (userAgent.startsWith("pnpm")) return "pnpm";
7496
+ if (userAgent.startsWith("yarn")) return "yarn";
7497
+ if (userAgent.startsWith("npm")) return "npm";
7498
+ return "npm";
7499
+ }
7500
+ function buildUpdateCommand(pm, tag = "latest") {
7501
+ const spec = `${PACKAGE_NAME}@${tag}`;
7502
+ switch (pm) {
7503
+ case "pnpm":
7504
+ return { command: "pnpm", args: ["add", "-g", spec] };
7505
+ case "yarn":
7506
+ return { command: "yarn", args: ["global", "add", spec] };
7507
+ case "bun":
7508
+ return { command: "bun", args: ["add", "-g", spec] };
7509
+ default:
7510
+ return { command: "npm", args: ["i", "-g", spec] };
7511
+ }
7512
+ }
7513
+ function formatUpdateCommand(cmd) {
7514
+ return [cmd.command, ...cmd.args].join(" ");
7515
+ }
7516
+ function compareVersions(a, b) {
7517
+ const parse = (v) => {
7518
+ const [core, prerelease] = v.replace(/^v/, "").split("-", 2);
7519
+ const parts = (core ?? "").split(".").map((n) => Number.parseInt(n, 10) || 0);
7520
+ return { parts, prerelease };
7521
+ };
7522
+ const pa = parse(a);
7523
+ const pb = parse(b);
7524
+ for (let i = 0; i < 3; i++) {
7525
+ const na = pa.parts[i] ?? 0;
7526
+ const nb = pb.parts[i] ?? 0;
7527
+ if (na !== nb) return na < nb ? -1 : 1;
7528
+ }
7529
+ if (pa.prerelease === pb.prerelease) return 0;
7530
+ if (pa.prerelease === void 0) return 1;
7531
+ if (pb.prerelease === void 0) return -1;
7532
+ return pa.prerelease < pb.prerelease ? -1 : 1;
7533
+ }
7534
+
7535
+ // src/commands/update.ts
7536
+ var PACKAGE_NAME2 = "@mrgoonie/multix";
7537
+ var REGISTRY_BASE = "https://registry.npmjs.org";
7538
+ var require2 = createRequire(import.meta.url);
7539
+ function getCurrentVersion() {
7540
+ const pkg2 = require2("../../package.json");
7541
+ return pkg2.version;
7542
+ }
7543
+ async function fetchRegistryVersion(tag = "latest") {
7544
+ try {
7545
+ const doc = await httpJson({
7546
+ url: `${REGISTRY_BASE}/${PACKAGE_NAME2}/${encodeURIComponent(tag)}`
7547
+ });
7548
+ if (!doc.version) {
7549
+ throw new Error(`Registry response for tag "${tag}" is missing a version field`);
7550
+ }
7551
+ return doc.version;
7552
+ } catch (e) {
7553
+ if (e instanceof HttpError) {
7554
+ if (e.status === 404) {
7555
+ throw new Error(`Dist-tag "${tag}" does not exist for ${PACKAGE_NAME2}`);
7556
+ }
7557
+ throw new Error(`Failed to reach npm registry: ${e.message}`);
7558
+ }
7559
+ const msg = e instanceof Error ? e.message : String(e);
7560
+ throw new Error(`Failed to reach npm registry: ${msg}`);
7561
+ }
7562
+ }
7563
+ function registerUpdateCommand(program2) {
7564
+ program2.command("update").description("Update multix to the latest (or a given dist-tag) published version").option("--check", "Only compare current vs. latest version; do not install").option(
7565
+ "--tag <tag>",
7566
+ "Install a specific dist-tag instead of latest (always installs, skipping the up-to-date check)"
7567
+ ).option("--dry-run", "Print the install command instead of running it").option("-v, --verbose", "Verbose logging").action(
7568
+ async (opts) => {
7569
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7570
+ const currentVersion = getCurrentVersion();
7571
+ const explicitTag = opts.tag !== void 0;
7572
+ const tag = opts.tag ?? "latest";
7573
+ logger.info(`Current version: ${currentVersion}`);
7574
+ logger.info(`Checking npm registry for "${tag}"...`);
7575
+ let latestVersion;
7576
+ try {
7577
+ latestVersion = await fetchRegistryVersion(tag);
7578
+ } catch (e) {
7579
+ logger.error(e instanceof Error ? e.message : String(e));
7580
+ process.exit(1);
7581
+ }
7582
+ const cmp = compareVersions(latestVersion, currentVersion);
7583
+ if (opts.check) {
7584
+ if (cmp > 0) {
7585
+ logger.info(`Update available: ${currentVersion} -> ${latestVersion}`);
7586
+ console.log(`Run \`multix update\` to install ${latestVersion}.`);
7587
+ } else {
7588
+ logger.success(`Already up to date (${currentVersion}).`);
7589
+ }
7590
+ return;
7591
+ }
7592
+ if (!explicitTag && cmp <= 0) {
7593
+ logger.success(`Already up to date (${currentVersion}).`);
7594
+ return;
7595
+ }
7596
+ if (explicitTag && cmp <= 0) {
7597
+ logger.info(
7598
+ `Requested tag "${tag}" (${latestVersion}) \u2014 installing regardless of current version (${currentVersion}).`
7599
+ );
7600
+ }
7601
+ const pm = detectPackageManager(process.argv[1] ?? process.execPath);
7602
+ const updateCmd = buildUpdateCommand(pm, tag);
7603
+ const commandStr = formatUpdateCommand(updateCmd);
7604
+ logger.info(`Detected package manager: ${pm}`);
7605
+ logger.info(`Updating ${currentVersion} -> ${latestVersion}`);
7606
+ if (opts.dryRun) {
7607
+ console.log(commandStr);
7608
+ return;
7609
+ }
7610
+ logger.info(`Running: ${commandStr}`);
7611
+ try {
7612
+ await execa5(updateCmd.command, updateCmd.args, { stdio: "inherit" });
7613
+ } catch (e) {
7614
+ const msg = e instanceof Error ? e.message : String(e);
7615
+ logger.error(`Update command failed: ${msg}`);
7616
+ logger.info(`You can run it manually: ${commandStr}`);
7617
+ process.exit(1);
7618
+ }
7619
+ logger.success(`Updated to ${latestVersion}.`);
7620
+ }
7621
+ );
7622
+ }
7623
+
6907
7624
  // src/commands/index.ts
6908
7625
  function registerCommands(program2) {
6909
7626
  registerCheckCommand(program2);
7627
+ registerUpdateCommand(program2);
6910
7628
  registerGeminiCommands(program2);
6911
7629
  registerMinimaxCommands(program2);
6912
7630
  registerOpenAICommands(program2);
@@ -6915,6 +7633,7 @@ function registerCommands(program2) {
6915
7633
  registerBytePlusCommands(program2);
6916
7634
  registerCloudflareCommands(program2);
6917
7635
  registerElevenLabsCommands(program2);
7636
+ registerFalCommands(program2);
6918
7637
  registerMediaCommands(program2);
6919
7638
  registerDocCommands(program2);
6920
7639
  }
@@ -6925,10 +7644,10 @@ init_http_client();
6925
7644
 
6926
7645
  // src/cli.ts
6927
7646
  loadEnv();
6928
- var require2 = createRequire(import.meta.url);
6929
- var pkg = require2("../package.json");
7647
+ var require3 = createRequire2(import.meta.url);
7648
+ var pkg = require3("../package.json");
6930
7649
  var program = new Command().name("multix").description(
6931
- "AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, ffmpeg, and ImageMagick"
7650
+ "AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, ffmpeg, and ImageMagick"
6932
7651
  ).version(pkg.version);
6933
7652
  registerCommands(program);
6934
7653
  program.parseAsync(process.argv).catch((err2) => {