@mrgoonie/multix 0.11.0 → 0.12.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 +7 -0
- package/README.md +37 -2
- package/dist/cli.js +996 -76
- package/package.json +3 -2
- package/skill/SKILL.md +34 -3
package/dist/cli.js
CHANGED
|
@@ -417,6 +417,13 @@ function getLoadedEnvFiles() {
|
|
|
417
417
|
function resolveKey(name) {
|
|
418
418
|
return process.env[name] || void 0;
|
|
419
419
|
}
|
|
420
|
+
function resolveFirstKey(...names) {
|
|
421
|
+
for (const name of names) {
|
|
422
|
+
const value = resolveKey(name)?.trim();
|
|
423
|
+
if (value) return { value, envUsed: name };
|
|
424
|
+
}
|
|
425
|
+
return void 0;
|
|
426
|
+
}
|
|
420
427
|
|
|
421
428
|
// src/providers/atlascloud/commands/env.ts
|
|
422
429
|
init_errors();
|
|
@@ -855,13 +862,13 @@ var AtlasCloudClient = class {
|
|
|
855
862
|
}
|
|
856
863
|
/** Submit a generation job. `body` holds `model` plus the model's own fields, flattened. */
|
|
857
864
|
async submit(kind, body) {
|
|
858
|
-
const
|
|
859
|
-
if (!
|
|
865
|
+
const path54 = SUBMIT_PATHS[kind];
|
|
866
|
+
if (!path54) throw new ValidationError(`Unknown Atlas Cloud media type: ${String(kind)}`);
|
|
860
867
|
if (typeof body.model !== "string" || !body.model.trim()) {
|
|
861
868
|
throw new ValidationError("Atlas Cloud request body needs a non-empty `model`");
|
|
862
869
|
}
|
|
863
870
|
const env = await fetchJson({
|
|
864
|
-
url: `${this.baseUrl}${
|
|
871
|
+
url: `${this.baseUrl}${path54}`,
|
|
865
872
|
method: "POST",
|
|
866
873
|
headers: this.authHeaders(),
|
|
867
874
|
body,
|
|
@@ -2175,9 +2182,9 @@ var BytePlusClient = class {
|
|
|
2175
2182
|
}
|
|
2176
2183
|
apiKey;
|
|
2177
2184
|
baseUrl;
|
|
2178
|
-
async request(
|
|
2185
|
+
async request(path54, opts = {}) {
|
|
2179
2186
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
2180
|
-
const url = new URL(base +
|
|
2187
|
+
const url = new URL(base + path54);
|
|
2181
2188
|
if (opts.query) {
|
|
2182
2189
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
2183
2190
|
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
@@ -2195,14 +2202,14 @@ var BytePlusClient = class {
|
|
|
2195
2202
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
2196
2203
|
});
|
|
2197
2204
|
}
|
|
2198
|
-
get(
|
|
2199
|
-
return this.request(
|
|
2205
|
+
get(path54, query, logger) {
|
|
2206
|
+
return this.request(path54, { method: "GET", query, logger });
|
|
2200
2207
|
}
|
|
2201
|
-
post(
|
|
2202
|
-
return this.request(
|
|
2208
|
+
post(path54, body, logger) {
|
|
2209
|
+
return this.request(path54, { method: "POST", body, logger });
|
|
2203
2210
|
}
|
|
2204
|
-
delete(
|
|
2205
|
-
return this.request(
|
|
2211
|
+
delete(path54, logger) {
|
|
2212
|
+
return this.request(path54, { method: "DELETE", logger });
|
|
2206
2213
|
}
|
|
2207
2214
|
};
|
|
2208
2215
|
function createBytePlusClient() {
|
|
@@ -2671,10 +2678,10 @@ function toRef(kind, resolved, role) {
|
|
|
2671
2678
|
return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
|
|
2672
2679
|
}
|
|
2673
2680
|
async function resolveRefSpec(kind, spec, logger) {
|
|
2674
|
-
const { path:
|
|
2675
|
-
if (kind === "image") return toRef(kind, await resolveImageInput(
|
|
2676
|
-
if (kind === "video") return toRef(kind, await resolveVideoInput(
|
|
2677
|
-
return toRef(kind, await resolveAudioInput(
|
|
2681
|
+
const { path: path54, role } = parseRefSpec(spec);
|
|
2682
|
+
if (kind === "image") return toRef(kind, await resolveImageInput(path54, logger), role);
|
|
2683
|
+
if (kind === "video") return toRef(kind, await resolveVideoInput(path54, logger), role);
|
|
2684
|
+
return toRef(kind, await resolveAudioInput(path54, logger), role);
|
|
2678
2685
|
}
|
|
2679
2686
|
async function resolveRefSpecs(specs, logger) {
|
|
2680
2687
|
const out = [];
|
|
@@ -4101,9 +4108,9 @@ var FalClient = class {
|
|
|
4101
4108
|
apiKey;
|
|
4102
4109
|
baseUrl;
|
|
4103
4110
|
/** `path` is appended verbatim to the base queue URL, e.g. "/fal-ai/flux/schnell". */
|
|
4104
|
-
async request(
|
|
4111
|
+
async request(path54, opts = {}) {
|
|
4105
4112
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
4106
|
-
const suffix =
|
|
4113
|
+
const suffix = path54.startsWith("/") ? path54 : `/${path54}`;
|
|
4107
4114
|
return this.requestAbsolute(base + suffix, opts);
|
|
4108
4115
|
}
|
|
4109
4116
|
/** Same auth/headers as `request`, but `url` is used verbatim (e.g. a status_url/response_url returned by submit). */
|
|
@@ -4120,11 +4127,11 @@ var FalClient = class {
|
|
|
4120
4127
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
4121
4128
|
});
|
|
4122
4129
|
}
|
|
4123
|
-
get(
|
|
4124
|
-
return this.request(
|
|
4130
|
+
get(path54, logger) {
|
|
4131
|
+
return this.request(path54, { method: "GET", logger });
|
|
4125
4132
|
}
|
|
4126
|
-
post(
|
|
4127
|
-
return this.request(
|
|
4133
|
+
post(path54, body, logger) {
|
|
4134
|
+
return this.request(path54, { method: "POST", body, logger });
|
|
4128
4135
|
}
|
|
4129
4136
|
getAbsolute(url, logger) {
|
|
4130
4137
|
return this.requestAbsolute(url, { method: "GET", logger });
|
|
@@ -6032,11 +6039,11 @@ var LeonardoClient = class {
|
|
|
6032
6039
|
}
|
|
6033
6040
|
apiKey;
|
|
6034
6041
|
baseUrl;
|
|
6035
|
-
async request(
|
|
6042
|
+
async request(path54, opts = {}) {
|
|
6036
6043
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
6037
|
-
const v2Override =
|
|
6044
|
+
const v2Override = path54.startsWith("v2:");
|
|
6038
6045
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
6039
|
-
const finalPath = v2Override ?
|
|
6046
|
+
const finalPath = v2Override ? path54.slice(3) : path54;
|
|
6040
6047
|
const url = new URL(finalBase + finalPath);
|
|
6041
6048
|
if (opts.query) {
|
|
6042
6049
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -6055,14 +6062,14 @@ var LeonardoClient = class {
|
|
|
6055
6062
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
6056
6063
|
});
|
|
6057
6064
|
}
|
|
6058
|
-
get(
|
|
6059
|
-
return this.request(
|
|
6065
|
+
get(path54, query, logger) {
|
|
6066
|
+
return this.request(path54, { method: "GET", query, logger });
|
|
6060
6067
|
}
|
|
6061
|
-
post(
|
|
6062
|
-
return this.request(
|
|
6068
|
+
post(path54, body, logger) {
|
|
6069
|
+
return this.request(path54, { method: "POST", body, logger });
|
|
6063
6070
|
}
|
|
6064
|
-
delete(
|
|
6065
|
-
return this.request(
|
|
6071
|
+
delete(path54, logger) {
|
|
6072
|
+
return this.request(path54, { method: "DELETE", logger });
|
|
6066
6073
|
}
|
|
6067
6074
|
};
|
|
6068
6075
|
function createLeonardoClient() {
|
|
@@ -8289,8 +8296,8 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
8289
8296
|
}
|
|
8290
8297
|
fs42.mkdirSync(path47.dirname(dest), { recursive: true });
|
|
8291
8298
|
const ws = fs42.createWriteStream(dest);
|
|
8292
|
-
const { pipeline:
|
|
8293
|
-
await
|
|
8299
|
+
const { pipeline: pipeline3 } = await import("stream/promises");
|
|
8300
|
+
await pipeline3(r.body, ws);
|
|
8294
8301
|
logger.success(`Saved ${dest}`);
|
|
8295
8302
|
if (opts.output) {
|
|
8296
8303
|
fs42.mkdirSync(path47.dirname(path47.resolve(opts.output)), { recursive: true });
|
|
@@ -8459,8 +8466,916 @@ function registerOpenRouterCommands(program2) {
|
|
|
8459
8466
|
registerOpenRouterVideoModelsCommand(openrouter);
|
|
8460
8467
|
}
|
|
8461
8468
|
|
|
8462
|
-
// src/
|
|
8469
|
+
// src/providers/replicate/models.ts
|
|
8470
|
+
init_errors();
|
|
8471
|
+
var DEFAULT_REPLICATE_BASE_URL = "https://api.replicate.com/v1";
|
|
8472
|
+
var DEFAULT_REPLICATE_IMAGE_MODEL = "black-forest-labs/flux-schnell";
|
|
8473
|
+
var DEFAULT_REPLICATE_VIDEO_MODEL = "prunaai/p-video";
|
|
8474
|
+
var TERMINAL_STATUSES = [
|
|
8475
|
+
"succeeded",
|
|
8476
|
+
"failed",
|
|
8477
|
+
"canceled",
|
|
8478
|
+
"aborted"
|
|
8479
|
+
];
|
|
8480
|
+
function isTerminalStatus2(status) {
|
|
8481
|
+
return TERMINAL_STATUSES.includes(status);
|
|
8482
|
+
}
|
|
8483
|
+
var NAME_RE = /^[a-z0-9][a-z0-9._-]*$/i;
|
|
8484
|
+
var VERSION_RE = /^[a-z0-9]+$/i;
|
|
8485
|
+
var BARE_VERSION_RE = /^[a-f0-9]{64}$/i;
|
|
8486
|
+
function parseModelRef(ref, explicitVersion) {
|
|
8487
|
+
const trimmed = ref.trim();
|
|
8488
|
+
if (explicitVersion !== void 0 && !VERSION_RE.test(explicitVersion)) {
|
|
8489
|
+
throw new ValidationError(`Invalid Replicate version id "${explicitVersion}"`);
|
|
8490
|
+
}
|
|
8491
|
+
if (BARE_VERSION_RE.test(trimmed)) {
|
|
8492
|
+
if (explicitVersion && explicitVersion !== trimmed) {
|
|
8493
|
+
throw new ValidationError("Conflicting version ids: model argument and --version differ");
|
|
8494
|
+
}
|
|
8495
|
+
return { kind: "version", version: trimmed };
|
|
8496
|
+
}
|
|
8497
|
+
const [path54, inlineVersion, ...rest] = trimmed.split(":");
|
|
8498
|
+
const [owner, name, ...extra] = (path54 ?? "").split("/");
|
|
8499
|
+
if (rest.length > 0 || extra.length > 0 || !owner || !name) {
|
|
8500
|
+
throw new ValidationError(
|
|
8501
|
+
`Invalid Replicate model "${ref}" \u2014 expected owner/name, owner/name:version, or a 64-char version id`
|
|
8502
|
+
);
|
|
8503
|
+
}
|
|
8504
|
+
if (!NAME_RE.test(owner) || !NAME_RE.test(name)) {
|
|
8505
|
+
throw new ValidationError(`Invalid Replicate model "${ref}" \u2014 owner and name must be slugs`);
|
|
8506
|
+
}
|
|
8507
|
+
if (inlineVersion !== void 0) {
|
|
8508
|
+
if (!VERSION_RE.test(inlineVersion)) {
|
|
8509
|
+
throw new ValidationError(`Invalid Replicate version id in "${ref}"`);
|
|
8510
|
+
}
|
|
8511
|
+
if (explicitVersion && explicitVersion !== inlineVersion) {
|
|
8512
|
+
throw new ValidationError("Conflicting version ids: model argument and --version differ");
|
|
8513
|
+
}
|
|
8514
|
+
return { kind: "version", version: inlineVersion, owner, name };
|
|
8515
|
+
}
|
|
8516
|
+
if (explicitVersion) return { kind: "version", version: explicitVersion, owner, name };
|
|
8517
|
+
return { kind: "official", owner, name };
|
|
8518
|
+
}
|
|
8519
|
+
function describeModelRef(ref) {
|
|
8520
|
+
if (ref.kind === "official") return `${ref.owner}/${ref.name}`;
|
|
8521
|
+
const short = ref.version.slice(0, 12);
|
|
8522
|
+
return ref.owner && ref.name ? `${ref.owner}/${ref.name}:${short}` : `version ${short}`;
|
|
8523
|
+
}
|
|
8524
|
+
var PREDICTION_ID_RE2 = /^[a-z0-9]+$/i;
|
|
8525
|
+
function isValidPredictionId(id) {
|
|
8526
|
+
return typeof id === "string" && PREDICTION_ID_RE2.test(id);
|
|
8527
|
+
}
|
|
8528
|
+
function assertPredictionId(id) {
|
|
8529
|
+
const trimmed = id.trim();
|
|
8530
|
+
if (!PREDICTION_ID_RE2.test(trimmed)) {
|
|
8531
|
+
throw new ValidationError(`Invalid Replicate prediction id "${id}"`);
|
|
8532
|
+
}
|
|
8533
|
+
return trimmed;
|
|
8534
|
+
}
|
|
8535
|
+
|
|
8536
|
+
// src/providers/replicate/node-client.ts
|
|
8537
|
+
init_errors();
|
|
8538
|
+
|
|
8539
|
+
// src/providers/replicate/client.ts
|
|
8540
|
+
init_errors();
|
|
8541
|
+
var PROVIDER = "replicate";
|
|
8542
|
+
var DETAIL_MAX = 300;
|
|
8543
|
+
var CREATE_MAX_RETRIES = 2;
|
|
8544
|
+
var CREATE_RETRY_CAP_MS = 3e4;
|
|
8545
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
8546
|
+
var UPLOAD_TIMEOUT_MS = 3e5;
|
|
8547
|
+
var ReplicateApiError = class extends ProviderError {
|
|
8548
|
+
constructor(message, status, retryAfterMs) {
|
|
8549
|
+
super(message, PROVIDER);
|
|
8550
|
+
this.status = status;
|
|
8551
|
+
this.retryAfterMs = retryAfterMs;
|
|
8552
|
+
this.name = "ReplicateApiError";
|
|
8553
|
+
}
|
|
8554
|
+
status;
|
|
8555
|
+
retryAfterMs;
|
|
8556
|
+
};
|
|
8557
|
+
var ReplicatePredictionError = class extends ProviderError {
|
|
8558
|
+
constructor(prediction2) {
|
|
8559
|
+
const detail = formatPredictionError(prediction2.error);
|
|
8560
|
+
super(
|
|
8561
|
+
`Replicate prediction ${prediction2.id} ${prediction2.status}${detail ? `: ${detail}` : ""}`,
|
|
8562
|
+
PROVIDER
|
|
8563
|
+
);
|
|
8564
|
+
this.prediction = prediction2;
|
|
8565
|
+
this.name = "ReplicatePredictionError";
|
|
8566
|
+
}
|
|
8567
|
+
prediction;
|
|
8568
|
+
};
|
|
8569
|
+
function formatPredictionError(error) {
|
|
8570
|
+
if (error === null || error === void 0 || error === "") return "";
|
|
8571
|
+
const text = typeof error === "string" ? error : JSON.stringify(error);
|
|
8572
|
+
return text.slice(0, DETAIL_MAX);
|
|
8573
|
+
}
|
|
8574
|
+
function errorDetail(snippet) {
|
|
8575
|
+
try {
|
|
8576
|
+
const body = JSON.parse(snippet);
|
|
8577
|
+
const detail = body.detail ?? body.title ?? body.error;
|
|
8578
|
+
if (typeof detail === "string" && detail) return detail.slice(0, DETAIL_MAX);
|
|
8579
|
+
} catch {
|
|
8580
|
+
}
|
|
8581
|
+
return snippet.trim().slice(0, DETAIL_MAX);
|
|
8582
|
+
}
|
|
8583
|
+
function mapReplicateError(error, subject) {
|
|
8584
|
+
if (!(error instanceof HttpError)) return error;
|
|
8585
|
+
const detail = errorDetail(error.snippet);
|
|
8586
|
+
const suffix = detail ? `: ${detail}` : "";
|
|
8587
|
+
const { status, retryAfterMs } = error;
|
|
8588
|
+
let message;
|
|
8589
|
+
if (status === 0) {
|
|
8590
|
+
message = `Network error contacting Replicate${suffix}`;
|
|
8591
|
+
} else if (status === 401) {
|
|
8592
|
+
message = "Replicate rejected the API token (401). Check REPLICATE_API_TOKEN (or REPLICATE_API_KEY); create one at https://replicate.com/account/api-tokens";
|
|
8593
|
+
} else if (status === 402) {
|
|
8594
|
+
message = `Replicate requires billing for this request (402)${suffix}. See https://replicate.com/account/billing`;
|
|
8595
|
+
} else if (status === 403) {
|
|
8596
|
+
message = `Replicate denied access to ${subject} (403)${suffix}`;
|
|
8597
|
+
} else if (status === 404) {
|
|
8598
|
+
message = subject.startsWith("prediction") ? `Replicate could not find ${subject} (404); it may belong to another account or have expired` : `Replicate could not find ${subject} (404). Check owner/name; community models need an explicit version (owner/name:version or --version)`;
|
|
8599
|
+
} else if (status === 422) {
|
|
8600
|
+
message = `Replicate rejected the input for ${subject} (422)${suffix}. Check the model's input schema on replicate.com`;
|
|
8601
|
+
} else if (status === 429) {
|
|
8602
|
+
const wait = retryAfterMs !== void 0 ? `; retry after ${Math.ceil(retryAfterMs / 1e3)}s` : "";
|
|
8603
|
+
message = `Replicate rate limit reached (429)${wait}`;
|
|
8604
|
+
} else if (status >= 500) {
|
|
8605
|
+
message = `Replicate server error (${status})${suffix}. Try again later`;
|
|
8606
|
+
} else {
|
|
8607
|
+
message = `Replicate request for ${subject} failed (${status})${suffix}`;
|
|
8608
|
+
}
|
|
8609
|
+
return new ReplicateApiError(message, status, retryAfterMs);
|
|
8610
|
+
}
|
|
8611
|
+
function validatePrediction(value) {
|
|
8612
|
+
if (value === null || typeof value !== "object" || typeof value.id !== "string" || typeof value.status !== "string") {
|
|
8613
|
+
throw new ProviderError("Malformed Replicate response: missing prediction id/status", PROVIDER);
|
|
8614
|
+
}
|
|
8615
|
+
if (!isValidPredictionId(value.id)) {
|
|
8616
|
+
throw new ProviderError("Malformed Replicate response: invalid prediction id", PROVIDER);
|
|
8617
|
+
}
|
|
8618
|
+
return value;
|
|
8619
|
+
}
|
|
8620
|
+
function assertSucceeded(prediction2) {
|
|
8621
|
+
if (prediction2.status === "succeeded") return prediction2;
|
|
8622
|
+
if (isTerminalStatus2(prediction2.status)) throw new ReplicatePredictionError(prediction2);
|
|
8623
|
+
throw new ProviderError(
|
|
8624
|
+
`Replicate prediction ${prediction2.id} is not finished (status: ${prediction2.status})`,
|
|
8625
|
+
PROVIDER
|
|
8626
|
+
);
|
|
8627
|
+
}
|
|
8628
|
+
var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8629
|
+
var ReplicateClient = class {
|
|
8630
|
+
apiKey;
|
|
8631
|
+
baseUrl;
|
|
8632
|
+
doFetch;
|
|
8633
|
+
sleep;
|
|
8634
|
+
constructor(opts) {
|
|
8635
|
+
if (!opts.apiKey) throw new ProviderError("Replicate API token is required", PROVIDER);
|
|
8636
|
+
this.apiKey = opts.apiKey;
|
|
8637
|
+
this.baseUrl = (opts.baseUrl || DEFAULT_REPLICATE_BASE_URL).replace(/\/+$/, "");
|
|
8638
|
+
this.doFetch = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
8639
|
+
this.sleep = opts.sleep ?? defaultSleep2;
|
|
8640
|
+
}
|
|
8641
|
+
headers(extra = {}) {
|
|
8642
|
+
return { accept: "application/json", authorization: `Bearer ${this.apiKey}`, ...extra };
|
|
8643
|
+
}
|
|
8644
|
+
/** Raw request: throws HttpError so polling can classify transient failures. */
|
|
8645
|
+
raw(path54, method, body, headers2, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
8646
|
+
return fetchJson({
|
|
8647
|
+
url: `${this.baseUrl}${path54}`,
|
|
8648
|
+
method,
|
|
8649
|
+
headers: this.headers(headers2),
|
|
8650
|
+
body,
|
|
8651
|
+
timeoutMs,
|
|
8652
|
+
fetch: this.doFetch
|
|
8653
|
+
});
|
|
8654
|
+
}
|
|
8655
|
+
/** Create a prediction on the official-model or versioned endpoint. */
|
|
8656
|
+
async createPrediction(opts) {
|
|
8657
|
+
const { model, input } = opts;
|
|
8658
|
+
const isOfficial = model.kind === "official";
|
|
8659
|
+
const path54 = isOfficial ? `/models/${model.owner}/${model.name}/predictions` : "/predictions";
|
|
8660
|
+
const body = isOfficial ? { input } : { version: model.version, input };
|
|
8661
|
+
const subject = isOfficial ? `model ${model.owner}/${model.name}` : `model version ${model.version}`;
|
|
8662
|
+
const headers2 = {};
|
|
8663
|
+
let timeoutMs = REQUEST_TIMEOUT_MS;
|
|
8664
|
+
if (opts.preferWaitSeconds !== void 0) {
|
|
8665
|
+
const seconds = Math.min(60, Math.max(1, Math.round(opts.preferWaitSeconds)));
|
|
8666
|
+
headers2.prefer = `wait=${seconds}`;
|
|
8667
|
+
timeoutMs += seconds * 1e3;
|
|
8668
|
+
}
|
|
8669
|
+
for (let attempt = 0; ; attempt++) {
|
|
8670
|
+
try {
|
|
8671
|
+
const res = await this.raw(path54, "POST", body, headers2, timeoutMs);
|
|
8672
|
+
return validatePrediction(res);
|
|
8673
|
+
} catch (error) {
|
|
8674
|
+
if (error instanceof HttpError && error.status === 429 && attempt < CREATE_MAX_RETRIES) {
|
|
8675
|
+
const delay = Math.min(error.retryAfterMs ?? 2e3 * (attempt + 1), CREATE_RETRY_CAP_MS);
|
|
8676
|
+
await this.sleep(delay);
|
|
8677
|
+
continue;
|
|
8678
|
+
}
|
|
8679
|
+
throw mapReplicateError(error, subject);
|
|
8680
|
+
}
|
|
8681
|
+
}
|
|
8682
|
+
}
|
|
8683
|
+
async getPrediction(id) {
|
|
8684
|
+
const safeId = assertPredictionId(id);
|
|
8685
|
+
try {
|
|
8686
|
+
return validatePrediction(await this.raw(`/predictions/${safeId}`, "GET"));
|
|
8687
|
+
} catch (error) {
|
|
8688
|
+
throw mapReplicateError(error, `prediction ${safeId}`);
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
8691
|
+
/** Cancel a running prediction (POST /predictions/{id}/cancel). */
|
|
8692
|
+
async cancelPrediction(id) {
|
|
8693
|
+
const safeId = assertPredictionId(id);
|
|
8694
|
+
try {
|
|
8695
|
+
return validatePrediction(await this.raw(`/predictions/${safeId}/cancel`, "POST"));
|
|
8696
|
+
} catch (error) {
|
|
8697
|
+
throw mapReplicateError(error, `prediction ${safeId}`);
|
|
8698
|
+
}
|
|
8699
|
+
}
|
|
8700
|
+
/**
|
|
8701
|
+
* Poll until the prediction reaches a terminal status (succeeded, failed,
|
|
8702
|
+
* canceled, aborted) and return it — callers decide success via
|
|
8703
|
+
* assertSucceeded. Retries network/429/5xx; throws PollTimeoutError on deadline.
|
|
8704
|
+
*/
|
|
8705
|
+
async waitForPrediction(prediction2, opts) {
|
|
8706
|
+
if (typeof prediction2 !== "string" && isTerminalStatus2(prediction2.status)) return prediction2;
|
|
8707
|
+
const safeId = assertPredictionId(typeof prediction2 === "string" ? prediction2 : prediction2.id);
|
|
8708
|
+
try {
|
|
8709
|
+
return await pollUntil({
|
|
8710
|
+
fetchStatus: async ({ remainingMs }) => validatePrediction(
|
|
8711
|
+
await this.raw(
|
|
8712
|
+
`/predictions/${safeId}`,
|
|
8713
|
+
"GET",
|
|
8714
|
+
void 0,
|
|
8715
|
+
void 0,
|
|
8716
|
+
// AbortSignal.timeout needs a positive integer; clocks may be fractional.
|
|
8717
|
+
Math.max(1, Math.ceil(Math.min(REQUEST_TIMEOUT_MS, remainingMs)))
|
|
8718
|
+
)
|
|
8719
|
+
),
|
|
8720
|
+
isTerminal: (p) => isTerminalStatus2(p.status),
|
|
8721
|
+
isTransientError: isTransientHttpError,
|
|
8722
|
+
timeoutMs: opts.timeoutMs,
|
|
8723
|
+
intervalMs: opts.intervalMs ?? 2e3,
|
|
8724
|
+
maxIntervalMs: opts.maxIntervalMs ?? 1e4,
|
|
8725
|
+
resumeHint: opts.resumeHint,
|
|
8726
|
+
onPoll: opts.onPoll,
|
|
8727
|
+
sleep: opts.sleep ?? this.sleep,
|
|
8728
|
+
now: opts.now
|
|
8729
|
+
});
|
|
8730
|
+
} catch (error) {
|
|
8731
|
+
if (error instanceof PollTimeoutError) throw error;
|
|
8732
|
+
throw mapReplicateError(error, `prediction ${safeId}`);
|
|
8733
|
+
}
|
|
8734
|
+
}
|
|
8735
|
+
/**
|
|
8736
|
+
* Upload a file via the Files API (multipart, < 100 MB) and return the file
|
|
8737
|
+
* resource; pass `urls.get` as a model input value.
|
|
8738
|
+
*/
|
|
8739
|
+
async uploadFile(opts) {
|
|
8740
|
+
const form = new FormData();
|
|
8741
|
+
const blob = opts.data.type ? opts.data : new Blob([opts.data], { type: opts.contentType });
|
|
8742
|
+
form.append("content", blob, opts.filename);
|
|
8743
|
+
form.append("filename", opts.filename);
|
|
8744
|
+
form.append("type", opts.contentType);
|
|
8745
|
+
const url = `${this.baseUrl}/files`;
|
|
8746
|
+
const subject = `file upload ${opts.filename}`;
|
|
8747
|
+
let response;
|
|
8748
|
+
try {
|
|
8749
|
+
response = await this.doFetch(url, {
|
|
8750
|
+
method: "POST",
|
|
8751
|
+
headers: this.headers(),
|
|
8752
|
+
body: form,
|
|
8753
|
+
signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MS)
|
|
8754
|
+
});
|
|
8755
|
+
} catch (cause) {
|
|
8756
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
8757
|
+
throw mapReplicateError(new HttpError(0, msg, url), subject);
|
|
8758
|
+
}
|
|
8759
|
+
if (!response.ok) {
|
|
8760
|
+
if (response.status === 413) {
|
|
8761
|
+
throw new ReplicateApiError(
|
|
8762
|
+
"Replicate rejected the upload: files must be less than 100MB",
|
|
8763
|
+
413
|
|
8764
|
+
);
|
|
8765
|
+
}
|
|
8766
|
+
throw mapReplicateError(await httpErrorFromResponse(response, url), subject);
|
|
8767
|
+
}
|
|
8768
|
+
const file = await response.json();
|
|
8769
|
+
if (typeof file?.urls?.get !== "string") {
|
|
8770
|
+
throw new ProviderError("Malformed Replicate file response: missing urls.get", PROVIDER);
|
|
8771
|
+
}
|
|
8772
|
+
return file;
|
|
8773
|
+
}
|
|
8774
|
+
};
|
|
8775
|
+
|
|
8776
|
+
// src/providers/replicate/node-client.ts
|
|
8777
|
+
var REPLICATE_TOKEN_ENV = "REPLICATE_API_TOKEN";
|
|
8778
|
+
var REPLICATE_TOKEN_FALLBACK_ENV = "REPLICATE_API_KEY";
|
|
8779
|
+
function resolveReplicateToken() {
|
|
8780
|
+
const found = resolveFirstKey(REPLICATE_TOKEN_ENV, REPLICATE_TOKEN_FALLBACK_ENV);
|
|
8781
|
+
return found ? { token: found.value, envUsed: found.envUsed } : void 0;
|
|
8782
|
+
}
|
|
8783
|
+
function requireReplicateToken() {
|
|
8784
|
+
const resolved = resolveReplicateToken();
|
|
8785
|
+
if (!resolved) {
|
|
8786
|
+
throw new ConfigError(
|
|
8787
|
+
`${REPLICATE_TOKEN_ENV} is not set (${REPLICATE_TOKEN_FALLBACK_ENV} is also accepted). Get one at https://replicate.com/account/api-tokens`
|
|
8788
|
+
);
|
|
8789
|
+
}
|
|
8790
|
+
return resolved.token;
|
|
8791
|
+
}
|
|
8792
|
+
function replicateBaseUrl() {
|
|
8793
|
+
return resolveKey("REPLICATE_BASE_URL")?.trim() || DEFAULT_REPLICATE_BASE_URL;
|
|
8794
|
+
}
|
|
8795
|
+
function replicateImageModel() {
|
|
8796
|
+
return resolveKey("REPLICATE_IMAGE_MODEL")?.trim() || DEFAULT_REPLICATE_IMAGE_MODEL;
|
|
8797
|
+
}
|
|
8798
|
+
function replicateVideoModel() {
|
|
8799
|
+
return resolveKey("REPLICATE_VIDEO_MODEL")?.trim() || DEFAULT_REPLICATE_VIDEO_MODEL;
|
|
8800
|
+
}
|
|
8801
|
+
function createReplicateClient() {
|
|
8802
|
+
return new ReplicateClient({ apiKey: requireReplicateToken(), baseUrl: replicateBaseUrl() });
|
|
8803
|
+
}
|
|
8804
|
+
|
|
8805
|
+
// src/providers/replicate/commands/cancel.ts
|
|
8806
|
+
function registerReplicateCancelCommand(parent) {
|
|
8807
|
+
parent.command("cancel <predictionId>").description("Cancel a running Replicate prediction").option("-v, --verbose", "Verbose logging").action(async (predictionId, opts) => {
|
|
8808
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
8809
|
+
const client = createReplicateClient();
|
|
8810
|
+
const prediction2 = await client.cancelPrediction(predictionId);
|
|
8811
|
+
if (prediction2.status === "canceled") {
|
|
8812
|
+
logger.success(`Prediction ${prediction2.id} canceled`);
|
|
8813
|
+
} else if (isTerminalStatus2(prediction2.status)) {
|
|
8814
|
+
logger.warn(`Prediction ${prediction2.id} already finished (status: ${prediction2.status})`);
|
|
8815
|
+
} else {
|
|
8816
|
+
logger.info(`Cancel requested for ${prediction2.id} (status: ${prediction2.status})`);
|
|
8817
|
+
}
|
|
8818
|
+
});
|
|
8819
|
+
}
|
|
8820
|
+
|
|
8821
|
+
// src/providers/replicate/commands/image.ts
|
|
8822
|
+
init_errors();
|
|
8823
|
+
|
|
8824
|
+
// src/providers/replicate/commands/prediction-flow.ts
|
|
8825
|
+
init_errors();
|
|
8826
|
+
|
|
8827
|
+
// src/providers/replicate/node-outputs.ts
|
|
8828
|
+
init_errors();
|
|
8829
|
+
init_redact();
|
|
8830
|
+
import { once as once2 } from "events";
|
|
8463
8831
|
import fs43 from "fs";
|
|
8832
|
+
import path48 from "path";
|
|
8833
|
+
import { Readable } from "stream";
|
|
8834
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
8835
|
+
|
|
8836
|
+
// src/providers/replicate/output.ts
|
|
8837
|
+
init_redact();
|
|
8838
|
+
var HTTP_RE = /^https?:\/\//i;
|
|
8839
|
+
var DATA_URI_RE = /^data:[^,]*,/i;
|
|
8840
|
+
function isMediaRef(value) {
|
|
8841
|
+
return typeof value === "string" && (HTTP_RE.test(value) || DATA_URI_RE.test(value));
|
|
8842
|
+
}
|
|
8843
|
+
function extractOutput(output) {
|
|
8844
|
+
const result = { media: [], texts: [] };
|
|
8845
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8846
|
+
const visit = (value, path54) => {
|
|
8847
|
+
if (value === null || value === void 0) return;
|
|
8848
|
+
if (typeof value === "string") {
|
|
8849
|
+
if (isMediaRef(value)) result.media.push({ url: value, path: path54 || "output" });
|
|
8850
|
+
else if (value.length > 0) result.texts.push(path54 ? `${path54}: ${value}` : value);
|
|
8851
|
+
return;
|
|
8852
|
+
}
|
|
8853
|
+
if (typeof value !== "object") {
|
|
8854
|
+
result.texts.push(path54 ? `${path54}: ${String(value)}` : String(value));
|
|
8855
|
+
return;
|
|
8856
|
+
}
|
|
8857
|
+
if (seen.has(value)) return;
|
|
8858
|
+
seen.add(value);
|
|
8859
|
+
if (Array.isArray(value)) {
|
|
8860
|
+
if (value.length > 0 && value.every((v) => typeof v === "string" && !isMediaRef(v))) {
|
|
8861
|
+
const joined = value.join("");
|
|
8862
|
+
if (joined.length > 0) result.texts.push(path54 ? `${path54}: ${joined}` : joined);
|
|
8863
|
+
return;
|
|
8864
|
+
}
|
|
8865
|
+
value.forEach((item, i) => visit(item, `${path54}[${i}]`));
|
|
8866
|
+
return;
|
|
8867
|
+
}
|
|
8868
|
+
for (const [key, val] of Object.entries(value)) {
|
|
8869
|
+
visit(val, path54 ? `${path54}.${key}` : key);
|
|
8870
|
+
}
|
|
8871
|
+
};
|
|
8872
|
+
visit(output, "");
|
|
8873
|
+
return result;
|
|
8874
|
+
}
|
|
8875
|
+
var MIME_TO_EXT = {
|
|
8876
|
+
"image/png": ".png",
|
|
8877
|
+
"image/jpeg": ".jpg",
|
|
8878
|
+
"image/jpg": ".jpg",
|
|
8879
|
+
"image/webp": ".webp",
|
|
8880
|
+
"image/gif": ".gif",
|
|
8881
|
+
"image/svg+xml": ".svg",
|
|
8882
|
+
"image/bmp": ".bmp",
|
|
8883
|
+
"image/tiff": ".tiff",
|
|
8884
|
+
"video/mp4": ".mp4",
|
|
8885
|
+
"video/quicktime": ".mov",
|
|
8886
|
+
"video/webm": ".webm",
|
|
8887
|
+
"audio/mpeg": ".mp3",
|
|
8888
|
+
"audio/mp3": ".mp3",
|
|
8889
|
+
"audio/wav": ".wav",
|
|
8890
|
+
"audio/x-wav": ".wav",
|
|
8891
|
+
"audio/ogg": ".ogg",
|
|
8892
|
+
"audio/flac": ".flac",
|
|
8893
|
+
"audio/mp4": ".m4a",
|
|
8894
|
+
"audio/aac": ".aac",
|
|
8895
|
+
"model/gltf-binary": ".glb",
|
|
8896
|
+
"application/pdf": ".pdf",
|
|
8897
|
+
"application/json": ".json",
|
|
8898
|
+
"application/zip": ".zip",
|
|
8899
|
+
"text/plain": ".txt"
|
|
8900
|
+
};
|
|
8901
|
+
var EXT_TO_MIME = {
|
|
8902
|
+
".png": "image/png",
|
|
8903
|
+
".jpg": "image/jpeg",
|
|
8904
|
+
".jpeg": "image/jpeg",
|
|
8905
|
+
".webp": "image/webp",
|
|
8906
|
+
".gif": "image/gif",
|
|
8907
|
+
".bmp": "image/bmp",
|
|
8908
|
+
".tif": "image/tiff",
|
|
8909
|
+
".tiff": "image/tiff",
|
|
8910
|
+
".svg": "image/svg+xml",
|
|
8911
|
+
".mp4": "video/mp4",
|
|
8912
|
+
".mov": "video/quicktime",
|
|
8913
|
+
".webm": "video/webm",
|
|
8914
|
+
".mp3": "audio/mpeg",
|
|
8915
|
+
".wav": "audio/wav",
|
|
8916
|
+
".ogg": "audio/ogg",
|
|
8917
|
+
".flac": "audio/flac",
|
|
8918
|
+
".m4a": "audio/mp4",
|
|
8919
|
+
".aac": "audio/aac",
|
|
8920
|
+
".glb": "model/gltf-binary",
|
|
8921
|
+
".pdf": "application/pdf",
|
|
8922
|
+
".json": "application/json",
|
|
8923
|
+
".zip": "application/zip",
|
|
8924
|
+
".txt": "text/plain"
|
|
8925
|
+
};
|
|
8926
|
+
function extFromContentType(contentType) {
|
|
8927
|
+
if (!contentType) return void 0;
|
|
8928
|
+
const mime = contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
8929
|
+
return MIME_TO_EXT[mime];
|
|
8930
|
+
}
|
|
8931
|
+
function mimeFromFilename(filename) {
|
|
8932
|
+
const dot = filename.lastIndexOf(".");
|
|
8933
|
+
const ext = dot === -1 ? "" : filename.slice(dot).toLowerCase();
|
|
8934
|
+
return EXT_TO_MIME[ext] ?? "application/octet-stream";
|
|
8935
|
+
}
|
|
8936
|
+
function extFromUrl4(url) {
|
|
8937
|
+
if (DATA_URI_RE.test(url)) {
|
|
8938
|
+
const mime = url.slice(5, url.indexOf(",")).split(";")[0];
|
|
8939
|
+
return extFromContentType(mime);
|
|
8940
|
+
}
|
|
8941
|
+
try {
|
|
8942
|
+
const pathname = new URL(url).pathname;
|
|
8943
|
+
const last = pathname.split("/").pop() ?? "";
|
|
8944
|
+
const dot = last.lastIndexOf(".");
|
|
8945
|
+
if (dot <= 0) return void 0;
|
|
8946
|
+
const ext = last.slice(dot).toLowerCase();
|
|
8947
|
+
return /^\.[a-z0-9]{1,5}$/.test(ext) ? ext : void 0;
|
|
8948
|
+
} catch {
|
|
8949
|
+
return void 0;
|
|
8950
|
+
}
|
|
8951
|
+
}
|
|
8952
|
+
function summarizePrediction(p) {
|
|
8953
|
+
const summary = { id: p.id, status: p.status };
|
|
8954
|
+
if (p.model) summary.model = p.model;
|
|
8955
|
+
if (p.version) summary.version = p.version;
|
|
8956
|
+
if (p.error !== void 0 && p.error !== null) summary.error = p.error;
|
|
8957
|
+
if (p.output !== void 0 && p.output !== null) summary.output = redactOutputUrls(p.output);
|
|
8958
|
+
if (p.metrics) summary.metrics = p.metrics;
|
|
8959
|
+
if (p.created_at) summary.created_at = p.created_at;
|
|
8960
|
+
if (p.completed_at) summary.completed_at = p.completed_at;
|
|
8961
|
+
if (p.urls?.web) summary.web_url = p.urls.web;
|
|
8962
|
+
return summary;
|
|
8963
|
+
}
|
|
8964
|
+
function redactOutputUrls(value) {
|
|
8965
|
+
if (typeof value === "string") {
|
|
8966
|
+
if (DATA_URI_RE.test(value)) return `${value.slice(0, value.indexOf(",") + 1)}[redacted]`;
|
|
8967
|
+
return HTTP_RE.test(value) ? redactUrl(value) : value;
|
|
8968
|
+
}
|
|
8969
|
+
if (Array.isArray(value)) return value.map(redactOutputUrls);
|
|
8970
|
+
if (value !== null && typeof value === "object") {
|
|
8971
|
+
return Object.fromEntries(
|
|
8972
|
+
Object.entries(value).map(([k, v]) => [k, redactOutputUrls(v)])
|
|
8973
|
+
);
|
|
8974
|
+
}
|
|
8975
|
+
return value;
|
|
8976
|
+
}
|
|
8977
|
+
function outputFilename(predictionId, index, ext) {
|
|
8978
|
+
const prefix = predictionId.replace(/[^a-z0-9]/gi, "").slice(0, 8) || "output";
|
|
8979
|
+
return `replicate-${prefix}-${String(index + 1).padStart(2, "0")}${ext}`;
|
|
8980
|
+
}
|
|
8981
|
+
|
|
8982
|
+
// src/providers/replicate/node-outputs.ts
|
|
8983
|
+
var DOWNLOAD_TIMEOUT_MS = 3e5;
|
|
8984
|
+
function isDirTarget(output) {
|
|
8985
|
+
if (/[\\/]$/.test(output)) return true;
|
|
8986
|
+
try {
|
|
8987
|
+
return fs43.statSync(output).isDirectory();
|
|
8988
|
+
} catch {
|
|
8989
|
+
return false;
|
|
8990
|
+
}
|
|
8991
|
+
}
|
|
8992
|
+
function resolveDestination(predictionId, index, count, ext, opts) {
|
|
8993
|
+
if (!opts.output) return path48.join(opts.outDir, outputFilename(predictionId, index, ext));
|
|
8994
|
+
const output = path48.resolve(opts.output);
|
|
8995
|
+
if (isDirTarget(opts.output)) return path48.join(output, outputFilename(predictionId, index, ext));
|
|
8996
|
+
const givenExt = path48.extname(output);
|
|
8997
|
+
if (count === 1) return givenExt ? output : `${output}${ext}`;
|
|
8998
|
+
const base = givenExt ? output.slice(0, -givenExt.length) : output;
|
|
8999
|
+
return `${base}-${String(index + 1).padStart(2, "0")}${ext}`;
|
|
9000
|
+
}
|
|
9001
|
+
async function writeResponse(response, dest) {
|
|
9002
|
+
if (!response.body) throw new HttpError(0, "Response body is null", response.url);
|
|
9003
|
+
const fileStream = fs43.createWriteStream(dest);
|
|
9004
|
+
try {
|
|
9005
|
+
await pipeline2(
|
|
9006
|
+
Readable.fromWeb(response.body),
|
|
9007
|
+
fileStream
|
|
9008
|
+
);
|
|
9009
|
+
} catch (err2) {
|
|
9010
|
+
if (!fileStream.closed) await once2(fileStream, "close");
|
|
9011
|
+
throw err2;
|
|
9012
|
+
}
|
|
9013
|
+
}
|
|
9014
|
+
async function saveOne(item, predictionId, index, count, opts) {
|
|
9015
|
+
if (item.url.startsWith("data:")) {
|
|
9016
|
+
const comma = item.url.indexOf(",");
|
|
9017
|
+
const meta = item.url.slice(5, comma);
|
|
9018
|
+
const payload = item.url.slice(comma + 1);
|
|
9019
|
+
const bytes = meta.endsWith(";base64") ? Buffer.from(payload, "base64") : Buffer.from(decodeURIComponent(payload));
|
|
9020
|
+
const dest2 = resolveDestination(
|
|
9021
|
+
predictionId,
|
|
9022
|
+
index,
|
|
9023
|
+
count,
|
|
9024
|
+
extFromUrl4(item.url) ?? ".bin",
|
|
9025
|
+
opts
|
|
9026
|
+
);
|
|
9027
|
+
fs43.mkdirSync(path48.dirname(dest2), { recursive: true });
|
|
9028
|
+
fs43.writeFileSync(dest2, bytes);
|
|
9029
|
+
return dest2;
|
|
9030
|
+
}
|
|
9031
|
+
const doFetch = opts.fetch ?? ((url, init) => globalThis.fetch(url, init));
|
|
9032
|
+
let response;
|
|
9033
|
+
try {
|
|
9034
|
+
response = await doFetch(item.url, { signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS) });
|
|
9035
|
+
} catch (cause) {
|
|
9036
|
+
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), item.url);
|
|
9037
|
+
}
|
|
9038
|
+
if (!response.ok) {
|
|
9039
|
+
const hint = response.status === 404 || response.status === 410 ? " (output may have expired)" : "";
|
|
9040
|
+
throw new HttpError(response.status, `download failed${hint}`, item.url);
|
|
9041
|
+
}
|
|
9042
|
+
const ext = extFromUrl4(item.url) ?? extFromContentType(response.headers.get("content-type")) ?? ".bin";
|
|
9043
|
+
const dest = resolveDestination(predictionId, index, count, ext, opts);
|
|
9044
|
+
fs43.mkdirSync(path48.dirname(dest), { recursive: true });
|
|
9045
|
+
try {
|
|
9046
|
+
await writeResponse(response, dest);
|
|
9047
|
+
} catch (err2) {
|
|
9048
|
+
fs43.rmSync(dest, { force: true });
|
|
9049
|
+
throw err2;
|
|
9050
|
+
}
|
|
9051
|
+
return dest;
|
|
9052
|
+
}
|
|
9053
|
+
async function saveMediaOutputs(predictionId, media, opts) {
|
|
9054
|
+
const saved = [];
|
|
9055
|
+
const failed = [];
|
|
9056
|
+
for (let i = 0; i < media.length; i++) {
|
|
9057
|
+
const item = media[i];
|
|
9058
|
+
if (!item) continue;
|
|
9059
|
+
opts.logger.debug(`Downloading ${item.path} from ${redactUrl(item.url).slice(0, 200)}`);
|
|
9060
|
+
try {
|
|
9061
|
+
const dest = await saveOne(item, predictionId, i, media.length, opts);
|
|
9062
|
+
saved.push(dest);
|
|
9063
|
+
opts.logger.success(`Saved ${dest}`);
|
|
9064
|
+
} catch (e) {
|
|
9065
|
+
failed.push(item.path);
|
|
9066
|
+
opts.logger.warn(`Download failed for ${item.path}: ${e instanceof Error ? e.message : e}`);
|
|
9067
|
+
}
|
|
9068
|
+
}
|
|
9069
|
+
if (failed.length > 0) {
|
|
9070
|
+
const what = saved.length === 0 ? `none of ${media.length} output file(s) could be downloaded` : `${failed.length} of ${media.length} output file(s) failed to download (${failed.join(", ")})`;
|
|
9071
|
+
const hint = opts.retryHint ? `. ${opts.retryHint}` : "";
|
|
9072
|
+
throw new ProviderError(`Prediction ${predictionId}: ${what}${hint}`, "replicate");
|
|
9073
|
+
}
|
|
9074
|
+
return saved;
|
|
9075
|
+
}
|
|
9076
|
+
|
|
9077
|
+
// src/providers/replicate/commands/prediction-flow.ts
|
|
9078
|
+
function addFlowOptions(cmd, defaultTimeoutMs) {
|
|
9079
|
+
return cmd.option("--async", "Submit and return immediately with the prediction id").option("--wait", "Wait for a terminal status (default unless --async)").option(
|
|
9080
|
+
"--prefer-wait [seconds]",
|
|
9081
|
+
"Ask Replicate to hold the create request open (1-60s, default 60); keeps polling if still running"
|
|
9082
|
+
).option("--wait-timeout <ms>", "Polling timeout in milliseconds", String(defaultTimeoutMs)).option("--download", "Download media outputs (default)").option("--no-download", "Print output URLs instead of downloading").option("--output <path>", "Output file path, or a directory for multiple outputs").option("-v, --verbose", "Verbose logging");
|
|
9083
|
+
}
|
|
9084
|
+
function parsePositiveInt(raw, flag, max) {
|
|
9085
|
+
const n = Number(raw);
|
|
9086
|
+
if (!Number.isInteger(n) || n <= 0 || max !== void 0 && n > max) {
|
|
9087
|
+
throw new ValidationError(
|
|
9088
|
+
`${flag} must be a positive integer${max !== void 0 ? ` up to ${max}` : ""}`
|
|
9089
|
+
);
|
|
9090
|
+
}
|
|
9091
|
+
return n;
|
|
9092
|
+
}
|
|
9093
|
+
function parsePreferWait(value) {
|
|
9094
|
+
if (value === void 0 || value === false) return void 0;
|
|
9095
|
+
if (value === true) return 60;
|
|
9096
|
+
return parsePositiveInt(value, "--prefer-wait", 60);
|
|
9097
|
+
}
|
|
9098
|
+
function resumeCommand2(id) {
|
|
9099
|
+
return `multix replicate status ${id} --wait --download`;
|
|
9100
|
+
}
|
|
9101
|
+
function printAsyncHandle(prediction2) {
|
|
9102
|
+
console.log(`Prediction submitted: ${prediction2.id} (status: ${prediction2.status})`);
|
|
9103
|
+
console.log(` Check: multix replicate status ${prediction2.id}`);
|
|
9104
|
+
console.log(` Resume: ${resumeCommand2(prediction2.id)}`);
|
|
9105
|
+
console.log(` Cancel: multix replicate cancel ${prediction2.id}`);
|
|
9106
|
+
}
|
|
9107
|
+
async function reportOutputs(prediction2, opts) {
|
|
9108
|
+
const { media, texts } = extractOutput(prediction2.output);
|
|
9109
|
+
for (const text of texts) console.log(text);
|
|
9110
|
+
if (media.length === 0) {
|
|
9111
|
+
if (texts.length === 0) console.log(JSON.stringify(summarizePrediction(prediction2), null, 2));
|
|
9112
|
+
return [];
|
|
9113
|
+
}
|
|
9114
|
+
if (!opts.download) {
|
|
9115
|
+
for (const m of media) console.log(m.url);
|
|
9116
|
+
return [];
|
|
9117
|
+
}
|
|
9118
|
+
const saved = await saveMediaOutputs(prediction2.id, media, {
|
|
9119
|
+
outDir: opts.output ? "" : getOutputDir(),
|
|
9120
|
+
output: opts.output,
|
|
9121
|
+
logger: opts.logger,
|
|
9122
|
+
retryHint: `Retry while the output URLs are live: multix replicate status ${prediction2.id} --download`
|
|
9123
|
+
});
|
|
9124
|
+
const finals = opts.finalize ? opts.finalize(saved) : saved;
|
|
9125
|
+
console.log(`
|
|
9126
|
+
Saved ${finals.length} file(s) from prediction ${prediction2.id}:`);
|
|
9127
|
+
for (const f of finals) console.log(` ${f}`);
|
|
9128
|
+
return finals;
|
|
9129
|
+
}
|
|
9130
|
+
async function completePrediction(client, prediction2, opts) {
|
|
9131
|
+
const id = typeof prediction2 === "string" ? prediction2 : prediction2.id;
|
|
9132
|
+
let lastStatus = typeof prediction2 === "string" ? "" : prediction2.status;
|
|
9133
|
+
if (typeof prediction2 === "string" || !isTerminalStatus2(prediction2.status)) {
|
|
9134
|
+
opts.logger.info(`Waiting for prediction ${id}...`);
|
|
9135
|
+
}
|
|
9136
|
+
const final = await client.waitForPrediction(prediction2, {
|
|
9137
|
+
timeoutMs: opts.waitTimeoutMs,
|
|
9138
|
+
resumeHint: `Resume with: ${resumeCommand2(id)}`,
|
|
9139
|
+
onPoll: (p, attempt) => {
|
|
9140
|
+
if (p.status !== lastStatus) opts.logger.debug(`attempt ${attempt}: ${p.status}`);
|
|
9141
|
+
lastStatus = p.status;
|
|
9142
|
+
}
|
|
9143
|
+
});
|
|
9144
|
+
assertSucceeded(final);
|
|
9145
|
+
opts.logger.success(`Prediction ${final.id} succeeded`);
|
|
9146
|
+
return reportOutputs(final, opts);
|
|
9147
|
+
}
|
|
9148
|
+
async function executePrediction2(params) {
|
|
9149
|
+
const { client, model, input, opts, logger } = params;
|
|
9150
|
+
if (opts.async && opts.wait)
|
|
9151
|
+
throw new ValidationError("--async and --wait are mutually exclusive");
|
|
9152
|
+
if (opts.async && opts.preferWait) {
|
|
9153
|
+
throw new ValidationError("--async and --prefer-wait are mutually exclusive");
|
|
9154
|
+
}
|
|
9155
|
+
const waitTimeoutMs = parsePositiveInt(opts.waitTimeout, "--wait-timeout");
|
|
9156
|
+
const preferWaitSeconds = parsePreferWait(opts.preferWait);
|
|
9157
|
+
logger.info(`Submitting to Replicate ${describeModelRef(model)}...`);
|
|
9158
|
+
const prediction2 = await client.createPrediction({ model, input, preferWaitSeconds });
|
|
9159
|
+
if (opts.async) {
|
|
9160
|
+
printAsyncHandle(prediction2);
|
|
9161
|
+
return;
|
|
9162
|
+
}
|
|
9163
|
+
logger.info(`Prediction ${prediction2.id} (status: ${prediction2.status})`);
|
|
9164
|
+
await completePrediction(client, prediction2, {
|
|
9165
|
+
waitTimeoutMs,
|
|
9166
|
+
logger,
|
|
9167
|
+
download: opts.download !== false,
|
|
9168
|
+
output: opts.output,
|
|
9169
|
+
finalize: params.finalize
|
|
9170
|
+
});
|
|
9171
|
+
}
|
|
9172
|
+
|
|
9173
|
+
// src/providers/replicate/commands/image.ts
|
|
9174
|
+
var ASPECT_RE = /^\d{1,2}:\d{1,2}$/;
|
|
9175
|
+
var OUTPUT_FORMATS = ["webp", "jpg", "png"];
|
|
9176
|
+
function buildImageInput(prompt, opts) {
|
|
9177
|
+
if (!prompt.trim()) throw new ValidationError("Prompt must not be empty");
|
|
9178
|
+
if (opts.aspectRatio && !ASPECT_RE.test(opts.aspectRatio)) {
|
|
9179
|
+
throw new ValidationError("--aspect-ratio must look like 16:9");
|
|
9180
|
+
}
|
|
9181
|
+
if (opts.outputFormat && !OUTPUT_FORMATS.includes(opts.outputFormat)) {
|
|
9182
|
+
throw new ValidationError(`--output-format must be one of: ${OUTPUT_FORMATS.join(", ")}`);
|
|
9183
|
+
}
|
|
9184
|
+
const extra = opts.input ? parseJsonInput(opts.input) : {};
|
|
9185
|
+
return {
|
|
9186
|
+
...extra,
|
|
9187
|
+
prompt,
|
|
9188
|
+
...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
|
|
9189
|
+
...opts.numOutputs ? { num_outputs: parsePositiveInt(opts.numOutputs, "--num-outputs", 10) } : {},
|
|
9190
|
+
...opts.seed !== void 0 ? { seed: parseSeed(opts.seed) } : {},
|
|
9191
|
+
...opts.outputFormat ? { output_format: opts.outputFormat } : {}
|
|
9192
|
+
};
|
|
9193
|
+
}
|
|
9194
|
+
function parseSeed(raw) {
|
|
9195
|
+
const n = Number(raw);
|
|
9196
|
+
if (!Number.isInteger(n) || n < 0)
|
|
9197
|
+
throw new ValidationError("--seed must be a non-negative integer");
|
|
9198
|
+
return n;
|
|
9199
|
+
}
|
|
9200
|
+
function registerReplicateImageCommand(parent) {
|
|
9201
|
+
const cmd = parent.command("image <prompt>").description(`Generate images via Replicate (default: ${replicateImageModel()})`).option("-m, --model <ref>", "Model: owner/name or owner/name:version").option("--aspect-ratio <ratio>", "Aspect ratio, e.g. 1:1, 16:9, 9:16").option("-n, --num-outputs <n>", "Number of images (model limits apply)").option("--seed <n>", "Seed for reproducible output").option("--output-format <fmt>", "Provider output format: webp | jpg | png").option("--input <json>", "Extra model-specific input fields as JSON or @file.json").addOption(imageFormatOption()).addOption(noWebpOption());
|
|
9202
|
+
addFlowOptions(cmd, 3e5).action(async (prompt, opts) => {
|
|
9203
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
9204
|
+
const model = parseModelRef(opts.model ?? replicateImageModel());
|
|
9205
|
+
const input = buildImageInput(prompt, opts);
|
|
9206
|
+
const client = createReplicateClient();
|
|
9207
|
+
await executePrediction2({
|
|
9208
|
+
client,
|
|
9209
|
+
model,
|
|
9210
|
+
input,
|
|
9211
|
+
opts,
|
|
9212
|
+
logger,
|
|
9213
|
+
finalize: (files) => finalizeGeneratedImages(files, {
|
|
9214
|
+
imageFormat: opts.imageFormat,
|
|
9215
|
+
webp: opts.webp,
|
|
9216
|
+
output: opts.output,
|
|
9217
|
+
logger,
|
|
9218
|
+
explicitProviderFormat: Boolean(opts.outputFormat)
|
|
9219
|
+
})
|
|
9220
|
+
});
|
|
9221
|
+
});
|
|
9222
|
+
}
|
|
9223
|
+
|
|
9224
|
+
// src/providers/replicate/node-input-files.ts
|
|
9225
|
+
init_errors();
|
|
9226
|
+
import fs44 from "fs";
|
|
9227
|
+
import path49 from "path";
|
|
9228
|
+
var DATA_URI_MAX_BYTES = 256 * 1024;
|
|
9229
|
+
var UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
9230
|
+
function formatMb2(bytes) {
|
|
9231
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
9232
|
+
}
|
|
9233
|
+
async function resolveFileInput(value, client, logger) {
|
|
9234
|
+
if (isMediaRef(value)) return value;
|
|
9235
|
+
const filePath = path49.resolve(value);
|
|
9236
|
+
let stat;
|
|
9237
|
+
try {
|
|
9238
|
+
stat = fs44.statSync(filePath);
|
|
9239
|
+
} catch {
|
|
9240
|
+
throw new ValidationError(`Input file not found: ${value} (pass a local path or an https URL)`);
|
|
9241
|
+
}
|
|
9242
|
+
if (!stat.isFile()) throw new ValidationError(`Input path is not a file: ${value}`);
|
|
9243
|
+
if (stat.size === 0) throw new ValidationError(`Input file is empty: ${value}`);
|
|
9244
|
+
if (stat.size >= UPLOAD_MAX_BYTES) {
|
|
9245
|
+
throw new ValidationError(
|
|
9246
|
+
`Input file ${value} is ${formatMb2(stat.size)}; Replicate accepts uploads under 100MB. Host it and pass an https URL instead.`
|
|
9247
|
+
);
|
|
9248
|
+
}
|
|
9249
|
+
const filename = path49.basename(filePath);
|
|
9250
|
+
const contentType = mimeFromFilename(filename);
|
|
9251
|
+
const bytes = fs44.readFileSync(filePath);
|
|
9252
|
+
if (stat.size <= DATA_URI_MAX_BYTES) {
|
|
9253
|
+
logger.debug(`Inlining ${filename} (${stat.size} bytes) as a data URI`);
|
|
9254
|
+
return `data:${contentType};base64,${bytes.toString("base64")}`;
|
|
9255
|
+
}
|
|
9256
|
+
logger.info(`Uploading ${filename} (${formatMb2(stat.size)}) to Replicate Files...`);
|
|
9257
|
+
const file = await client.uploadFile({
|
|
9258
|
+
data: new Blob([bytes], { type: contentType }),
|
|
9259
|
+
filename,
|
|
9260
|
+
contentType
|
|
9261
|
+
});
|
|
9262
|
+
logger.debug(`Uploaded ${filename} as file ${file.id}`);
|
|
9263
|
+
return file.urls.get;
|
|
9264
|
+
}
|
|
9265
|
+
function parseFileAssignments(values) {
|
|
9266
|
+
return (values ?? []).map((raw) => {
|
|
9267
|
+
const eq = raw.indexOf("=");
|
|
9268
|
+
const key = eq > 0 ? raw.slice(0, eq).trim() : "";
|
|
9269
|
+
const value = eq > 0 ? raw.slice(eq + 1).trim() : "";
|
|
9270
|
+
if (!key || !value || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
9271
|
+
throw new ValidationError(`Invalid --file "${raw}" \u2014 expected key=path (e.g. image=./a.png)`);
|
|
9272
|
+
}
|
|
9273
|
+
return [key, value];
|
|
9274
|
+
});
|
|
9275
|
+
}
|
|
9276
|
+
|
|
9277
|
+
// src/providers/replicate/commands/run.ts
|
|
9278
|
+
var collect8 = (value, previous = []) => [...previous, value];
|
|
9279
|
+
function registerReplicateRunCommand(parent) {
|
|
9280
|
+
const cmd = parent.command("run <model>").description(
|
|
9281
|
+
"Run any Replicate model: owner/name (official), owner/name:version, or a version id"
|
|
9282
|
+
).requiredOption("--input <json>", "Model input as JSON, or @path/to/input.json").option("--version <id>", "Explicit model version id (uses the versioned endpoint)").option(
|
|
9283
|
+
"--file <key=path>",
|
|
9284
|
+
"Set input <key> to a local file or URL (uploaded if needed); repeatable",
|
|
9285
|
+
collect8
|
|
9286
|
+
);
|
|
9287
|
+
addFlowOptions(cmd, 6e5).action(
|
|
9288
|
+
async (model, opts) => {
|
|
9289
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
9290
|
+
const ref = parseModelRef(model, opts.version);
|
|
9291
|
+
const input = parseJsonInput(opts.input);
|
|
9292
|
+
const files = parseFileAssignments(opts.file);
|
|
9293
|
+
const client = createReplicateClient();
|
|
9294
|
+
for (const [key, value] of files) {
|
|
9295
|
+
input[key] = await resolveFileInput(value, client, logger);
|
|
9296
|
+
}
|
|
9297
|
+
await executePrediction2({ client, model: ref, input, opts, logger });
|
|
9298
|
+
}
|
|
9299
|
+
);
|
|
9300
|
+
}
|
|
9301
|
+
|
|
9302
|
+
// src/providers/replicate/commands/status.ts
|
|
9303
|
+
function registerReplicateStatusCommand(parent) {
|
|
9304
|
+
parent.command("status <predictionId>").description("Get a Replicate prediction's status; --wait/--download to resume it").option("--wait", "Poll until the prediction reaches a terminal status").option("--download", "Download media outputs once succeeded").option("--output <path>", "Output file path, or a directory for multiple outputs").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "900000").option("-v, --verbose", "Verbose logging").action(async (predictionId, opts) => {
|
|
9305
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
9306
|
+
const client = createReplicateClient();
|
|
9307
|
+
if (opts.wait) {
|
|
9308
|
+
await completePrediction(client, predictionId, {
|
|
9309
|
+
waitTimeoutMs: parsePositiveInt(opts.waitTimeout, "--wait-timeout"),
|
|
9310
|
+
logger,
|
|
9311
|
+
download: Boolean(opts.download),
|
|
9312
|
+
output: opts.output
|
|
9313
|
+
});
|
|
9314
|
+
return;
|
|
9315
|
+
}
|
|
9316
|
+
const prediction2 = await client.getPrediction(predictionId);
|
|
9317
|
+
if (prediction2.status === "succeeded" && opts.download) {
|
|
9318
|
+
await reportOutputs(prediction2, { download: true, output: opts.output, logger });
|
|
9319
|
+
return;
|
|
9320
|
+
}
|
|
9321
|
+
console.log(JSON.stringify(summarizePrediction(prediction2), null, 2));
|
|
9322
|
+
if (!isTerminalStatus2(prediction2.status)) {
|
|
9323
|
+
logger.info(`Still running. Resume with: ${resumeCommand2(prediction2.id)}`);
|
|
9324
|
+
} else if (prediction2.status !== "succeeded") {
|
|
9325
|
+
throw new ReplicatePredictionError(prediction2);
|
|
9326
|
+
}
|
|
9327
|
+
});
|
|
9328
|
+
}
|
|
9329
|
+
|
|
9330
|
+
// src/providers/replicate/commands/video.ts
|
|
9331
|
+
init_errors();
|
|
9332
|
+
var ASPECT_RE2 = /^\d{1,2}:\d{1,2}$/;
|
|
9333
|
+
var RESOLUTION_RE = /^\d{3,4}p$/;
|
|
9334
|
+
function buildVideoInput(prompt, opts) {
|
|
9335
|
+
if (!prompt.trim()) throw new ValidationError("Prompt must not be empty");
|
|
9336
|
+
if (opts.aspectRatio && !ASPECT_RE2.test(opts.aspectRatio)) {
|
|
9337
|
+
throw new ValidationError("--aspect-ratio must look like 16:9");
|
|
9338
|
+
}
|
|
9339
|
+
if (opts.resolution && !RESOLUTION_RE.test(opts.resolution)) {
|
|
9340
|
+
throw new ValidationError("--resolution must look like 720p");
|
|
9341
|
+
}
|
|
9342
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(opts.imageKey)) {
|
|
9343
|
+
throw new ValidationError("--image-key must be an input field name such as image");
|
|
9344
|
+
}
|
|
9345
|
+
const extra = opts.input ? parseJsonInput(opts.input) : {};
|
|
9346
|
+
return {
|
|
9347
|
+
...extra,
|
|
9348
|
+
prompt,
|
|
9349
|
+
...opts.duration ? { duration: parsePositiveInt(opts.duration, "--duration", 60) } : {},
|
|
9350
|
+
...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
|
|
9351
|
+
...opts.resolution ? { resolution: opts.resolution } : {},
|
|
9352
|
+
...opts.seed !== void 0 ? { seed: parseSeed(opts.seed) } : {}
|
|
9353
|
+
};
|
|
9354
|
+
}
|
|
9355
|
+
function registerReplicateVideoCommand(parent) {
|
|
9356
|
+
const cmd = parent.command("video <prompt>").description(`Generate video via Replicate (default: ${replicateVideoModel()})`).option("-m, --model <ref>", "Model: owner/name or owner/name:version").option("--image <path|url>", "First-frame image (local files are inlined or uploaded)").option("--image-key <name>", "Input field that receives --image", "image").option("--duration <seconds>", "Duration in seconds (model limits apply)").option("--aspect-ratio <ratio>", "Aspect ratio, e.g. 16:9, 9:16").option("--resolution <res>", "Resolution, e.g. 720p, 1080p").option("--seed <n>", "Seed for reproducible output").option("--input <json>", "Extra model-specific input fields as JSON or @file.json");
|
|
9357
|
+
addFlowOptions(cmd, 9e5).action(async (prompt, opts) => {
|
|
9358
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
9359
|
+
const model = parseModelRef(opts.model ?? replicateVideoModel());
|
|
9360
|
+
const input = buildVideoInput(prompt, opts);
|
|
9361
|
+
const client = createReplicateClient();
|
|
9362
|
+
if (opts.image) input[opts.imageKey] = await resolveFileInput(opts.image, client, logger);
|
|
9363
|
+
await executePrediction2({ client, model, input, opts, logger });
|
|
9364
|
+
});
|
|
9365
|
+
}
|
|
9366
|
+
|
|
9367
|
+
// src/providers/replicate/commands/index.ts
|
|
9368
|
+
function registerReplicateCommands(program2) {
|
|
9369
|
+
const replicate = program2.command("replicate").description("Replicate: generic model runner, image, video, prediction status and cancel");
|
|
9370
|
+
registerReplicateRunCommand(replicate);
|
|
9371
|
+
registerReplicateImageCommand(replicate);
|
|
9372
|
+
registerReplicateVideoCommand(replicate);
|
|
9373
|
+
registerReplicateStatusCommand(replicate);
|
|
9374
|
+
registerReplicateCancelCommand(replicate);
|
|
9375
|
+
}
|
|
9376
|
+
|
|
9377
|
+
// src/commands/check.ts
|
|
9378
|
+
import fs45 from "fs";
|
|
8464
9379
|
import os4 from "os";
|
|
8465
9380
|
|
|
8466
9381
|
// src/commands/check-helpers/binary-check.ts
|
|
@@ -8518,6 +9433,7 @@ To configure multix, set at least one provider API key:
|
|
|
8518
9433
|
BytePlus : https://console.byteplus.com/auth/api-keys
|
|
8519
9434
|
fal.ai : https://fal.ai/dashboard/keys
|
|
8520
9435
|
Atlas Cloud : https://www.atlascloud.ai/console/api-keys
|
|
9436
|
+
Replicate : https://replicate.com/account/api-tokens
|
|
8521
9437
|
|
|
8522
9438
|
Option A \u2014 user global config (recommended):
|
|
8523
9439
|
echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
|
|
@@ -8592,6 +9508,13 @@ var PROVIDERS = [
|
|
|
8592
9508
|
envPrimary: "ATLASCLOUD_API_KEY",
|
|
8593
9509
|
optional: true,
|
|
8594
9510
|
link: "https://www.atlascloud.ai/console/api-keys"
|
|
9511
|
+
},
|
|
9512
|
+
{
|
|
9513
|
+
name: "Replicate",
|
|
9514
|
+
envPrimary: "REPLICATE_API_TOKEN",
|
|
9515
|
+
envFallback: "REPLICATE_API_KEY",
|
|
9516
|
+
optional: true,
|
|
9517
|
+
link: "https://replicate.com/account/api-tokens"
|
|
8595
9518
|
}
|
|
8596
9519
|
];
|
|
8597
9520
|
function resolveProviderKey(p) {
|
|
@@ -8599,13 +9522,9 @@ function resolveProviderKey(p) {
|
|
|
8599
9522
|
const first = listGeminiKeys()[0];
|
|
8600
9523
|
return first ? { key: first.value, envUsed: first.name } : {};
|
|
8601
9524
|
}
|
|
8602
|
-
const
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
const fb = resolveKey(p.envFallback);
|
|
8606
|
-
if (fb) return { key: fb, envUsed: p.envFallback };
|
|
8607
|
-
}
|
|
8608
|
-
return {};
|
|
9525
|
+
const names = p.envFallback ? [p.envPrimary, p.envFallback] : [p.envPrimary];
|
|
9526
|
+
const found = resolveFirstKey(...names);
|
|
9527
|
+
return found ? { key: found.value, envUsed: found.envUsed } : {};
|
|
8609
9528
|
}
|
|
8610
9529
|
function cloudflareReadiness() {
|
|
8611
9530
|
const accountId = resolveKey("CLOUDFLARE_ACCOUNT_ID")?.trim();
|
|
@@ -8647,7 +9566,7 @@ function registerCheckCommand(program2) {
|
|
|
8647
9566
|
}
|
|
8648
9567
|
if (!legacyClaudeEnvEnabled()) {
|
|
8649
9568
|
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os4.homedir();
|
|
8650
|
-
const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) =>
|
|
9569
|
+
const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) => fs45.existsSync(f));
|
|
8651
9570
|
if (legacy.length > 0) {
|
|
8652
9571
|
logger.info(
|
|
8653
9572
|
`Found ${legacy.length} legacy .claude env file(s); set MULTIX_LEGACY_CLAUDE_ENV=1 to load them`
|
|
@@ -8752,8 +9671,8 @@ function registerCheckCommand(program2) {
|
|
|
8752
9671
|
|
|
8753
9672
|
// src/commands/doc/convert.ts
|
|
8754
9673
|
init_errors();
|
|
8755
|
-
import
|
|
8756
|
-
import
|
|
9674
|
+
import fs46 from "fs";
|
|
9675
|
+
import path50 from "path";
|
|
8757
9676
|
|
|
8758
9677
|
// src/commands/doc/default-prompt.ts
|
|
8759
9678
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -8774,18 +9693,18 @@ async function convertDocuments(opts) {
|
|
|
8774
9693
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
8775
9694
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
8776
9695
|
for (const f of inputs) {
|
|
8777
|
-
if (!
|
|
9696
|
+
if (!fs46.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
8778
9697
|
}
|
|
8779
9698
|
let outputPath;
|
|
8780
9699
|
if (output) {
|
|
8781
|
-
outputPath =
|
|
9700
|
+
outputPath = path50.resolve(output);
|
|
8782
9701
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
8783
|
-
const base =
|
|
8784
|
-
outputPath =
|
|
9702
|
+
const base = path50.basename(inputs[0], path50.extname(inputs[0]));
|
|
9703
|
+
outputPath = path50.join(getOutputDir(), `${base}-extraction.md`);
|
|
8785
9704
|
} else {
|
|
8786
|
-
outputPath =
|
|
9705
|
+
outputPath = path50.join(getOutputDir(), "document-extraction.md");
|
|
8787
9706
|
}
|
|
8788
|
-
|
|
9707
|
+
fs46.mkdirSync(path50.dirname(outputPath), { recursive: true });
|
|
8789
9708
|
const results = [];
|
|
8790
9709
|
for (let i = 0; i < inputs.length; i++) {
|
|
8791
9710
|
const filePath = inputs[i];
|
|
@@ -8845,7 +9764,7 @@ async function convertDocuments(opts) {
|
|
|
8845
9764
|
`);
|
|
8846
9765
|
for (const r of results) {
|
|
8847
9766
|
lines.push(`
|
|
8848
|
-
## ${
|
|
9767
|
+
## ${path50.basename(r.file)}
|
|
8849
9768
|
`);
|
|
8850
9769
|
if (r.status === "success" && r.markdown) {
|
|
8851
9770
|
lines.push(`${r.markdown}
|
|
@@ -8858,7 +9777,7 @@ async function convertDocuments(opts) {
|
|
|
8858
9777
|
}
|
|
8859
9778
|
lines.push("\n---\n");
|
|
8860
9779
|
}
|
|
8861
|
-
|
|
9780
|
+
fs46.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
8862
9781
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
8863
9782
|
console.log(`
|
|
8864
9783
|
${"=".repeat(50)}`);
|
|
@@ -8906,12 +9825,12 @@ function registerDocCommands(program2) {
|
|
|
8906
9825
|
|
|
8907
9826
|
// src/commands/media/index.ts
|
|
8908
9827
|
init_errors();
|
|
8909
|
-
import
|
|
8910
|
-
import
|
|
9828
|
+
import fs49 from "fs";
|
|
9829
|
+
import path53 from "path";
|
|
8911
9830
|
|
|
8912
9831
|
// src/commands/media/batch.ts
|
|
8913
|
-
import
|
|
8914
|
-
import
|
|
9832
|
+
import fs47 from "fs";
|
|
9833
|
+
import path51 from "path";
|
|
8915
9834
|
|
|
8916
9835
|
// src/commands/media/detect-type.ts
|
|
8917
9836
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -9107,10 +10026,10 @@ async function batchOptimize(opts) {
|
|
|
9107
10026
|
verbose,
|
|
9108
10027
|
logger
|
|
9109
10028
|
} = opts;
|
|
9110
|
-
|
|
9111
|
-
const entries =
|
|
10029
|
+
fs47.mkdirSync(outputDir, { recursive: true });
|
|
10030
|
+
const entries = fs47.readdirSync(inputDir);
|
|
9112
10031
|
const mediaFiles = entries.filter((f) => {
|
|
9113
|
-
const ext =
|
|
10032
|
+
const ext = path51.extname(f).toLowerCase();
|
|
9114
10033
|
return ALL_EXTS.has(ext);
|
|
9115
10034
|
});
|
|
9116
10035
|
if (mediaFiles.length === 0) {
|
|
@@ -9121,9 +10040,9 @@ async function batchOptimize(opts) {
|
|
|
9121
10040
|
let succeeded = 0;
|
|
9122
10041
|
const errors = [];
|
|
9123
10042
|
for (const file of mediaFiles) {
|
|
9124
|
-
const inputPath =
|
|
9125
|
-
const outputPath =
|
|
9126
|
-
const ext =
|
|
10043
|
+
const inputPath = path51.join(inputDir, file);
|
|
10044
|
+
const outputPath = path51.join(outputDir, file);
|
|
10045
|
+
const ext = path51.extname(file).toLowerCase();
|
|
9127
10046
|
const kind = extToKind(ext);
|
|
9128
10047
|
logger?.debug(`Processing: ${file}`);
|
|
9129
10048
|
try {
|
|
@@ -9160,8 +10079,8 @@ async function batchOptimize(opts) {
|
|
|
9160
10079
|
|
|
9161
10080
|
// src/commands/media/split-video.ts
|
|
9162
10081
|
init_errors();
|
|
9163
|
-
import
|
|
9164
|
-
import
|
|
10082
|
+
import fs48 from "fs";
|
|
10083
|
+
import path52 from "path";
|
|
9165
10084
|
async function splitVideo(opts) {
|
|
9166
10085
|
await assertFfmpeg();
|
|
9167
10086
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -9172,10 +10091,10 @@ async function splitVideo(opts) {
|
|
|
9172
10091
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
9173
10092
|
return [input];
|
|
9174
10093
|
}
|
|
9175
|
-
|
|
9176
|
-
const ext =
|
|
9177
|
-
const baseName =
|
|
9178
|
-
const outputPattern =
|
|
10094
|
+
fs48.mkdirSync(outputDir, { recursive: true });
|
|
10095
|
+
const ext = path52.extname(input) || ".mp4";
|
|
10096
|
+
const baseName = path52.basename(input, ext);
|
|
10097
|
+
const outputPattern = path52.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
9179
10098
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
9180
10099
|
const args = [
|
|
9181
10100
|
"-i",
|
|
@@ -9194,8 +10113,8 @@ async function splitVideo(opts) {
|
|
|
9194
10113
|
await runFfmpeg(args, { verbose, logger });
|
|
9195
10114
|
const files = [];
|
|
9196
10115
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
9197
|
-
const fname =
|
|
9198
|
-
if (
|
|
10116
|
+
const fname = path52.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
10117
|
+
if (fs48.existsSync(fname)) files.push(fname);
|
|
9199
10118
|
}
|
|
9200
10119
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
9201
10120
|
return files;
|
|
@@ -9209,9 +10128,9 @@ function registerMediaCommands(program2) {
|
|
|
9209
10128
|
media.command("optimize").description("Optimize a single media file (video, audio, or image)").requiredOption("--input <file>", "Input media file").requiredOption("--output <file>", "Output file path").option("--target-size <MB>", "Target file size in MB").option("--quality <n>", "Quality: video CRF (0-51) or image (1-100)", "85").option("--max-width <px>", "Max image/video width in pixels", "1920").option("--bitrate <rate>", "Audio bitrate (e.g. 64k)", "64k").option("--resolution <WxH>", "Force video resolution (e.g. 1920x1080)").option("-v, --verbose", "Verbose logging").action(
|
|
9210
10129
|
async (opts) => {
|
|
9211
10130
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
9212
|
-
if (!
|
|
9213
|
-
|
|
9214
|
-
const ext =
|
|
10131
|
+
if (!fs49.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
10132
|
+
fs49.mkdirSync(path53.dirname(path53.resolve(opts.output)), { recursive: true });
|
|
10133
|
+
const ext = path53.extname(opts.input).toLowerCase();
|
|
9215
10134
|
const kind = extToKind(ext);
|
|
9216
10135
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
9217
10136
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -9251,7 +10170,7 @@ function registerMediaCommands(program2) {
|
|
|
9251
10170
|
media.command("split").description("Split a long video into fixed-duration chunks").requiredOption("--input <file>", "Input video file").option("--output-dir <dir>", "Output directory for chunks", "./chunks").option("--chunk-duration <sec>", "Chunk duration in seconds", "3600").option("-v, --verbose", "Verbose logging").action(
|
|
9252
10171
|
async (opts) => {
|
|
9253
10172
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
9254
|
-
if (!
|
|
10173
|
+
if (!fs49.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
9255
10174
|
const chunks = await splitVideo({
|
|
9256
10175
|
input: opts.input,
|
|
9257
10176
|
outputDir: opts.outputDir,
|
|
@@ -9267,7 +10186,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
9267
10186
|
media.command("batch").description("Batch optimize all media files in a directory").requiredOption("--input-dir <dir>", "Input directory").requiredOption("--output-dir <dir>", "Output directory").option("--quality <n>", "Quality setting", "85").option("--max-width <px>", "Max image/video width", "1920").option("--bitrate <rate>", "Audio bitrate", "64k").option("-v, --verbose", "Verbose logging").action(
|
|
9268
10187
|
async (opts) => {
|
|
9269
10188
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
9270
|
-
if (!
|
|
10189
|
+
if (!fs49.existsSync(opts.inputDir))
|
|
9271
10190
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
9272
10191
|
const result = await batchOptimize({
|
|
9273
10192
|
inputDir: opts.inputDir,
|
|
@@ -9448,6 +10367,7 @@ function registerCommands(program2) {
|
|
|
9448
10367
|
registerElevenLabsCommands(program2);
|
|
9449
10368
|
registerFalCommands(program2);
|
|
9450
10369
|
registerAtlasCloudCommands(program2);
|
|
10370
|
+
registerReplicateCommands(program2);
|
|
9451
10371
|
registerMediaCommands(program2);
|
|
9452
10372
|
registerDocCommands(program2);
|
|
9453
10373
|
}
|
|
@@ -9461,8 +10381,8 @@ loadEnv();
|
|
|
9461
10381
|
var require3 = createRequire2(import.meta.url);
|
|
9462
10382
|
var pkg = require3("../package.json");
|
|
9463
10383
|
var program = new Command().name("multix").description(
|
|
9464
|
-
"AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, ffmpeg, and ImageMagick"
|
|
9465
|
-
).version(pkg.version);
|
|
10384
|
+
"AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, Replicate, ffmpeg, and ImageMagick"
|
|
10385
|
+
).version(pkg.version).enablePositionalOptions();
|
|
9466
10386
|
registerCommands(program);
|
|
9467
10387
|
program.parseAsync(process.argv).catch((err2) => {
|
|
9468
10388
|
const msg = err2 instanceof Error ? err2.message : String(err2);
|