@mrgoonie/multix 1.0.3 → 1.0.4

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,13 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.0.4](https://github.com/mrgoonie/multix-cli/compare/v1.0.3...v1.0.4) (2026-10-11)
9
+
10
+
11
+ ### Fixed
12
+
13
+ * **core:** do not retry deterministic fetch failures ([#111](https://github.com/mrgoonie/multix-cli/issues/111)) ([8d2aa9b](https://github.com/mrgoonie/multix-cli/commit/8d2aa9baa3533cf199adc038911c9ea485b40083))
14
+
8
15
  ## [1.0.3](https://github.com/mrgoonie/multix-cli/compare/v1.0.2...v1.0.3) (2026-10-11)
9
16
 
10
17
 
package/dist/cli.js CHANGED
@@ -58,8 +58,8 @@ var init_errors = __esm({
58
58
  code;
59
59
  };
60
60
  ConfigError = class extends MultixError {
61
- constructor(message) {
62
- super(message, "CONFIG_ERROR");
61
+ constructor(message, options) {
62
+ super(message, "CONFIG_ERROR", options);
63
63
  this.name = "ConfigError";
64
64
  }
65
65
  };
@@ -768,7 +768,11 @@ var PollTimeoutError = class extends MultixError {
768
768
  function isTransientHttpError(error) {
769
769
  if (!(error instanceof HttpError)) return false;
770
770
  const { status } = error;
771
- return status === 0 || status === 408 || status === 429 || status >= 500;
771
+ return status === 408 || status === 429 || isRetryableServerStatus(status);
772
+ }
773
+ function isRetryableServerStatus(status) {
774
+ if (status === 501 || status === 505) return false;
775
+ return status === 0 || status >= 500;
772
776
  }
773
777
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
774
778
  async function pollUntil(opts) {
@@ -817,6 +821,97 @@ async function pollUntil(opts) {
817
821
  // src/providers/atlascloud/client.ts
818
822
  init_errors();
819
823
 
824
+ // src/core/fetch-error.ts
825
+ init_errors();
826
+ init_redact();
827
+ var MAX_CAUSE_DEPTH = 4;
828
+ var TLS_CODES = /* @__PURE__ */ new Set([
829
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
830
+ "SELF_SIGNED_CERT_IN_CHAIN",
831
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
832
+ "ERR_TLS_CERT_ALTNAME_INVALID"
833
+ ]);
834
+ function kindOfCode(code) {
835
+ if (typeof code !== "string") return void 0;
836
+ if (code === "ERR_INVALID_URL") return "url";
837
+ if (code === "ERR_INVALID_CHAR" || code === "ERR_INVALID_HTTP_TOKEN") return "header";
838
+ if (TLS_CODES.has(code) || code.startsWith("CERT_") || code.startsWith("UNABLE_TO_")) {
839
+ return "tls";
840
+ }
841
+ if (code === "UND_ERR_INVALID_ARG" || code === "UND_ERR_NOT_SUPPORTED") return "request";
842
+ return void 0;
843
+ }
844
+ function kindOfMessage(message) {
845
+ if (typeof message !== "string") return void 0;
846
+ if (/Invalid URL|Failed to parse URL/i.test(message)) return "url";
847
+ if (/ByteString|is not a legal HTTP header value|invalid header value|invalid header name/i.test(
848
+ message
849
+ )) {
850
+ return "header";
851
+ }
852
+ if (/duplex option is required|cannot have body|unexpected redirect/i.test(message)) {
853
+ return "request";
854
+ }
855
+ return void 0;
856
+ }
857
+ function causeChain(err2) {
858
+ const out = [];
859
+ let current = err2;
860
+ for (let depth = 0; depth <= MAX_CAUSE_DEPTH && current && typeof current === "object"; depth++) {
861
+ const e = current;
862
+ out.push(e);
863
+ if (Array.isArray(e.errors)) {
864
+ for (const member of e.errors) if (member && typeof member === "object") out.push(member);
865
+ }
866
+ current = e.cause;
867
+ }
868
+ return out;
869
+ }
870
+ function deterministicKind(err2) {
871
+ for (const e of causeChain(err2)) {
872
+ const kind = kindOfCode(e.code) ?? kindOfMessage(e.message);
873
+ if (kind) return kind;
874
+ }
875
+ return void 0;
876
+ }
877
+ function isNetworkFetchFailure(err2) {
878
+ return deterministicKind(err2) === void 0;
879
+ }
880
+ function scrub(message) {
881
+ return message.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/gi, (u) => redactUrl(u)).replace(/[?#](?!\[redacted\])[^\s"'<>]*=[^\s"'<>]*/g, "?[redacted]");
882
+ }
883
+ function messageOf(e) {
884
+ return typeof e?.message === "string" ? scrub(e.message) : "";
885
+ }
886
+ function detailOf(err2) {
887
+ const chain = causeChain(err2);
888
+ const parts = [messageOf(chain[0]), messageOf(chain[1])].filter(Boolean);
889
+ return parts.length > 0 ? parts.join(": ") : scrub(String(err2));
890
+ }
891
+ function toFetchError(err2, url, opts = {}) {
892
+ const options = { cause: err2 };
893
+ const prefix = opts.prefix ?? "";
894
+ const target = opts.target ?? redactUrl(url);
895
+ switch (deterministicKind(err2)) {
896
+ case "url":
897
+ return new ConfigError(`${prefix}Invalid request URL: ${target}`, options);
898
+ case "header":
899
+ return new ConfigError(
900
+ `${prefix}Invalid HTTP header value in the request to ${target}. Check the API key and other header values for non-ASCII characters such as smart quotes, or line breaks.`,
901
+ options
902
+ );
903
+ case "tls":
904
+ case "request":
905
+ return new ProviderError(
906
+ `${prefix}Request to ${target} failed: ${detailOf(err2)}`,
907
+ opts.provider ?? "http",
908
+ options
909
+ );
910
+ default:
911
+ return new HttpError(0, `${prefix}${detailOf(err2)}`, url, void 0, options);
912
+ }
913
+ }
914
+
820
915
  // src/core/fetch-json.ts
821
916
  init_errors();
822
917
 
@@ -844,8 +939,7 @@ async function fetchFollowingRedirects(url, init = {}, opts = {}) {
844
939
  response = await doFetch(current, hopInit);
845
940
  } catch (cause) {
846
941
  if (!opts.wrapNetworkErrors) throw cause;
847
- const msg = cause instanceof Error ? cause.message : String(cause);
848
- throw new HttpError(0, msg, current);
942
+ throw toFetchError(cause, current);
849
943
  }
850
944
  if (validateUrl && response.url && response.url !== current) {
851
945
  try {
@@ -913,8 +1007,7 @@ async function fetchWithValidatedRedirects(url, opts = {}) {
913
1007
  try {
914
1008
  return { response: await doFetch(url, init), url };
915
1009
  } catch (cause) {
916
- const msg = cause instanceof Error ? cause.message : String(cause);
917
- throw new HttpError(0, msg, url);
1010
+ throw toFetchError(cause, url);
918
1011
  }
919
1012
  }
920
1013
  return fetchFollowingRedirects(url, init, {
@@ -1365,7 +1458,7 @@ var AtlasCloudClient = class {
1365
1458
  );
1366
1459
  } catch (cause) {
1367
1460
  if (cause instanceof HttpError) throw cause;
1368
- throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
1461
+ throw toFetchError(cause, url, { provider: "atlascloud" });
1369
1462
  }
1370
1463
  if (!response.ok) throw await httpErrorFromResponse(response, url);
1371
1464
  const json = await response.json();
@@ -1688,8 +1781,7 @@ async function fetchBytes(urlOrDataUri) {
1688
1781
  signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
1689
1782
  });
1690
1783
  } catch (cause) {
1691
- const msg = cause instanceof Error ? cause.message : String(cause);
1692
- throw new HttpError(0, msg, urlOrDataUri);
1784
+ throw toFetchError(cause, urlOrDataUri);
1693
1785
  }
1694
1786
  if (!response.ok) {
1695
1787
  throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
@@ -4231,11 +4323,17 @@ function apiPostJson(endpoint2, payload, apiKey, opts = {}) {
4231
4323
  }
4232
4324
  async function apiDelete(endpoint2, apiKey) {
4233
4325
  const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
4234
- const res = await fetchSameOrigin(url, {
4235
- method: "DELETE",
4236
- headers: authHeaders(apiKey),
4237
- signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS2)
4238
- });
4326
+ let res;
4327
+ try {
4328
+ res = await fetchSameOrigin(url, {
4329
+ method: "DELETE",
4330
+ headers: authHeaders(apiKey),
4331
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS2)
4332
+ });
4333
+ } catch (cause) {
4334
+ if (cause instanceof HttpError) throw cause;
4335
+ throw toFetchError(cause, url, { provider: "elevenlabs" });
4336
+ }
4239
4337
  if (!res.ok) {
4240
4338
  const text = await res.text().catch(() => "");
4241
4339
  throw new HttpError(res.status, text.slice(0, 500), url);
@@ -4268,8 +4366,7 @@ async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
4268
4366
  res = await fetchSameOrigin(url.toString(), init);
4269
4367
  } catch (cause) {
4270
4368
  if (cause instanceof HttpError) throw cause;
4271
- const msg = cause instanceof Error ? cause.message : String(cause);
4272
- throw new HttpError(0, msg, url.toString());
4369
+ throw toFetchError(cause, url.toString(), { provider: "elevenlabs" });
4273
4370
  }
4274
4371
  if (!res.ok) {
4275
4372
  const text = await res.text().catch(() => "");
@@ -4289,8 +4386,7 @@ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
4289
4386
  });
4290
4387
  } catch (cause) {
4291
4388
  if (cause instanceof HttpError) throw cause;
4292
- const msg = cause instanceof Error ? cause.message : String(cause);
4293
- throw new HttpError(0, msg, url);
4389
+ throw toFetchError(cause, url, { provider: "elevenlabs" });
4294
4390
  }
4295
4391
  if (!res.ok) {
4296
4392
  const text = await res.text().catch(() => "");
@@ -4916,9 +5012,9 @@ var FAL_DEFAULTS = {
4916
5012
  };
4917
5013
 
4918
5014
  // src/providers/fal/run-helpers.ts
4919
- init_errors();
4920
5015
  import fs19 from "fs";
4921
5016
  import path22 from "path";
5017
+ init_errors();
4922
5018
  init_redact();
4923
5019
 
4924
5020
  // src/providers/fal/client.ts
@@ -5039,7 +5135,7 @@ function parseInputArg(raw) {
5039
5135
  return parseJsonInput(raw);
5040
5136
  }
5041
5137
  function isTransientError(e) {
5042
- if (e instanceof HttpError) return e.status === 0 || e.status >= 500;
5138
+ if (e instanceof HttpError) return isRetryableServerStatus(e.status);
5043
5139
  return false;
5044
5140
  }
5045
5141
  function sleep(ms, signal) {
@@ -5646,6 +5742,13 @@ async function uploadFetch(url, init, timeoutMs, step) {
5646
5742
  if (redirected || timedOut && step === "upload") {
5647
5743
  throw new ProviderError(message, "gemini", { cause: err2 });
5648
5744
  }
5745
+ if (!isNetworkFetchFailure(err2)) {
5746
+ throw toFetchError(err2, FILES_BASE, {
5747
+ provider: "gemini",
5748
+ prefix: "File upload failed: ",
5749
+ target: step === "start" ? FILES_BASE : "the upload session URL"
5750
+ });
5751
+ }
5649
5752
  throw new HttpError(0, message, FILES_BASE, void 0, { cause: err2 });
5650
5753
  }
5651
5754
  if (!resp.ok) {
@@ -7268,7 +7371,7 @@ var HeygenClient = class {
7268
7371
  return Math.min(error.retryAfterMs, MAX_RETRY_WAIT_MS);
7269
7372
  }
7270
7373
  if (!key) return void 0;
7271
- if (error.status === 0 || error.status >= 500 || code === "request_in_progress") {
7374
+ if (isRetryableServerStatus(error.status) || code === "request_in_progress") {
7272
7375
  return Math.min(1e3 * 2 ** (attempt - 1), MAX_RETRY_WAIT_MS);
7273
7376
  }
7274
7377
  return void 0;
@@ -7430,8 +7533,9 @@ var HeygenClient = class {
7430
7533
  );
7431
7534
  } catch (cause) {
7432
7535
  if (cause instanceof HttpError) throw toHeygenApiError(cause);
7433
- const msg = cause instanceof Error ? cause.message : String(cause);
7434
- const err3 = toHeygenApiError(new HttpError(0, msg, url));
7536
+ const failure = toFetchError(cause, url, { provider: "heygen" });
7537
+ if (!(failure instanceof HttpError)) throw failure;
7538
+ const err3 = toHeygenApiError(failure);
7435
7539
  if (attempt >= this.maxAttempts) throw err3;
7436
7540
  await this.sleep(this.retryDelay(err3, key, attempt) ?? 1e3);
7437
7541
  continue;
@@ -10298,7 +10402,7 @@ async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOU
10298
10402
  });
10299
10403
  } catch (cause) {
10300
10404
  if (cause instanceof HttpError) throw cause;
10301
- throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
10405
+ throw toFetchError(cause, url, { provider: "openai" });
10302
10406
  }
10303
10407
  if (!res.ok) {
10304
10408
  const text = await res.text().catch(() => "");
@@ -10318,7 +10422,7 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
10318
10422
  });
10319
10423
  } catch (cause) {
10320
10424
  if (cause instanceof HttpError) throw cause;
10321
- throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
10425
+ throw toFetchError(cause, url, { provider: "openai" });
10322
10426
  }
10323
10427
  if (!res.ok) {
10324
10428
  const text = await res.text().catch(() => "");
@@ -11424,8 +11528,8 @@ var CREATE_RETRY_CAP_MS = 3e4;
11424
11528
  var REQUEST_TIMEOUT_MS = 6e4;
11425
11529
  var UPLOAD_TIMEOUT_MS = 3e5;
11426
11530
  var ReplicateApiError = class extends ProviderError {
11427
- constructor(message, status, retryAfterMs) {
11428
- super(message, PROVIDER);
11531
+ constructor(message, status, retryAfterMs, options) {
11532
+ super(message, PROVIDER, options);
11429
11533
  this.status = status;
11430
11534
  this.retryAfterMs = retryAfterMs;
11431
11535
  this.name = "ReplicateApiError";
@@ -11485,7 +11589,7 @@ function mapReplicateError(error, subject) {
11485
11589
  } else {
11486
11590
  message = `Replicate request for ${subject} failed (${status})${suffix}`;
11487
11591
  }
11488
- return new ReplicateApiError(message, status, retryAfterMs);
11592
+ return new ReplicateApiError(message, status, retryAfterMs, { cause: error });
11489
11593
  }
11490
11594
  var ORPHAN_HINT = "The prediction may have been created anyway (Replicate has no idempotency key); check https://replicate.com/predictions before retrying to avoid paying twice";
11491
11595
  function withOrphanHint(error) {
@@ -11493,10 +11597,14 @@ function withOrphanHint(error) {
11493
11597
  const { status } = error;
11494
11598
  const mayExist = status === 0 || status >= 200 && status < 300 || status >= 500;
11495
11599
  if (!mayExist) return error;
11496
- return new ReplicateApiError(`${error.message}. ${ORPHAN_HINT}`, status, error.retryAfterMs);
11600
+ return new ReplicateApiError(`${error.message}. ${ORPHAN_HINT}`, status, error.retryAfterMs, {
11601
+ cause: error.cause
11602
+ });
11497
11603
  }
11498
11604
  if (error instanceof ProviderError) {
11499
- return new ProviderError(`${error.message}. ${ORPHAN_HINT}`, error.provider);
11605
+ return new ProviderError(`${error.message}. ${ORPHAN_HINT}`, error.provider, {
11606
+ cause: error.cause
11607
+ });
11500
11608
  }
11501
11609
  return error;
11502
11610
  }
@@ -11650,8 +11758,7 @@ var ReplicateClient = class {
11650
11758
  );
11651
11759
  } catch (cause) {
11652
11760
  if (cause instanceof HttpError) throw mapReplicateError(cause, subject);
11653
- const msg = cause instanceof Error ? cause.message : String(cause);
11654
- throw mapReplicateError(new HttpError(0, msg, url), subject);
11761
+ throw mapReplicateError(toFetchError(cause, url, { provider: PROVIDER }), subject);
11655
11762
  }
11656
11763
  if (!response.ok) {
11657
11764
  if (response.status === 413) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrgoonie/multix",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "AI multimodal CLI — OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, Replicate, HeyGen, ffmpeg, ImageMagick, doc-to-md",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: multix
3
3
  description: AI multimodal CLI — generate and edit images (OpenAI, Codex driver, Gemini Nano Banana, Imagen, MiniMax, OpenRouter, Leonardo, BytePlus Seedream, Cloudflare Workers AI FLUX.1 Schnell, fal.ai FLUX, Replicate FLUX), generate video (Gemini Veo, MiniMax Hailuo, OpenRouter via Veo/Seedance/Kling, Leonardo via Motion/Veo/Kling/Hailuo/Seedance/LTX, BytePlus Seedance, Cloudflare AI Gateway via Replicate, fal.ai queue models, Replicate direct API models incl. P-Video, HeyGen Video Agent and scripted avatar videos), video translation and lipsync (HeyGen), TTS/STT/music (OpenAI, Gemini Flash TTS incl. Gemini 3.8 with delivery-style control, MiniMax, Cloudflare Workers AI MeloTTS, ElevenLabs), 3D models (BytePlus Hyper3D/Hitem3d), analyze/transcribe media, convert documents to Markdown, optimize media via ffmpeg/ImageMagick, and self-update the CLI. Use whenever the user wants to create, edit, or transform images/video/audio/3D/documents from a CLI, mentions any of: OpenAI, Codex image driver, image-to-image, i2i, image edit, watercolor/cyberpunk style transfer, reference image, OpenRouter, Nano Banana, Flux, Seedream, Veo, Hailuo, Seedance, Kling, Cloudflare Workers AI, AI Gateway, fal.ai, Atlas Cloud, Replicate, replicate.com models, Replicate predictions, HeyGen, avatar video, talking head, Video Agent, video translation, dubbing, lipsync, video generation, text-to-video, image-to-video, i2v, ElevenLabs voice cloning, updating/upgrading multix, or asks for batch media optimization.
4
- version: 1.0.3 # x-release-please-version
4
+ version: 1.0.4 # x-release-please-version
5
5
  ---
6
6
 
7
7
  # multix — AI Multimodal CLI