@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 +7 -0
- package/dist/cli.js +140 -33
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
7434
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
"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.
|
|
4
|
+
version: 1.0.4 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|