@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/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 path52 = SUBMIT_PATHS[kind];
859
- if (!path52) throw new ValidationError(`Unknown Atlas Cloud media type: ${String(kind)}`);
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}${path52}`,
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(path52, opts = {}) {
2185
+ async request(path54, opts = {}) {
2179
2186
  const base = this.baseUrl.replace(/\/$/, "");
2180
- const url = new URL(base + path52);
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(path52, query, logger) {
2199
- return this.request(path52, { method: "GET", query, logger });
2205
+ get(path54, query, logger) {
2206
+ return this.request(path54, { method: "GET", query, logger });
2200
2207
  }
2201
- post(path52, body, logger) {
2202
- return this.request(path52, { method: "POST", body, logger });
2208
+ post(path54, body, logger) {
2209
+ return this.request(path54, { method: "POST", body, logger });
2203
2210
  }
2204
- delete(path52, logger) {
2205
- return this.request(path52, { method: "DELETE", logger });
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: path52, role } = parseRefSpec(spec);
2675
- if (kind === "image") return toRef(kind, await resolveImageInput(path52, logger), role);
2676
- if (kind === "video") return toRef(kind, await resolveVideoInput(path52, logger), role);
2677
- return toRef(kind, await resolveAudioInput(path52, logger), role);
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(path52, opts = {}) {
4111
+ async request(path54, opts = {}) {
4105
4112
  const base = this.baseUrl.replace(/\/$/, "");
4106
- const suffix = path52.startsWith("/") ? path52 : `/${path52}`;
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(path52, logger) {
4124
- return this.request(path52, { method: "GET", logger });
4130
+ get(path54, logger) {
4131
+ return this.request(path54, { method: "GET", logger });
4125
4132
  }
4126
- post(path52, body, logger) {
4127
- return this.request(path52, { method: "POST", body, logger });
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(path52, opts = {}) {
6042
+ async request(path54, opts = {}) {
6036
6043
  const base = this.baseUrl.replace(/\/$/, "");
6037
- const v2Override = path52.startsWith("v2:");
6044
+ const v2Override = path54.startsWith("v2:");
6038
6045
  const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
6039
- const finalPath = v2Override ? path52.slice(3) : path52;
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(path52, query, logger) {
6059
- return this.request(path52, { method: "GET", query, logger });
6065
+ get(path54, query, logger) {
6066
+ return this.request(path54, { method: "GET", query, logger });
6060
6067
  }
6061
- post(path52, body, logger) {
6062
- return this.request(path52, { method: "POST", body, logger });
6068
+ post(path54, body, logger) {
6069
+ return this.request(path54, { method: "POST", body, logger });
6063
6070
  }
6064
- delete(path52, logger) {
6065
- return this.request(path52, { method: "DELETE", logger });
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: pipeline2 } = await import("stream/promises");
8293
- await pipeline2(r.body, ws);
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/commands/check.ts
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 primary = resolveKey(p.envPrimary);
8603
- if (primary) return { key: primary, envUsed: p.envPrimary };
8604
- if (p.envFallback) {
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) => fs43.existsSync(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 fs44 from "fs";
8756
- import path48 from "path";
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 (!fs44.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
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 = path48.resolve(output);
9700
+ outputPath = path50.resolve(output);
8782
9701
  } else if (autoName && inputs.length === 1 && inputs[0]) {
8783
- const base = path48.basename(inputs[0], path48.extname(inputs[0]));
8784
- outputPath = path48.join(getOutputDir(), `${base}-extraction.md`);
9702
+ const base = path50.basename(inputs[0], path50.extname(inputs[0]));
9703
+ outputPath = path50.join(getOutputDir(), `${base}-extraction.md`);
8785
9704
  } else {
8786
- outputPath = path48.join(getOutputDir(), "document-extraction.md");
9705
+ outputPath = path50.join(getOutputDir(), "document-extraction.md");
8787
9706
  }
8788
- fs44.mkdirSync(path48.dirname(outputPath), { recursive: true });
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
- ## ${path48.basename(r.file)}
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
- fs44.writeFileSync(outputPath, lines.join("\n"), "utf8");
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 fs47 from "fs";
8910
- import path51 from "path";
9828
+ import fs49 from "fs";
9829
+ import path53 from "path";
8911
9830
 
8912
9831
  // src/commands/media/batch.ts
8913
- import fs45 from "fs";
8914
- import path49 from "path";
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
- fs45.mkdirSync(outputDir, { recursive: true });
9111
- const entries = fs45.readdirSync(inputDir);
10029
+ fs47.mkdirSync(outputDir, { recursive: true });
10030
+ const entries = fs47.readdirSync(inputDir);
9112
10031
  const mediaFiles = entries.filter((f) => {
9113
- const ext = path49.extname(f).toLowerCase();
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 = path49.join(inputDir, file);
9125
- const outputPath = path49.join(outputDir, file);
9126
- const ext = path49.extname(file).toLowerCase();
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 fs46 from "fs";
9164
- import path50 from "path";
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
- fs46.mkdirSync(outputDir, { recursive: true });
9176
- const ext = path50.extname(input) || ".mp4";
9177
- const baseName = path50.basename(input, ext);
9178
- const outputPattern = path50.join(outputDir, `${baseName}_chunk_%03d${ext}`);
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 = path50.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
9198
- if (fs46.existsSync(fname)) files.push(fname);
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 (!fs47.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
9213
- fs47.mkdirSync(path51.dirname(path51.resolve(opts.output)), { recursive: true });
9214
- const ext = path51.extname(opts.input).toLowerCase();
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 (!fs47.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
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 (!fs47.existsSync(opts.inputDir))
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);