@mrgoonie/multix 0.15.2 → 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 +7 -0
- package/dist/cli.js +186 -129
- 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
|
+
## [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
|
+
|
|
8
15
|
## [0.15.2](https://github.com/mrgoonie/multix-cli/compare/v0.15.1...v0.15.2) (2026-10-11)
|
|
9
16
|
|
|
10
17
|
|
package/dist/cli.js
CHANGED
|
@@ -541,6 +541,113 @@ init_errors();
|
|
|
541
541
|
|
|
542
542
|
// src/core/fetch-json.ts
|
|
543
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
|
|
544
651
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
545
652
|
var SNIPPET_MAX = 500;
|
|
546
653
|
function parseRetryAfter(value, now = Date.now()) {
|
|
@@ -566,6 +673,10 @@ async function httpErrorFromResponse(response, url) {
|
|
|
566
673
|
parseRetryAfter(response.headers.get("retry-after"))
|
|
567
674
|
);
|
|
568
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
|
+
}
|
|
569
680
|
async function fetchJson(opts) {
|
|
570
681
|
const { url, method = "GET", headers: headers2 = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
571
682
|
const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
|
|
@@ -580,13 +691,11 @@ async function fetchJson(opts) {
|
|
|
580
691
|
if (body !== void 0) {
|
|
581
692
|
init.body = JSON.stringify(body);
|
|
582
693
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
throw new HttpError(0, msg, url);
|
|
589
|
-
}
|
|
694
|
+
const { response } = await fetchFollowingRedirects(url, init, {
|
|
695
|
+
fetch: doFetch,
|
|
696
|
+
crossOrigin: carriesCredentialHeaders(headers2) ? "error" : "drop-headers",
|
|
697
|
+
wrapNetworkErrors: true
|
|
698
|
+
});
|
|
590
699
|
if (!response.ok) {
|
|
591
700
|
throw await httpErrorFromResponse(response, url);
|
|
592
701
|
}
|
|
@@ -934,13 +1043,18 @@ var AtlasCloudClient = class {
|
|
|
934
1043
|
const url = `${this.baseUrl}/model/uploadMedia`;
|
|
935
1044
|
let response;
|
|
936
1045
|
try {
|
|
937
|
-
response = await
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
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
|
+
);
|
|
943
1056
|
} catch (cause) {
|
|
1057
|
+
if (cause instanceof HttpError) throw cause;
|
|
944
1058
|
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
945
1059
|
}
|
|
946
1060
|
if (!response.ok) throw await httpErrorFromResponse(response, url);
|
|
@@ -1133,7 +1247,6 @@ async function httpJson(opts) {
|
|
|
1133
1247
|
}
|
|
1134
1248
|
var MAX_DOWNLOAD_ENV = "MULTIX_MAX_DOWNLOAD_MB";
|
|
1135
1249
|
var DEFAULT_MAX_DOWNLOAD_MB = 2048;
|
|
1136
|
-
var MAX_REDIRECTS = 5;
|
|
1137
1250
|
function formatMb2(bytes) {
|
|
1138
1251
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1139
1252
|
}
|
|
@@ -1163,44 +1276,6 @@ function maxDownloadBytes() {
|
|
|
1163
1276
|
}
|
|
1164
1277
|
return bytes;
|
|
1165
1278
|
}
|
|
1166
|
-
async function fetchWithValidatedRedirects(url, opts = {}) {
|
|
1167
|
-
const doFetch = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
1168
|
-
const { validateUrl, signal } = opts;
|
|
1169
|
-
const maxRedirects = opts.maxRedirects ?? MAX_REDIRECTS;
|
|
1170
|
-
let current = url;
|
|
1171
|
-
let headers2 = opts.headers;
|
|
1172
|
-
for (let hop = 0; ; hop++) {
|
|
1173
|
-
await validateUrl?.(current);
|
|
1174
|
-
let response;
|
|
1175
|
-
try {
|
|
1176
|
-
response = await doFetch(current, {
|
|
1177
|
-
...signal ? { signal } : {},
|
|
1178
|
-
...headers2 ? { headers: headers2 } : {},
|
|
1179
|
-
...validateUrl ? { redirect: "manual" } : {}
|
|
1180
|
-
});
|
|
1181
|
-
} catch (cause) {
|
|
1182
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
1183
|
-
throw new HttpError(0, msg, current);
|
|
1184
|
-
}
|
|
1185
|
-
const location = response.headers.get("location");
|
|
1186
|
-
if (!validateUrl || !location || response.status < 300 || response.status >= 400) {
|
|
1187
|
-
return { response, url: current };
|
|
1188
|
-
}
|
|
1189
|
-
await response.body?.cancel().catch(() => {
|
|
1190
|
-
});
|
|
1191
|
-
if (hop >= maxRedirects) {
|
|
1192
|
-
throw new HttpError(response.status, `too many redirects (over ${maxRedirects})`, current);
|
|
1193
|
-
}
|
|
1194
|
-
let next;
|
|
1195
|
-
try {
|
|
1196
|
-
next = new URL(location, current);
|
|
1197
|
-
} catch {
|
|
1198
|
-
throw new HttpError(response.status, "redirect with a malformed Location header", current);
|
|
1199
|
-
}
|
|
1200
|
-
if (next.origin !== new URL(current).origin) headers2 = void 0;
|
|
1201
|
-
current = next.toString();
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
1279
|
function removeQuietly(file) {
|
|
1205
1280
|
try {
|
|
1206
1281
|
fs3.rmSync(file, { force: true });
|
|
@@ -1262,7 +1337,7 @@ async function downloadToFile(url, opts) {
|
|
|
1262
1337
|
yield chunk;
|
|
1263
1338
|
}
|
|
1264
1339
|
}
|
|
1265
|
-
const part = `${dest}.part`;
|
|
1340
|
+
const part = `${dest}.${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.part`;
|
|
1266
1341
|
let fileStream;
|
|
1267
1342
|
try {
|
|
1268
1343
|
fileStream = fs3.createWriteStream(part);
|
|
@@ -3132,8 +3207,13 @@ init_errors();
|
|
|
3132
3207
|
function cloudflareProviderFailure(status) {
|
|
3133
3208
|
return new ProviderError(`Cloudflare request failed with HTTP ${status}`, "Cloudflare");
|
|
3134
3209
|
}
|
|
3210
|
+
async function cloudflareResponseFailure(response) {
|
|
3211
|
+
await response.body?.cancel().catch(() => {
|
|
3212
|
+
});
|
|
3213
|
+
return cloudflareProviderFailure(response.status);
|
|
3214
|
+
}
|
|
3135
3215
|
async function readCloudflareJson(response) {
|
|
3136
|
-
if (!response.ok) throw
|
|
3216
|
+
if (!response.ok) throw await cloudflareResponseFailure(response);
|
|
3137
3217
|
try {
|
|
3138
3218
|
return await response.json();
|
|
3139
3219
|
} catch {
|
|
@@ -3153,12 +3233,11 @@ function videoHeaders(config, opts) {
|
|
|
3153
3233
|
};
|
|
3154
3234
|
}
|
|
3155
3235
|
function send(opts, url, init) {
|
|
3156
|
-
const f = opts.fetch;
|
|
3157
3236
|
const withTimeout = {
|
|
3158
3237
|
...init,
|
|
3159
3238
|
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
3160
3239
|
};
|
|
3161
|
-
return
|
|
3240
|
+
return fetchSameOrigin(url, withTimeout, opts.fetch);
|
|
3162
3241
|
}
|
|
3163
3242
|
function parseCloudflareVideoPrediction(value) {
|
|
3164
3243
|
if (!value || typeof value !== "object") {
|
|
@@ -3211,7 +3290,7 @@ function gatewayUrl(accountId) {
|
|
|
3211
3290
|
async function runCloudflareJson(model, input) {
|
|
3212
3291
|
const config = requireCloudflareConfig();
|
|
3213
3292
|
const useGateway = Boolean(config.gatewayId);
|
|
3214
|
-
const response = await
|
|
3293
|
+
const response = await fetchSameOrigin(
|
|
3215
3294
|
useGateway ? gatewayUrl(config.accountId) : nativeUrl(config.accountId, model),
|
|
3216
3295
|
{
|
|
3217
3296
|
method: "POST",
|
|
@@ -3228,7 +3307,7 @@ async function runCloudflareJson(model, input) {
|
|
|
3228
3307
|
async function runCloudflareBytes(model, input) {
|
|
3229
3308
|
const config = requireCloudflareConfig();
|
|
3230
3309
|
const useGateway = Boolean(config.gatewayId);
|
|
3231
|
-
const response = await
|
|
3310
|
+
const response = await fetchSameOrigin(
|
|
3232
3311
|
useGateway ? gatewayUrl(config.accountId) : nativeUrl(config.accountId, model),
|
|
3233
3312
|
{
|
|
3234
3313
|
method: "POST",
|
|
@@ -3236,7 +3315,7 @@ async function runCloudflareBytes(model, input) {
|
|
|
3236
3315
|
body: JSON.stringify(useGateway ? { model, input } : input)
|
|
3237
3316
|
}
|
|
3238
3317
|
);
|
|
3239
|
-
if (!response.ok) throw
|
|
3318
|
+
if (!response.ok) throw await cloudflareResponseFailure(response);
|
|
3240
3319
|
const contentType = response.headers.get("content-type") ?? "";
|
|
3241
3320
|
if (!contentType.includes("audio/mpeg")) {
|
|
3242
3321
|
throw new ProviderError("Cloudflare did not return MPEG audio", "Cloudflare");
|
|
@@ -3558,7 +3637,7 @@ function apiPostJson(endpoint2, payload, apiKey, opts = {}) {
|
|
|
3558
3637
|
}
|
|
3559
3638
|
async function apiDelete(endpoint2, apiKey) {
|
|
3560
3639
|
const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
|
|
3561
|
-
const res = await
|
|
3640
|
+
const res = await fetchSameOrigin(url, {
|
|
3562
3641
|
method: "DELETE",
|
|
3563
3642
|
headers: authHeaders(apiKey),
|
|
3564
3643
|
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS2)
|
|
@@ -3592,8 +3671,9 @@ async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
|
|
|
3592
3671
|
}
|
|
3593
3672
|
let res;
|
|
3594
3673
|
try {
|
|
3595
|
-
res = await
|
|
3674
|
+
res = await fetchSameOrigin(url.toString(), init);
|
|
3596
3675
|
} catch (cause) {
|
|
3676
|
+
if (cause instanceof HttpError) throw cause;
|
|
3597
3677
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
3598
3678
|
throw new HttpError(0, msg, url.toString());
|
|
3599
3679
|
}
|
|
@@ -3607,13 +3687,14 @@ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
|
|
|
3607
3687
|
const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
|
|
3608
3688
|
let res;
|
|
3609
3689
|
try {
|
|
3610
|
-
res = await
|
|
3690
|
+
res = await fetchSameOrigin(url, {
|
|
3611
3691
|
method: "POST",
|
|
3612
3692
|
headers: authHeaders(apiKey),
|
|
3613
3693
|
body: form,
|
|
3614
3694
|
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
3615
3695
|
});
|
|
3616
3696
|
} catch (cause) {
|
|
3697
|
+
if (cause instanceof HttpError) throw cause;
|
|
3617
3698
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
3618
3699
|
throw new HttpError(0, msg, url);
|
|
3619
3700
|
}
|
|
@@ -3822,7 +3903,7 @@ Dubbed file: ${dest}`);
|
|
|
3822
3903
|
}
|
|
3823
3904
|
async function downloadDub(url, apiKey, dest) {
|
|
3824
3905
|
fs15.mkdirSync(path18.dirname(dest), { recursive: true });
|
|
3825
|
-
const res = await
|
|
3906
|
+
const { response: res } = await fetchWithValidatedRedirects(url, {
|
|
3826
3907
|
headers: { "xi-api-key": apiKey },
|
|
3827
3908
|
signal: AbortSignal.timeout(6e5)
|
|
3828
3909
|
});
|
|
@@ -3831,7 +3912,6 @@ async function downloadDub(url, apiKey, dest) {
|
|
|
3831
3912
|
}
|
|
3832
3913
|
const arr = new Uint8Array(await res.arrayBuffer());
|
|
3833
3914
|
fs15.writeFileSync(dest, arr);
|
|
3834
|
-
void downloadFile;
|
|
3835
3915
|
}
|
|
3836
3916
|
|
|
3837
3917
|
// src/providers/elevenlabs/commands/isolate.ts
|
|
@@ -5814,13 +5894,14 @@ function checkVeoResult(op) {
|
|
|
5814
5894
|
return op;
|
|
5815
5895
|
}
|
|
5816
5896
|
async function downloadVeoVideo(apiKey, uri, dest) {
|
|
5817
|
-
let
|
|
5897
|
+
let parsed;
|
|
5818
5898
|
try {
|
|
5819
|
-
|
|
5899
|
+
parsed = new URL(uri);
|
|
5820
5900
|
} catch {
|
|
5821
5901
|
throw new ProviderError("Veo returned an invalid video URI", "gemini");
|
|
5822
5902
|
}
|
|
5823
|
-
|
|
5903
|
+
const keyed = parsed.protocol === "https:" && parsed.hostname === API_HOST;
|
|
5904
|
+
await downloadFile(uri, dest, 3e5, keyed ? headers(apiKey) : void 0);
|
|
5824
5905
|
}
|
|
5825
5906
|
async function runVeo(model, request, dest, logger) {
|
|
5826
5907
|
const body = buildVeoRequest(request);
|
|
@@ -6316,10 +6397,8 @@ var HEYGEN_OPERATIONS = {
|
|
|
6316
6397
|
};
|
|
6317
6398
|
|
|
6318
6399
|
// src/providers/heygen/node-helpers.ts
|
|
6319
|
-
import { once } from "events";
|
|
6320
6400
|
import fs29 from "fs";
|
|
6321
6401
|
import path32 from "path";
|
|
6322
|
-
import { pipeline as pipeline2 } from "stream/promises";
|
|
6323
6402
|
init_errors();
|
|
6324
6403
|
init_redact();
|
|
6325
6404
|
|
|
@@ -6666,13 +6745,18 @@ var HeygenClient = class {
|
|
|
6666
6745
|
form.append("file", typed, input.filename);
|
|
6667
6746
|
let response;
|
|
6668
6747
|
try {
|
|
6669
|
-
response = await
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
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
|
+
);
|
|
6675
6758
|
} catch (cause) {
|
|
6759
|
+
if (cause instanceof HttpError) throw toHeygenApiError(cause);
|
|
6676
6760
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
6677
6761
|
const err3 = toHeygenApiError(new HttpError(0, msg, url));
|
|
6678
6762
|
if (attempt >= this.maxAttempts) throw err3;
|
|
@@ -7323,44 +7407,11 @@ function inferExtension(url, fallback) {
|
|
|
7323
7407
|
function safeSlug(value) {
|
|
7324
7408
|
return value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "out";
|
|
7325
7409
|
}
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
for (let hop = 0; ; hop++) {
|
|
7332
|
-
if (!isHttpsUrl(current)) {
|
|
7333
|
-
throw new HttpError(0, "refusing to follow a redirect to a non-https URL", current);
|
|
7334
|
-
}
|
|
7335
|
-
let response;
|
|
7336
|
-
try {
|
|
7337
|
-
response = await globalThis.fetch(current, { signal, redirect: "manual" });
|
|
7338
|
-
} catch (cause) {
|
|
7339
|
-
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), current);
|
|
7340
|
-
}
|
|
7341
|
-
if (response.url && !isHttpsUrl(response.url)) {
|
|
7342
|
-
await response.body?.cancel();
|
|
7343
|
-
throw new HttpError(0, "redirected to a non-https URL", response.url);
|
|
7344
|
-
}
|
|
7345
|
-
const location = response.headers.get("location");
|
|
7346
|
-
if (isRedirect(response.status) && location) {
|
|
7347
|
-
await response.body?.cancel();
|
|
7348
|
-
if (hop >= MAX_REDIRECTS2) throw new HttpError(0, "too many redirects", url);
|
|
7349
|
-
current = new URL(location, current).href;
|
|
7350
|
-
continue;
|
|
7351
|
-
}
|
|
7352
|
-
if (!response.ok) throw new HttpError(response.status, "(download failed)", current);
|
|
7353
|
-
if (!response.body) throw new HttpError(0, "Response body is null", current);
|
|
7354
|
-
fs29.mkdirSync(path32.dirname(dest), { recursive: true });
|
|
7355
|
-
const fileStream = fs29.createWriteStream(dest);
|
|
7356
|
-
try {
|
|
7357
|
-
await pipeline2(response.body, fileStream);
|
|
7358
|
-
} catch (err2) {
|
|
7359
|
-
if (!fileStream.closed) await once(fileStream, "close");
|
|
7360
|
-
throw err2;
|
|
7361
|
-
}
|
|
7362
|
-
return;
|
|
7363
|
-
}
|
|
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 });
|
|
7364
7415
|
}
|
|
7365
7416
|
async function downloadOutput(url, dest, resumeHint) {
|
|
7366
7417
|
if (!url) {
|
|
@@ -7371,12 +7422,9 @@ async function downloadOutput(url, dest, resumeHint) {
|
|
|
7371
7422
|
`Refusing to download HeyGen output from a non-https URL (${redactUrl(url)})`
|
|
7372
7423
|
);
|
|
7373
7424
|
}
|
|
7374
|
-
const tmp = `${dest}.${process.pid}-${Date.now()}.part`;
|
|
7375
7425
|
try {
|
|
7376
|
-
await
|
|
7377
|
-
fs29.renameSync(tmp, dest);
|
|
7426
|
+
await downloadHttps(url, dest, 6e5);
|
|
7378
7427
|
} catch (e) {
|
|
7379
|
-
fs29.rmSync(tmp, { force: true });
|
|
7380
7428
|
const status = e instanceof HttpError ? e.status : 0;
|
|
7381
7429
|
const expired = status === 403 || status === 404 || status === 410;
|
|
7382
7430
|
const reason = e instanceof Error ? e.message : String(e);
|
|
@@ -7438,7 +7486,7 @@ function captionPathFor(dest, capExt) {
|
|
|
7438
7486
|
const capDest = `${stem}.${capExt}`;
|
|
7439
7487
|
return capDest.toLowerCase() === dest.toLowerCase() ? `${stem}.captions.${capExt}` : capDest;
|
|
7440
7488
|
}
|
|
7441
|
-
var downloadThumbHttpsOnly = (url, dest) =>
|
|
7489
|
+
var downloadThumbHttpsOnly = (url, dest) => downloadHttps(url, dest, 12e4);
|
|
7442
7490
|
function summarizeState(state) {
|
|
7443
7491
|
const out = {
|
|
7444
7492
|
kind: state.kind,
|
|
@@ -9585,13 +9633,14 @@ async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOU
|
|
|
9585
9633
|
const url = endpoint(pathname);
|
|
9586
9634
|
let res;
|
|
9587
9635
|
try {
|
|
9588
|
-
res = await
|
|
9636
|
+
res = await fetchSameOrigin(url, {
|
|
9589
9637
|
method: "POST",
|
|
9590
9638
|
headers: authHeaders4(apiKey),
|
|
9591
9639
|
body: form,
|
|
9592
9640
|
signal: AbortSignal.timeout(timeoutMs)
|
|
9593
9641
|
});
|
|
9594
9642
|
} catch (cause) {
|
|
9643
|
+
if (cause instanceof HttpError) throw cause;
|
|
9595
9644
|
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
9596
9645
|
}
|
|
9597
9646
|
if (!res.ok) {
|
|
@@ -9604,13 +9653,14 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
9604
9653
|
const url = endpoint(pathname);
|
|
9605
9654
|
let res;
|
|
9606
9655
|
try {
|
|
9607
|
-
res = await
|
|
9656
|
+
res = await fetchSameOrigin(url, {
|
|
9608
9657
|
method: "POST",
|
|
9609
9658
|
headers: authHeaders4(apiKey, { "Content-Type": "application/json" }),
|
|
9610
9659
|
body: JSON.stringify(body),
|
|
9611
9660
|
signal: AbortSignal.timeout(timeoutMs)
|
|
9612
9661
|
});
|
|
9613
9662
|
} catch (cause) {
|
|
9663
|
+
if (cause instanceof HttpError) throw cause;
|
|
9614
9664
|
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
9615
9665
|
}
|
|
9616
9666
|
if (!res.ok) {
|
|
@@ -10450,7 +10500,9 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
10450
10500
|
const apiKey = requireOpenRouterKey();
|
|
10451
10501
|
const url = videoContentUrl(jobId);
|
|
10452
10502
|
const dest = path50.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
10453
|
-
const r = await
|
|
10503
|
+
const { response: r } = await fetchWithValidatedRedirects(url, {
|
|
10504
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
10505
|
+
});
|
|
10454
10506
|
if (!r.ok) {
|
|
10455
10507
|
logger.error(`Download HTTP ${r.status}`);
|
|
10456
10508
|
process.exit(1);
|
|
@@ -10461,8 +10513,8 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
10461
10513
|
}
|
|
10462
10514
|
fs43.mkdirSync(path50.dirname(dest), { recursive: true });
|
|
10463
10515
|
const ws = fs43.createWriteStream(dest);
|
|
10464
|
-
const { pipeline:
|
|
10465
|
-
await
|
|
10516
|
+
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
10517
|
+
await pipeline2(r.body, ws);
|
|
10466
10518
|
logger.success(`Saved ${dest}`);
|
|
10467
10519
|
if (opts.output) {
|
|
10468
10520
|
fs43.mkdirSync(path50.dirname(path50.resolve(opts.output)), { recursive: true });
|
|
@@ -10924,13 +10976,18 @@ var ReplicateClient = class {
|
|
|
10924
10976
|
const subject = `file upload ${opts.filename}`;
|
|
10925
10977
|
let response;
|
|
10926
10978
|
try {
|
|
10927
|
-
response = await
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
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
|
+
);
|
|
10933
10989
|
} catch (cause) {
|
|
10990
|
+
if (cause instanceof HttpError) throw mapReplicateError(cause, subject);
|
|
10934
10991
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
10935
10992
|
throw mapReplicateError(new HttpError(0, msg, url), subject);
|
|
10936
10993
|
}
|
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
|