@mrgoonie/multix 0.15.1 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.15.3](https://github.com/mrgoonie/multix-cli/compare/v0.15.2...v0.15.3) (2026-10-11)
9
+
10
+
11
+ ### Fixed
12
+
13
+ * **core:** never forward API-key headers across origins on redirect ([#101](https://github.com/mrgoonie/multix-cli/issues/101)) ([5fd673d](https://github.com/mrgoonie/multix-cli/commit/5fd673ddac9e30b592348cafd6ab68343fa644d7))
14
+
15
+ ## [0.15.2](https://github.com/mrgoonie/multix-cli/compare/v0.15.1...v0.15.2) (2026-10-11)
16
+
17
+
18
+ ### Fixed
19
+
20
+ * **doc:** do not retry configuration and non-transient errors ([#96](https://github.com/mrgoonie/multix-cli/issues/96)) ([6c505a6](https://github.com/mrgoonie/multix-cli/commit/6c505a64f88cbcc3ee992ca4112473400189305f))
21
+
8
22
  ## [0.15.1](https://github.com/mrgoonie/multix-cli/compare/v0.15.0...v0.15.1) (2026-10-11)
9
23
 
10
24
 
package/dist/cli.js CHANGED
@@ -489,7 +489,8 @@ var PollTimeoutError = class extends MultixError {
489
489
  };
490
490
  function isTransientHttpError(error) {
491
491
  if (!(error instanceof HttpError)) return false;
492
- return error.status === 0 || error.status === 429 || error.status >= 500;
492
+ const { status } = error;
493
+ return status === 0 || status === 408 || status === 429 || status >= 500;
493
494
  }
494
495
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
495
496
  async function pollUntil(opts) {
@@ -540,6 +541,113 @@ init_errors();
540
541
 
541
542
  // src/core/fetch-json.ts
542
543
  init_errors();
544
+
545
+ // src/core/redirect-fetch.ts
546
+ init_errors();
547
+ var MAX_REDIRECTS = 5;
548
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
549
+ var BODY_HEADERS = ["content-type", "content-length", "content-encoding", "content-language"];
550
+ function withoutBodyHeaders(headers2) {
551
+ if (headers2 === void 0) return void 0;
552
+ const copy = new Headers(headers2);
553
+ for (const name of BODY_HEADERS) copy.delete(name);
554
+ return copy;
555
+ }
556
+ async function fetchFollowingRedirects(url, init = {}, opts = {}) {
557
+ const doFetch = opts.fetch ?? ((input, i) => globalThis.fetch(input, i));
558
+ const { validateUrl } = opts;
559
+ const maxRedirects = opts.maxRedirects ?? MAX_REDIRECTS;
560
+ let current = url;
561
+ let hopInit = { ...init, redirect: "manual" };
562
+ for (let hop = 0; ; hop++) {
563
+ await validateUrl?.(current);
564
+ let response;
565
+ try {
566
+ response = await doFetch(current, hopInit);
567
+ } catch (cause) {
568
+ if (!opts.wrapNetworkErrors) throw cause;
569
+ const msg = cause instanceof Error ? cause.message : String(cause);
570
+ throw new HttpError(0, msg, current);
571
+ }
572
+ if (validateUrl && response.url && response.url !== current) {
573
+ try {
574
+ await validateUrl(response.url);
575
+ } catch (err2) {
576
+ await response.body?.cancel().catch(() => {
577
+ });
578
+ throw err2;
579
+ }
580
+ }
581
+ const location = response.headers.get("location");
582
+ if (!REDIRECT_STATUSES.has(response.status) || !location) {
583
+ return { response, url: current };
584
+ }
585
+ let next;
586
+ try {
587
+ next = new URL(location, current);
588
+ } catch {
589
+ }
590
+ const crossOrigin = next !== void 0 && next.origin !== new URL(current).origin;
591
+ if (crossOrigin && opts.crossOrigin === "stop") return { response, url: current };
592
+ await response.body?.cancel().catch(() => {
593
+ });
594
+ if (hop >= maxRedirects) {
595
+ throw new HttpError(response.status, `too many redirects (over ${maxRedirects})`, current);
596
+ }
597
+ if (!next) {
598
+ throw new HttpError(response.status, "redirect with a malformed Location header", current);
599
+ }
600
+ if (crossOrigin) {
601
+ if (opts.crossOrigin === "error") {
602
+ throw new HttpError(
603
+ response.status,
604
+ `refusing to follow a redirect to another origin (${next.origin})`,
605
+ current
606
+ );
607
+ }
608
+ hopInit = { ...hopInit, headers: void 0 };
609
+ }
610
+ const method = (hopInit.method ?? "GET").toUpperCase();
611
+ const toGet = response.status === 303 && method !== "GET" && method !== "HEAD" || (response.status === 301 || response.status === 302) && method === "POST";
612
+ if (toGet) {
613
+ hopInit = {
614
+ ...hopInit,
615
+ method: "GET",
616
+ body: void 0,
617
+ headers: withoutBodyHeaders(hopInit.headers)
618
+ };
619
+ }
620
+ current = next.toString();
621
+ }
622
+ }
623
+ async function fetchSameOrigin(url, init = {}, fetchImpl) {
624
+ const { response } = await fetchFollowingRedirects(url, init, {
625
+ fetch: fetchImpl,
626
+ crossOrigin: "stop"
627
+ });
628
+ return response;
629
+ }
630
+ async function fetchWithValidatedRedirects(url, opts = {}) {
631
+ const { signal, headers: headers2, validateUrl } = opts;
632
+ const init = { ...signal ? { signal } : {}, ...headers2 ? { headers: headers2 } : {} };
633
+ if (!validateUrl && !headers2) {
634
+ const doFetch = opts.fetch ?? ((input, i) => globalThis.fetch(input, i));
635
+ try {
636
+ return { response: await doFetch(url, init), url };
637
+ } catch (cause) {
638
+ const msg = cause instanceof Error ? cause.message : String(cause);
639
+ throw new HttpError(0, msg, url);
640
+ }
641
+ }
642
+ return fetchFollowingRedirects(url, init, {
643
+ validateUrl,
644
+ maxRedirects: opts.maxRedirects,
645
+ fetch: opts.fetch,
646
+ wrapNetworkErrors: true
647
+ });
648
+ }
649
+
650
+ // src/core/fetch-json.ts
543
651
  var DEFAULT_TIMEOUT_MS = 12e4;
544
652
  var SNIPPET_MAX = 500;
545
653
  function parseRetryAfter(value, now = Date.now()) {
@@ -565,6 +673,10 @@ async function httpErrorFromResponse(response, url) {
565
673
  parseRetryAfter(response.headers.get("retry-after"))
566
674
  );
567
675
  }
676
+ var NON_CREDENTIAL_HEADERS = /* @__PURE__ */ new Set(["accept", "accept-language", "content-type", "user-agent"]);
677
+ function carriesCredentialHeaders(headers2) {
678
+ return Object.keys(headers2).some((name) => !NON_CREDENTIAL_HEADERS.has(name.toLowerCase()));
679
+ }
568
680
  async function fetchJson(opts) {
569
681
  const { url, method = "GET", headers: headers2 = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
570
682
  const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
@@ -579,13 +691,11 @@ async function fetchJson(opts) {
579
691
  if (body !== void 0) {
580
692
  init.body = JSON.stringify(body);
581
693
  }
582
- let response;
583
- try {
584
- response = await doFetch(url, init);
585
- } catch (cause) {
586
- const msg = cause instanceof Error ? cause.message : String(cause);
587
- throw new HttpError(0, msg, url);
588
- }
694
+ const { response } = await fetchFollowingRedirects(url, init, {
695
+ fetch: doFetch,
696
+ crossOrigin: carriesCredentialHeaders(headers2) ? "error" : "drop-headers",
697
+ wrapNetworkErrors: true
698
+ });
589
699
  if (!response.ok) {
590
700
  throw await httpErrorFromResponse(response, url);
591
701
  }
@@ -902,7 +1012,7 @@ var AtlasCloudClient = class {
902
1012
  }
903
1013
  /**
904
1014
  * Poll until the prediction is terminal (success or failure) or the deadline
905
- * passes. Network errors, 429 (honouring Retry-After) and 5xx are retried.
1015
+ * passes. Network errors, 408, 429 (honouring Retry-After) and 5xx are retried.
906
1016
  * Throws PollTimeoutError on timeout; returns the terminal prediction otherwise.
907
1017
  */
908
1018
  async waitForPrediction(id, opts) {
@@ -933,13 +1043,18 @@ var AtlasCloudClient = class {
933
1043
  const url = `${this.baseUrl}/model/uploadMedia`;
934
1044
  let response;
935
1045
  try {
936
- response = await this.doFetch(url, {
937
- method: "POST",
938
- headers: this.authHeaders(),
939
- body: form,
940
- signal: AbortSignal.timeout(this.uploadTimeoutMs)
941
- });
1046
+ response = await fetchSameOrigin(
1047
+ url,
1048
+ {
1049
+ method: "POST",
1050
+ headers: this.authHeaders(),
1051
+ body: form,
1052
+ signal: AbortSignal.timeout(this.uploadTimeoutMs)
1053
+ },
1054
+ (input, init) => this.doFetch(input, init)
1055
+ );
942
1056
  } catch (cause) {
1057
+ if (cause instanceof HttpError) throw cause;
943
1058
  throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
944
1059
  }
945
1060
  if (!response.ok) throw await httpErrorFromResponse(response, url);
@@ -1132,7 +1247,6 @@ async function httpJson(opts) {
1132
1247
  }
1133
1248
  var MAX_DOWNLOAD_ENV = "MULTIX_MAX_DOWNLOAD_MB";
1134
1249
  var DEFAULT_MAX_DOWNLOAD_MB = 2048;
1135
- var MAX_REDIRECTS = 5;
1136
1250
  function formatMb2(bytes) {
1137
1251
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1138
1252
  }
@@ -1162,44 +1276,6 @@ function maxDownloadBytes() {
1162
1276
  }
1163
1277
  return bytes;
1164
1278
  }
1165
- async function fetchWithValidatedRedirects(url, opts = {}) {
1166
- const doFetch = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
1167
- const { validateUrl, signal } = opts;
1168
- const maxRedirects = opts.maxRedirects ?? MAX_REDIRECTS;
1169
- let current = url;
1170
- let headers2 = opts.headers;
1171
- for (let hop = 0; ; hop++) {
1172
- await validateUrl?.(current);
1173
- let response;
1174
- try {
1175
- response = await doFetch(current, {
1176
- ...signal ? { signal } : {},
1177
- ...headers2 ? { headers: headers2 } : {},
1178
- ...validateUrl ? { redirect: "manual" } : {}
1179
- });
1180
- } catch (cause) {
1181
- const msg = cause instanceof Error ? cause.message : String(cause);
1182
- throw new HttpError(0, msg, current);
1183
- }
1184
- const location = response.headers.get("location");
1185
- if (!validateUrl || !location || response.status < 300 || response.status >= 400) {
1186
- return { response, url: current };
1187
- }
1188
- await response.body?.cancel().catch(() => {
1189
- });
1190
- if (hop >= maxRedirects) {
1191
- throw new HttpError(response.status, `too many redirects (over ${maxRedirects})`, current);
1192
- }
1193
- let next;
1194
- try {
1195
- next = new URL(location, current);
1196
- } catch {
1197
- throw new HttpError(response.status, "redirect with a malformed Location header", current);
1198
- }
1199
- if (next.origin !== new URL(current).origin) headers2 = void 0;
1200
- current = next.toString();
1201
- }
1202
- }
1203
1279
  function removeQuietly(file) {
1204
1280
  try {
1205
1281
  fs3.rmSync(file, { force: true });
@@ -1261,7 +1337,7 @@ async function downloadToFile(url, opts) {
1261
1337
  yield chunk;
1262
1338
  }
1263
1339
  }
1264
- const part = `${dest}.part`;
1340
+ const part = `${dest}.${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.part`;
1265
1341
  let fileStream;
1266
1342
  try {
1267
1343
  fileStream = fs3.createWriteStream(part);
@@ -3131,8 +3207,13 @@ init_errors();
3131
3207
  function cloudflareProviderFailure(status) {
3132
3208
  return new ProviderError(`Cloudflare request failed with HTTP ${status}`, "Cloudflare");
3133
3209
  }
3210
+ async function cloudflareResponseFailure(response) {
3211
+ await response.body?.cancel().catch(() => {
3212
+ });
3213
+ return cloudflareProviderFailure(response.status);
3214
+ }
3134
3215
  async function readCloudflareJson(response) {
3135
- if (!response.ok) throw cloudflareProviderFailure(response.status);
3216
+ if (!response.ok) throw await cloudflareResponseFailure(response);
3136
3217
  try {
3137
3218
  return await response.json();
3138
3219
  } catch {
@@ -3152,12 +3233,11 @@ function videoHeaders(config, opts) {
3152
3233
  };
3153
3234
  }
3154
3235
  function send(opts, url, init) {
3155
- const f = opts.fetch;
3156
3236
  const withTimeout = {
3157
3237
  ...init,
3158
3238
  signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
3159
3239
  };
3160
- return f ? f(url, withTimeout) : globalThis.fetch(url, withTimeout);
3240
+ return fetchSameOrigin(url, withTimeout, opts.fetch);
3161
3241
  }
3162
3242
  function parseCloudflareVideoPrediction(value) {
3163
3243
  if (!value || typeof value !== "object") {
@@ -3210,7 +3290,7 @@ function gatewayUrl(accountId) {
3210
3290
  async function runCloudflareJson(model, input) {
3211
3291
  const config = requireCloudflareConfig();
3212
3292
  const useGateway = Boolean(config.gatewayId);
3213
- const response = await globalThis.fetch(
3293
+ const response = await fetchSameOrigin(
3214
3294
  useGateway ? gatewayUrl(config.accountId) : nativeUrl(config.accountId, model),
3215
3295
  {
3216
3296
  method: "POST",
@@ -3227,7 +3307,7 @@ async function runCloudflareJson(model, input) {
3227
3307
  async function runCloudflareBytes(model, input) {
3228
3308
  const config = requireCloudflareConfig();
3229
3309
  const useGateway = Boolean(config.gatewayId);
3230
- const response = await globalThis.fetch(
3310
+ const response = await fetchSameOrigin(
3231
3311
  useGateway ? gatewayUrl(config.accountId) : nativeUrl(config.accountId, model),
3232
3312
  {
3233
3313
  method: "POST",
@@ -3235,7 +3315,7 @@ async function runCloudflareBytes(model, input) {
3235
3315
  body: JSON.stringify(useGateway ? { model, input } : input)
3236
3316
  }
3237
3317
  );
3238
- if (!response.ok) throw cloudflareProviderFailure(response.status);
3318
+ if (!response.ok) throw await cloudflareResponseFailure(response);
3239
3319
  const contentType = response.headers.get("content-type") ?? "";
3240
3320
  if (!contentType.includes("audio/mpeg")) {
3241
3321
  throw new ProviderError("Cloudflare did not return MPEG audio", "Cloudflare");
@@ -3557,7 +3637,7 @@ function apiPostJson(endpoint2, payload, apiKey, opts = {}) {
3557
3637
  }
3558
3638
  async function apiDelete(endpoint2, apiKey) {
3559
3639
  const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
3560
- const res = await globalThis.fetch(url, {
3640
+ const res = await fetchSameOrigin(url, {
3561
3641
  method: "DELETE",
3562
3642
  headers: authHeaders(apiKey),
3563
3643
  signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS2)
@@ -3591,8 +3671,9 @@ async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
3591
3671
  }
3592
3672
  let res;
3593
3673
  try {
3594
- res = await globalThis.fetch(url.toString(), init);
3674
+ res = await fetchSameOrigin(url.toString(), init);
3595
3675
  } catch (cause) {
3676
+ if (cause instanceof HttpError) throw cause;
3596
3677
  const msg = cause instanceof Error ? cause.message : String(cause);
3597
3678
  throw new HttpError(0, msg, url.toString());
3598
3679
  }
@@ -3606,13 +3687,14 @@ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
3606
3687
  const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
3607
3688
  let res;
3608
3689
  try {
3609
- res = await globalThis.fetch(url, {
3690
+ res = await fetchSameOrigin(url, {
3610
3691
  method: "POST",
3611
3692
  headers: authHeaders(apiKey),
3612
3693
  body: form,
3613
3694
  signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
3614
3695
  });
3615
3696
  } catch (cause) {
3697
+ if (cause instanceof HttpError) throw cause;
3616
3698
  const msg = cause instanceof Error ? cause.message : String(cause);
3617
3699
  throw new HttpError(0, msg, url);
3618
3700
  }
@@ -3821,7 +3903,7 @@ Dubbed file: ${dest}`);
3821
3903
  }
3822
3904
  async function downloadDub(url, apiKey, dest) {
3823
3905
  fs15.mkdirSync(path18.dirname(dest), { recursive: true });
3824
- const res = await globalThis.fetch(url, {
3906
+ const { response: res } = await fetchWithValidatedRedirects(url, {
3825
3907
  headers: { "xi-api-key": apiKey },
3826
3908
  signal: AbortSignal.timeout(6e5)
3827
3909
  });
@@ -3830,7 +3912,6 @@ async function downloadDub(url, apiKey, dest) {
3830
3912
  }
3831
3913
  const arr = new Uint8Array(await res.arrayBuffer());
3832
3914
  fs15.writeFileSync(dest, arr);
3833
- void downloadFile;
3834
3915
  }
3835
3916
 
3836
3917
  // src/providers/elevenlabs/commands/isolate.ts
@@ -5813,13 +5894,14 @@ function checkVeoResult(op) {
5813
5894
  return op;
5814
5895
  }
5815
5896
  async function downloadVeoVideo(apiKey, uri, dest) {
5816
- let host = "";
5897
+ let parsed;
5817
5898
  try {
5818
- host = new URL(uri).hostname;
5899
+ parsed = new URL(uri);
5819
5900
  } catch {
5820
5901
  throw new ProviderError("Veo returned an invalid video URI", "gemini");
5821
5902
  }
5822
- await downloadFile(uri, dest, 3e5, host === API_HOST ? headers(apiKey) : void 0);
5903
+ const keyed = parsed.protocol === "https:" && parsed.hostname === API_HOST;
5904
+ await downloadFile(uri, dest, 3e5, keyed ? headers(apiKey) : void 0);
5823
5905
  }
5824
5906
  async function runVeo(model, request, dest, logger) {
5825
5907
  const body = buildVeoRequest(request);
@@ -6315,10 +6397,8 @@ var HEYGEN_OPERATIONS = {
6315
6397
  };
6316
6398
 
6317
6399
  // src/providers/heygen/node-helpers.ts
6318
- import { once } from "events";
6319
6400
  import fs29 from "fs";
6320
6401
  import path32 from "path";
6321
- import { pipeline as pipeline2 } from "stream/promises";
6322
6402
  init_errors();
6323
6403
  init_redact();
6324
6404
 
@@ -6665,13 +6745,18 @@ var HeygenClient = class {
6665
6745
  form.append("file", typed, input.filename);
6666
6746
  let response;
6667
6747
  try {
6668
- response = await doFetch(url, {
6669
- method: "POST",
6670
- headers: this.headers(key),
6671
- body: form,
6672
- signal: AbortSignal.timeout(this.timeoutMs)
6673
- });
6748
+ response = await fetchSameOrigin(
6749
+ url,
6750
+ {
6751
+ method: "POST",
6752
+ headers: this.headers(key),
6753
+ body: form,
6754
+ signal: AbortSignal.timeout(this.timeoutMs)
6755
+ },
6756
+ doFetch
6757
+ );
6674
6758
  } catch (cause) {
6759
+ if (cause instanceof HttpError) throw toHeygenApiError(cause);
6675
6760
  const msg = cause instanceof Error ? cause.message : String(cause);
6676
6761
  const err3 = toHeygenApiError(new HttpError(0, msg, url));
6677
6762
  if (attempt >= this.maxAttempts) throw err3;
@@ -7322,44 +7407,11 @@ function inferExtension(url, fallback) {
7322
7407
  function safeSlug(value) {
7323
7408
  return value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "out";
7324
7409
  }
7325
- var MAX_REDIRECTS2 = 5;
7326
- var isRedirect = (status) => [301, 302, 303, 307, 308].includes(status);
7327
- async function downloadHttpsOnly(url, dest, timeoutMs) {
7328
- const signal = AbortSignal.timeout(timeoutMs);
7329
- let current = url;
7330
- for (let hop = 0; ; hop++) {
7331
- if (!isHttpsUrl(current)) {
7332
- throw new HttpError(0, "refusing to follow a redirect to a non-https URL", current);
7333
- }
7334
- let response;
7335
- try {
7336
- response = await globalThis.fetch(current, { signal, redirect: "manual" });
7337
- } catch (cause) {
7338
- throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), current);
7339
- }
7340
- if (response.url && !isHttpsUrl(response.url)) {
7341
- await response.body?.cancel();
7342
- throw new HttpError(0, "redirected to a non-https URL", response.url);
7343
- }
7344
- const location = response.headers.get("location");
7345
- if (isRedirect(response.status) && location) {
7346
- await response.body?.cancel();
7347
- if (hop >= MAX_REDIRECTS2) throw new HttpError(0, "too many redirects", url);
7348
- current = new URL(location, current).href;
7349
- continue;
7350
- }
7351
- if (!response.ok) throw new HttpError(response.status, "(download failed)", current);
7352
- if (!response.body) throw new HttpError(0, "Response body is null", current);
7353
- fs29.mkdirSync(path32.dirname(dest), { recursive: true });
7354
- const fileStream = fs29.createWriteStream(dest);
7355
- try {
7356
- await pipeline2(response.body, fileStream);
7357
- } catch (err2) {
7358
- if (!fileStream.closed) await once(fileStream, "close");
7359
- throw err2;
7360
- }
7361
- return;
7362
- }
7410
+ function requireHttps2(url) {
7411
+ if (!isHttpsUrl(url)) throw new HttpError(0, "redirected to a non-https URL", url);
7412
+ }
7413
+ function downloadHttps(url, dest, timeoutMs) {
7414
+ return downloadToFile(url, { dest, timeoutMs, validateUrl: requireHttps2 });
7363
7415
  }
7364
7416
  async function downloadOutput(url, dest, resumeHint) {
7365
7417
  if (!url) {
@@ -7370,12 +7422,9 @@ async function downloadOutput(url, dest, resumeHint) {
7370
7422
  `Refusing to download HeyGen output from a non-https URL (${redactUrl(url)})`
7371
7423
  );
7372
7424
  }
7373
- const tmp = `${dest}.${process.pid}-${Date.now()}.part`;
7374
7425
  try {
7375
- await downloadHttpsOnly(url, tmp, 6e5);
7376
- fs29.renameSync(tmp, dest);
7426
+ await downloadHttps(url, dest, 6e5);
7377
7427
  } catch (e) {
7378
- fs29.rmSync(tmp, { force: true });
7379
7428
  const status = e instanceof HttpError ? e.status : 0;
7380
7429
  const expired = status === 403 || status === 404 || status === 410;
7381
7430
  const reason = e instanceof Error ? e.message : String(e);
@@ -7437,7 +7486,7 @@ function captionPathFor(dest, capExt) {
7437
7486
  const capDest = `${stem}.${capExt}`;
7438
7487
  return capDest.toLowerCase() === dest.toLowerCase() ? `${stem}.captions.${capExt}` : capDest;
7439
7488
  }
7440
- var downloadThumbHttpsOnly = (url, dest) => downloadHttpsOnly(url, dest, 12e4);
7489
+ var downloadThumbHttpsOnly = (url, dest) => downloadHttps(url, dest, 12e4);
7441
7490
  function summarizeState(state) {
7442
7491
  const out = {
7443
7492
  kind: state.kind,
@@ -9584,13 +9633,14 @@ async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOU
9584
9633
  const url = endpoint(pathname);
9585
9634
  let res;
9586
9635
  try {
9587
- res = await globalThis.fetch(url, {
9636
+ res = await fetchSameOrigin(url, {
9588
9637
  method: "POST",
9589
9638
  headers: authHeaders4(apiKey),
9590
9639
  body: form,
9591
9640
  signal: AbortSignal.timeout(timeoutMs)
9592
9641
  });
9593
9642
  } catch (cause) {
9643
+ if (cause instanceof HttpError) throw cause;
9594
9644
  throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
9595
9645
  }
9596
9646
  if (!res.ok) {
@@ -9603,13 +9653,14 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
9603
9653
  const url = endpoint(pathname);
9604
9654
  let res;
9605
9655
  try {
9606
- res = await globalThis.fetch(url, {
9656
+ res = await fetchSameOrigin(url, {
9607
9657
  method: "POST",
9608
9658
  headers: authHeaders4(apiKey, { "Content-Type": "application/json" }),
9609
9659
  body: JSON.stringify(body),
9610
9660
  signal: AbortSignal.timeout(timeoutMs)
9611
9661
  });
9612
9662
  } catch (cause) {
9663
+ if (cause instanceof HttpError) throw cause;
9613
9664
  throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
9614
9665
  }
9615
9666
  if (!res.ok) {
@@ -10449,7 +10500,9 @@ async function downloadOpenRouterVideo(opts) {
10449
10500
  const apiKey = requireOpenRouterKey();
10450
10501
  const url = videoContentUrl(jobId);
10451
10502
  const dest = path50.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
10452
- const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
10503
+ const { response: r } = await fetchWithValidatedRedirects(url, {
10504
+ headers: { Authorization: `Bearer ${apiKey}` }
10505
+ });
10453
10506
  if (!r.ok) {
10454
10507
  logger.error(`Download HTTP ${r.status}`);
10455
10508
  process.exit(1);
@@ -10460,8 +10513,8 @@ async function downloadOpenRouterVideo(opts) {
10460
10513
  }
10461
10514
  fs43.mkdirSync(path50.dirname(dest), { recursive: true });
10462
10515
  const ws = fs43.createWriteStream(dest);
10463
- const { pipeline: pipeline3 } = await import("stream/promises");
10464
- await pipeline3(r.body, ws);
10516
+ const { pipeline: pipeline2 } = await import("stream/promises");
10517
+ await pipeline2(r.body, ws);
10465
10518
  logger.success(`Saved ${dest}`);
10466
10519
  if (opts.output) {
10467
10520
  fs43.mkdirSync(path50.dirname(path50.resolve(opts.output)), { recursive: true });
@@ -10877,7 +10930,7 @@ var ReplicateClient = class {
10877
10930
  /**
10878
10931
  * Poll until the prediction reaches a terminal status (succeeded, failed,
10879
10932
  * canceled, aborted) and return it — callers decide success via
10880
- * assertSucceeded. Retries network/429/5xx; throws PollTimeoutError on deadline.
10933
+ * assertSucceeded. Retries network/408/429/5xx; throws PollTimeoutError on deadline.
10881
10934
  */
10882
10935
  async waitForPrediction(prediction, opts) {
10883
10936
  if (typeof prediction !== "string" && isTerminalStatus2(prediction.status)) return prediction;
@@ -10923,13 +10976,18 @@ var ReplicateClient = class {
10923
10976
  const subject = `file upload ${opts.filename}`;
10924
10977
  let response;
10925
10978
  try {
10926
- response = await this.doFetch(url, {
10927
- method: "POST",
10928
- headers: this.headers(),
10929
- body: form,
10930
- signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
10931
- });
10979
+ response = await fetchSameOrigin(
10980
+ url,
10981
+ {
10982
+ method: "POST",
10983
+ headers: this.headers(),
10984
+ body: form,
10985
+ signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
10986
+ },
10987
+ this.doFetch
10988
+ );
10932
10989
  } catch (cause) {
10990
+ if (cause instanceof HttpError) throw mapReplicateError(cause, subject);
10933
10991
  const msg = cause instanceof Error ? cause.message : String(cause);
10934
10992
  throw mapReplicateError(new HttpError(0, msg, url), subject);
10935
10993
  }
@@ -12061,6 +12119,7 @@ async function convertResolved(inputs, ctx) {
12061
12119
  } catch (e) {
12062
12120
  errorMsg = labelError(e instanceof Error ? e.message : String(e), input);
12063
12121
  if (isGeminiRateLimitError(e)) break;
12122
+ if (!isTransientHttpError(e)) break;
12064
12123
  if (attempt < 2) {
12065
12124
  const wait = 2 ** attempt * 1e3;
12066
12125
  logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrgoonie/multix",
3
- "version": "0.15.1",
3
+ "version": "0.15.3",
4
4
  "description": "AI multimodal CLI — OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, Replicate, HeyGen, ffmpeg, ImageMagick, doc-to-md",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: multix
3
3
  description: AI multimodal CLI — generate and edit images (OpenAI, Codex driver, Gemini Nano Banana, Imagen, MiniMax, OpenRouter, Leonardo, BytePlus Seedream, Cloudflare Workers AI FLUX.1 Schnell, fal.ai FLUX, Replicate FLUX), generate video (Gemini Veo, MiniMax Hailuo, OpenRouter via Veo/Seedance/Kling, Leonardo via Motion/Veo/Kling/Hailuo/Seedance/LTX, BytePlus Seedance, Cloudflare AI Gateway via Replicate, fal.ai queue models, Replicate direct API models incl. P-Video, HeyGen Video Agent and scripted avatar videos), video translation and lipsync (HeyGen), TTS/STT/music (OpenAI, Gemini Flash TTS incl. Gemini 3.8 with delivery-style control, MiniMax, Cloudflare Workers AI MeloTTS, ElevenLabs), 3D models (BytePlus Hyper3D/Hitem3d), analyze/transcribe media, convert documents to Markdown, optimize media via ffmpeg/ImageMagick, and self-update the CLI. Use whenever the user wants to create, edit, or transform images/video/audio/3D/documents from a CLI, mentions any of: OpenAI, Codex image driver, image-to-image, i2i, image edit, watercolor/cyberpunk style transfer, reference image, OpenRouter, Nano Banana, Flux, Seedream, Veo, Hailuo, Seedance, Kling, Cloudflare Workers AI, AI Gateway, fal.ai, Atlas Cloud, Replicate, replicate.com models, Replicate predictions, HeyGen, avatar video, talking head, Video Agent, video translation, dubbing, lipsync, video generation, text-to-video, image-to-video, i2v, ElevenLabs voice cloning, updating/upgrading multix, or asks for batch media optimization.
4
- version: 0.15.1 # x-release-please-version
4
+ version: 0.15.3 # x-release-please-version
5
5
  ---
6
6
 
7
7
  # multix — AI Multimodal CLI