@mrgoonie/multix 1.0.3 → 1.0.5
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/README.md +3 -2
- package/dist/cli.js +244 -44
- package/package.json +1 -1
- package/skill/SKILL.md +3 -3
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
|
+
## [1.0.5](https://github.com/mrgoonie/multix-cli/compare/v1.0.4...v1.0.5) (2026-10-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
* **core:** only load multix settings from a project .env ([#116](https://github.com/mrgoonie/multix-cli/issues/116)) ([896c0c0](https://github.com/mrgoonie/multix-cli/commit/896c0c00caf46ac46593f67f9a75508a4ac404cd))
|
|
14
|
+
|
|
15
|
+
## [1.0.4](https://github.com/mrgoonie/multix-cli/compare/v1.0.3...v1.0.4) (2026-10-11)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
* **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))
|
|
21
|
+
|
|
8
22
|
## [1.0.3](https://github.com/mrgoonie/multix-cli/compare/v1.0.2...v1.0.3) (2026-10-11)
|
|
9
23
|
|
|
10
24
|
|
package/README.md
CHANGED
|
@@ -115,10 +115,11 @@ Set at least one provider key. Add to `.env` in your project root or `~/.multix/
|
|
|
115
115
|
|
|
116
116
|
Set extra keys as `GEMINI_API_KEY_2`, `GEMINI_API_KEY_3`, … (any numeric suffix; numbered keys alone are enough). `multix` tries `GEMINI_API_KEY` first, then the numbered keys in ascending order, skipping duplicates. When a key returns HTTP 429 (or 400/403 with `RESOURCE_EXHAUSTED`), it cools down for the server's `Retry-After`/`retryDelay` (default 60s) and the same request is retried with the next ready key; other errors are not rotated. A Files API upload is repeated on the new key if the following generate call is rate-limited. Veo rotates on submit only, so a rate limit while polling or downloading never starts a second billed job. `doc convert` stops its own retries on a rate limit. Cooldowns last for the current process only, and logs name the env var, never the key. `multix check` lists the keys in rotation and pings each one. Quotas are per Google Cloud project: keys from the same project share one quota, so rotation only helps across projects.
|
|
117
117
|
|
|
118
|
-
Priority: `process.env` > `cwd/.env` > `~/.multix/.env`. Set `MULTIX_LEGACY_CLAUDE_ENV=1` to also read legacy AgentKit files (`.claude/skills/{ai-multimodal,ak-ai-multimodal}/.env`, `.claude/skills/.env`, `.claude/.env`, project first, then home) with the lowest priority (credential names only, never endpoint/proxy settings); `multix check -v` lists the env files that were loaded.
|
|
118
|
+
Priority: `process.env` > `cwd/.env` > `~/.multix/.env`. Set `MULTIX_LEGACY_CLAUDE_ENV=1` (shell or `~/.multix/.env`) to also read legacy AgentKit files (`.claude/skills/{ai-multimodal,ak-ai-multimodal}/.env`, `.claude/skills/.env`, `.claude/.env`, project first, then home) with the lowest priority (credential names only, never endpoint/proxy settings); `multix check -v` lists the env files that were loaded.
|
|
119
119
|
|
|
120
|
-
|
|
120
|
+
A project `.env` may only set variables `multix` reads: provider keys, the provider settings in `.env.example` (`*_BASE_URL`, model overrides, Cloudflare IDs), the documented `MULTIX_*` settings (`MULTIX_OUTPUT_DIR`, so a project can choose where outputs go, `MULTIX_IMAGE_FORMAT`, `MULTIX_MAX_DOWNLOAD_MB`, `MULTIX_UPLOAD_MIN_KBPS`) and `NO_COLOR`. The opt-ins `MULTIX_ALLOW_PROJECT_BASE_URL` and `MULTIX_LEGACY_CLAUDE_ENV` are refused there; set them in the shell or `~/.multix/.env`. Other names, such as `NODE_OPTIONS`, `npm_config_registry`, `LD_PRELOAD` or `HTTPS_PROXY`, are not loaded, so they never reach `multix` or the tools it starts (`npm` in `multix update`, ffmpeg, ImageMagick, cwebp); one warning on stderr lists the ignored names, never their values. The shell and `~/.multix/.env` are not filtered.
|
|
121
121
|
|
|
122
|
+
`*_BASE_URL`, `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_AI_GATEWAY_ID` decide where your API key is sent. Values from the shell or `~/.multix/.env` are always used. A value from the project `.env` (or a legacy `.claude` file under it) is used only when every key the provider sends with it comes from that same project file (the repository's own key). That protects your key, not your data: a project `.env` that ships its own key and base URL routes that project's requests, including your prompts and any files you upload, to that host, and its key replaces yours from `~/.multix/.env`, so review a cloned repository's `.env` before running `multix` in it. If a key comes from anywhere else, the project value is ignored: the `~/.multix/.env` value is used with a warning (variable, file and key source named, never the value), or, if there is none, the command stops with an error instead of falling back to the vendor default. `multix check` shows whether each such setting is in effect or ignored. To use a project value for one command, run `MULTIX_ALLOW_PROJECT_BASE_URL=1 multix ...`; do not put the opt-in in `~/.multix/.env`, which disables the check for every directory. Non-default base URLs must be `https://` (plain `http://` only for `localhost`, `127.0.0.1`, `[::1]`), and `multix` warns once per variable on stderr with the provider and hostname only.
|
|
122
123
|
## Commands
|
|
123
124
|
|
|
124
125
|
### `multix check`
|
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
|
};
|
|
@@ -402,11 +402,15 @@ function parseFile(file) {
|
|
|
402
402
|
return void 0;
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
|
-
function loadFile(file, source, accept = () => true) {
|
|
405
|
+
function loadFile(file, source, accept = () => true, onRefused) {
|
|
406
406
|
const parsed = parseFile(file);
|
|
407
407
|
if (!parsed) return void 0;
|
|
408
408
|
for (const [name, value] of Object.entries(parsed)) {
|
|
409
|
-
if (!accept(name)
|
|
409
|
+
if (!accept(name)) {
|
|
410
|
+
onRefused?.(name);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (process.env[name] !== void 0) continue;
|
|
410
414
|
process.env[name] = value;
|
|
411
415
|
fileOrigins.set(envNameKey(name), { source, file, value });
|
|
412
416
|
}
|
|
@@ -421,7 +425,13 @@ function byNameKey(values) {
|
|
|
421
425
|
}
|
|
422
426
|
return out;
|
|
423
427
|
}
|
|
424
|
-
|
|
428
|
+
var MAX_LISTED_NAMES = 10;
|
|
429
|
+
function listNames(names) {
|
|
430
|
+
const shown = names.slice(0, MAX_LISTED_NAMES).join(", ");
|
|
431
|
+
const rest = names.length - MAX_LISTED_NAMES;
|
|
432
|
+
return rest > 0 ? `${shown} and ${rest} more` : shown;
|
|
433
|
+
}
|
|
434
|
+
function loadEnv({ isProjectEnvName: isProjectEnvName2 }) {
|
|
425
435
|
if (loaded) return;
|
|
426
436
|
loaded = true;
|
|
427
437
|
loadedFiles = [];
|
|
@@ -429,12 +439,22 @@ function loadEnv() {
|
|
|
429
439
|
userFileValues = {};
|
|
430
440
|
const cwd = process.cwd();
|
|
431
441
|
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os.homedir();
|
|
432
|
-
|
|
442
|
+
const projectFile = path.resolve(cwd, ".env");
|
|
443
|
+
const refused = [];
|
|
444
|
+
loadFile(projectFile, "project", isProjectEnvName2, (name) => refused.push(name));
|
|
445
|
+
if (refused.length > 0) {
|
|
446
|
+
warnOnce(
|
|
447
|
+
"project-env-refused",
|
|
448
|
+
`Ignoring ${refused.length} variable(s) in project env file ${projectFile}: ${listNames(refused)}. A project .env may only set provider keys and documented multix settings; set anything else in your shell.`
|
|
449
|
+
);
|
|
450
|
+
}
|
|
433
451
|
if (home) userFileValues = byNameKey(loadFile(path.join(home, ".multix", ".env"), "user"));
|
|
434
452
|
if (legacyClaudeEnvEnabled()) {
|
|
435
453
|
const projectLegacy = new Set(legacyFilesUnder(cwd));
|
|
454
|
+
const projectLegacyName = (name) => isLegacyAllowedName(name) && isProjectEnvName2(name);
|
|
436
455
|
for (const file of legacyClaudeEnvFiles(cwd, home)) {
|
|
437
|
-
|
|
456
|
+
if (projectLegacy.has(file)) loadFile(file, "project", projectLegacyName);
|
|
457
|
+
else loadFile(file, "legacy", isLegacyAllowedName);
|
|
438
458
|
}
|
|
439
459
|
}
|
|
440
460
|
}
|
|
@@ -768,7 +788,11 @@ var PollTimeoutError = class extends MultixError {
|
|
|
768
788
|
function isTransientHttpError(error) {
|
|
769
789
|
if (!(error instanceof HttpError)) return false;
|
|
770
790
|
const { status } = error;
|
|
771
|
-
return status ===
|
|
791
|
+
return status === 408 || status === 429 || isRetryableServerStatus(status);
|
|
792
|
+
}
|
|
793
|
+
function isRetryableServerStatus(status) {
|
|
794
|
+
if (status === 501 || status === 505) return false;
|
|
795
|
+
return status === 0 || status >= 500;
|
|
772
796
|
}
|
|
773
797
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
774
798
|
async function pollUntil(opts) {
|
|
@@ -817,6 +841,97 @@ async function pollUntil(opts) {
|
|
|
817
841
|
// src/providers/atlascloud/client.ts
|
|
818
842
|
init_errors();
|
|
819
843
|
|
|
844
|
+
// src/core/fetch-error.ts
|
|
845
|
+
init_errors();
|
|
846
|
+
init_redact();
|
|
847
|
+
var MAX_CAUSE_DEPTH = 4;
|
|
848
|
+
var TLS_CODES = /* @__PURE__ */ new Set([
|
|
849
|
+
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
850
|
+
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
851
|
+
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
852
|
+
"ERR_TLS_CERT_ALTNAME_INVALID"
|
|
853
|
+
]);
|
|
854
|
+
function kindOfCode(code) {
|
|
855
|
+
if (typeof code !== "string") return void 0;
|
|
856
|
+
if (code === "ERR_INVALID_URL") return "url";
|
|
857
|
+
if (code === "ERR_INVALID_CHAR" || code === "ERR_INVALID_HTTP_TOKEN") return "header";
|
|
858
|
+
if (TLS_CODES.has(code) || code.startsWith("CERT_") || code.startsWith("UNABLE_TO_")) {
|
|
859
|
+
return "tls";
|
|
860
|
+
}
|
|
861
|
+
if (code === "UND_ERR_INVALID_ARG" || code === "UND_ERR_NOT_SUPPORTED") return "request";
|
|
862
|
+
return void 0;
|
|
863
|
+
}
|
|
864
|
+
function kindOfMessage(message) {
|
|
865
|
+
if (typeof message !== "string") return void 0;
|
|
866
|
+
if (/Invalid URL|Failed to parse URL/i.test(message)) return "url";
|
|
867
|
+
if (/ByteString|is not a legal HTTP header value|invalid header value|invalid header name/i.test(
|
|
868
|
+
message
|
|
869
|
+
)) {
|
|
870
|
+
return "header";
|
|
871
|
+
}
|
|
872
|
+
if (/duplex option is required|cannot have body|unexpected redirect/i.test(message)) {
|
|
873
|
+
return "request";
|
|
874
|
+
}
|
|
875
|
+
return void 0;
|
|
876
|
+
}
|
|
877
|
+
function causeChain(err2) {
|
|
878
|
+
const out = [];
|
|
879
|
+
let current = err2;
|
|
880
|
+
for (let depth = 0; depth <= MAX_CAUSE_DEPTH && current && typeof current === "object"; depth++) {
|
|
881
|
+
const e = current;
|
|
882
|
+
out.push(e);
|
|
883
|
+
if (Array.isArray(e.errors)) {
|
|
884
|
+
for (const member of e.errors) if (member && typeof member === "object") out.push(member);
|
|
885
|
+
}
|
|
886
|
+
current = e.cause;
|
|
887
|
+
}
|
|
888
|
+
return out;
|
|
889
|
+
}
|
|
890
|
+
function deterministicKind(err2) {
|
|
891
|
+
for (const e of causeChain(err2)) {
|
|
892
|
+
const kind = kindOfCode(e.code) ?? kindOfMessage(e.message);
|
|
893
|
+
if (kind) return kind;
|
|
894
|
+
}
|
|
895
|
+
return void 0;
|
|
896
|
+
}
|
|
897
|
+
function isNetworkFetchFailure(err2) {
|
|
898
|
+
return deterministicKind(err2) === void 0;
|
|
899
|
+
}
|
|
900
|
+
function scrub(message) {
|
|
901
|
+
return message.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/gi, (u) => redactUrl(u)).replace(/[?#](?!\[redacted\])[^\s"'<>]*=[^\s"'<>]*/g, "?[redacted]");
|
|
902
|
+
}
|
|
903
|
+
function messageOf(e) {
|
|
904
|
+
return typeof e?.message === "string" ? scrub(e.message) : "";
|
|
905
|
+
}
|
|
906
|
+
function detailOf(err2) {
|
|
907
|
+
const chain = causeChain(err2);
|
|
908
|
+
const parts = [messageOf(chain[0]), messageOf(chain[1])].filter(Boolean);
|
|
909
|
+
return parts.length > 0 ? parts.join(": ") : scrub(String(err2));
|
|
910
|
+
}
|
|
911
|
+
function toFetchError(err2, url, opts = {}) {
|
|
912
|
+
const options = { cause: err2 };
|
|
913
|
+
const prefix = opts.prefix ?? "";
|
|
914
|
+
const target = opts.target ?? redactUrl(url);
|
|
915
|
+
switch (deterministicKind(err2)) {
|
|
916
|
+
case "url":
|
|
917
|
+
return new ConfigError(`${prefix}Invalid request URL: ${target}`, options);
|
|
918
|
+
case "header":
|
|
919
|
+
return new ConfigError(
|
|
920
|
+
`${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.`,
|
|
921
|
+
options
|
|
922
|
+
);
|
|
923
|
+
case "tls":
|
|
924
|
+
case "request":
|
|
925
|
+
return new ProviderError(
|
|
926
|
+
`${prefix}Request to ${target} failed: ${detailOf(err2)}`,
|
|
927
|
+
opts.provider ?? "http",
|
|
928
|
+
options
|
|
929
|
+
);
|
|
930
|
+
default:
|
|
931
|
+
return new HttpError(0, `${prefix}${detailOf(err2)}`, url, void 0, options);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
820
935
|
// src/core/fetch-json.ts
|
|
821
936
|
init_errors();
|
|
822
937
|
|
|
@@ -844,8 +959,7 @@ async function fetchFollowingRedirects(url, init = {}, opts = {}) {
|
|
|
844
959
|
response = await doFetch(current, hopInit);
|
|
845
960
|
} catch (cause) {
|
|
846
961
|
if (!opts.wrapNetworkErrors) throw cause;
|
|
847
|
-
|
|
848
|
-
throw new HttpError(0, msg, current);
|
|
962
|
+
throw toFetchError(cause, current);
|
|
849
963
|
}
|
|
850
964
|
if (validateUrl && response.url && response.url !== current) {
|
|
851
965
|
try {
|
|
@@ -913,8 +1027,7 @@ async function fetchWithValidatedRedirects(url, opts = {}) {
|
|
|
913
1027
|
try {
|
|
914
1028
|
return { response: await doFetch(url, init), url };
|
|
915
1029
|
} catch (cause) {
|
|
916
|
-
|
|
917
|
-
throw new HttpError(0, msg, url);
|
|
1030
|
+
throw toFetchError(cause, url);
|
|
918
1031
|
}
|
|
919
1032
|
}
|
|
920
1033
|
return fetchFollowingRedirects(url, init, {
|
|
@@ -1365,7 +1478,7 @@ var AtlasCloudClient = class {
|
|
|
1365
1478
|
);
|
|
1366
1479
|
} catch (cause) {
|
|
1367
1480
|
if (cause instanceof HttpError) throw cause;
|
|
1368
|
-
throw
|
|
1481
|
+
throw toFetchError(cause, url, { provider: "atlascloud" });
|
|
1369
1482
|
}
|
|
1370
1483
|
if (!response.ok) throw await httpErrorFromResponse(response, url);
|
|
1371
1484
|
const json = await response.json();
|
|
@@ -1455,7 +1568,7 @@ function uploadMimeTypeFor(filename) {
|
|
|
1455
1568
|
|
|
1456
1569
|
// src/providers/atlascloud/commands/env.ts
|
|
1457
1570
|
var ATLAS_KEY_URL = "https://www.atlascloud.ai/console/api-keys";
|
|
1458
|
-
var
|
|
1571
|
+
var ATLAS_MODEL_ENV = {
|
|
1459
1572
|
image: "ATLASCLOUD_IMAGE_MODEL",
|
|
1460
1573
|
imageEdit: "ATLASCLOUD_IMAGE_EDIT_MODEL",
|
|
1461
1574
|
video: "ATLASCLOUD_VIDEO_MODEL",
|
|
@@ -1466,7 +1579,7 @@ var MODEL_ENV = {
|
|
|
1466
1579
|
imageTo3d: "ATLASCLOUD_IMAGE_TO_3D_MODEL"
|
|
1467
1580
|
};
|
|
1468
1581
|
function atlasDefaultModel(key) {
|
|
1469
|
-
return resolveKey(
|
|
1582
|
+
return resolveKey(ATLAS_MODEL_ENV[key])?.trim() || ATLAS_DEFAULT_MODELS[key];
|
|
1470
1583
|
}
|
|
1471
1584
|
function requireAtlasKey() {
|
|
1472
1585
|
const apiKey = resolveRoutedKey(ATLAS_ROUTING);
|
|
@@ -1688,8 +1801,7 @@ async function fetchBytes(urlOrDataUri) {
|
|
|
1688
1801
|
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
1689
1802
|
});
|
|
1690
1803
|
} catch (cause) {
|
|
1691
|
-
|
|
1692
|
-
throw new HttpError(0, msg, urlOrDataUri);
|
|
1804
|
+
throw toFetchError(cause, urlOrDataUri);
|
|
1693
1805
|
}
|
|
1694
1806
|
if (!response.ok) {
|
|
1695
1807
|
throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
|
|
@@ -4231,11 +4343,17 @@ function apiPostJson(endpoint2, payload, apiKey, opts = {}) {
|
|
|
4231
4343
|
}
|
|
4232
4344
|
async function apiDelete(endpoint2, apiKey) {
|
|
4233
4345
|
const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4346
|
+
let res;
|
|
4347
|
+
try {
|
|
4348
|
+
res = await fetchSameOrigin(url, {
|
|
4349
|
+
method: "DELETE",
|
|
4350
|
+
headers: authHeaders(apiKey),
|
|
4351
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS2)
|
|
4352
|
+
});
|
|
4353
|
+
} catch (cause) {
|
|
4354
|
+
if (cause instanceof HttpError) throw cause;
|
|
4355
|
+
throw toFetchError(cause, url, { provider: "elevenlabs" });
|
|
4356
|
+
}
|
|
4239
4357
|
if (!res.ok) {
|
|
4240
4358
|
const text = await res.text().catch(() => "");
|
|
4241
4359
|
throw new HttpError(res.status, text.slice(0, 500), url);
|
|
@@ -4268,8 +4386,7 @@ async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
|
|
|
4268
4386
|
res = await fetchSameOrigin(url.toString(), init);
|
|
4269
4387
|
} catch (cause) {
|
|
4270
4388
|
if (cause instanceof HttpError) throw cause;
|
|
4271
|
-
|
|
4272
|
-
throw new HttpError(0, msg, url.toString());
|
|
4389
|
+
throw toFetchError(cause, url.toString(), { provider: "elevenlabs" });
|
|
4273
4390
|
}
|
|
4274
4391
|
if (!res.ok) {
|
|
4275
4392
|
const text = await res.text().catch(() => "");
|
|
@@ -4289,8 +4406,7 @@ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
|
|
|
4289
4406
|
});
|
|
4290
4407
|
} catch (cause) {
|
|
4291
4408
|
if (cause instanceof HttpError) throw cause;
|
|
4292
|
-
|
|
4293
|
-
throw new HttpError(0, msg, url);
|
|
4409
|
+
throw toFetchError(cause, url, { provider: "elevenlabs" });
|
|
4294
4410
|
}
|
|
4295
4411
|
if (!res.ok) {
|
|
4296
4412
|
const text = await res.text().catch(() => "");
|
|
@@ -4916,9 +5032,9 @@ var FAL_DEFAULTS = {
|
|
|
4916
5032
|
};
|
|
4917
5033
|
|
|
4918
5034
|
// src/providers/fal/run-helpers.ts
|
|
4919
|
-
init_errors();
|
|
4920
5035
|
import fs19 from "fs";
|
|
4921
5036
|
import path22 from "path";
|
|
5037
|
+
init_errors();
|
|
4922
5038
|
init_redact();
|
|
4923
5039
|
|
|
4924
5040
|
// src/providers/fal/client.ts
|
|
@@ -5039,7 +5155,7 @@ function parseInputArg(raw) {
|
|
|
5039
5155
|
return parseJsonInput(raw);
|
|
5040
5156
|
}
|
|
5041
5157
|
function isTransientError(e) {
|
|
5042
|
-
if (e instanceof HttpError) return e.status
|
|
5158
|
+
if (e instanceof HttpError) return isRetryableServerStatus(e.status);
|
|
5043
5159
|
return false;
|
|
5044
5160
|
}
|
|
5045
5161
|
function sleep(ms, signal) {
|
|
@@ -5646,6 +5762,13 @@ async function uploadFetch(url, init, timeoutMs, step) {
|
|
|
5646
5762
|
if (redirected || timedOut && step === "upload") {
|
|
5647
5763
|
throw new ProviderError(message, "gemini", { cause: err2 });
|
|
5648
5764
|
}
|
|
5765
|
+
if (!isNetworkFetchFailure(err2)) {
|
|
5766
|
+
throw toFetchError(err2, FILES_BASE, {
|
|
5767
|
+
provider: "gemini",
|
|
5768
|
+
prefix: "File upload failed: ",
|
|
5769
|
+
target: step === "start" ? FILES_BASE : "the upload session URL"
|
|
5770
|
+
});
|
|
5771
|
+
}
|
|
5649
5772
|
throw new HttpError(0, message, FILES_BASE, void 0, { cause: err2 });
|
|
5650
5773
|
}
|
|
5651
5774
|
if (!resp.ok) {
|
|
@@ -7268,7 +7391,7 @@ var HeygenClient = class {
|
|
|
7268
7391
|
return Math.min(error.retryAfterMs, MAX_RETRY_WAIT_MS);
|
|
7269
7392
|
}
|
|
7270
7393
|
if (!key) return void 0;
|
|
7271
|
-
if (error.status
|
|
7394
|
+
if (isRetryableServerStatus(error.status) || code === "request_in_progress") {
|
|
7272
7395
|
return Math.min(1e3 * 2 ** (attempt - 1), MAX_RETRY_WAIT_MS);
|
|
7273
7396
|
}
|
|
7274
7397
|
return void 0;
|
|
@@ -7430,8 +7553,9 @@ var HeygenClient = class {
|
|
|
7430
7553
|
);
|
|
7431
7554
|
} catch (cause) {
|
|
7432
7555
|
if (cause instanceof HttpError) throw toHeygenApiError(cause);
|
|
7433
|
-
const
|
|
7434
|
-
|
|
7556
|
+
const failure = toFetchError(cause, url, { provider: "heygen" });
|
|
7557
|
+
if (!(failure instanceof HttpError)) throw failure;
|
|
7558
|
+
const err3 = toHeygenApiError(failure);
|
|
7435
7559
|
if (attempt >= this.maxAttempts) throw err3;
|
|
7436
7560
|
await this.sleep(this.retryDelay(err3, key, attempt) ?? 1e3);
|
|
7437
7561
|
continue;
|
|
@@ -10298,7 +10422,7 @@ async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOU
|
|
|
10298
10422
|
});
|
|
10299
10423
|
} catch (cause) {
|
|
10300
10424
|
if (cause instanceof HttpError) throw cause;
|
|
10301
|
-
throw
|
|
10425
|
+
throw toFetchError(cause, url, { provider: "openai" });
|
|
10302
10426
|
}
|
|
10303
10427
|
if (!res.ok) {
|
|
10304
10428
|
const text = await res.text().catch(() => "");
|
|
@@ -10318,7 +10442,7 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
10318
10442
|
});
|
|
10319
10443
|
} catch (cause) {
|
|
10320
10444
|
if (cause instanceof HttpError) throw cause;
|
|
10321
|
-
throw
|
|
10445
|
+
throw toFetchError(cause, url, { provider: "openai" });
|
|
10322
10446
|
}
|
|
10323
10447
|
if (!res.ok) {
|
|
10324
10448
|
const text = await res.text().catch(() => "");
|
|
@@ -11424,8 +11548,8 @@ var CREATE_RETRY_CAP_MS = 3e4;
|
|
|
11424
11548
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
11425
11549
|
var UPLOAD_TIMEOUT_MS = 3e5;
|
|
11426
11550
|
var ReplicateApiError = class extends ProviderError {
|
|
11427
|
-
constructor(message, status, retryAfterMs) {
|
|
11428
|
-
super(message, PROVIDER);
|
|
11551
|
+
constructor(message, status, retryAfterMs, options) {
|
|
11552
|
+
super(message, PROVIDER, options);
|
|
11429
11553
|
this.status = status;
|
|
11430
11554
|
this.retryAfterMs = retryAfterMs;
|
|
11431
11555
|
this.name = "ReplicateApiError";
|
|
@@ -11485,7 +11609,7 @@ function mapReplicateError(error, subject) {
|
|
|
11485
11609
|
} else {
|
|
11486
11610
|
message = `Replicate request for ${subject} failed (${status})${suffix}`;
|
|
11487
11611
|
}
|
|
11488
|
-
return new ReplicateApiError(message, status, retryAfterMs);
|
|
11612
|
+
return new ReplicateApiError(message, status, retryAfterMs, { cause: error });
|
|
11489
11613
|
}
|
|
11490
11614
|
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
11615
|
function withOrphanHint(error) {
|
|
@@ -11493,10 +11617,14 @@ function withOrphanHint(error) {
|
|
|
11493
11617
|
const { status } = error;
|
|
11494
11618
|
const mayExist = status === 0 || status >= 200 && status < 300 || status >= 500;
|
|
11495
11619
|
if (!mayExist) return error;
|
|
11496
|
-
return new ReplicateApiError(`${error.message}. ${ORPHAN_HINT}`, status, error.retryAfterMs
|
|
11620
|
+
return new ReplicateApiError(`${error.message}. ${ORPHAN_HINT}`, status, error.retryAfterMs, {
|
|
11621
|
+
cause: error.cause
|
|
11622
|
+
});
|
|
11497
11623
|
}
|
|
11498
11624
|
if (error instanceof ProviderError) {
|
|
11499
|
-
return new ProviderError(`${error.message}. ${ORPHAN_HINT}`, error.provider
|
|
11625
|
+
return new ProviderError(`${error.message}. ${ORPHAN_HINT}`, error.provider, {
|
|
11626
|
+
cause: error.cause
|
|
11627
|
+
});
|
|
11500
11628
|
}
|
|
11501
11629
|
return error;
|
|
11502
11630
|
}
|
|
@@ -11650,8 +11778,7 @@ var ReplicateClient = class {
|
|
|
11650
11778
|
);
|
|
11651
11779
|
} catch (cause) {
|
|
11652
11780
|
if (cause instanceof HttpError) throw mapReplicateError(cause, subject);
|
|
11653
|
-
|
|
11654
|
-
throw mapReplicateError(new HttpError(0, msg, url), subject);
|
|
11781
|
+
throw mapReplicateError(toFetchError(cause, url, { provider: PROVIDER }), subject);
|
|
11655
11782
|
}
|
|
11656
11783
|
if (!response.ok) {
|
|
11657
11784
|
if (response.status === 413) {
|
|
@@ -12355,12 +12482,12 @@ To configure multix, set at least one provider API key:
|
|
|
12355
12482
|
var FFMPEG_HINT = "ffmpeg not found \u2014 media optimize/split/batch commands unavailable.\n Linux: sudo apt-get install ffmpeg\n macOS: brew install ffmpeg\n Windows: https://ffmpeg.org/download.html";
|
|
12356
12483
|
var MAGICK_HINT = "ImageMagick (magick) not found \u2014 image optimization unavailable.\n Linux: sudo apt-get install imagemagick\n macOS: brew install imagemagick\n Windows: https://imagemagick.org/script/download.php";
|
|
12357
12484
|
|
|
12358
|
-
// src/
|
|
12485
|
+
// src/providers/provider-keys.ts
|
|
12359
12486
|
function keyNames(routing) {
|
|
12360
12487
|
const [envPrimary, envFallback] = routing.keyEnvs;
|
|
12361
12488
|
return envFallback ? { envPrimary, envFallback } : { envPrimary };
|
|
12362
12489
|
}
|
|
12363
|
-
var
|
|
12490
|
+
var PROVIDER_KEYS = [
|
|
12364
12491
|
{
|
|
12365
12492
|
name: "Gemini",
|
|
12366
12493
|
envPrimary: "GEMINI_API_KEY",
|
|
@@ -12428,6 +12555,8 @@ var PROVIDERS = [
|
|
|
12428
12555
|
link: "https://app.heygen.com/developers/api"
|
|
12429
12556
|
}
|
|
12430
12557
|
];
|
|
12558
|
+
|
|
12559
|
+
// src/commands/check.ts
|
|
12431
12560
|
function resolveProviderKey(p) {
|
|
12432
12561
|
if (p.envPrimary === "GEMINI_API_KEY") {
|
|
12433
12562
|
const first = listGeminiKeys()[0];
|
|
@@ -12536,7 +12665,7 @@ function registerCheckCommand(program2) {
|
|
|
12536
12665
|
}
|
|
12537
12666
|
let anyKey = false;
|
|
12538
12667
|
const resolved = /* @__PURE__ */ new Map();
|
|
12539
|
-
for (const p of
|
|
12668
|
+
for (const p of PROVIDER_KEYS) {
|
|
12540
12669
|
const r = resolveProviderKey(p);
|
|
12541
12670
|
resolved.set(p.name, r);
|
|
12542
12671
|
if (r.key) anyKey = true;
|
|
@@ -13378,8 +13507,79 @@ function registerCommands(program2) {
|
|
|
13378
13507
|
init_errors();
|
|
13379
13508
|
init_redact();
|
|
13380
13509
|
|
|
13510
|
+
// src/project-env-names.ts
|
|
13511
|
+
var OWN_SETTINGS = [
|
|
13512
|
+
"MULTIX_OUTPUT_DIR",
|
|
13513
|
+
"MULTIX_IMAGE_FORMAT",
|
|
13514
|
+
MAX_DOWNLOAD_ENV,
|
|
13515
|
+
UPLOAD_MIN_KBPS_ENV,
|
|
13516
|
+
// Read before the project file loads, so setting it there has no effect.
|
|
13517
|
+
"MULTIX_DISABLE_HOME_ENV"
|
|
13518
|
+
];
|
|
13519
|
+
var INLINE_SETTINGS = [
|
|
13520
|
+
"GEMINI_MODEL",
|
|
13521
|
+
"MULTIMODAL_MODEL",
|
|
13522
|
+
"IMAGE_GEN_MODEL",
|
|
13523
|
+
"GEMINI_IMAGE_GEN_MODEL",
|
|
13524
|
+
"VIDEO_GEN_MODEL",
|
|
13525
|
+
"GEMINI_TTS_MODEL",
|
|
13526
|
+
"TTS_MODEL",
|
|
13527
|
+
"OPENAI_IMAGE_MODEL",
|
|
13528
|
+
"OPENAI_TTS_MODEL",
|
|
13529
|
+
"OPENAI_STT_MODEL",
|
|
13530
|
+
"OPENROUTER_IMAGE_MODEL",
|
|
13531
|
+
"OPENROUTER_VIDEO_MODEL",
|
|
13532
|
+
"OPENROUTER_FALLBACK_MODELS",
|
|
13533
|
+
"OPENROUTER_SITE_URL",
|
|
13534
|
+
"OPENROUTER_APP_NAME",
|
|
13535
|
+
"MINIMAX_IMAGE_MODEL",
|
|
13536
|
+
"MINIMAX_VIDEO_MODEL",
|
|
13537
|
+
"MINIMAX_SPEECH_MODEL",
|
|
13538
|
+
"MINIMAX_MUSIC_MODEL",
|
|
13539
|
+
"LEONARDO_DEFAULT_MODEL",
|
|
13540
|
+
"LEONARDO_VIDEO_MODEL",
|
|
13541
|
+
"BYTEPLUS_IMAGE_MODEL",
|
|
13542
|
+
"BYTEPLUS_VIDEO_MODEL",
|
|
13543
|
+
"BYTEPLUS_3D_MODEL",
|
|
13544
|
+
"BYTEPLUS_VIDEO_PARAMS_MODE",
|
|
13545
|
+
"FAL_IMAGE_MODEL",
|
|
13546
|
+
"FAL_VIDEO_MODEL",
|
|
13547
|
+
"FAL_VIDEO_IMAGE_MODEL",
|
|
13548
|
+
"REPLICATE_IMAGE_MODEL",
|
|
13549
|
+
"REPLICATE_VIDEO_MODEL",
|
|
13550
|
+
"CLOUDFLARE_AI_IMAGE_MODEL",
|
|
13551
|
+
"CLOUDFLARE_AI_TTS_MODEL",
|
|
13552
|
+
// Read in providers/cloudflare/client.ts (collectPayloadHeader).
|
|
13553
|
+
"CLOUDFLARE_AI_GATEWAY_COLLECT_LOG_PAYLOAD",
|
|
13554
|
+
"NO_COLOR"
|
|
13555
|
+
];
|
|
13556
|
+
var ROUTING = [
|
|
13557
|
+
ATLAS_ROUTING,
|
|
13558
|
+
BYTEPLUS_ROUTING,
|
|
13559
|
+
FAL_ROUTING,
|
|
13560
|
+
HEYGEN_ROUTING,
|
|
13561
|
+
LEONARDO_ROUTING,
|
|
13562
|
+
REPLICATE_ROUTING
|
|
13563
|
+
];
|
|
13564
|
+
var PROJECT_ENV_NAMES = /* @__PURE__ */ new Set([
|
|
13565
|
+
...PROVIDER_KEYS.flatMap((p) => p.envFallback ? [p.envPrimary, p.envFallback] : [p.envPrimary]),
|
|
13566
|
+
...ROUTING.flatMap((r) => [r.name, ...r.keyEnvs]),
|
|
13567
|
+
CLOUDFLARE_ACCOUNT_ID_ENV,
|
|
13568
|
+
CLOUDFLARE_GATEWAY_ID_ENV,
|
|
13569
|
+
...CLOUDFLARE_VIDEO_CREDENTIALS,
|
|
13570
|
+
...Object.values(ATLAS_MODEL_ENV),
|
|
13571
|
+
MAX_UPLOAD_ENV,
|
|
13572
|
+
...OWN_SETTINGS,
|
|
13573
|
+
...INLINE_SETTINGS
|
|
13574
|
+
]);
|
|
13575
|
+
var GEMINI_NUMBERED_KEY = /^GEMINI_API_KEY_\d+$/;
|
|
13576
|
+
function isProjectEnvName(name, platform = process.platform) {
|
|
13577
|
+
const key = envNameKey(name, platform);
|
|
13578
|
+
return PROJECT_ENV_NAMES.has(key) || GEMINI_NUMBERED_KEY.test(key);
|
|
13579
|
+
}
|
|
13580
|
+
|
|
13381
13581
|
// src/cli.ts
|
|
13382
|
-
loadEnv();
|
|
13582
|
+
loadEnv({ isProjectEnvName });
|
|
13383
13583
|
var require3 = createRequire2(import.meta.url);
|
|
13384
13584
|
var pkg = require3("../package.json");
|
|
13385
13585
|
var program = new Command().name("multix").description(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrgoonie/multix",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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.5 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|
|
@@ -54,7 +54,7 @@ CLOUDFLARE_API_TOKEN # Cloudflare Workers AI / AI Gateway token
|
|
|
54
54
|
# Video additionally requires CLOUDFLARE_AI_GATEWAY_ID and REPLICATE_API_TOKEN.
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Priority: `process.env` > `cwd/.env` > `~/.multix/.env`. Set `MULTIX_LEGACY_CLAUDE_ENV=1` to also read legacy AgentKit `.claude` env files (credential names only, lowest priority); `multix check -v` lists loaded env files.
|
|
57
|
+
Priority: `process.env` > `cwd/.env` > `~/.multix/.env`. Set `MULTIX_LEGACY_CLAUDE_ENV=1` to also read legacy AgentKit `.claude` env files (credential names only, lowest priority); `multix check -v` lists loaded env files. A project `.env` may only set names multix reads (provider keys and settings from `.env.example`, the documented `MULTIX_*` settings such as `MULTIX_OUTPUT_DIR`, `NO_COLOR`; never `MULTIX_ALLOW_PROJECT_BASE_URL` or `MULTIX_LEGACY_CLAUDE_ENV`); anything else there (`NODE_OPTIONS`, `npm_config_*`, `LD_PRELOAD`, proxy/TLS variables) is not loaded, with one stderr warning naming the ignored variables. Put such variables in the shell.
|
|
58
58
|
|
|
59
59
|
## Gemini text-output tasks
|
|
60
60
|
|
|
@@ -413,7 +413,7 @@ Requires `cwebp` (libwebp: `brew install webp`, `apt install webp`) on `PATH`.
|
|
|
413
413
|
| `REPLICATE_IMAGE_MODEL` / `REPLICATE_VIDEO_MODEL` | Replicate default models (default `black-forest-labs/flux-schnell`, `prunaai/p-video`) |
|
|
414
414
|
| `CLOUDFLARE_AI_IMAGE_MODEL` / `CLOUDFLARE_AI_TTS_MODEL` | Fixed-model settings: only `@cf/black-forest-labs/flux-1-schnell` and `@cf/myshell-ai/melotts` respectively are accepted |
|
|
415
415
|
| `MULTIX_DISABLE_HOME_ENV` | `1` to skip every home-directory env file (`~/.multix/.env`, `~/.claude/...`) |
|
|
416
|
-
| `MULTIX_LEGACY_CLAUDE_ENV` | `1` to also read legacy `.claude` env files (credential names only) |
|
|
416
|
+
| `MULTIX_LEGACY_CLAUDE_ENV` | `1` to also read legacy `.claude` env files (credential names only); shell or `~/.multix/.env` only, ignored in a project `.env` |
|
|
417
417
|
| `MULTIX_ALLOW_PROJECT_BASE_URL` | Set `1` for one command (`MULTIX_ALLOW_PROJECT_BASE_URL=1 multix ...`) to honour `*_BASE_URL`, `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_AI_GATEWAY_ID` from the project `.env` even when the key comes from elsewhere. Without it, a project value is used only when every key sent with it is in the same project file (a project `.env` that ships its own key and base URL then receives that project's prompts and uploaded files, and its key replaces yours, so review a cloned repo's `.env` first); otherwise the `~/.multix/.env` value is used with a warning, or the command stops with a config error. `multix check` shows the effective state. Ignored in a project `.env`; avoid `~/.multix/.env` (it trusts every directory). Non-default base URLs must be `https://` (http only for localhost/127.0.0.1/[::1]) |
|
|
418
418
|
|
|
419
419
|
## Quick examples for agents
|