@mrgoonie/multix 0.15.2 → 1.0.0
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 +18 -0
- package/README.md +2 -0
- package/dist/cli.js +538 -202
- package/package.json +1 -1
- package/skill/SKILL.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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.0](https://github.com/mrgoonie/multix-cli/compare/v0.15.3...v1.0.0) (2026-10-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* **core:** a project `.env` can no longer redirect a provider base URL or Cloudflare routing ID while the API key comes from the shell or `~/.multix/.env`. Keep the key in the same project `.env`, move the override to `~/.multix/.env`, or opt in explicitly as described in the environment reference.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
* **core:** only trust project .env endpoint overrides that ship their own key ([#97](https://github.com/mrgoonie/multix-cli/issues/97)) ([5c503ec](https://github.com/mrgoonie/multix-cli/commit/5c503eca08aeca6998cf5cf520180a32c0aeee0c))
|
|
18
|
+
|
|
19
|
+
## [0.15.3](https://github.com/mrgoonie/multix-cli/compare/v0.15.2...v0.15.3) (2026-10-11)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
* **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))
|
|
25
|
+
|
|
8
26
|
## [0.15.2](https://github.com/mrgoonie/multix-cli/compare/v0.15.1...v0.15.2) (2026-10-11)
|
|
9
27
|
|
|
10
28
|
|
package/README.md
CHANGED
|
@@ -117,6 +117,8 @@ Set extra keys as `GEMINI_API_KEY_2`, `GEMINI_API_KEY_3`, … (any numeric suffi
|
|
|
117
117
|
|
|
118
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.
|
|
119
119
|
|
|
120
|
+
`*_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. This covers base URLs and Cloudflare routing IDs only; other variables a project `.env` can set are tracked in [#99](https://github.com/mrgoonie/multix-cli/issues/99).
|
|
121
|
+
|
|
120
122
|
## Commands
|
|
121
123
|
|
|
122
124
|
### `multix check`
|
package/dist/cli.js
CHANGED
|
@@ -308,6 +308,11 @@ function validateRawInput(spec, body) {
|
|
|
308
308
|
|
|
309
309
|
// src/providers/atlascloud/models.ts
|
|
310
310
|
var DEFAULT_ATLAS_BASE_URL = "https://api.atlascloud.ai/api/v1";
|
|
311
|
+
var ATLAS_ROUTING = {
|
|
312
|
+
name: "ATLASCLOUD_BASE_URL",
|
|
313
|
+
defaultUrl: DEFAULT_ATLAS_BASE_URL,
|
|
314
|
+
keyEnvs: ["ATLASCLOUD_API_KEY"]
|
|
315
|
+
};
|
|
311
316
|
var SUBMIT_PATHS = {
|
|
312
317
|
image: "/model/generateImage",
|
|
313
318
|
video: "/model/generateVideo",
|
|
@@ -339,13 +344,20 @@ import fs2 from "fs/promises";
|
|
|
339
344
|
import path2 from "path";
|
|
340
345
|
|
|
341
346
|
// src/core/env-loader.ts
|
|
347
|
+
init_errors();
|
|
342
348
|
init_redact();
|
|
343
349
|
import fs from "fs";
|
|
344
350
|
import os from "os";
|
|
345
351
|
import path from "path";
|
|
346
|
-
import {
|
|
352
|
+
import { parse as dotenvParse } from "dotenv";
|
|
353
|
+
function envNameKey(name, platform = process.platform) {
|
|
354
|
+
return platform === "win32" ? name.toUpperCase() : name;
|
|
355
|
+
}
|
|
347
356
|
var loaded = false;
|
|
348
357
|
var loadedFiles = [];
|
|
358
|
+
var fileOrigins = /* @__PURE__ */ new Map();
|
|
359
|
+
var userFileValues = {};
|
|
360
|
+
var warned = /* @__PURE__ */ new Set();
|
|
349
361
|
var LEGACY_SKILL_DIRS = ["ai-multimodal", "ak-ai-multimodal"];
|
|
350
362
|
function legacyFilesUnder(root) {
|
|
351
363
|
const claude = path.join(root, ".claude");
|
|
@@ -368,8 +380,7 @@ function legacyClaudeEnvFiles(cwd, home) {
|
|
|
368
380
|
return files;
|
|
369
381
|
}
|
|
370
382
|
function legacyClaudeEnvEnabled(env = process.env) {
|
|
371
|
-
|
|
372
|
-
return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
383
|
+
return isTruthy(env.MULTIX_LEGACY_CLAUDE_ENV);
|
|
373
384
|
}
|
|
374
385
|
var LEGACY_ALLOWED_NAME = /^[A-Z][A-Z0-9_]*_(API_KEY(_\d+)?|API_TOKEN|ACCOUNT_ID|GATEWAY_ID)$|^(FAL_KEY|ARK_API_KEY)$/;
|
|
375
386
|
function isLegacyAllowedName(name) {
|
|
@@ -382,36 +393,174 @@ function isReadableFile(file) {
|
|
|
382
393
|
return false;
|
|
383
394
|
}
|
|
384
395
|
}
|
|
385
|
-
function
|
|
386
|
-
if (!isReadableFile(file)) return;
|
|
387
|
-
const result = dotenvConfig({ path: file, override: false });
|
|
388
|
-
if (!result.error) loadedFiles.push(file);
|
|
389
|
-
}
|
|
390
|
-
function loadLegacyFile(file) {
|
|
391
|
-
if (!isReadableFile(file)) return;
|
|
392
|
-
let parsed;
|
|
396
|
+
function parseFile(file) {
|
|
397
|
+
if (!isReadableFile(file)) return void 0;
|
|
393
398
|
try {
|
|
394
|
-
|
|
399
|
+
return dotenvParse(fs.readFileSync(file));
|
|
395
400
|
} catch {
|
|
396
|
-
return;
|
|
401
|
+
return void 0;
|
|
397
402
|
}
|
|
403
|
+
}
|
|
404
|
+
function loadFile(file, source, accept = () => true) {
|
|
405
|
+
const parsed = parseFile(file);
|
|
406
|
+
if (!parsed) return void 0;
|
|
398
407
|
for (const [name, value] of Object.entries(parsed)) {
|
|
399
|
-
if (
|
|
408
|
+
if (!accept(name) || process.env[name] !== void 0) continue;
|
|
409
|
+
process.env[name] = value;
|
|
410
|
+
fileOrigins.set(envNameKey(name), { source, file, value });
|
|
400
411
|
}
|
|
401
412
|
loadedFiles.push(file);
|
|
413
|
+
return parsed;
|
|
414
|
+
}
|
|
415
|
+
function byNameKey(values) {
|
|
416
|
+
const out = {};
|
|
417
|
+
for (const [name, value] of Object.entries(values ?? {})) {
|
|
418
|
+
const key = envNameKey(name);
|
|
419
|
+
if (!(key in out)) out[key] = value;
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
402
422
|
}
|
|
403
423
|
function loadEnv() {
|
|
404
424
|
if (loaded) return;
|
|
405
425
|
loaded = true;
|
|
406
426
|
loadedFiles = [];
|
|
427
|
+
fileOrigins = /* @__PURE__ */ new Map();
|
|
428
|
+
userFileValues = {};
|
|
407
429
|
const cwd = process.cwd();
|
|
408
430
|
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os.homedir();
|
|
409
|
-
loadFile(path.resolve(cwd, ".env"));
|
|
410
|
-
if (home) loadFile(path.join(home, ".multix", ".env"));
|
|
431
|
+
loadFile(path.resolve(cwd, ".env"), "project");
|
|
432
|
+
if (home) userFileValues = byNameKey(loadFile(path.join(home, ".multix", ".env"), "user"));
|
|
411
433
|
if (legacyClaudeEnvEnabled()) {
|
|
412
|
-
|
|
434
|
+
const projectLegacy = new Set(legacyFilesUnder(cwd));
|
|
435
|
+
for (const file of legacyClaudeEnvFiles(cwd, home)) {
|
|
436
|
+
loadFile(file, projectLegacy.has(file) ? "project" : "legacy", isLegacyAllowedName);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function envFileOrigin(name) {
|
|
441
|
+
const origin = fileOrigins.get(envNameKey(name));
|
|
442
|
+
if (!origin || process.env[name] !== origin.value) return void 0;
|
|
443
|
+
return { source: origin.source, file: origin.file };
|
|
444
|
+
}
|
|
445
|
+
function isTruthy(value) {
|
|
446
|
+
return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
447
|
+
}
|
|
448
|
+
var ALLOW_PROJECT_BASE_URL_ENV = "MULTIX_ALLOW_PROJECT_BASE_URL";
|
|
449
|
+
function projectBaseUrlAllowed() {
|
|
450
|
+
if (!isTruthy(process.env[ALLOW_PROJECT_BASE_URL_ENV])) return false;
|
|
451
|
+
return envFileOrigin(ALLOW_PROJECT_BASE_URL_ENV)?.source !== "project";
|
|
452
|
+
}
|
|
453
|
+
var OPT_IN_HINT = `To use the project value for one command, run it as \`${ALLOW_PROJECT_BASE_URL_ENV}=1 multix ...\`. Do not put ${ALLOW_PROJECT_BASE_URL_ENV} in ~/.multix/.env: that trusts the .env of every directory you run multix in.`;
|
|
454
|
+
function describeSource(origin) {
|
|
455
|
+
if (!origin) return "the shell";
|
|
456
|
+
if (origin.source === "user") return "~/.multix/.env";
|
|
457
|
+
if (origin.source === "legacy") return origin.file;
|
|
458
|
+
return `another project file (${origin.file})`;
|
|
459
|
+
}
|
|
460
|
+
function foreignCredential(credentials, file) {
|
|
461
|
+
for (const spec of credentials) {
|
|
462
|
+
const names = typeof spec === "string" ? [spec] : spec;
|
|
463
|
+
const used = names.find((n) => process.env[n]?.trim());
|
|
464
|
+
if (!used) continue;
|
|
465
|
+
const origin = envFileOrigin(used);
|
|
466
|
+
if (origin?.source === "project" && origin.file === file) continue;
|
|
467
|
+
return `${used} comes from ${describeSource(origin)}`;
|
|
468
|
+
}
|
|
469
|
+
return void 0;
|
|
470
|
+
}
|
|
471
|
+
function inspectTrustedSetting(name, credentials, harmless) {
|
|
472
|
+
const value = process.env[name]?.trim() || void 0;
|
|
473
|
+
const origin = envFileOrigin(name);
|
|
474
|
+
if (origin?.source !== "project") {
|
|
475
|
+
return value ? { status: "used", value, source: origin?.source ?? "shell" } : { status: "unset" };
|
|
476
|
+
}
|
|
477
|
+
const userValue = userFileValues[envNameKey(name)]?.trim() || void 0;
|
|
478
|
+
if (value === void 0) {
|
|
479
|
+
return userValue ? { status: "fallback", value: userValue, file: origin.file, reason: "it is empty" } : { status: "unset" };
|
|
480
|
+
}
|
|
481
|
+
const project = (basis) => ({
|
|
482
|
+
status: "used",
|
|
483
|
+
value,
|
|
484
|
+
source: "project",
|
|
485
|
+
basis
|
|
486
|
+
});
|
|
487
|
+
if (value === harmless) return project("default");
|
|
488
|
+
if (projectBaseUrlAllowed()) return project("opt-in");
|
|
489
|
+
const reason = foreignCredential(credentials, origin.file);
|
|
490
|
+
if (!reason) return project("same-file");
|
|
491
|
+
return userValue ? { status: "fallback", value: userValue, file: origin.file, reason } : { status: "refused", file: origin.file, reason };
|
|
492
|
+
}
|
|
493
|
+
function trustedSettingRefusal(name, state) {
|
|
494
|
+
return `${name} is set in project env file ${state.file}, but ${state.reason}, so the project is not trusted to choose where that key is sent. Set ${name} in your shell or ~/.multix/.env, or keep the key in the same project file. ${OPT_IN_HINT}`;
|
|
495
|
+
}
|
|
496
|
+
function applyTrustedSetting(name, state) {
|
|
497
|
+
if (state.status === "unset") return void 0;
|
|
498
|
+
if (state.status === "used") return state.value;
|
|
499
|
+
if (state.status === "refused") throw new ConfigError(trustedSettingRefusal(name, state));
|
|
500
|
+
const hint = state.reason === "it is empty" ? "" : ` ${OPT_IN_HINT}`;
|
|
501
|
+
warnOnce(
|
|
502
|
+
`ignored:${name}`,
|
|
503
|
+
`Ignoring ${name} from project env file ${state.file} (${state.reason}); using the value from ~/.multix/.env.${hint}`
|
|
504
|
+
);
|
|
505
|
+
return state.value;
|
|
506
|
+
}
|
|
507
|
+
function warnOnce(key, message) {
|
|
508
|
+
if (warned.has(key)) return;
|
|
509
|
+
warned.add(key);
|
|
510
|
+
console.warn(`\u26A0 ${message}`);
|
|
511
|
+
}
|
|
512
|
+
function hostOf(url) {
|
|
513
|
+
try {
|
|
514
|
+
return new URL(url).hostname || "(no host)";
|
|
515
|
+
} catch {
|
|
516
|
+
return "(unparseable URL)";
|
|
413
517
|
}
|
|
414
518
|
}
|
|
519
|
+
var stripSlash = (url) => url.replace(/\/+$/, "");
|
|
520
|
+
var LOOPBACK_HTTP_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
521
|
+
function baseUrlSchemeError(name, value) {
|
|
522
|
+
let url;
|
|
523
|
+
try {
|
|
524
|
+
url = new URL(value);
|
|
525
|
+
} catch {
|
|
526
|
+
return `${name} must be an absolute https:// URL`;
|
|
527
|
+
}
|
|
528
|
+
if (url.protocol === "https:") return void 0;
|
|
529
|
+
if (url.protocol === "http:" && LOOPBACK_HTTP_HOSTS.has(url.hostname)) return void 0;
|
|
530
|
+
return `${name} must use https:// (host ${url.hostname || "(none)"}); plain http:// is only allowed for localhost, 127.0.0.1, or [::1]`;
|
|
531
|
+
}
|
|
532
|
+
function defaultHarmless(name, defaultUrl) {
|
|
533
|
+
const raw = process.env[name]?.trim();
|
|
534
|
+
return raw && stripSlash(raw) === stripSlash(defaultUrl) ? raw : void 0;
|
|
535
|
+
}
|
|
536
|
+
function inspectBaseUrl(name, defaultUrl, credentials) {
|
|
537
|
+
const state = inspectTrustedSetting(name, credentials, defaultHarmless(name, defaultUrl));
|
|
538
|
+
if (state.status !== "used" && state.status !== "fallback") return state;
|
|
539
|
+
if (stripSlash(state.value) === stripSlash(defaultUrl)) return state;
|
|
540
|
+
const schemeError = baseUrlSchemeError(name, state.value);
|
|
541
|
+
return schemeError ? { ...state, schemeError } : state;
|
|
542
|
+
}
|
|
543
|
+
function inspectProviderBaseUrl(routing) {
|
|
544
|
+
return inspectBaseUrl(routing.name, routing.defaultUrl, [routing.keyEnvs]);
|
|
545
|
+
}
|
|
546
|
+
function resolveBaseUrl(name, defaultUrl, credentials) {
|
|
547
|
+
const state = inspectBaseUrl(name, defaultUrl, credentials);
|
|
548
|
+
const value = applyTrustedSetting(name, state);
|
|
549
|
+
if (!value || stripSlash(value) === stripSlash(defaultUrl)) return defaultUrl;
|
|
550
|
+
if (state.schemeError) throw new ConfigError(state.schemeError);
|
|
551
|
+
const provider = name.replace(/_BASE_URL$/, "");
|
|
552
|
+
warnOnce(
|
|
553
|
+
`custom:${name}`,
|
|
554
|
+
`${name} is set: ${provider} API requests, including your API key, go to ${hostOf(value)} instead of the default host.`
|
|
555
|
+
);
|
|
556
|
+
return value;
|
|
557
|
+
}
|
|
558
|
+
function resolveProviderBaseUrl(routing) {
|
|
559
|
+
return resolveBaseUrl(routing.name, routing.defaultUrl, [routing.keyEnvs]);
|
|
560
|
+
}
|
|
561
|
+
function resolveRoutedKey(routing) {
|
|
562
|
+
return resolveFirstKey(...routing.keyEnvs)?.value;
|
|
563
|
+
}
|
|
415
564
|
function getLoadedEnvFiles() {
|
|
416
565
|
return [...loadedFiles];
|
|
417
566
|
}
|
|
@@ -541,6 +690,113 @@ init_errors();
|
|
|
541
690
|
|
|
542
691
|
// src/core/fetch-json.ts
|
|
543
692
|
init_errors();
|
|
693
|
+
|
|
694
|
+
// src/core/redirect-fetch.ts
|
|
695
|
+
init_errors();
|
|
696
|
+
var MAX_REDIRECTS = 5;
|
|
697
|
+
var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
698
|
+
var BODY_HEADERS = ["content-type", "content-length", "content-encoding", "content-language"];
|
|
699
|
+
function withoutBodyHeaders(headers2) {
|
|
700
|
+
if (headers2 === void 0) return void 0;
|
|
701
|
+
const copy = new Headers(headers2);
|
|
702
|
+
for (const name of BODY_HEADERS) copy.delete(name);
|
|
703
|
+
return copy;
|
|
704
|
+
}
|
|
705
|
+
async function fetchFollowingRedirects(url, init = {}, opts = {}) {
|
|
706
|
+
const doFetch = opts.fetch ?? ((input, i) => globalThis.fetch(input, i));
|
|
707
|
+
const { validateUrl } = opts;
|
|
708
|
+
const maxRedirects = opts.maxRedirects ?? MAX_REDIRECTS;
|
|
709
|
+
let current = url;
|
|
710
|
+
let hopInit = { ...init, redirect: "manual" };
|
|
711
|
+
for (let hop = 0; ; hop++) {
|
|
712
|
+
await validateUrl?.(current);
|
|
713
|
+
let response;
|
|
714
|
+
try {
|
|
715
|
+
response = await doFetch(current, hopInit);
|
|
716
|
+
} catch (cause) {
|
|
717
|
+
if (!opts.wrapNetworkErrors) throw cause;
|
|
718
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
719
|
+
throw new HttpError(0, msg, current);
|
|
720
|
+
}
|
|
721
|
+
if (validateUrl && response.url && response.url !== current) {
|
|
722
|
+
try {
|
|
723
|
+
await validateUrl(response.url);
|
|
724
|
+
} catch (err2) {
|
|
725
|
+
await response.body?.cancel().catch(() => {
|
|
726
|
+
});
|
|
727
|
+
throw err2;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const location = response.headers.get("location");
|
|
731
|
+
if (!REDIRECT_STATUSES.has(response.status) || !location) {
|
|
732
|
+
return { response, url: current };
|
|
733
|
+
}
|
|
734
|
+
let next;
|
|
735
|
+
try {
|
|
736
|
+
next = new URL(location, current);
|
|
737
|
+
} catch {
|
|
738
|
+
}
|
|
739
|
+
const crossOrigin = next !== void 0 && next.origin !== new URL(current).origin;
|
|
740
|
+
if (crossOrigin && opts.crossOrigin === "stop") return { response, url: current };
|
|
741
|
+
await response.body?.cancel().catch(() => {
|
|
742
|
+
});
|
|
743
|
+
if (hop >= maxRedirects) {
|
|
744
|
+
throw new HttpError(response.status, `too many redirects (over ${maxRedirects})`, current);
|
|
745
|
+
}
|
|
746
|
+
if (!next) {
|
|
747
|
+
throw new HttpError(response.status, "redirect with a malformed Location header", current);
|
|
748
|
+
}
|
|
749
|
+
if (crossOrigin) {
|
|
750
|
+
if (opts.crossOrigin === "error") {
|
|
751
|
+
throw new HttpError(
|
|
752
|
+
response.status,
|
|
753
|
+
`refusing to follow a redirect to another origin (${next.origin})`,
|
|
754
|
+
current
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
hopInit = { ...hopInit, headers: void 0 };
|
|
758
|
+
}
|
|
759
|
+
const method = (hopInit.method ?? "GET").toUpperCase();
|
|
760
|
+
const toGet = response.status === 303 && method !== "GET" && method !== "HEAD" || (response.status === 301 || response.status === 302) && method === "POST";
|
|
761
|
+
if (toGet) {
|
|
762
|
+
hopInit = {
|
|
763
|
+
...hopInit,
|
|
764
|
+
method: "GET",
|
|
765
|
+
body: void 0,
|
|
766
|
+
headers: withoutBodyHeaders(hopInit.headers)
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
current = next.toString();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
async function fetchSameOrigin(url, init = {}, fetchImpl) {
|
|
773
|
+
const { response } = await fetchFollowingRedirects(url, init, {
|
|
774
|
+
fetch: fetchImpl,
|
|
775
|
+
crossOrigin: "stop"
|
|
776
|
+
});
|
|
777
|
+
return response;
|
|
778
|
+
}
|
|
779
|
+
async function fetchWithValidatedRedirects(url, opts = {}) {
|
|
780
|
+
const { signal, headers: headers2, validateUrl } = opts;
|
|
781
|
+
const init = { ...signal ? { signal } : {}, ...headers2 ? { headers: headers2 } : {} };
|
|
782
|
+
if (!validateUrl && !headers2) {
|
|
783
|
+
const doFetch = opts.fetch ?? ((input, i) => globalThis.fetch(input, i));
|
|
784
|
+
try {
|
|
785
|
+
return { response: await doFetch(url, init), url };
|
|
786
|
+
} catch (cause) {
|
|
787
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
788
|
+
throw new HttpError(0, msg, url);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return fetchFollowingRedirects(url, init, {
|
|
792
|
+
validateUrl,
|
|
793
|
+
maxRedirects: opts.maxRedirects,
|
|
794
|
+
fetch: opts.fetch,
|
|
795
|
+
wrapNetworkErrors: true
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// src/core/fetch-json.ts
|
|
544
800
|
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
545
801
|
var SNIPPET_MAX = 500;
|
|
546
802
|
function parseRetryAfter(value, now = Date.now()) {
|
|
@@ -566,6 +822,10 @@ async function httpErrorFromResponse(response, url) {
|
|
|
566
822
|
parseRetryAfter(response.headers.get("retry-after"))
|
|
567
823
|
);
|
|
568
824
|
}
|
|
825
|
+
var NON_CREDENTIAL_HEADERS = /* @__PURE__ */ new Set(["accept", "accept-language", "content-type", "user-agent"]);
|
|
826
|
+
function carriesCredentialHeaders(headers2) {
|
|
827
|
+
return Object.keys(headers2).some((name) => !NON_CREDENTIAL_HEADERS.has(name.toLowerCase()));
|
|
828
|
+
}
|
|
569
829
|
async function fetchJson(opts) {
|
|
570
830
|
const { url, method = "GET", headers: headers2 = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
571
831
|
const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
|
|
@@ -580,13 +840,11 @@ async function fetchJson(opts) {
|
|
|
580
840
|
if (body !== void 0) {
|
|
581
841
|
init.body = JSON.stringify(body);
|
|
582
842
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
throw new HttpError(0, msg, url);
|
|
589
|
-
}
|
|
843
|
+
const { response } = await fetchFollowingRedirects(url, init, {
|
|
844
|
+
fetch: doFetch,
|
|
845
|
+
crossOrigin: carriesCredentialHeaders(headers2) ? "error" : "drop-headers",
|
|
846
|
+
wrapNetworkErrors: true
|
|
847
|
+
});
|
|
590
848
|
if (!response.ok) {
|
|
591
849
|
throw await httpErrorFromResponse(response, url);
|
|
592
850
|
}
|
|
@@ -934,13 +1192,18 @@ var AtlasCloudClient = class {
|
|
|
934
1192
|
const url = `${this.baseUrl}/model/uploadMedia`;
|
|
935
1193
|
let response;
|
|
936
1194
|
try {
|
|
937
|
-
response = await
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1195
|
+
response = await fetchSameOrigin(
|
|
1196
|
+
url,
|
|
1197
|
+
{
|
|
1198
|
+
method: "POST",
|
|
1199
|
+
headers: this.authHeaders(),
|
|
1200
|
+
body: form,
|
|
1201
|
+
signal: AbortSignal.timeout(this.uploadTimeoutMs)
|
|
1202
|
+
},
|
|
1203
|
+
(input, init) => this.doFetch(input, init)
|
|
1204
|
+
);
|
|
943
1205
|
} catch (cause) {
|
|
1206
|
+
if (cause instanceof HttpError) throw cause;
|
|
944
1207
|
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
945
1208
|
}
|
|
946
1209
|
if (!response.ok) throw await httpErrorFromResponse(response, url);
|
|
@@ -1044,12 +1307,17 @@ var MODEL_ENV = {
|
|
|
1044
1307
|
function atlasDefaultModel(key) {
|
|
1045
1308
|
return resolveKey(MODEL_ENV[key])?.trim() || ATLAS_DEFAULT_MODELS[key];
|
|
1046
1309
|
}
|
|
1047
|
-
function
|
|
1048
|
-
const apiKey =
|
|
1310
|
+
function requireAtlasKey() {
|
|
1311
|
+
const apiKey = resolveRoutedKey(ATLAS_ROUTING);
|
|
1049
1312
|
if (!apiKey) {
|
|
1050
1313
|
throw new ConfigError(`ATLASCLOUD_API_KEY is not set. Get one at ${ATLAS_KEY_URL}`);
|
|
1051
1314
|
}
|
|
1052
|
-
return
|
|
1315
|
+
return apiKey;
|
|
1316
|
+
}
|
|
1317
|
+
function createAtlasClientFromEnv() {
|
|
1318
|
+
const apiKey = requireAtlasKey();
|
|
1319
|
+
const baseUrl = resolveProviderBaseUrl(ATLAS_ROUTING);
|
|
1320
|
+
return new AtlasCloudClient({ apiKey, baseUrl });
|
|
1053
1321
|
}
|
|
1054
1322
|
async function resolveMediaRef(client, ref, logger) {
|
|
1055
1323
|
const trimmed = ref.trim();
|
|
@@ -1133,7 +1401,6 @@ async function httpJson(opts) {
|
|
|
1133
1401
|
}
|
|
1134
1402
|
var MAX_DOWNLOAD_ENV = "MULTIX_MAX_DOWNLOAD_MB";
|
|
1135
1403
|
var DEFAULT_MAX_DOWNLOAD_MB = 2048;
|
|
1136
|
-
var MAX_REDIRECTS = 5;
|
|
1137
1404
|
function formatMb2(bytes) {
|
|
1138
1405
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1139
1406
|
}
|
|
@@ -1163,44 +1430,6 @@ function maxDownloadBytes() {
|
|
|
1163
1430
|
}
|
|
1164
1431
|
return bytes;
|
|
1165
1432
|
}
|
|
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
1433
|
function removeQuietly(file) {
|
|
1205
1434
|
try {
|
|
1206
1435
|
fs3.rmSync(file, { force: true });
|
|
@@ -1262,7 +1491,7 @@ async function downloadToFile(url, opts) {
|
|
|
1262
1491
|
yield chunk;
|
|
1263
1492
|
}
|
|
1264
1493
|
}
|
|
1265
|
-
const part = `${dest}.part`;
|
|
1494
|
+
const part = `${dest}.${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.part`;
|
|
1266
1495
|
let fileStream;
|
|
1267
1496
|
try {
|
|
1268
1497
|
fileStream = fs3.createWriteStream(part);
|
|
@@ -2271,6 +2500,11 @@ init_errors();
|
|
|
2271
2500
|
|
|
2272
2501
|
// src/providers/byteplus/models.ts
|
|
2273
2502
|
var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
|
|
2503
|
+
var BYTEPLUS_ROUTING = {
|
|
2504
|
+
name: "BYTEPLUS_BASE_URL",
|
|
2505
|
+
defaultUrl: DEFAULT_BYTEPLUS_BASE_URL,
|
|
2506
|
+
keyEnvs: ["BYTEPLUS_API_KEY", "ARK_API_KEY"]
|
|
2507
|
+
};
|
|
2274
2508
|
var DEFAULT_BYTEPLUS_IMAGE_MODEL = "seedream-4-0-250828";
|
|
2275
2509
|
var DEFAULT_BYTEPLUS_VIDEO_MODEL = "seedance-2.0";
|
|
2276
2510
|
var DEFAULT_BYTEPLUS_3D_MODEL = "hyper3d-gen2-260112";
|
|
@@ -2294,7 +2528,7 @@ function bytePlusVideoParamsMode() {
|
|
|
2294
2528
|
|
|
2295
2529
|
// src/providers/byteplus/client.ts
|
|
2296
2530
|
function requireBytePlusKey() {
|
|
2297
|
-
const key =
|
|
2531
|
+
const key = resolveRoutedKey(BYTEPLUS_ROUTING);
|
|
2298
2532
|
if (!key) {
|
|
2299
2533
|
throw new ConfigError(
|
|
2300
2534
|
"BytePlus API key not set. Set BYTEPLUS_API_KEY or ARK_API_KEY. Get one at https://console.byteplus.com/auth/api-keys"
|
|
@@ -2303,7 +2537,7 @@ function requireBytePlusKey() {
|
|
|
2303
2537
|
return key;
|
|
2304
2538
|
}
|
|
2305
2539
|
function bytePlusBaseUrl() {
|
|
2306
|
-
return
|
|
2540
|
+
return resolveProviderBaseUrl(BYTEPLUS_ROUTING);
|
|
2307
2541
|
}
|
|
2308
2542
|
var BytePlusClient = class {
|
|
2309
2543
|
constructor(apiKey, baseUrl = bytePlusBaseUrl()) {
|
|
@@ -3084,11 +3318,15 @@ var CLOUDFLARE_TTS_MODEL = "@cf/myshell-ai/melotts";
|
|
|
3084
3318
|
var CLOUDFLARE_VIDEO_MODEL = "prunaai/p-video";
|
|
3085
3319
|
|
|
3086
3320
|
// src/providers/cloudflare/models.ts
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3321
|
+
var CLOUDFLARE_ACCOUNT_ID_ENV = "CLOUDFLARE_ACCOUNT_ID";
|
|
3322
|
+
var CLOUDFLARE_GATEWAY_ID_ENV = "CLOUDFLARE_AI_GATEWAY_ID";
|
|
3323
|
+
var CLOUDFLARE_TOKEN_ENV = "CLOUDFLARE_API_TOKEN";
|
|
3324
|
+
var GATEWAY_REPLICATE_TOKEN_ENV = "REPLICATE_API_TOKEN";
|
|
3325
|
+
var CLOUDFLARE_NATIVE_CREDENTIALS = [CLOUDFLARE_TOKEN_ENV];
|
|
3326
|
+
var CLOUDFLARE_VIDEO_CREDENTIALS = [
|
|
3327
|
+
CLOUDFLARE_TOKEN_ENV,
|
|
3328
|
+
GATEWAY_REPLICATE_TOKEN_ENV
|
|
3329
|
+
];
|
|
3092
3330
|
function getCloudflareModel(kind, requested) {
|
|
3093
3331
|
const expected = kind === "image" ? CLOUDFLARE_IMAGE_MODEL : CLOUDFLARE_TTS_MODEL;
|
|
3094
3332
|
const overrideName = kind === "image" ? "CLOUDFLARE_AI_IMAGE_MODEL" : "CLOUDFLARE_AI_TTS_MODEL";
|
|
@@ -3108,22 +3346,57 @@ function validateImageSteps(value) {
|
|
|
3108
3346
|
}
|
|
3109
3347
|
return steps;
|
|
3110
3348
|
}
|
|
3111
|
-
|
|
3349
|
+
var requiredMessage = (name) => `${name} is required for Cloudflare media generation`;
|
|
3350
|
+
var token = (name) => resolveKey(name)?.trim() || void 0;
|
|
3351
|
+
function inspectCloudflareConfig(use = "native") {
|
|
3352
|
+
const credentials = use === "video" ? CLOUDFLARE_VIDEO_CREDENTIALS : CLOUDFLARE_NATIVE_CREDENTIALS;
|
|
3353
|
+
const accountId = inspectTrustedSetting(CLOUDFLARE_ACCOUNT_ID_ENV, credentials);
|
|
3354
|
+
const gatewayId = inspectTrustedSetting(CLOUDFLARE_GATEWAY_ID_ENV, credentials);
|
|
3355
|
+
const result = (problem, refused = false) => ({
|
|
3356
|
+
problem,
|
|
3357
|
+
refused,
|
|
3358
|
+
accountId,
|
|
3359
|
+
gatewayId
|
|
3360
|
+
});
|
|
3361
|
+
if (accountId.status === "refused") {
|
|
3362
|
+
return result(trustedSettingRefusal(CLOUDFLARE_ACCOUNT_ID_ENV, accountId), true);
|
|
3363
|
+
}
|
|
3364
|
+
if (accountId.status === "unset") return result(requiredMessage(CLOUDFLARE_ACCOUNT_ID_ENV));
|
|
3365
|
+
if (!token(CLOUDFLARE_TOKEN_ENV)) return result(requiredMessage(CLOUDFLARE_TOKEN_ENV));
|
|
3366
|
+
if (gatewayId.status === "refused") {
|
|
3367
|
+
return result(trustedSettingRefusal(CLOUDFLARE_GATEWAY_ID_ENV, gatewayId), true);
|
|
3368
|
+
}
|
|
3369
|
+
if (use === "video") {
|
|
3370
|
+
if (gatewayId.status === "unset") {
|
|
3371
|
+
return result(`${CLOUDFLARE_GATEWAY_ID_ENV} is required for Cloudflare video generation`);
|
|
3372
|
+
}
|
|
3373
|
+
if (!token(GATEWAY_REPLICATE_TOKEN_ENV)) {
|
|
3374
|
+
return result(requiredMessage(GATEWAY_REPLICATE_TOKEN_ENV));
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
return result();
|
|
3378
|
+
}
|
|
3379
|
+
function requireConfig(use) {
|
|
3380
|
+
const check = inspectCloudflareConfig(use);
|
|
3381
|
+
if (check.problem) throw new ConfigError(check.problem);
|
|
3112
3382
|
return {
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3383
|
+
// applyTrustedSetting prints the one-time warning when a ~/.multix/.env fallback is used.
|
|
3384
|
+
accountId: applyTrustedSetting(CLOUDFLARE_ACCOUNT_ID_ENV, check.accountId),
|
|
3385
|
+
apiToken: token(CLOUDFLARE_TOKEN_ENV),
|
|
3386
|
+
gatewayId: applyTrustedSetting(CLOUDFLARE_GATEWAY_ID_ENV, check.gatewayId),
|
|
3387
|
+
...use === "video" ? { replicateApiToken: token(GATEWAY_REPLICATE_TOKEN_ENV) } : {}
|
|
3116
3388
|
};
|
|
3117
3389
|
}
|
|
3390
|
+
function requireCloudflareConfig() {
|
|
3391
|
+
return requireConfig("native");
|
|
3392
|
+
}
|
|
3118
3393
|
function requireCloudflareVideoConfig() {
|
|
3119
|
-
const config =
|
|
3120
|
-
if (!config.gatewayId) {
|
|
3121
|
-
throw new ConfigError("CLOUDFLARE_AI_GATEWAY_ID is required for Cloudflare video generation");
|
|
3122
|
-
}
|
|
3394
|
+
const config = requireConfig("video");
|
|
3123
3395
|
return {
|
|
3124
|
-
|
|
3396
|
+
accountId: config.accountId,
|
|
3397
|
+
apiToken: config.apiToken,
|
|
3125
3398
|
gatewayId: config.gatewayId,
|
|
3126
|
-
replicateApiToken:
|
|
3399
|
+
replicateApiToken: config.replicateApiToken
|
|
3127
3400
|
};
|
|
3128
3401
|
}
|
|
3129
3402
|
|
|
@@ -3132,8 +3405,13 @@ init_errors();
|
|
|
3132
3405
|
function cloudflareProviderFailure(status) {
|
|
3133
3406
|
return new ProviderError(`Cloudflare request failed with HTTP ${status}`, "Cloudflare");
|
|
3134
3407
|
}
|
|
3408
|
+
async function cloudflareResponseFailure(response) {
|
|
3409
|
+
await response.body?.cancel().catch(() => {
|
|
3410
|
+
});
|
|
3411
|
+
return cloudflareProviderFailure(response.status);
|
|
3412
|
+
}
|
|
3135
3413
|
async function readCloudflareJson(response) {
|
|
3136
|
-
if (!response.ok) throw
|
|
3414
|
+
if (!response.ok) throw await cloudflareResponseFailure(response);
|
|
3137
3415
|
try {
|
|
3138
3416
|
return await response.json();
|
|
3139
3417
|
} catch {
|
|
@@ -3153,12 +3431,11 @@ function videoHeaders(config, opts) {
|
|
|
3153
3431
|
};
|
|
3154
3432
|
}
|
|
3155
3433
|
function send(opts, url, init) {
|
|
3156
|
-
const f = opts.fetch;
|
|
3157
3434
|
const withTimeout = {
|
|
3158
3435
|
...init,
|
|
3159
3436
|
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
3160
3437
|
};
|
|
3161
|
-
return
|
|
3438
|
+
return fetchSameOrigin(url, withTimeout, opts.fetch);
|
|
3162
3439
|
}
|
|
3163
3440
|
function parseCloudflareVideoPrediction(value) {
|
|
3164
3441
|
if (!value || typeof value !== "object") {
|
|
@@ -3211,7 +3488,7 @@ function gatewayUrl(accountId) {
|
|
|
3211
3488
|
async function runCloudflareJson(model, input) {
|
|
3212
3489
|
const config = requireCloudflareConfig();
|
|
3213
3490
|
const useGateway = Boolean(config.gatewayId);
|
|
3214
|
-
const response = await
|
|
3491
|
+
const response = await fetchSameOrigin(
|
|
3215
3492
|
useGateway ? gatewayUrl(config.accountId) : nativeUrl(config.accountId, model),
|
|
3216
3493
|
{
|
|
3217
3494
|
method: "POST",
|
|
@@ -3228,7 +3505,7 @@ async function runCloudflareJson(model, input) {
|
|
|
3228
3505
|
async function runCloudflareBytes(model, input) {
|
|
3229
3506
|
const config = requireCloudflareConfig();
|
|
3230
3507
|
const useGateway = Boolean(config.gatewayId);
|
|
3231
|
-
const response = await
|
|
3508
|
+
const response = await fetchSameOrigin(
|
|
3232
3509
|
useGateway ? gatewayUrl(config.accountId) : nativeUrl(config.accountId, model),
|
|
3233
3510
|
{
|
|
3234
3511
|
method: "POST",
|
|
@@ -3236,7 +3513,7 @@ async function runCloudflareBytes(model, input) {
|
|
|
3236
3513
|
body: JSON.stringify(useGateway ? { model, input } : input)
|
|
3237
3514
|
}
|
|
3238
3515
|
);
|
|
3239
|
-
if (!response.ok) throw
|
|
3516
|
+
if (!response.ok) throw await cloudflareResponseFailure(response);
|
|
3240
3517
|
const contentType = response.headers.get("content-type") ?? "";
|
|
3241
3518
|
if (!contentType.includes("audio/mpeg")) {
|
|
3242
3519
|
throw new ProviderError("Cloudflare did not return MPEG audio", "Cloudflare");
|
|
@@ -3558,7 +3835,7 @@ function apiPostJson(endpoint2, payload, apiKey, opts = {}) {
|
|
|
3558
3835
|
}
|
|
3559
3836
|
async function apiDelete(endpoint2, apiKey) {
|
|
3560
3837
|
const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
|
|
3561
|
-
const res = await
|
|
3838
|
+
const res = await fetchSameOrigin(url, {
|
|
3562
3839
|
method: "DELETE",
|
|
3563
3840
|
headers: authHeaders(apiKey),
|
|
3564
3841
|
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS2)
|
|
@@ -3592,8 +3869,9 @@ async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
|
|
|
3592
3869
|
}
|
|
3593
3870
|
let res;
|
|
3594
3871
|
try {
|
|
3595
|
-
res = await
|
|
3872
|
+
res = await fetchSameOrigin(url.toString(), init);
|
|
3596
3873
|
} catch (cause) {
|
|
3874
|
+
if (cause instanceof HttpError) throw cause;
|
|
3597
3875
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
3598
3876
|
throw new HttpError(0, msg, url.toString());
|
|
3599
3877
|
}
|
|
@@ -3607,13 +3885,14 @@ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
|
|
|
3607
3885
|
const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
|
|
3608
3886
|
let res;
|
|
3609
3887
|
try {
|
|
3610
|
-
res = await
|
|
3888
|
+
res = await fetchSameOrigin(url, {
|
|
3611
3889
|
method: "POST",
|
|
3612
3890
|
headers: authHeaders(apiKey),
|
|
3613
3891
|
body: form,
|
|
3614
3892
|
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
3615
3893
|
});
|
|
3616
3894
|
} catch (cause) {
|
|
3895
|
+
if (cause instanceof HttpError) throw cause;
|
|
3617
3896
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
3618
3897
|
throw new HttpError(0, msg, url);
|
|
3619
3898
|
}
|
|
@@ -3822,7 +4101,7 @@ Dubbed file: ${dest}`);
|
|
|
3822
4101
|
}
|
|
3823
4102
|
async function downloadDub(url, apiKey, dest) {
|
|
3824
4103
|
fs15.mkdirSync(path18.dirname(dest), { recursive: true });
|
|
3825
|
-
const res = await
|
|
4104
|
+
const { response: res } = await fetchWithValidatedRedirects(url, {
|
|
3826
4105
|
headers: { "xi-api-key": apiKey },
|
|
3827
4106
|
signal: AbortSignal.timeout(6e5)
|
|
3828
4107
|
});
|
|
@@ -3831,7 +4110,6 @@ async function downloadDub(url, apiKey, dest) {
|
|
|
3831
4110
|
}
|
|
3832
4111
|
const arr = new Uint8Array(await res.arrayBuffer());
|
|
3833
4112
|
fs15.writeFileSync(dest, arr);
|
|
3834
|
-
void downloadFile;
|
|
3835
4113
|
}
|
|
3836
4114
|
|
|
3837
4115
|
// src/providers/elevenlabs/commands/isolate.ts
|
|
@@ -4227,6 +4505,11 @@ import path22 from "path";
|
|
|
4227
4505
|
|
|
4228
4506
|
// src/providers/fal/models.ts
|
|
4229
4507
|
var DEFAULT_FAL_BASE_URL = "https://queue.fal.run";
|
|
4508
|
+
var FAL_ROUTING = {
|
|
4509
|
+
name: "FAL_BASE_URL",
|
|
4510
|
+
defaultUrl: DEFAULT_FAL_BASE_URL,
|
|
4511
|
+
keyEnvs: ["FAL_KEY"]
|
|
4512
|
+
};
|
|
4230
4513
|
var FAL_DEFAULTS = {
|
|
4231
4514
|
// FLUX.1 [schnell] — fast, low-cost default text-to-image model.
|
|
4232
4515
|
imageModel: process.env.FAL_IMAGE_MODEL ?? "fal-ai/flux/schnell",
|
|
@@ -4246,14 +4529,14 @@ init_errors();
|
|
|
4246
4529
|
init_errors();
|
|
4247
4530
|
init_redact();
|
|
4248
4531
|
function requireFalKey() {
|
|
4249
|
-
const key =
|
|
4532
|
+
const key = resolveRoutedKey(FAL_ROUTING);
|
|
4250
4533
|
if (!key) {
|
|
4251
4534
|
throw new ConfigError("FAL_KEY is not set. Get one at https://fal.ai/dashboard/keys");
|
|
4252
4535
|
}
|
|
4253
4536
|
return key;
|
|
4254
4537
|
}
|
|
4255
4538
|
function falBaseUrl() {
|
|
4256
|
-
return
|
|
4539
|
+
return resolveProviderBaseUrl(FAL_ROUTING);
|
|
4257
4540
|
}
|
|
4258
4541
|
var FalClient = class {
|
|
4259
4542
|
constructor(apiKey, baseUrl = falBaseUrl()) {
|
|
@@ -5814,13 +6097,14 @@ function checkVeoResult(op) {
|
|
|
5814
6097
|
return op;
|
|
5815
6098
|
}
|
|
5816
6099
|
async function downloadVeoVideo(apiKey, uri, dest) {
|
|
5817
|
-
let
|
|
6100
|
+
let parsed;
|
|
5818
6101
|
try {
|
|
5819
|
-
|
|
6102
|
+
parsed = new URL(uri);
|
|
5820
6103
|
} catch {
|
|
5821
6104
|
throw new ProviderError("Veo returned an invalid video URI", "gemini");
|
|
5822
6105
|
}
|
|
5823
|
-
|
|
6106
|
+
const keyed = parsed.protocol === "https:" && parsed.hostname === API_HOST;
|
|
6107
|
+
await downloadFile(uri, dest, 3e5, keyed ? headers(apiKey) : void 0);
|
|
5824
6108
|
}
|
|
5825
6109
|
async function runVeo(model, request, dest, logger) {
|
|
5826
6110
|
const body = buildVeoRequest(request);
|
|
@@ -6210,6 +6494,11 @@ function registerGeminiCommands(program2) {
|
|
|
6210
6494
|
|
|
6211
6495
|
// src/providers/heygen/models.ts
|
|
6212
6496
|
var DEFAULT_HEYGEN_BASE_URL = "https://api.heygen.com";
|
|
6497
|
+
var HEYGEN_ROUTING = {
|
|
6498
|
+
name: "HEYGEN_BASE_URL",
|
|
6499
|
+
defaultUrl: DEFAULT_HEYGEN_BASE_URL,
|
|
6500
|
+
keyEnvs: ["HEYGEN_API_KEY"]
|
|
6501
|
+
};
|
|
6213
6502
|
var HEYGEN_API_SETTINGS_URL = "https://app.heygen.com/developers/api";
|
|
6214
6503
|
var HEYGEN_USAGE_URL = "https://app.heygen.com/developers/usage";
|
|
6215
6504
|
var AVATAR_VIDEO_RESOLUTIONS = ["4k", "1080p", "720p"];
|
|
@@ -6316,10 +6605,8 @@ var HEYGEN_OPERATIONS = {
|
|
|
6316
6605
|
};
|
|
6317
6606
|
|
|
6318
6607
|
// src/providers/heygen/node-helpers.ts
|
|
6319
|
-
import { once } from "events";
|
|
6320
6608
|
import fs29 from "fs";
|
|
6321
6609
|
import path32 from "path";
|
|
6322
|
-
import { pipeline as pipeline2 } from "stream/promises";
|
|
6323
6610
|
init_errors();
|
|
6324
6611
|
init_redact();
|
|
6325
6612
|
|
|
@@ -6666,13 +6953,18 @@ var HeygenClient = class {
|
|
|
6666
6953
|
form.append("file", typed, input.filename);
|
|
6667
6954
|
let response;
|
|
6668
6955
|
try {
|
|
6669
|
-
response = await
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6956
|
+
response = await fetchSameOrigin(
|
|
6957
|
+
url,
|
|
6958
|
+
{
|
|
6959
|
+
method: "POST",
|
|
6960
|
+
headers: this.headers(key),
|
|
6961
|
+
body: form,
|
|
6962
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
6963
|
+
},
|
|
6964
|
+
doFetch
|
|
6965
|
+
);
|
|
6675
6966
|
} catch (cause) {
|
|
6967
|
+
if (cause instanceof HttpError) throw toHeygenApiError(cause);
|
|
6676
6968
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
6677
6969
|
const err3 = toHeygenApiError(new HttpError(0, msg, url));
|
|
6678
6970
|
if (attempt >= this.maxAttempts) throw err3;
|
|
@@ -7228,7 +7520,7 @@ function qualityMode(value) {
|
|
|
7228
7520
|
|
|
7229
7521
|
// src/providers/heygen/node-helpers.ts
|
|
7230
7522
|
function requireHeygenKey() {
|
|
7231
|
-
const key =
|
|
7523
|
+
const key = resolveRoutedKey(HEYGEN_ROUTING);
|
|
7232
7524
|
if (!key) {
|
|
7233
7525
|
throw new ConfigError(
|
|
7234
7526
|
`HEYGEN_API_KEY is not set. Create an API key at ${HEYGEN_API_SETTINGS_URL} (API usage is billed separately from HeyGen web plans).`
|
|
@@ -7238,11 +7530,8 @@ function requireHeygenKey() {
|
|
|
7238
7530
|
}
|
|
7239
7531
|
function createHeygenClientFromEnv() {
|
|
7240
7532
|
const apiKey = requireHeygenKey();
|
|
7241
|
-
const
|
|
7242
|
-
return new HeygenClient({
|
|
7243
|
-
apiKey,
|
|
7244
|
-
baseUrl: override ? checkBaseUrl(override, "HEYGEN_BASE_URL") : DEFAULT_HEYGEN_BASE_URL
|
|
7245
|
-
});
|
|
7533
|
+
const baseUrl = resolveProviderBaseUrl(HEYGEN_ROUTING);
|
|
7534
|
+
return new HeygenClient({ apiKey, baseUrl: checkBaseUrl(baseUrl, "HEYGEN_BASE_URL") });
|
|
7246
7535
|
}
|
|
7247
7536
|
async function uploadLocalFile(client, filePath, logger) {
|
|
7248
7537
|
const abs = path32.resolve(filePath);
|
|
@@ -7323,44 +7612,11 @@ function inferExtension(url, fallback) {
|
|
|
7323
7612
|
function safeSlug(value) {
|
|
7324
7613
|
return value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "out";
|
|
7325
7614
|
}
|
|
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
|
-
}
|
|
7615
|
+
function requireHttps2(url) {
|
|
7616
|
+
if (!isHttpsUrl(url)) throw new HttpError(0, "redirected to a non-https URL", url);
|
|
7617
|
+
}
|
|
7618
|
+
function downloadHttps(url, dest, timeoutMs) {
|
|
7619
|
+
return downloadToFile(url, { dest, timeoutMs, validateUrl: requireHttps2 });
|
|
7364
7620
|
}
|
|
7365
7621
|
async function downloadOutput(url, dest, resumeHint) {
|
|
7366
7622
|
if (!url) {
|
|
@@ -7371,12 +7627,9 @@ async function downloadOutput(url, dest, resumeHint) {
|
|
|
7371
7627
|
`Refusing to download HeyGen output from a non-https URL (${redactUrl(url)})`
|
|
7372
7628
|
);
|
|
7373
7629
|
}
|
|
7374
|
-
const tmp = `${dest}.${process.pid}-${Date.now()}.part`;
|
|
7375
7630
|
try {
|
|
7376
|
-
await
|
|
7377
|
-
fs29.renameSync(tmp, dest);
|
|
7631
|
+
await downloadHttps(url, dest, 6e5);
|
|
7378
7632
|
} catch (e) {
|
|
7379
|
-
fs29.rmSync(tmp, { force: true });
|
|
7380
7633
|
const status = e instanceof HttpError ? e.status : 0;
|
|
7381
7634
|
const expired = status === 403 || status === 404 || status === 410;
|
|
7382
7635
|
const reason = e instanceof Error ? e.message : String(e);
|
|
@@ -7438,7 +7691,7 @@ function captionPathFor(dest, capExt) {
|
|
|
7438
7691
|
const capDest = `${stem}.${capExt}`;
|
|
7439
7692
|
return capDest.toLowerCase() === dest.toLowerCase() ? `${stem}.captions.${capExt}` : capDest;
|
|
7440
7693
|
}
|
|
7441
|
-
var downloadThumbHttpsOnly = (url, dest) =>
|
|
7694
|
+
var downloadThumbHttpsOnly = (url, dest) => downloadHttps(url, dest, 12e4);
|
|
7442
7695
|
function summarizeState(state) {
|
|
7443
7696
|
const out = {
|
|
7444
7697
|
kind: state.kind,
|
|
@@ -8138,6 +8391,11 @@ init_errors();
|
|
|
8138
8391
|
|
|
8139
8392
|
// src/providers/leonardo/models.ts
|
|
8140
8393
|
var DEFAULT_LEONARDO_BASE_URL = "https://cloud.leonardo.ai/api/rest/v1";
|
|
8394
|
+
var LEONARDO_ROUTING = {
|
|
8395
|
+
name: "LEONARDO_BASE_URL",
|
|
8396
|
+
defaultUrl: DEFAULT_LEONARDO_BASE_URL,
|
|
8397
|
+
keyEnvs: ["LEONARDO_API_KEY"]
|
|
8398
|
+
};
|
|
8141
8399
|
var DEFAULT_LEONARDO_MODEL_ID = "7b592283-e8a7-4c5a-9ba6-d18c31f258b9";
|
|
8142
8400
|
var LEONARDO_DEFAULTS = {
|
|
8143
8401
|
modelId: process.env.LEONARDO_DEFAULT_MODEL ?? DEFAULT_LEONARDO_MODEL_ID,
|
|
@@ -8186,7 +8444,7 @@ function isGptImageModel(modelId) {
|
|
|
8186
8444
|
|
|
8187
8445
|
// src/providers/leonardo/client.ts
|
|
8188
8446
|
function requireLeonardoKey() {
|
|
8189
|
-
const key =
|
|
8447
|
+
const key = resolveRoutedKey(LEONARDO_ROUTING);
|
|
8190
8448
|
if (!key) {
|
|
8191
8449
|
throw new ConfigError(
|
|
8192
8450
|
"LEONARDO_API_KEY is not set. Get one at https://app.leonardo.ai/settings/api-keys"
|
|
@@ -8195,7 +8453,7 @@ function requireLeonardoKey() {
|
|
|
8195
8453
|
return key;
|
|
8196
8454
|
}
|
|
8197
8455
|
function leonardoBaseUrl() {
|
|
8198
|
-
return
|
|
8456
|
+
return resolveProviderBaseUrl(LEONARDO_ROUTING);
|
|
8199
8457
|
}
|
|
8200
8458
|
var LeonardoClient = class {
|
|
8201
8459
|
constructor(apiKey, baseUrl = leonardoBaseUrl()) {
|
|
@@ -9585,13 +9843,14 @@ async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOU
|
|
|
9585
9843
|
const url = endpoint(pathname);
|
|
9586
9844
|
let res;
|
|
9587
9845
|
try {
|
|
9588
|
-
res = await
|
|
9846
|
+
res = await fetchSameOrigin(url, {
|
|
9589
9847
|
method: "POST",
|
|
9590
9848
|
headers: authHeaders4(apiKey),
|
|
9591
9849
|
body: form,
|
|
9592
9850
|
signal: AbortSignal.timeout(timeoutMs)
|
|
9593
9851
|
});
|
|
9594
9852
|
} catch (cause) {
|
|
9853
|
+
if (cause instanceof HttpError) throw cause;
|
|
9595
9854
|
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
9596
9855
|
}
|
|
9597
9856
|
if (!res.ok) {
|
|
@@ -9604,13 +9863,14 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
9604
9863
|
const url = endpoint(pathname);
|
|
9605
9864
|
let res;
|
|
9606
9865
|
try {
|
|
9607
|
-
res = await
|
|
9866
|
+
res = await fetchSameOrigin(url, {
|
|
9608
9867
|
method: "POST",
|
|
9609
9868
|
headers: authHeaders4(apiKey, { "Content-Type": "application/json" }),
|
|
9610
9869
|
body: JSON.stringify(body),
|
|
9611
9870
|
signal: AbortSignal.timeout(timeoutMs)
|
|
9612
9871
|
});
|
|
9613
9872
|
} catch (cause) {
|
|
9873
|
+
if (cause instanceof HttpError) throw cause;
|
|
9614
9874
|
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
9615
9875
|
}
|
|
9616
9876
|
if (!res.ok) {
|
|
@@ -10450,7 +10710,9 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
10450
10710
|
const apiKey = requireOpenRouterKey();
|
|
10451
10711
|
const url = videoContentUrl(jobId);
|
|
10452
10712
|
const dest = path50.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
10453
|
-
const r = await
|
|
10713
|
+
const { response: r } = await fetchWithValidatedRedirects(url, {
|
|
10714
|
+
headers: { Authorization: `Bearer ${apiKey}` }
|
|
10715
|
+
});
|
|
10454
10716
|
if (!r.ok) {
|
|
10455
10717
|
logger.error(`Download HTTP ${r.status}`);
|
|
10456
10718
|
process.exit(1);
|
|
@@ -10461,8 +10723,8 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
10461
10723
|
}
|
|
10462
10724
|
fs43.mkdirSync(path50.dirname(dest), { recursive: true });
|
|
10463
10725
|
const ws = fs43.createWriteStream(dest);
|
|
10464
|
-
const { pipeline:
|
|
10465
|
-
await
|
|
10726
|
+
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
10727
|
+
await pipeline2(r.body, ws);
|
|
10466
10728
|
logger.success(`Saved ${dest}`);
|
|
10467
10729
|
if (opts.output) {
|
|
10468
10730
|
fs43.mkdirSync(path50.dirname(path50.resolve(opts.output)), { recursive: true });
|
|
@@ -10634,6 +10896,11 @@ function registerOpenRouterCommands(program2) {
|
|
|
10634
10896
|
// src/providers/replicate/models.ts
|
|
10635
10897
|
init_errors();
|
|
10636
10898
|
var DEFAULT_REPLICATE_BASE_URL = "https://api.replicate.com/v1";
|
|
10899
|
+
var REPLICATE_ROUTING = {
|
|
10900
|
+
name: "REPLICATE_BASE_URL",
|
|
10901
|
+
defaultUrl: DEFAULT_REPLICATE_BASE_URL,
|
|
10902
|
+
keyEnvs: ["REPLICATE_API_TOKEN", "REPLICATE_API_KEY"]
|
|
10903
|
+
};
|
|
10637
10904
|
var DEFAULT_REPLICATE_IMAGE_MODEL = "black-forest-labs/flux-schnell";
|
|
10638
10905
|
var DEFAULT_REPLICATE_VIDEO_MODEL = "prunaai/p-video";
|
|
10639
10906
|
var TERMINAL_STATUSES = [
|
|
@@ -10924,13 +11191,18 @@ var ReplicateClient = class {
|
|
|
10924
11191
|
const subject = `file upload ${opts.filename}`;
|
|
10925
11192
|
let response;
|
|
10926
11193
|
try {
|
|
10927
|
-
response = await
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
|
|
10932
|
-
|
|
11194
|
+
response = await fetchSameOrigin(
|
|
11195
|
+
url,
|
|
11196
|
+
{
|
|
11197
|
+
method: "POST",
|
|
11198
|
+
headers: this.headers(),
|
|
11199
|
+
body: form,
|
|
11200
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
|
|
11201
|
+
},
|
|
11202
|
+
this.doFetch
|
|
11203
|
+
);
|
|
10933
11204
|
} catch (cause) {
|
|
11205
|
+
if (cause instanceof HttpError) throw mapReplicateError(cause, subject);
|
|
10934
11206
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
10935
11207
|
throw mapReplicateError(new HttpError(0, msg, url), subject);
|
|
10936
11208
|
}
|
|
@@ -10952,10 +11224,9 @@ var ReplicateClient = class {
|
|
|
10952
11224
|
};
|
|
10953
11225
|
|
|
10954
11226
|
// src/providers/replicate/node-client.ts
|
|
10955
|
-
var REPLICATE_TOKEN_ENV =
|
|
10956
|
-
var REPLICATE_TOKEN_FALLBACK_ENV = "REPLICATE_API_KEY";
|
|
11227
|
+
var [REPLICATE_TOKEN_ENV, REPLICATE_TOKEN_FALLBACK_ENV] = REPLICATE_ROUTING.keyEnvs;
|
|
10957
11228
|
function resolveReplicateToken() {
|
|
10958
|
-
const found = resolveFirstKey(
|
|
11229
|
+
const found = resolveFirstKey(...REPLICATE_ROUTING.keyEnvs);
|
|
10959
11230
|
return found ? { token: found.value, envUsed: found.envUsed } : void 0;
|
|
10960
11231
|
}
|
|
10961
11232
|
function requireReplicateToken() {
|
|
@@ -10968,7 +11239,7 @@ function requireReplicateToken() {
|
|
|
10968
11239
|
return resolved.token;
|
|
10969
11240
|
}
|
|
10970
11241
|
function replicateBaseUrl() {
|
|
10971
|
-
return
|
|
11242
|
+
return resolveProviderBaseUrl(REPLICATE_ROUTING);
|
|
10972
11243
|
}
|
|
10973
11244
|
function replicateImageModel() {
|
|
10974
11245
|
return resolveKey("REPLICATE_IMAGE_MODEL")?.trim() || DEFAULT_REPLICATE_IMAGE_MODEL;
|
|
@@ -11307,7 +11578,7 @@ async function saveOne(item, predictionId, index, count, opts) {
|
|
|
11307
11578
|
fs44.writeFileSync(dest, bytes);
|
|
11308
11579
|
return dest;
|
|
11309
11580
|
}
|
|
11310
|
-
const
|
|
11581
|
+
const warned2 = opts.warnedHosts;
|
|
11311
11582
|
try {
|
|
11312
11583
|
const { dest } = await downloadToFile(item.url, {
|
|
11313
11584
|
dest: (response) => {
|
|
@@ -11319,8 +11590,8 @@ async function saveOne(item, predictionId, index, count, opts) {
|
|
|
11319
11590
|
fetch: opts.fetch,
|
|
11320
11591
|
validateUrl: (url) => {
|
|
11321
11592
|
const { host, trusted } = checkOutputUrl(url);
|
|
11322
|
-
if (!trusted && !
|
|
11323
|
-
|
|
11593
|
+
if (!trusted && !warned2.has(host)) {
|
|
11594
|
+
warned2.add(host);
|
|
11324
11595
|
opts.logger.warn(
|
|
11325
11596
|
`Output ${item.path} is served from ${host}, not ${REPLICATE_DELIVERY_HOST}`
|
|
11326
11597
|
);
|
|
@@ -11750,6 +12021,10 @@ var FFMPEG_HINT = "ffmpeg not found \u2014 media optimize/split/batch commands u
|
|
|
11750
12021
|
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";
|
|
11751
12022
|
|
|
11752
12023
|
// src/commands/check.ts
|
|
12024
|
+
function keyNames(routing) {
|
|
12025
|
+
const [envPrimary, envFallback] = routing.keyEnvs;
|
|
12026
|
+
return envFallback ? { envPrimary, envFallback } : { envPrimary };
|
|
12027
|
+
}
|
|
11753
12028
|
var PROVIDERS = [
|
|
11754
12029
|
{
|
|
11755
12030
|
name: "Gemini",
|
|
@@ -11777,14 +12052,13 @@ var PROVIDERS = [
|
|
|
11777
12052
|
},
|
|
11778
12053
|
{
|
|
11779
12054
|
name: "Leonardo.Ai",
|
|
11780
|
-
|
|
12055
|
+
...keyNames(LEONARDO_ROUTING),
|
|
11781
12056
|
optional: true,
|
|
11782
12057
|
link: "https://app.leonardo.ai/settings/api-keys"
|
|
11783
12058
|
},
|
|
11784
12059
|
{
|
|
11785
12060
|
name: "BytePlus",
|
|
11786
|
-
|
|
11787
|
-
envFallback: "ARK_API_KEY",
|
|
12061
|
+
...keyNames(BYTEPLUS_ROUTING),
|
|
11788
12062
|
optional: true,
|
|
11789
12063
|
link: "https://console.byteplus.com/auth/api-keys"
|
|
11790
12064
|
},
|
|
@@ -11796,26 +12070,25 @@ var PROVIDERS = [
|
|
|
11796
12070
|
},
|
|
11797
12071
|
{
|
|
11798
12072
|
name: "fal.ai",
|
|
11799
|
-
|
|
12073
|
+
...keyNames(FAL_ROUTING),
|
|
11800
12074
|
optional: true,
|
|
11801
12075
|
link: "https://fal.ai/dashboard/keys"
|
|
11802
12076
|
},
|
|
11803
12077
|
{
|
|
11804
12078
|
name: "Atlas Cloud",
|
|
11805
|
-
|
|
12079
|
+
...keyNames(ATLAS_ROUTING),
|
|
11806
12080
|
optional: true,
|
|
11807
12081
|
link: "https://www.atlascloud.ai/console/api-keys"
|
|
11808
12082
|
},
|
|
11809
12083
|
{
|
|
11810
12084
|
name: "Replicate",
|
|
11811
|
-
|
|
11812
|
-
envFallback: "REPLICATE_API_KEY",
|
|
12085
|
+
...keyNames(REPLICATE_ROUTING),
|
|
11813
12086
|
optional: true,
|
|
11814
12087
|
link: "https://replicate.com/account/api-tokens"
|
|
11815
12088
|
},
|
|
11816
12089
|
{
|
|
11817
12090
|
name: "HeyGen",
|
|
11818
|
-
|
|
12091
|
+
...keyNames(HEYGEN_ROUTING),
|
|
11819
12092
|
optional: true,
|
|
11820
12093
|
link: "https://app.heygen.com/developers/api"
|
|
11821
12094
|
}
|
|
@@ -11830,14 +12103,64 @@ function resolveProviderKey(p) {
|
|
|
11830
12103
|
return found ? { key: found.value, envUsed: found.envUsed } : {};
|
|
11831
12104
|
}
|
|
11832
12105
|
function cloudflareReadiness() {
|
|
11833
|
-
const
|
|
11834
|
-
const
|
|
11835
|
-
|
|
11836
|
-
|
|
11837
|
-
|
|
11838
|
-
|
|
11839
|
-
|
|
12106
|
+
const nativeCheck = inspectCloudflareConfig("native");
|
|
12107
|
+
const videoCheck = inspectCloudflareConfig("video");
|
|
12108
|
+
return { native: !nativeCheck.problem, video: !videoCheck.problem, nativeCheck, videoCheck };
|
|
12109
|
+
}
|
|
12110
|
+
var BASE_URL_ROUTING = [
|
|
12111
|
+
ATLAS_ROUTING,
|
|
12112
|
+
BYTEPLUS_ROUTING,
|
|
12113
|
+
FAL_ROUTING,
|
|
12114
|
+
HEYGEN_ROUTING,
|
|
12115
|
+
LEONARDO_ROUTING,
|
|
12116
|
+
REPLICATE_ROUTING
|
|
12117
|
+
];
|
|
12118
|
+
function usedFrom(state) {
|
|
12119
|
+
if (state.source === "project") {
|
|
12120
|
+
if (state.basis === "default") return "project .env; same as the default endpoint";
|
|
12121
|
+
if (state.basis === "opt-in") return `project .env; allowed by ${ALLOW_PROJECT_BASE_URL_ENV}`;
|
|
12122
|
+
return "project .env, which also holds every key sent with it";
|
|
12123
|
+
}
|
|
12124
|
+
if (state.source === "user") return "~/.multix/.env";
|
|
12125
|
+
if (state.source === "legacy") return "legacy .claude file";
|
|
12126
|
+
return "shell";
|
|
12127
|
+
}
|
|
12128
|
+
function settingLine(label, state, commands = "commands that use it") {
|
|
12129
|
+
const stops = `${commands} will stop with an error`;
|
|
12130
|
+
if (state.status === "unset") return void 0;
|
|
12131
|
+
if (state.status === "refused") {
|
|
12132
|
+
return {
|
|
12133
|
+
level: "warn",
|
|
12134
|
+
message: `${label} ignored: set in project .env while ${state.reason}; ${stops}`
|
|
12135
|
+
};
|
|
12136
|
+
}
|
|
12137
|
+
if (state.schemeError) return { level: "warn", message: `${state.schemeError}; ${stops}` };
|
|
12138
|
+
if (state.status === "used") {
|
|
12139
|
+
return { level: "info", message: `${label} in effect (from ${usedFrom(state)})` };
|
|
12140
|
+
}
|
|
12141
|
+
const why = state.reason === "it is empty" ? "set empty in project .env" : `set in project .env while ${state.reason}`;
|
|
12142
|
+
return { level: "warn", message: `${label} ignored: ${why}; using ~/.multix/.env` };
|
|
12143
|
+
}
|
|
12144
|
+
var sameState = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
12145
|
+
function routingSettingReport() {
|
|
12146
|
+
const lines = [];
|
|
12147
|
+
const push = (line) => {
|
|
12148
|
+
if (line) lines.push(line);
|
|
11840
12149
|
};
|
|
12150
|
+
for (const routing of BASE_URL_ROUTING) {
|
|
12151
|
+
push(settingLine(routing.name, inspectProviderBaseUrl(routing)));
|
|
12152
|
+
}
|
|
12153
|
+
const { nativeCheck, videoCheck } = cloudflareReadiness();
|
|
12154
|
+
for (const [name, key] of [
|
|
12155
|
+
[CLOUDFLARE_ACCOUNT_ID_ENV, "accountId"],
|
|
12156
|
+
[CLOUDFLARE_GATEWAY_ID_ENV, "gatewayId"]
|
|
12157
|
+
]) {
|
|
12158
|
+
push(settingLine(name, nativeCheck[key]));
|
|
12159
|
+
if (!sameState(nativeCheck[key], videoCheck[key])) {
|
|
12160
|
+
push(settingLine(`${name} (video)`, videoCheck[key], "video commands"));
|
|
12161
|
+
}
|
|
12162
|
+
}
|
|
12163
|
+
return lines;
|
|
11841
12164
|
}
|
|
11842
12165
|
async function checkCodexImageReadiness(hasAnyProviderKey, verbose = false, checker = checkCodexAuthenticated) {
|
|
11843
12166
|
if (hasAnyProviderKey && !verbose) return false;
|
|
@@ -11901,18 +12224,31 @@ function registerCheckCommand(program2) {
|
|
|
11901
12224
|
}
|
|
11902
12225
|
}
|
|
11903
12226
|
const cloudflare = cloudflareReadiness();
|
|
12227
|
+
const blocked = (what) => logger.warn(
|
|
12228
|
+
`${what} will stop: a project .env setting is not trusted (see Endpoint Overrides)`
|
|
12229
|
+
);
|
|
11904
12230
|
if (cloudflare.native) {
|
|
11905
12231
|
anyKey = true;
|
|
11906
12232
|
logger.success("Cloudflare Workers AI credentials configured (image and speech)");
|
|
11907
12233
|
if (cloudflare.video) {
|
|
11908
12234
|
logger.success("Cloudflare AI Gateway video credentials configured");
|
|
12235
|
+
} else if (cloudflare.videoCheck.refused) {
|
|
12236
|
+
blocked("Cloudflare video");
|
|
11909
12237
|
} else {
|
|
11910
|
-
logger.info(
|
|
12238
|
+
logger.info(`Cloudflare video unavailable: ${cloudflare.videoCheck.problem}`);
|
|
11911
12239
|
}
|
|
12240
|
+
} else if (cloudflare.nativeCheck.refused) {
|
|
12241
|
+
blocked("Cloudflare image, speech and video");
|
|
11912
12242
|
} else {
|
|
11913
|
-
logger.info(
|
|
11914
|
-
|
|
11915
|
-
|
|
12243
|
+
logger.info(`Cloudflare media unavailable (optional): ${cloudflare.nativeCheck.problem}`);
|
|
12244
|
+
}
|
|
12245
|
+
const routing = routingSettingReport();
|
|
12246
|
+
if (routing.length > 0) {
|
|
12247
|
+
logger.header("Endpoint Overrides");
|
|
12248
|
+
for (const line of routing) {
|
|
12249
|
+
if (line.level === "warn") logger.warn(line.message);
|
|
12250
|
+
else logger.info(line.message);
|
|
12251
|
+
}
|
|
11916
12252
|
}
|
|
11917
12253
|
const shouldCheckCodex = !anyKey || opts.verbose;
|
|
11918
12254
|
let codexReady = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrgoonie/multix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
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.
|
|
4
|
+
version: 1.0.0 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|
|
@@ -414,6 +414,7 @@ Requires `cwebp` (libwebp: `brew install webp`, `apt install webp`) on `PATH`.
|
|
|
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
416
|
| `MULTIX_LEGACY_CLAUDE_ENV` | `1` to also read legacy `.claude` env files (credential names only) |
|
|
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]) |
|
|
417
418
|
|
|
418
419
|
## Quick examples for agents
|
|
419
420
|
|