@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 +14 -0
- package/dist/cli.js +191 -132
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
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
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5897
|
+
let parsed;
|
|
5817
5898
|
try {
|
|
5818
|
-
|
|
5899
|
+
parsed = new URL(uri);
|
|
5819
5900
|
} catch {
|
|
5820
5901
|
throw new ProviderError("Veo returned an invalid video URI", "gemini");
|
|
5821
5902
|
}
|
|
5822
|
-
|
|
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
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
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
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
10464
|
-
await
|
|
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
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
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.
|
|
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.
|
|
4
|
+
version: 0.15.3 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|