@mrgoonie/multix 0.9.0 → 0.11.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
@@ -99,12 +99,1923 @@ var init_errors = __esm({
99
99
  import { createRequire as createRequire2 } from "module";
100
100
  import { Command } from "commander";
101
101
 
102
+ // src/providers/atlascloud/commands/audio.ts
103
+ init_errors();
104
+
105
+ // src/providers/atlascloud/model-input.ts
106
+ init_errors();
107
+ var SUBMIT_PATH_RE = /\/model\/generate(Image|Video|Audio)$/;
108
+ function resolveRef(doc, ref) {
109
+ if (typeof ref !== "string" || !ref.startsWith("#/")) return void 0;
110
+ let node = doc;
111
+ for (const part of ref.slice(2).split("/")) {
112
+ node = node?.[part];
113
+ if (node === void 0) return void 0;
114
+ }
115
+ return node;
116
+ }
117
+ function parseModelSpec(model, doc) {
118
+ if (!doc || typeof doc !== "object") return void 0;
119
+ const d = doc;
120
+ let endpoint2;
121
+ let input;
122
+ for (const [p, ops] of Object.entries(d.paths ?? {})) {
123
+ const m = SUBMIT_PATH_RE.exec(p);
124
+ if (!m?.[1] || !ops?.post) continue;
125
+ endpoint2 = m[1].toLowerCase();
126
+ const schema = ops.post.requestBody?.content?.["application/json"]?.schema;
127
+ input = schema?.$ref ? resolveRef(d, schema.$ref) : schema;
128
+ break;
129
+ }
130
+ input ??= d.components?.schemas?.Input;
131
+ if (!input || typeof input.properties !== "object") return void 0;
132
+ return {
133
+ model,
134
+ endpoint: endpoint2,
135
+ properties: input.properties,
136
+ required: Array.isArray(input.required) ? input.required.filter((r) => typeof r === "string") : []
137
+ };
138
+ }
139
+ var FIELD_CANDIDATES = {
140
+ prompt: ["prompt"],
141
+ negativePrompt: ["negative_prompt"],
142
+ seed: ["seed"],
143
+ size: ["size", "image_size"],
144
+ aspectRatio: ["aspect_ratio"],
145
+ resolution: ["resolution"],
146
+ duration: ["duration"],
147
+ numImages: ["num_images"],
148
+ editImages: [
149
+ "images",
150
+ "image",
151
+ "image_urls",
152
+ "reference_images",
153
+ "image_url",
154
+ "reference_image_urls"
155
+ ],
156
+ primaryImage: [
157
+ "image",
158
+ "image_url",
159
+ "images",
160
+ "image_urls",
161
+ "reference_images",
162
+ "reference_image_urls"
163
+ ],
164
+ speechText: ["text", "prompt"],
165
+ audioInput: ["audio_url", "audio"],
166
+ voice: ["voice_id", "voice"],
167
+ audioFormat: ["format", "codec", "output_format"],
168
+ language: ["language"],
169
+ modelFormat: ["format", "output_format", "target_formats"]
170
+ };
171
+ var FALLBACK_FIELD_TYPES = {
172
+ seed: "integer",
173
+ num_images: "integer",
174
+ duration: "number",
175
+ images: "array"
176
+ };
177
+ function coerce(flag, field, prop, value) {
178
+ const type = prop.type;
179
+ if (Array.isArray(value)) {
180
+ if (type === "array") return value;
181
+ if (value.length === 1) return coerceScalar(flag, field, type, value[0]);
182
+ throw new ValidationError(
183
+ `${flag}: field "${field}" of this model accepts a single value, got ${value.length}`
184
+ );
185
+ }
186
+ if (type === "array") return [coerceScalar(flag, field, prop.items?.type, value)];
187
+ return coerceScalar(flag, field, type, value);
188
+ }
189
+ function coerceScalar(flag, field, type, value) {
190
+ if (type === "integer" || type === "number") {
191
+ const n = typeof value === "number" ? value : Number(String(value).trim());
192
+ if (!Number.isFinite(n) || type === "integer" && !Number.isInteger(n)) {
193
+ throw new ValidationError(`${flag}: "${value}" is not a valid ${type} for "${field}"`);
194
+ }
195
+ return n;
196
+ }
197
+ if (type === "boolean") {
198
+ if (typeof value === "boolean") return value;
199
+ const s = String(value).toLowerCase();
200
+ if (s === "true") return true;
201
+ if (s === "false") return false;
202
+ throw new ValidationError(`${flag}: "${value}" is not a boolean for "${field}"`);
203
+ }
204
+ if (type === "string") return String(value);
205
+ return value;
206
+ }
207
+ function checkConstraints(flag, field, prop, v) {
208
+ const values = Array.isArray(v) ? v : [v];
209
+ if (Array.isArray(prop.enum) && prop.enum.length > 0 && !Array.isArray(v)) {
210
+ if (!prop.enum.some((e) => e === v || String(e) === String(v))) {
211
+ throw new ValidationError(
212
+ `${flag}: "${String(v)}" is not supported by this model. Valid ${field}: ${prop.enum.map(String).join(", ")}`
213
+ );
214
+ }
215
+ }
216
+ for (const item of values) {
217
+ if (typeof item !== "number") continue;
218
+ if (prop.minimum !== void 0 && item < prop.minimum) {
219
+ throw new ValidationError(
220
+ `${flag}: ${item} is below the minimum ${prop.minimum} for "${field}"`
221
+ );
222
+ }
223
+ if (prop.maximum !== void 0 && item > prop.maximum) {
224
+ throw new ValidationError(
225
+ `${flag}: ${item} exceeds the maximum ${prop.maximum} for "${field}"`
226
+ );
227
+ }
228
+ }
229
+ if (Array.isArray(v) && prop.maxItems !== void 0 && v.length > prop.maxItems) {
230
+ throw new ValidationError(
231
+ `${flag}: this model accepts at most ${prop.maxItems} item(s) for "${field}", got ${v.length}`
232
+ );
233
+ }
234
+ }
235
+ function pickField(spec, req) {
236
+ const declared = req.candidates.filter((c2) => spec.properties[c2] !== void 0);
237
+ const compatible = declared.filter((c2) => {
238
+ if (spec.properties[c2]?.type !== "boolean") return true;
239
+ const v = req.value;
240
+ return typeof v === "boolean" || typeof v === "string" && /^(true|false)$/i.test(v);
241
+ });
242
+ if (Array.isArray(req.value) && req.value.length > 1) {
243
+ return compatible.find((c2) => spec.properties[c2]?.type === "array") ?? compatible[0];
244
+ }
245
+ return compatible[0];
246
+ }
247
+ function buildModelInput(spec, requests, base = {}) {
248
+ const body = { ...base };
249
+ for (const req of requests) {
250
+ if (req.value === void 0 || Array.isArray(req.value) && req.value.length === 0) continue;
251
+ if (!spec) {
252
+ const field2 = req.candidates[0];
253
+ if (field2) {
254
+ const type = FALLBACK_FIELD_TYPES[field2] ?? "string";
255
+ body[field2] = coerce(req.flag, field2, { type }, req.value);
256
+ }
257
+ continue;
258
+ }
259
+ const field = pickField(spec, req);
260
+ if (!field) {
261
+ throw new ValidationError(
262
+ `Model ${spec.model} does not accept ${req.flag}. Its inputs: ${listInputs(spec)}. Use \`multix atlas run ${spec.model} --input '{...}'\` for model-specific fields.`
263
+ );
264
+ }
265
+ const prop = spec.properties[field];
266
+ const value = coerce(req.flag, field, prop, req.value);
267
+ checkConstraints(req.flag, field, prop, value);
268
+ body[field] = value;
269
+ }
270
+ if (spec) finalizeAgainstSpec(spec, body, requests);
271
+ return body;
272
+ }
273
+ function listInputs(spec) {
274
+ return Object.keys(spec.properties).filter((k) => k !== "model").join(", ");
275
+ }
276
+ var CONTENT_FIELDS = /* @__PURE__ */ new Set(["prompt", "text", "lyrics"]);
277
+ function finalizeAgainstSpec(spec, body, requests = []) {
278
+ const missing = [];
279
+ for (const field of spec.required) {
280
+ if (field === "model" || body[field] !== void 0) continue;
281
+ const def = spec.properties[field]?.default;
282
+ if (def !== void 0 && def !== "" && !CONTENT_FIELDS.has(field)) {
283
+ body[field] = def;
284
+ continue;
285
+ }
286
+ const flag = requests.find((r) => r.candidates.includes(field))?.flag;
287
+ missing.push(flag ? `${field} (pass ${flag})` : field);
288
+ }
289
+ if (missing.length > 0) {
290
+ throw new ValidationError(`Model ${spec.model} requires: ${missing.join(", ")}`);
291
+ }
292
+ }
293
+ function validateRawInput(spec, body) {
294
+ const unknown = [];
295
+ for (const [field, value] of Object.entries(body)) {
296
+ if (field === "model") continue;
297
+ const prop = spec.properties[field];
298
+ if (!prop) {
299
+ unknown.push(field);
300
+ continue;
301
+ }
302
+ checkConstraints(`--input.${field}`, field, prop, value);
303
+ }
304
+ finalizeAgainstSpec(spec, body);
305
+ return unknown;
306
+ }
307
+
308
+ // src/providers/atlascloud/models.ts
309
+ var DEFAULT_ATLAS_BASE_URL = "https://api.atlascloud.ai/api/v1";
310
+ var SUBMIT_PATHS = {
311
+ image: "/model/generateImage",
312
+ video: "/model/generateVideo",
313
+ audio: "/model/generateAudio"
314
+ };
315
+ var ATLAS_MEDIA_KINDS = ["image", "video", "audio"];
316
+ var ATLAS_DEFAULT_MODELS = {
317
+ /** FLUX.1 [schnell], $0.003/image — fastest, cheapest text-to-image. */
318
+ image: "black-forest-labs/flux-schnell",
319
+ /** GPT Image 1 Mini edit — cheapest image edit; takes `images[]`. */
320
+ imageEdit: "openai/gpt-image-1-mini/edit",
321
+ /** Seedance 1.0 Pro Fast — low-cost text-to-video, 2-12s, 480p-1080p. */
322
+ video: "bytedance/seedance-v1-pro-fast/text-to-video",
323
+ /** Matching image-to-video model; takes a single `image`. */
324
+ imageToVideo: "bytedance/seedance-v1-pro-fast/image-to-video",
325
+ /** xAI TTS v1 — cheapest text-to-speech. */
326
+ tts: "xai/tts-v1",
327
+ /** Seed ASR 2.0 — documented speech-to-text example; returns text. */
328
+ stt: "bytedance/seed-asr-2.0",
329
+ /** Hunyuan 3D Rapid — low-cost text-to-3D (GLB by default). */
330
+ textTo3d: "tencent/hunyuan3d-rapid/text-to-3d",
331
+ /** Hunyuan 3D Rapid — low-cost image-to-3D (GLB by default). */
332
+ imageTo3d: "tencent/hunyuan3d-rapid/image-to-3d"
333
+ };
334
+
335
+ // src/providers/atlascloud/commands/env.ts
336
+ import { openAsBlob } from "fs";
337
+ import fs2 from "fs/promises";
338
+ import path2 from "path";
339
+
340
+ // src/core/env-loader.ts
341
+ init_redact();
342
+ import fs from "fs";
343
+ import os from "os";
344
+ import path from "path";
345
+ import { config as dotenvConfig, parse as dotenvParse } from "dotenv";
346
+ var loaded = false;
347
+ var loadedFiles = [];
348
+ var LEGACY_SKILL_DIRS = ["ai-multimodal", "ak-ai-multimodal"];
349
+ function legacyFilesUnder(root) {
350
+ const claude = path.join(root, ".claude");
351
+ return [
352
+ ...LEGACY_SKILL_DIRS.map((dir) => path.join(claude, "skills", dir, ".env")),
353
+ path.join(claude, "skills", ".env"),
354
+ path.join(claude, ".env")
355
+ ];
356
+ }
357
+ function samePath(a, b) {
358
+ const norm2 = (p) => {
359
+ const r = path.resolve(p);
360
+ return process.platform === "win32" ? r.toLowerCase() : r;
361
+ };
362
+ return norm2(a) === norm2(b);
363
+ }
364
+ function legacyClaudeEnvFiles(cwd, home) {
365
+ const files = legacyFilesUnder(cwd);
366
+ if (home && !samePath(home, cwd)) files.push(...legacyFilesUnder(home));
367
+ return files;
368
+ }
369
+ function legacyClaudeEnvEnabled(env = process.env) {
370
+ const value = env.MULTIX_LEGACY_CLAUDE_ENV;
371
+ return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
372
+ }
373
+ var LEGACY_ALLOWED_NAME = /^[A-Z][A-Z0-9_]*_(API_KEY(_\d+)?|API_TOKEN|ACCOUNT_ID|GATEWAY_ID)$|^(FAL_KEY|ARK_API_KEY)$/;
374
+ function isLegacyAllowedName(name) {
375
+ return LEGACY_ALLOWED_NAME.test(name);
376
+ }
377
+ function isReadableFile(file) {
378
+ try {
379
+ return fs.statSync(file).isFile();
380
+ } catch {
381
+ return false;
382
+ }
383
+ }
384
+ function loadFile(file) {
385
+ if (!isReadableFile(file)) return;
386
+ const result = dotenvConfig({ path: file, override: false });
387
+ if (!result.error) loadedFiles.push(file);
388
+ }
389
+ function loadLegacyFile(file) {
390
+ if (!isReadableFile(file)) return;
391
+ let parsed;
392
+ try {
393
+ parsed = dotenvParse(fs.readFileSync(file));
394
+ } catch {
395
+ return;
396
+ }
397
+ for (const [name, value] of Object.entries(parsed)) {
398
+ if (isLegacyAllowedName(name) && process.env[name] === void 0) process.env[name] = value;
399
+ }
400
+ loadedFiles.push(file);
401
+ }
402
+ function loadEnv() {
403
+ if (loaded) return;
404
+ loaded = true;
405
+ loadedFiles = [];
406
+ const cwd = process.cwd();
407
+ const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os.homedir();
408
+ loadFile(path.resolve(cwd, ".env"));
409
+ if (home) loadFile(path.join(home, ".multix", ".env"));
410
+ if (legacyClaudeEnvEnabled()) {
411
+ for (const file of legacyClaudeEnvFiles(cwd, home)) loadLegacyFile(file);
412
+ }
413
+ }
414
+ function getLoadedEnvFiles() {
415
+ return [...loadedFiles];
416
+ }
417
+ function resolveKey(name) {
418
+ return process.env[name] || void 0;
419
+ }
420
+
421
+ // src/providers/atlascloud/commands/env.ts
422
+ init_errors();
423
+
424
+ // src/core/logger.ts
425
+ var ANSI = {
426
+ reset: "\x1B[0m",
427
+ bold: "\x1B[1m",
428
+ green: "\x1B[32m",
429
+ yellow: "\x1B[33m",
430
+ red: "\x1B[31m",
431
+ blue: "\x1B[34m",
432
+ cyan: "\x1B[36m",
433
+ dim: "\x1B[2m"
434
+ };
435
+ function c(color, text) {
436
+ if (process.env.NO_COLOR || !process.stdout.isTTY) return text;
437
+ return `${color}${text}${ANSI.reset}`;
438
+ }
439
+ function createLogger(opts) {
440
+ const { verbose } = opts;
441
+ return {
442
+ info(msg) {
443
+ console.log(c(ANSI.blue, ` ${msg}`));
444
+ },
445
+ success(msg) {
446
+ console.log(c(ANSI.green, `\u2713 ${msg}`));
447
+ },
448
+ warn(msg) {
449
+ console.warn(c(ANSI.yellow, `\u26A0 ${msg}`));
450
+ },
451
+ error(msg) {
452
+ console.error(c(ANSI.red, `\u2717 ${msg}`));
453
+ },
454
+ debug(msg) {
455
+ if (verbose) console.log(c(ANSI.dim, ` [debug] ${msg}`));
456
+ },
457
+ header(msg) {
458
+ const line = "=".repeat(60);
459
+ console.log(c(ANSI.bold + ANSI.cyan, `
460
+ ${line}
461
+ ${msg}
462
+ ${line}
463
+ `));
464
+ }
465
+ };
466
+ }
467
+ var defaultLogger = createLogger({ verbose: false });
468
+
469
+ // src/providers/atlascloud/commands/env.ts
470
+ init_redact();
471
+
472
+ // src/core/async-poll.ts
473
+ init_errors();
474
+ var PollTimeoutError = class extends MultixError {
475
+ constructor(message, lastStatus) {
476
+ super(message, "POLL_TIMEOUT");
477
+ this.lastStatus = lastStatus;
478
+ this.name = "PollTimeoutError";
479
+ }
480
+ lastStatus;
481
+ };
482
+ function isTransientHttpError(error) {
483
+ if (!(error instanceof HttpError)) return false;
484
+ return error.status === 0 || error.status === 429 || error.status >= 500;
485
+ }
486
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
487
+ async function pollUntil(opts) {
488
+ const now = opts.now ?? Date.now;
489
+ const sleep5 = opts.sleep ?? defaultSleep;
490
+ const random = opts.random ?? Math.random;
491
+ const isTransient = opts.isTransientError ?? isTransientHttpError;
492
+ const maxInterval = opts.maxIntervalMs ?? 15e3;
493
+ const minInterval = opts.minIntervalMs ?? 500;
494
+ const factor = opts.backoffFactor ?? 1.5;
495
+ const jitter = opts.jitter ?? true;
496
+ const deadline = now() + opts.timeoutMs;
497
+ let interval = opts.intervalMs ?? 3e3;
498
+ let attempt = 0;
499
+ let last;
500
+ let lastError;
501
+ for (; ; ) {
502
+ attempt++;
503
+ let retryAfterMs;
504
+ try {
505
+ last = await opts.fetchStatus({ remainingMs: Math.max(1, deadline - now()), attempt });
506
+ lastError = void 0;
507
+ opts.onPoll?.(last, attempt);
508
+ if (opts.isTerminal(last)) return last;
509
+ } catch (error) {
510
+ if (!isTransient(error)) throw error;
511
+ lastError = error;
512
+ if (error instanceof HttpError) retryAfterMs = error.retryAfterMs;
513
+ }
514
+ const remaining = deadline - now();
515
+ if (remaining <= 0) {
516
+ const hint = opts.resumeHint ? ` ${opts.resumeHint}` : "";
517
+ const cause = lastError instanceof Error ? ` Last error: ${lastError.message}.` : "";
518
+ throw new PollTimeoutError(
519
+ `Timed out after ${opts.timeoutMs}ms waiting for job.${cause}${hint}`,
520
+ last
521
+ );
522
+ }
523
+ let delay = retryAfterMs ?? interval;
524
+ if (jitter && retryAfterMs === void 0) delay *= 0.8 + random() * 0.4;
525
+ await sleep5(Math.min(Math.max(delay, minInterval), remaining));
526
+ interval = Math.min(interval * factor, maxInterval);
527
+ }
528
+ }
529
+
530
+ // src/providers/atlascloud/client.ts
531
+ init_errors();
532
+
533
+ // src/core/fetch-json.ts
534
+ init_errors();
535
+ var DEFAULT_TIMEOUT_MS = 12e4;
536
+ var SNIPPET_MAX = 500;
537
+ function parseRetryAfter(value, now = Date.now()) {
538
+ if (!value) return void 0;
539
+ const trimmed = value.trim();
540
+ if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1e3);
541
+ const date = Date.parse(trimmed);
542
+ if (Number.isNaN(date)) return void 0;
543
+ return Math.max(0, date - now);
544
+ }
545
+ async function httpErrorFromResponse(response, url) {
546
+ let snippet = "";
547
+ try {
548
+ const text = await response.text();
549
+ snippet = text.slice(0, SNIPPET_MAX);
550
+ } catch {
551
+ snippet = "(could not read body)";
552
+ }
553
+ return new HttpError(
554
+ response.status,
555
+ snippet,
556
+ url,
557
+ parseRetryAfter(response.headers.get("retry-after"))
558
+ );
559
+ }
560
+ async function fetchJson(opts) {
561
+ const { url, method = "GET", headers: headers2 = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
562
+ const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
563
+ const init = {
564
+ method,
565
+ headers: {
566
+ "Content-Type": "application/json",
567
+ ...headers2
568
+ },
569
+ signal: AbortSignal.timeout(timeoutMs)
570
+ };
571
+ if (body !== void 0) {
572
+ init.body = JSON.stringify(body);
573
+ }
574
+ let response;
575
+ try {
576
+ response = await doFetch(url, init);
577
+ } catch (cause) {
578
+ const msg = cause instanceof Error ? cause.message : String(cause);
579
+ throw new HttpError(0, msg, url);
580
+ }
581
+ if (!response.ok) {
582
+ throw await httpErrorFromResponse(response, url);
583
+ }
584
+ let text;
585
+ try {
586
+ text = await response.text();
587
+ } catch (cause) {
588
+ const msg = cause instanceof Error ? cause.message : String(cause);
589
+ throw new HttpError(0, `Failed to read response body: ${msg}`, url);
590
+ }
591
+ if (text.trim() === "") return void 0;
592
+ try {
593
+ return JSON.parse(text);
594
+ } catch {
595
+ throw new HttpError(
596
+ response.status,
597
+ `Invalid JSON response: ${text.slice(0, SNIPPET_MAX)}`,
598
+ url
599
+ );
600
+ }
601
+ }
602
+
603
+ // src/providers/atlascloud/prediction-output.ts
604
+ var SUCCESS_STATUSES = /* @__PURE__ */ new Set(["completed", "succeeded", "success"]);
605
+ var FAILURE_STATUSES = /* @__PURE__ */ new Set(["failed", "error", "canceled", "cancelled", "timeout"]);
606
+ function norm(status) {
607
+ return typeof status === "string" ? status.trim().toLowerCase() : "";
608
+ }
609
+ function isSuccessStatus(status) {
610
+ return SUCCESS_STATUSES.has(norm(status));
611
+ }
612
+ function isFailureStatus(status) {
613
+ return FAILURE_STATUSES.has(norm(status));
614
+ }
615
+ function isTerminalStatus(status) {
616
+ return isSuccessStatus(status) || isFailureStatus(status);
617
+ }
618
+ function predictionErrorMessage(error) {
619
+ if (error === null || error === void 0) return "";
620
+ if (typeof error === "string") return error.trim();
621
+ if (typeof error === "number" || typeof error === "boolean") return String(error);
622
+ if (typeof error === "object") {
623
+ const obj = error;
624
+ for (const key of ["message", "msg", "error", "detail", "reason"]) {
625
+ const v = obj[key];
626
+ if (typeof v === "string" && v.trim()) return v.trim();
627
+ }
628
+ try {
629
+ return JSON.stringify(error);
630
+ } catch {
631
+ return "";
632
+ }
633
+ }
634
+ return "";
635
+ }
636
+ function collectOutputs(prediction2) {
637
+ const raw = prediction2.outputs ?? prediction2.output;
638
+ const list = Array.isArray(raw) ? raw : raw ? [raw] : [];
639
+ const out = [];
640
+ for (const item of list) {
641
+ if (typeof item === "string") {
642
+ if (item.trim()) out.push(item);
643
+ continue;
644
+ }
645
+ if (item && typeof item === "object") {
646
+ const obj = item;
647
+ const url = ["url", "download_url", "audio_url", "video_url"].map((k) => obj[k]).find((v) => typeof v === "string" && v.length > 0);
648
+ out.push(url ?? JSON.stringify(item));
649
+ }
650
+ }
651
+ return out;
652
+ }
653
+ function isMediaOutput(value) {
654
+ const s = value.trim();
655
+ return /^https?:\/\//i.test(s) || /^data:[^,]*,/i.test(s);
656
+ }
657
+ var EXT_CATEGORY = {
658
+ png: "image",
659
+ jpg: "image",
660
+ jpeg: "image",
661
+ webp: "image",
662
+ gif: "image",
663
+ bmp: "image",
664
+ avif: "image",
665
+ mp4: "video",
666
+ webm: "video",
667
+ mov: "video",
668
+ mkv: "video",
669
+ avi: "video",
670
+ mp3: "audio",
671
+ wav: "audio",
672
+ aac: "audio",
673
+ m4a: "audio",
674
+ flac: "audio",
675
+ ogg: "audio",
676
+ opus: "audio",
677
+ pcm: "audio",
678
+ glb: "3d",
679
+ gltf: "3d",
680
+ obj: "3d",
681
+ fbx: "3d",
682
+ stl: "3d",
683
+ ply: "3d",
684
+ usdz: "3d",
685
+ usdc: "3d",
686
+ usd: "3d",
687
+ zip: "3d"
688
+ };
689
+ var MIME_EXT = {
690
+ "image/png": "png",
691
+ "image/jpeg": "jpg",
692
+ "image/webp": "webp",
693
+ "image/gif": "gif",
694
+ "video/mp4": "mp4",
695
+ "video/webm": "webm",
696
+ "audio/mpeg": "mp3",
697
+ "audio/mp3": "mp3",
698
+ "audio/wav": "wav",
699
+ "audio/x-wav": "wav",
700
+ "audio/ogg": "ogg",
701
+ "audio/flac": "flac",
702
+ "model/gltf-binary": "glb",
703
+ "video/quicktime": "mov",
704
+ "audio/aac": "aac",
705
+ "audio/mp4": "m4a",
706
+ "application/zip": "zip"
707
+ };
708
+ function extensionForContentType(contentType) {
709
+ const mime = contentType?.split(";")[0]?.trim().toLowerCase();
710
+ return mime ? MIME_EXT[mime] : void 0;
711
+ }
712
+ function outputExtension(value) {
713
+ const s = value.trim();
714
+ const dataMatch = /^data:([^;,]+)/i.exec(s);
715
+ if (dataMatch?.[1]) return MIME_EXT[dataMatch[1].toLowerCase()];
716
+ let pathname;
717
+ try {
718
+ pathname = new URL(s).pathname;
719
+ } catch {
720
+ pathname = s.split(/[?#]/)[0] ?? s;
721
+ }
722
+ const m = /\.([a-z0-9]{2,5})$/i.exec(pathname);
723
+ if (!m?.[1]) return void 0;
724
+ const ext = m[1].toLowerCase();
725
+ return ext === "jpeg" ? "jpg" : ext;
726
+ }
727
+ function outputCategory(value) {
728
+ const ext = outputExtension(value);
729
+ return ext ? EXT_CATEGORY[ext] ?? "unknown" : "unknown";
730
+ }
731
+ function defaultExtensionFor(category) {
732
+ switch (category) {
733
+ case "image":
734
+ return "png";
735
+ case "video":
736
+ return "mp4";
737
+ case "audio":
738
+ return "mp3";
739
+ case "3d":
740
+ return "glb";
741
+ case "text":
742
+ return "txt";
743
+ default:
744
+ return "bin";
745
+ }
746
+ }
747
+ function classifyOutputs(prediction2) {
748
+ const outputs = collectOutputs(prediction2);
749
+ if (outputs.length === 0) return { mode: "empty" };
750
+ const urls = outputs.filter(isMediaOutput);
751
+ const texts = outputs.filter((o) => !isMediaOutput(o));
752
+ if (urls.length === 0) return { mode: "text", texts };
753
+ return { mode: "media", urls, texts };
754
+ }
755
+ function hasNsfwFlag(prediction2) {
756
+ return Array.isArray(prediction2.has_nsfw_contents) && prediction2.has_nsfw_contents.some(Boolean);
757
+ }
758
+ function outputFileName(predictionId, index, ext) {
759
+ const prefix = predictionId.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 8) || "output";
760
+ return `atlas-${prefix}-${String(index + 1).padStart(2, "0")}.${ext}`;
761
+ }
762
+
763
+ // src/providers/atlascloud/client.ts
764
+ var ATLAS_PROVIDER = "atlascloud";
765
+ var DEFAULT_UPLOAD_TIMEOUT_MS = 3e5;
766
+ var CATALOG_TIMEOUT_MS = 15e3;
767
+ var PREDICTION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
768
+ var AtlasPredictionFailedError = class extends ProviderError {
769
+ constructor(prediction2) {
770
+ const reason = predictionErrorMessage(prediction2.error) || `status "${prediction2.status}"`;
771
+ super(`Atlas Cloud prediction ${prediction2.id} failed: ${reason}`, ATLAS_PROVIDER);
772
+ this.prediction = prediction2;
773
+ this.name = "AtlasPredictionFailedError";
774
+ }
775
+ prediction;
776
+ };
777
+ function assertValidPredictionId(id) {
778
+ if (!PREDICTION_ID_RE.test(id)) {
779
+ throw new ValidationError(`Invalid Atlas Cloud prediction id: "${id}"`);
780
+ }
781
+ }
782
+ function assertPredictionSucceeded(prediction2) {
783
+ if (isFailureStatus(prediction2.status)) throw new AtlasPredictionFailedError(prediction2);
784
+ if (!isSuccessStatus(prediction2.status)) {
785
+ throw new ProviderError(
786
+ `Atlas Cloud prediction ${prediction2.id} is not finished (status "${prediction2.status}")`,
787
+ ATLAS_PROVIDER
788
+ );
789
+ }
790
+ }
791
+ function terminalPredictionFromError(error, id) {
792
+ if (!(error instanceof HttpError) || error.status < 500) return null;
793
+ const snippet = error.snippet;
794
+ try {
795
+ const parsed = JSON.parse(snippet);
796
+ const data = parsed?.data;
797
+ if (data && typeof data === "object" && isTerminalStatus(data.status)) {
798
+ return { ...data, id: data.id || id };
799
+ }
800
+ return null;
801
+ } catch {
802
+ }
803
+ const dataAt = snippet.search(/"data"\s*:\s*\{/);
804
+ if (dataAt === -1) return null;
805
+ const inData = snippet.slice(dataAt);
806
+ const status = /"status"\s*:\s*"([^"]+)"/.exec(inData)?.[1];
807
+ if (status?.toLowerCase() !== "failed") return null;
808
+ const err2 = /"error"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(inData)?.[1];
809
+ return { id, status, error: err2 ?? snippet };
810
+ }
811
+ function envelopeData(env, what) {
812
+ if (!env || typeof env !== "object") {
813
+ throw new ProviderError(`Atlas Cloud ${what}: empty response`, ATLAS_PROVIDER);
814
+ }
815
+ if (env.code !== void 0 && env.code !== 200 && env.code !== 0 && env.data === void 0) {
816
+ throw new ProviderError(
817
+ `Atlas Cloud ${what} failed (code ${env.code}): ${env.msg || env.message || "unknown error"}`,
818
+ ATLAS_PROVIDER
819
+ );
820
+ }
821
+ if (env.data === void 0 || env.data === null) {
822
+ throw new ProviderError(`Atlas Cloud ${what}: response has no data`, ATLAS_PROVIDER);
823
+ }
824
+ return env.data;
825
+ }
826
+ var AtlasCloudClient = class {
827
+ baseUrl;
828
+ apiKey;
829
+ fetchImpl;
830
+ timeoutMs;
831
+ uploadTimeoutMs;
832
+ catalog;
833
+ constructor(opts) {
834
+ if (!opts.apiKey?.trim()) {
835
+ throw new ConfigError(
836
+ "ATLASCLOUD_API_KEY is not set. Get one at https://www.atlascloud.ai/console/api-keys"
837
+ );
838
+ }
839
+ this.apiKey = opts.apiKey.trim();
840
+ this.baseUrl = (opts.baseUrl?.trim() || DEFAULT_ATLAS_BASE_URL).replace(/\/+$/, "");
841
+ this.fetchImpl = opts.fetch;
842
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
843
+ this.uploadTimeoutMs = opts.uploadTimeoutMs ?? DEFAULT_UPLOAD_TIMEOUT_MS;
844
+ }
845
+ doFetch(input, init) {
846
+ const f = this.fetchImpl;
847
+ return f ? f(input, init) : globalThis.fetch(input, init);
848
+ }
849
+ /** Catalog and schema lookups are advisory; fail fast so a slow catalog cannot stall a command. */
850
+ catalogTimeoutMs() {
851
+ return Math.min(this.timeoutMs, CATALOG_TIMEOUT_MS);
852
+ }
853
+ authHeaders() {
854
+ return { Authorization: `Bearer ${this.apiKey}`, Accept: "application/json" };
855
+ }
856
+ /** Submit a generation job. `body` holds `model` plus the model's own fields, flattened. */
857
+ async submit(kind, body) {
858
+ const path52 = SUBMIT_PATHS[kind];
859
+ if (!path52) throw new ValidationError(`Unknown Atlas Cloud media type: ${String(kind)}`);
860
+ if (typeof body.model !== "string" || !body.model.trim()) {
861
+ throw new ValidationError("Atlas Cloud request body needs a non-empty `model`");
862
+ }
863
+ const env = await fetchJson({
864
+ url: `${this.baseUrl}${path52}`,
865
+ method: "POST",
866
+ headers: this.authHeaders(),
867
+ body,
868
+ timeoutMs: this.timeoutMs,
869
+ fetch: this.fetchImpl
870
+ });
871
+ const data = envelopeData(env, "submit");
872
+ if (typeof data.id !== "string" || !data.id) {
873
+ throw new ProviderError("Atlas Cloud submit returned no prediction id", ATLAS_PROVIDER);
874
+ }
875
+ return data;
876
+ }
877
+ /** Fetch a prediction once. A failed task reported as HTTP 5xx is returned, not thrown. */
878
+ async getPrediction(id, timeoutMs) {
879
+ assertValidPredictionId(id);
880
+ try {
881
+ const env = await fetchJson({
882
+ url: `${this.baseUrl}/model/prediction/${id}`,
883
+ headers: this.authHeaders(),
884
+ timeoutMs: timeoutMs ?? this.timeoutMs,
885
+ fetch: this.fetchImpl
886
+ });
887
+ const data = envelopeData(env, "prediction");
888
+ return { ...data, id: data.id || id };
889
+ } catch (error) {
890
+ const terminal = terminalPredictionFromError(error, id);
891
+ if (terminal) return terminal;
892
+ throw error;
893
+ }
894
+ }
895
+ /**
896
+ * Poll until the prediction is terminal (success or failure) or the deadline
897
+ * passes. Network errors, 429 (honouring Retry-After) and 5xx are retried.
898
+ * Throws PollTimeoutError on timeout; returns the terminal prediction otherwise.
899
+ */
900
+ async waitForPrediction(id, opts) {
901
+ assertValidPredictionId(id);
902
+ return pollUntil({
903
+ // Cap each request at the time left so one hung poll cannot overrun the deadline.
904
+ fetchStatus: ({ remainingMs }) => this.getPrediction(id, Math.ceil(Math.max(1e3, Math.min(this.timeoutMs, remainingMs)))),
905
+ isTerminal: (p) => isTerminalStatus(p.status),
906
+ timeoutMs: opts.timeoutMs,
907
+ intervalMs: opts.intervalMs ?? 3e3,
908
+ maxIntervalMs: opts.maxIntervalMs ?? 15e3,
909
+ isTransientError: isTransientHttpError,
910
+ onPoll: opts.onPoll,
911
+ resumeHint: opts.resumeHint,
912
+ sleep: opts.sleep,
913
+ now: opts.now
914
+ });
915
+ }
916
+ /**
917
+ * Upload bytes and return a temporary URL usable as model input.
918
+ * The filename extension matters: Atlas and downstream models classify by it.
919
+ */
920
+ async uploadMedia(file, filename, mimeType) {
921
+ if (!filename.trim()) throw new ValidationError("uploadMedia needs a filename");
922
+ const blob = file instanceof Blob ? file : new Blob([file], { type: mimeType ?? "application/octet-stream" });
923
+ const form = new FormData();
924
+ form.append("file", blob, filename);
925
+ const url = `${this.baseUrl}/model/uploadMedia`;
926
+ let response;
927
+ try {
928
+ response = await this.doFetch(url, {
929
+ method: "POST",
930
+ headers: this.authHeaders(),
931
+ body: form,
932
+ signal: AbortSignal.timeout(this.uploadTimeoutMs)
933
+ });
934
+ } catch (cause) {
935
+ throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
936
+ }
937
+ if (!response.ok) throw await httpErrorFromResponse(response, url);
938
+ const json = await response.json();
939
+ const data = json.data ?? {};
940
+ const fileUrl = [data.download_url, data.url, json.url].find(
941
+ (v) => typeof v === "string" && /^https?:\/\//i.test(v)
942
+ );
943
+ if (!fileUrl) {
944
+ throw new ProviderError(
945
+ `Atlas Cloud upload returned no URL (code ${json.code ?? "?"}: ${json.msg ?? json.message ?? "no message"})`,
946
+ ATLAS_PROVIDER
947
+ );
948
+ }
949
+ return {
950
+ url: fileUrl,
951
+ filename: typeof data.filename === "string" ? data.filename : void 0,
952
+ size: typeof data.size === "number" ? data.size : void 0,
953
+ type: typeof data.type === "string" ? data.type : void 0
954
+ };
955
+ }
956
+ /** Public model catalog, fetched once per client. */
957
+ listModels() {
958
+ this.catalog ??= fetchJson({
959
+ url: `${this.baseUrl}/models`,
960
+ timeoutMs: this.catalogTimeoutMs(),
961
+ fetch: this.fetchImpl
962
+ }).then((env) => {
963
+ const data = envelopeData(env, "model catalog");
964
+ if (!Array.isArray(data)) {
965
+ throw new ProviderError("Atlas Cloud model catalog is not a list", ATLAS_PROVIDER);
966
+ }
967
+ return data.filter((m) => m && typeof m.model === "string");
968
+ }).catch((error) => {
969
+ this.catalog = void 0;
970
+ throw error;
971
+ });
972
+ return this.catalog;
973
+ }
974
+ /** Exact (case-sensitive) catalog lookup; model ids are never rewritten. */
975
+ async findModel(model) {
976
+ return (await this.listModels()).find((m) => m.model === model);
977
+ }
978
+ /**
979
+ * Load the model's published request schema and submit endpoint.
980
+ * Returns undefined when the model is not in the public catalog or publishes no schema.
981
+ */
982
+ async getModelSpec(model) {
983
+ const entry = await this.findModel(model);
984
+ if (!entry?.schema || !/^https:\/\//i.test(entry.schema)) return void 0;
985
+ const doc = await fetchJson({
986
+ url: entry.schema,
987
+ timeoutMs: this.catalogTimeoutMs(),
988
+ fetch: this.fetchImpl
989
+ });
990
+ return parseModelSpec(model, doc);
991
+ }
992
+ };
993
+
994
+ // src/providers/atlascloud/upload-mime.ts
995
+ var UPLOAD_MIME_TYPES = {
996
+ png: "image/png",
997
+ jpg: "image/jpeg",
998
+ jpeg: "image/jpeg",
999
+ webp: "image/webp",
1000
+ gif: "image/gif",
1001
+ bmp: "image/bmp",
1002
+ avif: "image/avif",
1003
+ heic: "image/heic",
1004
+ mp4: "video/mp4",
1005
+ mov: "video/quicktime",
1006
+ webm: "video/webm",
1007
+ mkv: "video/x-matroska",
1008
+ avi: "video/x-msvideo",
1009
+ mp3: "audio/mpeg",
1010
+ wav: "audio/wav",
1011
+ m4a: "audio/mp4",
1012
+ aac: "audio/aac",
1013
+ ogg: "audio/ogg",
1014
+ flac: "audio/flac",
1015
+ opus: "audio/opus"
1016
+ };
1017
+ var UPLOAD_EXTENSIONS = Object.keys(UPLOAD_MIME_TYPES);
1018
+ function uploadMimeTypeFor(filename) {
1019
+ const ext = /\.([a-z0-9]+)$/i.exec(filename)?.[1]?.toLowerCase() ?? "";
1020
+ return UPLOAD_MIME_TYPES[ext];
1021
+ }
1022
+
1023
+ // src/providers/atlascloud/commands/env.ts
1024
+ var ATLAS_KEY_URL = "https://www.atlascloud.ai/console/api-keys";
1025
+ var MODEL_ENV = {
1026
+ image: "ATLASCLOUD_IMAGE_MODEL",
1027
+ imageEdit: "ATLASCLOUD_IMAGE_EDIT_MODEL",
1028
+ video: "ATLASCLOUD_VIDEO_MODEL",
1029
+ imageToVideo: "ATLASCLOUD_IMAGE_TO_VIDEO_MODEL",
1030
+ tts: "ATLASCLOUD_TTS_MODEL",
1031
+ stt: "ATLASCLOUD_STT_MODEL",
1032
+ textTo3d: "ATLASCLOUD_TEXT_TO_3D_MODEL",
1033
+ imageTo3d: "ATLASCLOUD_IMAGE_TO_3D_MODEL"
1034
+ };
1035
+ function atlasDefaultModel(key) {
1036
+ return resolveKey(MODEL_ENV[key])?.trim() || ATLAS_DEFAULT_MODELS[key];
1037
+ }
1038
+ function createAtlasClientFromEnv() {
1039
+ const apiKey = resolveKey("ATLASCLOUD_API_KEY")?.trim();
1040
+ if (!apiKey) {
1041
+ throw new ConfigError(`ATLASCLOUD_API_KEY is not set. Get one at ${ATLAS_KEY_URL}`);
1042
+ }
1043
+ return new AtlasCloudClient({ apiKey, baseUrl: resolveKey("ATLASCLOUD_BASE_URL") });
1044
+ }
1045
+ async function resolveMediaRef(client, ref, logger) {
1046
+ const trimmed = ref.trim();
1047
+ if (/^https?:\/\//i.test(trimmed) || /^data:/i.test(trimmed)) return trimmed;
1048
+ const abs = path2.resolve(trimmed);
1049
+ const name = path2.basename(abs);
1050
+ const mime = uploadMimeTypeFor(name);
1051
+ if (!mime) {
1052
+ throw new ValidationError(
1053
+ `Refusing to upload ${trimmed}: not a supported media file. Allowed extensions: ${UPLOAD_EXTENSIONS.join(", ")}`
1054
+ );
1055
+ }
1056
+ let stat;
1057
+ try {
1058
+ stat = await fs2.stat(abs);
1059
+ } catch {
1060
+ throw new ValidationError(`Reference file not found: ${trimmed}`);
1061
+ }
1062
+ if (!stat.isFile()) throw new ValidationError(`Reference is not a file: ${trimmed}`);
1063
+ if (stat.size === 0) throw new ValidationError(`Reference file is empty: ${trimmed}`);
1064
+ const maxBytes = maxUploadBytes();
1065
+ if (stat.size > maxBytes) {
1066
+ throw new ValidationError(
1067
+ `${trimmed} is ${formatMb(stat.size)}, above the ${formatMb(maxBytes)} upload limit (set ${MAX_UPLOAD_ENV} to change it)`
1068
+ );
1069
+ }
1070
+ logger.info(`Uploading ${name} (${Math.ceil(stat.size / 1024)} KB) to Atlas Cloud...`);
1071
+ const blob = await openAsBlob(abs, { type: mime });
1072
+ const uploaded = await client.uploadMedia(blob, name, mime);
1073
+ logger.debug(`Uploaded ${name} -> ${redactUrl(uploaded.url)}`);
1074
+ return uploaded.url;
1075
+ }
1076
+ var MAX_UPLOAD_ENV = "ATLASCLOUD_MAX_UPLOAD_MB";
1077
+ var DEFAULT_MAX_UPLOAD_MB = 200;
1078
+ function maxUploadBytes() {
1079
+ const raw = resolveKey(MAX_UPLOAD_ENV)?.trim();
1080
+ if (!raw) return DEFAULT_MAX_UPLOAD_MB * 1024 * 1024;
1081
+ const mb = Number(raw);
1082
+ if (!Number.isFinite(mb) || mb <= 0) {
1083
+ throw new ConfigError(`${MAX_UPLOAD_ENV} must be a positive number of megabytes, got "${raw}"`);
1084
+ }
1085
+ return Math.floor(mb * 1024 * 1024);
1086
+ }
1087
+ function formatMb(bytes) {
1088
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1089
+ }
1090
+ function createAtlasLogger(opts) {
1091
+ const verbose = opts.verbose ?? false;
1092
+ const base = createLogger({ verbose });
1093
+ if (!opts.stdoutReserved) return base;
1094
+ return {
1095
+ ...base,
1096
+ info: (msg) => console.error(` ${msg}`),
1097
+ success: (msg) => console.error(`\u2713 ${msg}`),
1098
+ debug: (msg) => {
1099
+ if (verbose) console.error(` [debug] ${msg}`);
1100
+ },
1101
+ header: (msg) => console.error(msg)
1102
+ };
1103
+ }
1104
+ async function resolveMediaRefs(client, refs, logger) {
1105
+ const urls = [];
1106
+ for (const ref of refs) urls.push(await resolveMediaRef(client, ref, logger));
1107
+ return urls;
1108
+ }
1109
+
1110
+ // src/providers/atlascloud/commands/prediction-runner.ts
1111
+ init_errors();
1112
+ import fs8 from "fs";
1113
+ import path7 from "path";
1114
+
1115
+ // src/core/http-client.ts
1116
+ init_errors();
1117
+ import { once } from "events";
1118
+ import fs3 from "fs";
1119
+ import path3 from "path";
1120
+ import { pipeline } from "stream/promises";
1121
+ async function httpJson(opts) {
1122
+ return fetchJson(opts);
1123
+ }
1124
+ async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers2) {
1125
+ fs3.mkdirSync(path3.dirname(dest), { recursive: true });
1126
+ let response;
1127
+ try {
1128
+ response = await globalThis.fetch(url, {
1129
+ signal: AbortSignal.timeout(timeoutMs),
1130
+ ...headers2 ? { headers: headers2 } : {}
1131
+ });
1132
+ } catch (cause) {
1133
+ const msg = cause instanceof Error ? cause.message : String(cause);
1134
+ throw new HttpError(0, msg, url);
1135
+ }
1136
+ if (!response.ok) {
1137
+ throw new HttpError(response.status, "(download failed)", url);
1138
+ }
1139
+ if (!response.body) {
1140
+ throw new HttpError(0, "Response body is null", url);
1141
+ }
1142
+ const fileStream = fs3.createWriteStream(dest);
1143
+ try {
1144
+ await pipeline(response.body, fileStream);
1145
+ } catch (err2) {
1146
+ if (!fileStream.closed) await once(fileStream, "close");
1147
+ throw err2;
1148
+ }
1149
+ return response.headers.get("content-type") ?? void 0;
1150
+ }
1151
+ async function fetchBytes(urlOrDataUri) {
1152
+ if (urlOrDataUri.startsWith("data:")) {
1153
+ const commaIdx = urlOrDataUri.indexOf(",");
1154
+ if (commaIdx === -1) throw new Error("Malformed data URI: missing comma");
1155
+ const encoded = urlOrDataUri.slice(commaIdx + 1);
1156
+ return Buffer.from(encoded, "base64");
1157
+ }
1158
+ let response;
1159
+ try {
1160
+ response = await globalThis.fetch(urlOrDataUri, {
1161
+ signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
1162
+ });
1163
+ } catch (cause) {
1164
+ const msg = cause instanceof Error ? cause.message : String(cause);
1165
+ throw new HttpError(0, msg, urlOrDataUri);
1166
+ }
1167
+ if (!response.ok) {
1168
+ throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
1169
+ }
1170
+ return Buffer.from(await response.arrayBuffer());
1171
+ }
1172
+
1173
+ // src/core/image-webp-finalize.ts
1174
+ init_errors();
1175
+ import { spawnSync } from "child_process";
1176
+ import fs5 from "fs";
1177
+ import path4 from "path";
1178
+ import { Option } from "commander";
1179
+
1180
+ // src/core/image-header.ts
1181
+ import fs4 from "fs";
1182
+ function sniffImageKind(buf) {
1183
+ if (buf.length >= 8 && buf.readUInt32BE(0) === 2303741511) return "png";
1184
+ if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpeg";
1185
+ if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP")
1186
+ return "webp";
1187
+ return void 0;
1188
+ }
1189
+ function readPng(buf) {
1190
+ if (buf.length < 33) return void 0;
1191
+ const width = buf.readUInt32BE(16);
1192
+ const height = buf.readUInt32BE(20);
1193
+ const colorType = buf[25];
1194
+ let hasAlpha = colorType === 4 || colorType === 6;
1195
+ let offset = 8;
1196
+ while (!hasAlpha && offset + 8 <= buf.length) {
1197
+ const len = buf.readUInt32BE(offset);
1198
+ const type = buf.toString("ascii", offset + 4, offset + 8);
1199
+ if (type === "tRNS") hasAlpha = true;
1200
+ if (type === "IDAT" || type === "IEND") break;
1201
+ offset += 12 + len;
1202
+ }
1203
+ return { kind: "png", width, height, hasAlpha };
1204
+ }
1205
+ function readJpeg(buf) {
1206
+ let offset = 2;
1207
+ while (offset + 9 < buf.length) {
1208
+ if (buf[offset] !== 255) {
1209
+ offset++;
1210
+ continue;
1211
+ }
1212
+ const marker = buf[offset + 1] ?? 0;
1213
+ if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
1214
+ return {
1215
+ kind: "jpeg",
1216
+ height: buf.readUInt16BE(offset + 5),
1217
+ width: buf.readUInt16BE(offset + 7),
1218
+ hasAlpha: false
1219
+ };
1220
+ }
1221
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
1222
+ offset += 2;
1223
+ continue;
1224
+ }
1225
+ offset += 2 + buf.readUInt16BE(offset + 2);
1226
+ }
1227
+ return void 0;
1228
+ }
1229
+ function readWebp(buf) {
1230
+ if (buf.length < 30) return void 0;
1231
+ const chunk = buf.toString("ascii", 12, 16);
1232
+ if (chunk === "VP8X") {
1233
+ const flags = buf[20] ?? 0;
1234
+ return {
1235
+ kind: "webp",
1236
+ width: 1 + buf.readUIntLE(24, 3),
1237
+ height: 1 + buf.readUIntLE(27, 3),
1238
+ hasAlpha: (flags & 16) !== 0
1239
+ };
1240
+ }
1241
+ if (chunk === "VP8L") {
1242
+ const bits = buf.readUInt32LE(21);
1243
+ return {
1244
+ kind: "webp",
1245
+ width: 1 + (bits & 16383),
1246
+ height: 1 + (bits >>> 14 & 16383),
1247
+ hasAlpha: (bits >>> 28 & 1) === 1
1248
+ };
1249
+ }
1250
+ if (chunk === "VP8 ") {
1251
+ return {
1252
+ kind: "webp",
1253
+ width: buf.readUInt16LE(26) & 16383,
1254
+ height: buf.readUInt16LE(28) & 16383,
1255
+ hasAlpha: false
1256
+ };
1257
+ }
1258
+ return void 0;
1259
+ }
1260
+ function readImageHeader(buf) {
1261
+ const kind = sniffImageKind(buf);
1262
+ if (kind === "png") return readPng(buf);
1263
+ if (kind === "jpeg") return readJpeg(buf);
1264
+ if (kind === "webp") return readWebp(buf);
1265
+ return void 0;
1266
+ }
1267
+ function readImageHeaderFromFile(file) {
1268
+ return readImageHeader(fs4.readFileSync(file));
1269
+ }
1270
+
1271
+ // src/core/image-webp-finalize.ts
1272
+ var CWEBP_ARGS = ["-q", "85", "-m", "6", "-metadata", "none"];
1273
+ var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff"]);
1274
+ function imageFormatOption() {
1275
+ return new Option(
1276
+ "--image-format <mode>",
1277
+ "Final image format: webp (default, cwebp -q 85) | original (keep provider format/quality)"
1278
+ );
1279
+ }
1280
+ function noWebpOption() {
1281
+ return new Option("--no-webp", "Keep the provider's original image format and quality");
1282
+ }
1283
+ function resolveImageFormatMode(opts, explicitProviderFormat = false) {
1284
+ if (opts.webp === false && opts.imageFormat === void 0) return "original";
1285
+ const raw = opts.imageFormat ?? process.env.MULTIX_IMAGE_FORMAT;
1286
+ if (raw !== void 0 && raw !== "") {
1287
+ const mode = raw.toLowerCase();
1288
+ if (mode !== "webp" && mode !== "original") {
1289
+ throw new ValidationError(`Invalid --image-format: ${raw}. Valid: webp, original`);
1290
+ }
1291
+ return mode;
1292
+ }
1293
+ if (explicitProviderFormat) return "original";
1294
+ const ext = opts.output ? path4.extname(opts.output).toLowerCase() : "";
1295
+ if (ext && ext !== ".webp" && IMAGE_EXTS.has(ext)) return "original";
1296
+ return "webp";
1297
+ }
1298
+ function hasCwebp() {
1299
+ const res = spawnSync("cwebp", ["-version"], { stdio: "ignore" });
1300
+ return !res.error && res.status === 0;
1301
+ }
1302
+ function webpTarget(file) {
1303
+ const ext = path4.extname(file);
1304
+ return ext.toLowerCase() === ".webp" ? file : `${file.slice(0, -ext.length || void 0)}.webp`;
1305
+ }
1306
+ function convertToWebp(file, logger, keepPath = false) {
1307
+ const src = readImageHeaderFromFile(file);
1308
+ if (!src) {
1309
+ logger?.warn(`Skipping WebP conversion (unrecognized image): ${file}`);
1310
+ return file;
1311
+ }
1312
+ if (src.kind === "webp") {
1313
+ const target2 = keepPath ? file : webpTarget(file);
1314
+ if (target2 !== file) fs5.renameSync(file, target2);
1315
+ return target2;
1316
+ }
1317
+ const target = keepPath ? file : webpTarget(file);
1318
+ const tmp = `${target}.tmp-${process.pid}.webp`;
1319
+ const res = spawnSync("cwebp", [...CWEBP_ARGS, file, "-o", tmp], { encoding: "utf8" });
1320
+ if (res.error || res.status !== 0) {
1321
+ fs5.rmSync(tmp, { force: true });
1322
+ logger?.warn(`cwebp failed, keeping original ${file}: ${res.stderr || res.error?.message}`);
1323
+ return file;
1324
+ }
1325
+ const out = fs5.existsSync(tmp) ? readImageHeaderFromFile(tmp) : void 0;
1326
+ const problem = !out ? "output is not a valid WebP" : out.width !== src.width || out.height !== src.height ? `size changed ${src.width}x${src.height} -> ${out.width}x${out.height}` : src.hasAlpha && !out.hasAlpha ? "transparency lost" : void 0;
1327
+ if (problem) {
1328
+ fs5.rmSync(tmp, { force: true });
1329
+ logger?.warn(`WebP verification failed (${problem}), keeping original ${file}`);
1330
+ return file;
1331
+ }
1332
+ if (target !== file) fs5.rmSync(file, { force: true });
1333
+ fs5.renameSync(tmp, target);
1334
+ logger?.debug(`WebP verified ${out?.width}x${out?.height} alpha=${out?.hasAlpha}: ${target}`);
1335
+ return target;
1336
+ }
1337
+ function finalizeGeneratedImages(files, opts) {
1338
+ const mode = resolveImageFormatMode(opts, opts.explicitProviderFormat);
1339
+ if (mode === "original" || files.length === 0) return files;
1340
+ if (!hasCwebp()) {
1341
+ opts.logger?.warn(
1342
+ "cwebp not found on PATH; keeping original image format (install libwebp, or pass --image-format original)"
1343
+ );
1344
+ return files;
1345
+ }
1346
+ const outputAbs = opts.output ? path4.resolve(opts.output) : void 0;
1347
+ const wroteOutputDirectly = outputAbs !== void 0 && files.some((f) => path4.resolve(f) === outputAbs);
1348
+ const finals = files.map(
1349
+ (f) => convertToWebp(f, opts.logger, outputAbs !== void 0 && path4.resolve(f) === outputAbs)
1350
+ );
1351
+ if (opts.output && outputAbs && !wroteOutputDirectly && finals[0] && fs5.existsSync(outputAbs)) {
1352
+ const outExt = path4.extname(outputAbs).toLowerCase();
1353
+ if ((outExt === ".webp" || outExt === "") && path4.extname(finals[0]).toLowerCase() === ".webp") {
1354
+ fs5.copyFileSync(finals[0], outputAbs);
1355
+ }
1356
+ }
1357
+ return finals;
1358
+ }
1359
+
1360
+ // src/core/output-dir.ts
1361
+ import fs6 from "fs";
1362
+ import path5 from "path";
1363
+ var cachedOutputDir;
1364
+ function getOutputDir() {
1365
+ if (cachedOutputDir) return cachedOutputDir;
1366
+ const dir = process.env.MULTIX_OUTPUT_DIR ? path5.resolve(process.env.MULTIX_OUTPUT_DIR) : path5.resolve(process.cwd(), "multix-output");
1367
+ fs6.mkdirSync(dir, { recursive: true });
1368
+ cachedOutputDir = dir;
1369
+ return dir;
1370
+ }
1371
+
1372
+ // src/providers/atlascloud/commands/prediction-runner.ts
1373
+ init_redact();
1374
+
1375
+ // src/core/video-thumb.ts
1376
+ import fs7 from "fs";
1377
+ import path6 from "path";
1378
+ var THUMB_KEYS = /* @__PURE__ */ new Set([
1379
+ "cover_image_url",
1380
+ "coverImageUrl",
1381
+ "cover_url",
1382
+ "coverUrl",
1383
+ "thumbnail_url",
1384
+ "thumbnailUrl",
1385
+ "thumb_url",
1386
+ "thumbUrl",
1387
+ "preview_url",
1388
+ "previewUrl",
1389
+ "first_frame_url",
1390
+ "firstFrameUrl",
1391
+ "poster_url",
1392
+ "posterUrl",
1393
+ "image_url",
1394
+ "imageUrl"
1395
+ ]);
1396
+ var IMAGE_EXT_RE = /\.(jpe?g|png|webp|gif|bmp)(\?|$)/i;
1397
+ function isImageUrl(value) {
1398
+ if (typeof value !== "string") return false;
1399
+ if (!/^https?:\/\//i.test(value)) return false;
1400
+ return IMAGE_EXT_RE.test(value);
1401
+ }
1402
+ function detectThumbUrl(obj) {
1403
+ if (obj === null || obj === void 0) return null;
1404
+ if (Array.isArray(obj)) {
1405
+ for (const item of obj) {
1406
+ const found = detectThumbUrl(item);
1407
+ if (found) return found;
1408
+ }
1409
+ return null;
1410
+ }
1411
+ if (typeof obj !== "object") return null;
1412
+ const record = obj;
1413
+ for (const [key, value] of Object.entries(record)) {
1414
+ if (THUMB_KEYS.has(key) && isImageUrl(value)) return value;
1415
+ }
1416
+ for (const value of Object.values(record)) {
1417
+ if (value && typeof value === "object") {
1418
+ const found = detectThumbUrl(value);
1419
+ if (found) return found;
1420
+ }
1421
+ }
1422
+ return null;
1423
+ }
1424
+ async function downloadThumbBeside(thumbUrl, videoPath, logger) {
1425
+ const match = thumbUrl.match(IMAGE_EXT_RE);
1426
+ const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
1427
+ const dir = path6.dirname(videoPath);
1428
+ const base = path6.basename(videoPath, path6.extname(videoPath));
1429
+ const dest = path6.join(dir, `${base}_thumb.${ext}`);
1430
+ try {
1431
+ await downloadFile(thumbUrl, dest);
1432
+ logger?.success(`Saved thumb ${dest}`);
1433
+ return dest;
1434
+ } catch (e) {
1435
+ logger?.warn(`Thumb download failed: ${e instanceof Error ? e.message : String(e)}`);
1436
+ return null;
1437
+ }
1438
+ }
1439
+ async function maybeDownloadThumb(source, videoPath, opts = {}) {
1440
+ if (opts.skip) return null;
1441
+ const url = detectThumbUrl(source);
1442
+ if (!url) {
1443
+ opts.logger?.debug("No thumbnail URL detected in provider response");
1444
+ return null;
1445
+ }
1446
+ const saved = await downloadThumbBeside(url, videoPath, opts.logger);
1447
+ if (saved && opts.copyTo) {
1448
+ const copyExt = path6.extname(saved);
1449
+ const copyBase = path6.basename(opts.copyTo, path6.extname(opts.copyTo));
1450
+ const copyDir = path6.dirname(path6.resolve(opts.copyTo));
1451
+ const copyDest = path6.join(copyDir, `${copyBase}_thumb${copyExt}`);
1452
+ fs7.mkdirSync(copyDir, { recursive: true });
1453
+ fs7.copyFileSync(saved, copyDest);
1454
+ opts.logger?.success(`Copied thumb to ${copyDest}`);
1455
+ }
1456
+ return saved;
1457
+ }
1458
+
1459
+ // src/providers/atlascloud/commands/prediction-runner.ts
1460
+ function addRunOptions(cmd, cfg) {
1461
+ cmd.option("--async", "Submit and exit; print the prediction id (resume with `atlas status`)").option("--wait-timeout <ms>", "Max time to wait for completion", String(cfg.waitTimeoutMs)).option("--output <path>", "Output file (single output) or directory (path without extension)").option("--no-download", "Do not download media; print output URLs instead").option("--no-validate", "Skip client-side validation against the model's published schema");
1462
+ if (cfg.images) cmd.addOption(imageFormatOption()).addOption(noWebpOption());
1463
+ if (cfg.video) cmd.option("--no-thumb", "Skip downloading the video thumbnail if present");
1464
+ return cmd.option("-v, --verbose", "Verbose logging");
1465
+ }
1466
+ function addRefOption(cmd, description) {
1467
+ return cmd.option(
1468
+ "--ref <pathOrUrl>",
1469
+ `${description} (repeat --ref for several)`,
1470
+ (value, prev) => [...prev, value],
1471
+ []
1472
+ );
1473
+ }
1474
+ function requireRefs(refs) {
1475
+ if (!refs || refs.length === 0) {
1476
+ throw new ValidationError("Missing required option --ref <pathOrUrl>");
1477
+ }
1478
+ return refs;
1479
+ }
1480
+ function parseTimeoutMs(raw, fallback) {
1481
+ if (raw === void 0) return fallback;
1482
+ const n = Number(raw);
1483
+ if (!Number.isInteger(n) || n <= 0) {
1484
+ throw new ValidationError(`--wait-timeout must be a positive integer (ms), got "${raw}"`);
1485
+ }
1486
+ return n;
1487
+ }
1488
+ var KIND_COMMAND = {
1489
+ image: "image",
1490
+ video: "video",
1491
+ audio: "audio"
1492
+ };
1493
+ async function loadModelSpec(client, model, logger, validate) {
1494
+ if (validate === false) return void 0;
1495
+ try {
1496
+ const spec = await client.getModelSpec(model);
1497
+ if (!spec) {
1498
+ logger.warn(
1499
+ `Model ${model} is not in the Atlas Cloud public catalog or has no published schema; sending without validation.`
1500
+ );
1501
+ }
1502
+ return spec;
1503
+ } catch (e) {
1504
+ logger.warn(
1505
+ `Could not load the schema for ${model} (${e instanceof Error ? e.message : String(e)}); sending without validation.`
1506
+ );
1507
+ return void 0;
1508
+ }
1509
+ }
1510
+ function assertEndpoint(spec, expected, model) {
1511
+ if (!spec?.endpoint || spec.endpoint === expected) return;
1512
+ throw new ValidationError(
1513
+ `Model ${model} is a ${spec.endpoint} model (served by generate${capitalize(spec.endpoint)}), not ${expected}. Use \`multix atlas ${KIND_COMMAND[spec.endpoint]} ...\` or \`multix atlas run ${model} --type ${spec.endpoint} --input '{...}'\`.`
1514
+ );
1515
+ }
1516
+ function capitalize(s) {
1517
+ return s.charAt(0).toUpperCase() + s.slice(1);
1518
+ }
1519
+ function resumeCommand(id) {
1520
+ return `multix atlas status ${id} --wait --download`;
1521
+ }
1522
+ async function executePrediction(o) {
1523
+ const { client, logger, opts } = o;
1524
+ const timeoutMs = parseTimeoutMs(opts.waitTimeout, o.defaultWaitTimeoutMs);
1525
+ logger.info(`Submitting to ${o.model} (generate${capitalize(o.kind)})...`);
1526
+ const submitted = await client.submit(o.kind, { ...o.body, model: o.model });
1527
+ const id = submitted.id;
1528
+ if (opts.async) {
1529
+ console.log(id);
1530
+ console.error(`Prediction submitted. Resume with: ${resumeCommand(id)}`);
1531
+ return { predictionId: id, files: [], texts: [] };
1532
+ }
1533
+ logger.info(`Prediction ${id} \u2014 waiting up to ${Math.round(timeoutMs / 1e3)}s...`);
1534
+ const final = isTerminalStatus(submitted.status) ? submitted : await client.waitForPrediction(id, {
1535
+ timeoutMs,
1536
+ intervalMs: o.pollIntervalMs,
1537
+ resumeHint: `Resume with: ${resumeCommand(id)}`,
1538
+ onPoll: (p, n) => logger.debug(`poll ${n}: ${p.status}`)
1539
+ });
1540
+ assertPredictionSucceeded(final);
1541
+ const delivered = await deliverPrediction(final, { logger, opts, hint: o.hint });
1542
+ return { predictionId: id, prediction: final, ...delivered };
1543
+ }
1544
+ function isDirectoryOutput(output) {
1545
+ return /[\\/]$/.test(output) || path7.extname(output) === "" || fs8.existsSync(output) && fs8.statSync(output).isDirectory();
1546
+ }
1547
+ function planOutputPaths(urls, cfg) {
1548
+ const exts = urls.map((u) => {
1549
+ const cat = outputCategory(u);
1550
+ return outputExtension(u) ?? defaultExtensionFor(cat !== "unknown" ? cat : cfg.hint ?? "unknown");
1551
+ });
1552
+ const output = cfg.output;
1553
+ if (!output || isDirectoryOutput(output)) {
1554
+ const dir = output ? path7.resolve(output) : cfg.outDir();
1555
+ return exts.map((ext, i) => path7.join(dir, outputFileName(cfg.predictionId, i, ext)));
1556
+ }
1557
+ const abs = path7.resolve(output);
1558
+ if (urls.length === 1) return [abs];
1559
+ const base = abs.slice(0, -path7.extname(abs).length);
1560
+ return exts.map((ext, i) => `${base}-${String(i + 1).padStart(2, "0")}.${ext}`);
1561
+ }
1562
+ async function deliverPrediction(prediction2, cfg) {
1563
+ const { logger, opts } = cfg;
1564
+ if (prediction2.price !== void 0 && prediction2.price !== "") {
1565
+ logger.info(`Cost: $${prediction2.price}`);
1566
+ }
1567
+ const classified = classifyOutputs(prediction2);
1568
+ if (classified.mode === "empty") {
1569
+ throw new ProviderError(
1570
+ hasNsfwFlag(prediction2) ? `Prediction ${prediction2.id} returned no output: blocked by content safety (has_nsfw_contents). Try a different prompt or input.` : `Prediction ${prediction2.id} completed but returned no outputs.`,
1571
+ ATLAS_PROVIDER
1572
+ );
1573
+ }
1574
+ for (const text of classified.texts) console.log(text);
1575
+ if (classified.mode === "text") {
1576
+ if (opts.output) {
1577
+ const dest = isDirectoryOutput(opts.output) ? path7.resolve(opts.output, outputFileName(prediction2.id, 0, "txt")) : path7.resolve(opts.output);
1578
+ fs8.mkdirSync(path7.dirname(dest), { recursive: true });
1579
+ fs8.writeFileSync(dest, `${classified.texts.join("\n\n")}
1580
+ `, "utf8");
1581
+ logger.success(`Saved text output to ${dest}`);
1582
+ return { files: [dest], texts: classified.texts };
1583
+ }
1584
+ return { files: [], texts: classified.texts };
1585
+ }
1586
+ if (opts.download === false) {
1587
+ for (const u of classified.urls) console.log(u);
1588
+ return { files: [], texts: classified.texts };
1589
+ }
1590
+ const dests = planOutputPaths(classified.urls, {
1591
+ predictionId: prediction2.id,
1592
+ outDir: getOutputDir,
1593
+ output: opts.output,
1594
+ hint: cfg.hint
1595
+ });
1596
+ const explicitFile = classified.urls.length === 1 && opts.output !== void 0 && !isDirectoryOutput(opts.output);
1597
+ const saved = [];
1598
+ let failed = 0;
1599
+ for (let i = 0; i < classified.urls.length; i++) {
1600
+ const url = classified.urls[i];
1601
+ const planned = dests[i];
1602
+ const part = `${planned}.part`;
1603
+ try {
1604
+ const contentType = await downloadFile(url, part);
1605
+ const dest = explicitFile || outputExtension(url) ? planned : withContentTypeExtension(planned, contentType);
1606
+ fs8.renameSync(part, dest);
1607
+ saved.push(dest);
1608
+ logger.debug(`Downloaded ${redactUrl(url)} -> ${dest}`);
1609
+ } catch (e) {
1610
+ fs8.rmSync(part, { force: true });
1611
+ failed++;
1612
+ logger.warn(
1613
+ `Download failed (${redactUrl(url)}): ${e instanceof Error ? e.message : String(e)}`
1614
+ );
1615
+ }
1616
+ }
1617
+ if (saved.length === 0) {
1618
+ throw new ProviderError(
1619
+ `Prediction ${prediction2.id} produced ${classified.urls.length} output(s) but none downloaded. Retry with: ${resumeCommand(prediction2.id)}`,
1620
+ ATLAS_PROVIDER
1621
+ );
1622
+ }
1623
+ const images = saved.filter((f) => outputCategory(f) === "image");
1624
+ const fileOutput = opts.output && !isDirectoryOutput(opts.output) ? opts.output : void 0;
1625
+ const finalImages = finalizeGeneratedImages(images, {
1626
+ imageFormat: opts.imageFormat,
1627
+ webp: opts.webp,
1628
+ output: fileOutput,
1629
+ logger
1630
+ });
1631
+ const files = saved.map((f) => finalImages[images.indexOf(f)] ?? f);
1632
+ const firstVideo = files.find((f) => outputCategory(f) === "video");
1633
+ if (firstVideo) {
1634
+ await maybeDownloadThumb(prediction2, firstVideo, { skip: opts.thumb === false, logger });
1635
+ }
1636
+ console.log(`
1637
+ Prediction ${prediction2.id}: saved ${files.length} file(s)`);
1638
+ for (const f of files) console.log(` ${f}`);
1639
+ if (failed > 0) {
1640
+ throw new ProviderError(
1641
+ `Prediction ${prediction2.id}: downloaded ${saved.length} of ${classified.urls.length} output(s); ${failed} failed. Saved: ${files.join(", ")}. Retry with: ${resumeCommand(prediction2.id)}`,
1642
+ ATLAS_PROVIDER
1643
+ );
1644
+ }
1645
+ return { files, texts: classified.texts };
1646
+ }
1647
+ function withContentTypeExtension(planned, contentType) {
1648
+ const ext = extensionForContentType(contentType);
1649
+ if (!ext) return planned;
1650
+ const current = path7.extname(planned);
1651
+ return `${current ? planned.slice(0, -current.length) : planned}.${ext}`;
1652
+ }
1653
+
1654
+ // src/providers/atlascloud/commands/generation-command.ts
1655
+ async function runGenerationCommand(cfg) {
1656
+ const logger = createAtlasLogger({
1657
+ verbose: cfg.opts.verbose,
1658
+ stdoutReserved: cfg.opts.async
1659
+ });
1660
+ const client = createAtlasClientFromEnv();
1661
+ const spec = await loadModelSpec(client, cfg.model, logger, cfg.opts.validate);
1662
+ const kind = cfg.endpointFromSchema ? spec?.endpoint ?? cfg.kind : cfg.kind;
1663
+ assertEndpoint(spec, kind, cfg.model);
1664
+ const refs = cfg.refs ?? [];
1665
+ buildModelInput(spec, cfg.requests(refs));
1666
+ const refUrls = refs.length > 0 ? await resolveMediaRefs(client, refs, logger) : [];
1667
+ const body = buildModelInput(spec, cfg.requests(refUrls));
1668
+ logger.debug(`Request fields: ${Object.keys(body).join(", ")}`);
1669
+ await executePrediction({
1670
+ client,
1671
+ logger,
1672
+ kind,
1673
+ model: cfg.model,
1674
+ body,
1675
+ opts: cfg.opts,
1676
+ hint: cfg.hint,
1677
+ defaultWaitTimeoutMs: cfg.defaultWaitTimeoutMs,
1678
+ pollIntervalMs: cfg.pollIntervalMs
1679
+ });
1680
+ }
1681
+
1682
+ // src/providers/atlascloud/commands/audio.ts
1683
+ var AUDIO_WAIT_MS = 6e5;
1684
+ var AUDIO_POLL_MS = 3e3;
1685
+ function registerAtlasAudioCommand(parent) {
1686
+ const audio = parent.command("audio [text]").description(
1687
+ `Speech, music, lyrics or transcription via Atlas Cloud (default TTS: ${ATLAS_DEFAULT_MODELS.tts}; with --audio, STT: ${ATLAS_DEFAULT_MODELS.stt})`
1688
+ ).option(
1689
+ "-m, --model <id>",
1690
+ "Atlas Cloud audio model id (env: ATLASCLOUD_TTS_MODEL / ATLASCLOUD_STT_MODEL)"
1691
+ ).option("--audio <pathOrUrl>", "Input audio for speech-to-text (local files are uploaded)").option("--voice <id>", "Voice id (model-dependent)").option("--format <fmt>", "Output audio format, e.g. mp3, wav (model-dependent)").option("--language <code>", "Language (model-dependent)");
1692
+ addRunOptions(audio, { waitTimeoutMs: AUDIO_WAIT_MS }).action(
1693
+ async (text, opts) => {
1694
+ if (!text && !opts.audio) {
1695
+ throw new ValidationError(
1696
+ "Provide text to synthesize, or --audio <pathOrUrl> to transcribe"
1697
+ );
1698
+ }
1699
+ const model = opts.model ?? atlasDefaultModel(opts.audio ? "stt" : "tts");
1700
+ await runGenerationCommand({
1701
+ model,
1702
+ kind: "audio",
1703
+ refs: opts.audio ? [opts.audio] : [],
1704
+ requests: (refs) => [
1705
+ { flag: "[text]", value: text, candidates: FIELD_CANDIDATES.speechText },
1706
+ { flag: "--audio", value: refs[0], candidates: FIELD_CANDIDATES.audioInput },
1707
+ { flag: "--voice", value: opts.voice, candidates: FIELD_CANDIDATES.voice },
1708
+ { flag: "--format", value: opts.format, candidates: FIELD_CANDIDATES.audioFormat },
1709
+ { flag: "--language", value: opts.language, candidates: FIELD_CANDIDATES.language }
1710
+ ],
1711
+ opts,
1712
+ hint: "audio",
1713
+ defaultWaitTimeoutMs: AUDIO_WAIT_MS,
1714
+ pollIntervalMs: AUDIO_POLL_MS
1715
+ });
1716
+ }
1717
+ );
1718
+ }
1719
+
1720
+ // src/providers/atlascloud/commands/image.ts
1721
+ var IMAGE_WAIT_MS = 3e5;
1722
+ var IMAGE_POLL_MS = 2e3;
1723
+ function imageRequests(prompt, o) {
1724
+ return [
1725
+ { flag: "--prompt", value: prompt, candidates: FIELD_CANDIDATES.prompt },
1726
+ { flag: "--size", value: o.size, candidates: FIELD_CANDIDATES.size },
1727
+ { flag: "--aspect-ratio", value: o.aspectRatio, candidates: FIELD_CANDIDATES.aspectRatio },
1728
+ { flag: "--resolution", value: o.resolution, candidates: FIELD_CANDIDATES.resolution },
1729
+ { flag: "--num-images", value: o.numImages, candidates: FIELD_CANDIDATES.numImages },
1730
+ { flag: "--seed", value: o.seed, candidates: FIELD_CANDIDATES.seed },
1731
+ { flag: "--negative-prompt", value: o.negativePrompt, candidates: FIELD_CANDIDATES.negativePrompt }
1732
+ ];
1733
+ }
1734
+ function addImageFlags(cmd) {
1735
+ return cmd.option("--size <size>", "Output size in the model's format (e.g. 1024*1024 or 1024x1024)").option("--aspect-ratio <ratio>", "Aspect ratio, e.g. 1:1, 16:9 (model-dependent)").option("--resolution <res>", "Resolution tier, e.g. 1k, 2k (model-dependent)").option("-n, --num-images <n>", "Number of images (model-dependent)").option("--seed <n>", "Seed").option("--negative-prompt <text>", "Negative prompt (model-dependent)");
1736
+ }
1737
+ function registerAtlasImageCommands(parent) {
1738
+ const image = parent.command("image <prompt>").description(`Text-to-image via Atlas Cloud (default: ${ATLAS_DEFAULT_MODELS.image})`).option("-m, --model <id>", "Atlas Cloud image model id (env: ATLASCLOUD_IMAGE_MODEL)");
1739
+ addImageFlags(image);
1740
+ addRunOptions(image, { waitTimeoutMs: IMAGE_WAIT_MS, images: true }).action(
1741
+ async (prompt, opts) => {
1742
+ await runGenerationCommand({
1743
+ model: opts.model ?? atlasDefaultModel("image"),
1744
+ kind: "image",
1745
+ requests: () => imageRequests(prompt, opts),
1746
+ opts,
1747
+ hint: "image",
1748
+ defaultWaitTimeoutMs: IMAGE_WAIT_MS,
1749
+ pollIntervalMs: IMAGE_POLL_MS
1750
+ });
1751
+ }
1752
+ );
1753
+ const edit = parent.command("image-to-image").description(
1754
+ `Edit or restyle reference images via Atlas Cloud (default: ${ATLAS_DEFAULT_MODELS.imageEdit})`
1755
+ ).requiredOption("--prompt <text>", "Edit instruction");
1756
+ addRefOption(edit, "Reference image; local files are uploaded first; required").option(
1757
+ "-m, --model <id>",
1758
+ "Atlas Cloud image-edit model id (env: ATLASCLOUD_IMAGE_EDIT_MODEL)"
1759
+ );
1760
+ addImageFlags(edit);
1761
+ addRunOptions(edit, { waitTimeoutMs: IMAGE_WAIT_MS, images: true }).action(
1762
+ async (opts) => {
1763
+ await runGenerationCommand({
1764
+ model: opts.model ?? atlasDefaultModel("imageEdit"),
1765
+ kind: "image",
1766
+ refs: requireRefs(opts.ref),
1767
+ requests: (refs) => [
1768
+ ...imageRequests(opts.prompt, opts),
1769
+ { flag: "--ref", value: refs, candidates: FIELD_CANDIDATES.editImages }
1770
+ ],
1771
+ opts,
1772
+ hint: "image",
1773
+ defaultWaitTimeoutMs: IMAGE_WAIT_MS,
1774
+ pollIntervalMs: IMAGE_POLL_MS
1775
+ });
1776
+ }
1777
+ );
1778
+ }
1779
+
1780
+ // src/providers/atlascloud/commands/run.ts
1781
+ init_errors();
1782
+
1783
+ // src/core/json-input.ts
1784
+ init_errors();
1785
+ import fs9 from "fs";
1786
+ function parseJsonInput(raw, flag = "--input") {
1787
+ let text;
1788
+ if (raw.startsWith("@")) {
1789
+ try {
1790
+ text = fs9.readFileSync(raw.slice(1), "utf8");
1791
+ } catch (e) {
1792
+ const msg = e instanceof Error ? e.message : String(e);
1793
+ throw new ValidationError(`Cannot read ${flag} file: ${msg}`);
1794
+ }
1795
+ } else {
1796
+ text = raw;
1797
+ }
1798
+ try {
1799
+ const parsed = JSON.parse(text);
1800
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
1801
+ throw new Error("input must be a JSON object");
1802
+ }
1803
+ return parsed;
1804
+ } catch (e) {
1805
+ const msg = e instanceof Error ? e.message : String(e);
1806
+ throw new ValidationError(`Invalid ${flag} JSON: ${msg}`);
1807
+ }
1808
+ }
1809
+
1810
+ // src/providers/atlascloud/commands/run.ts
1811
+ var RUN_WAIT_MS = 6e5;
1812
+ function parseMediaKind(raw) {
1813
+ if (raw === void 0) return void 0;
1814
+ const kind = raw.toLowerCase();
1815
+ if (!ATLAS_MEDIA_KINDS.includes(kind)) {
1816
+ throw new ValidationError(`Invalid --type "${raw}". Valid: ${ATLAS_MEDIA_KINDS.join(", ")}`);
1817
+ }
1818
+ return kind;
1819
+ }
1820
+ function registerAtlasRunCommand(parent) {
1821
+ const run = parent.command("run <modelId>").description("Run any Atlas Cloud model with a raw JSON payload (fields per the model's docs)").requiredOption("--input <json>", "Model input as JSON, or @path/to/input.json").option(
1822
+ "--type <type>",
1823
+ "Endpoint: image | video | audio (default: from the model's published schema; 3D uses image). Required with --no-validate or for models without a schema"
1824
+ );
1825
+ addRunOptions(run, { waitTimeoutMs: RUN_WAIT_MS, images: true, video: true }).action(
1826
+ async (modelId, opts) => {
1827
+ const logger = createAtlasLogger({ verbose: opts.verbose, stdoutReserved: opts.async });
1828
+ const input = parseJsonInput(opts.input);
1829
+ if (input.model !== void 0 && input.model !== modelId) {
1830
+ throw new ValidationError(
1831
+ `--input.model "${String(input.model)}" does not match <modelId> "${modelId}"`
1832
+ );
1833
+ }
1834
+ const explicit = parseMediaKind(opts.type);
1835
+ const client = createAtlasClientFromEnv();
1836
+ const spec = await loadModelSpec(client, modelId, logger, opts.validate);
1837
+ const kind = explicit ?? spec?.endpoint;
1838
+ if (!kind) {
1839
+ throw new ValidationError(
1840
+ `Cannot determine the endpoint for ${modelId}; pass --type ${ATLAS_MEDIA_KINDS.join("|")}`
1841
+ );
1842
+ }
1843
+ assertEndpoint(spec, kind, modelId);
1844
+ if (spec) {
1845
+ const unknown = validateRawInput(spec, input);
1846
+ if (unknown.length > 0) {
1847
+ logger.warn(
1848
+ `Fields not in ${modelId}'s published schema (sent anyway): ${unknown.join(", ")}`
1849
+ );
1850
+ }
1851
+ }
1852
+ await executePrediction({
1853
+ client,
1854
+ logger,
1855
+ kind,
1856
+ model: modelId,
1857
+ body: input,
1858
+ opts,
1859
+ defaultWaitTimeoutMs: RUN_WAIT_MS,
1860
+ pollIntervalMs: kind === "video" ? 5e3 : 3e3
1861
+ });
1862
+ }
1863
+ );
1864
+ }
1865
+
1866
+ // src/providers/atlascloud/commands/status.ts
1867
+ var STATUS_WAIT_MS = 6e5;
1868
+ function registerAtlasStatusCommand(parent) {
1869
+ parent.command("status <predictionId>").description("Get an Atlas Cloud prediction; --wait to poll, --download to save outputs").option("--wait", "Poll until the prediction finishes").option("--wait-timeout <ms>", "Max time to wait (with --wait)", String(STATUS_WAIT_MS)).option("--download", "Save outputs when finished (implies --wait)").option("--output <path>", "Output file (single output) or directory (path without extension)").addOption(imageFormatOption()).addOption(noWebpOption()).option("--no-thumb", "Skip downloading the video thumbnail if present").option("-v, --verbose", "Verbose logging").action(async (predictionId, opts) => {
1870
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1871
+ const client = createAtlasClientFromEnv();
1872
+ const wait = Boolean(opts.wait || opts.download);
1873
+ const prediction2 = wait ? await client.waitForPrediction(predictionId, {
1874
+ timeoutMs: parseTimeoutMs(opts.waitTimeout, STATUS_WAIT_MS),
1875
+ resumeHint: `Resume with: ${resumeCommand(predictionId)}`,
1876
+ onPoll: (p, n) => logger.debug(`poll ${n}: ${p.status}`)
1877
+ }) : await client.getPrediction(predictionId);
1878
+ if (!opts.download) {
1879
+ console.log(JSON.stringify(prediction2, null, 2));
1880
+ if (isFailureStatus(prediction2.status)) throw new AtlasPredictionFailedError(prediction2);
1881
+ return;
1882
+ }
1883
+ assertPredictionSucceeded(prediction2);
1884
+ await deliverPrediction(prediction2, { logger, opts: { ...opts, download: true } });
1885
+ });
1886
+ }
1887
+
1888
+ // src/providers/atlascloud/commands/three-d.ts
1889
+ init_errors();
1890
+ var THREE_D_WAIT_MS = 9e5;
1891
+ var THREE_D_POLL_MS = 5e3;
1892
+ function registerAtlas3dCommand(parent) {
1893
+ const cmd = parent.command("3d [prompt]").alias("generate-3d").description(
1894
+ `Text- or image-to-3D via Atlas Cloud (defaults: ${ATLAS_DEFAULT_MODELS.textTo3d}; with --ref ${ATLAS_DEFAULT_MODELS.imageTo3d})`
1895
+ );
1896
+ addRefOption(cmd, "Reference image for image-to-3D (local files are uploaded)").option(
1897
+ "-m, --model <id>",
1898
+ "Atlas Cloud 3D model id (env: ATLASCLOUD_TEXT_TO_3D_MODEL / ATLASCLOUD_IMAGE_TO_3D_MODEL)"
1899
+ ).option("--format <fmt>", "Mesh format, e.g. GLB, OBJ, FBX (model-dependent)").option("--seed <n>", "Seed (model-dependent)");
1900
+ addRunOptions(cmd, { waitTimeoutMs: THREE_D_WAIT_MS }).action(
1901
+ async (prompt, opts) => {
1902
+ const refs = opts.ref ?? [];
1903
+ if (!prompt && refs.length === 0) {
1904
+ throw new ValidationError("Provide a prompt (text-to-3D) or --ref <image> (image-to-3D)");
1905
+ }
1906
+ await runGenerationCommand({
1907
+ model: opts.model ?? atlasDefaultModel(refs.length > 0 ? "imageTo3d" : "textTo3d"),
1908
+ kind: "image",
1909
+ endpointFromSchema: true,
1910
+ refs,
1911
+ requests: (urls) => [
1912
+ { flag: "[prompt]", value: prompt, candidates: FIELD_CANDIDATES.prompt },
1913
+ { flag: "--ref", value: urls, candidates: FIELD_CANDIDATES.primaryImage },
1914
+ { flag: "--format", value: opts.format, candidates: FIELD_CANDIDATES.modelFormat },
1915
+ { flag: "--seed", value: opts.seed, candidates: FIELD_CANDIDATES.seed }
1916
+ ],
1917
+ opts,
1918
+ hint: "3d",
1919
+ defaultWaitTimeoutMs: THREE_D_WAIT_MS,
1920
+ pollIntervalMs: THREE_D_POLL_MS
1921
+ });
1922
+ }
1923
+ );
1924
+ }
1925
+
1926
+ // src/providers/atlascloud/commands/upload.ts
1927
+ function registerAtlasUploadCommand(parent) {
1928
+ parent.command("upload <file>").description("Upload a local file to Atlas Cloud and print its temporary URL").option("-v, --verbose", "Verbose logging").action(async (file, opts) => {
1929
+ const logger = createAtlasLogger({ verbose: opts.verbose, stdoutReserved: true });
1930
+ if (/^(https?|data):/i.test(file.trim())) {
1931
+ logger.warn("Input is already a URL; nothing to upload.");
1932
+ console.log(file.trim());
1933
+ return;
1934
+ }
1935
+ const client = createAtlasClientFromEnv();
1936
+ console.log(await resolveMediaRef(client, file, logger));
1937
+ });
1938
+ }
1939
+
1940
+ // src/providers/atlascloud/commands/video.ts
1941
+ var VIDEO_WAIT_MS = 9e5;
1942
+ var VIDEO_POLL_MS = 5e3;
1943
+ function videoRequests(prompt, o) {
1944
+ return [
1945
+ { flag: "--prompt", value: prompt, candidates: FIELD_CANDIDATES.prompt },
1946
+ { flag: "--duration", value: o.duration, candidates: FIELD_CANDIDATES.duration },
1947
+ { flag: "--resolution", value: o.resolution, candidates: FIELD_CANDIDATES.resolution },
1948
+ { flag: "--aspect-ratio", value: o.aspectRatio, candidates: FIELD_CANDIDATES.aspectRatio },
1949
+ { flag: "--negative-prompt", value: o.negativePrompt, candidates: FIELD_CANDIDATES.negativePrompt },
1950
+ { flag: "--seed", value: o.seed, candidates: FIELD_CANDIDATES.seed }
1951
+ ];
1952
+ }
1953
+ function addVideoFlags(cmd) {
1954
+ return cmd.option("--duration <s>", "Duration in seconds (model-dependent values)").option("--resolution <res>", "Resolution, e.g. 480p, 720p, 1080p (model-dependent)").option("--aspect-ratio <ratio>", "Aspect ratio, e.g. 16:9, 9:16 (model-dependent)").option("--negative-prompt <text>", "Negative prompt (model-dependent)").option("--seed <n>", "Seed");
1955
+ }
1956
+ function registerAtlasVideoCommands(parent) {
1957
+ const video = parent.command("video <prompt>").description(`Text-to-video via Atlas Cloud (default: ${ATLAS_DEFAULT_MODELS.video})`).option("-m, --model <id>", "Atlas Cloud video model id (env: ATLASCLOUD_VIDEO_MODEL)");
1958
+ addVideoFlags(video);
1959
+ addRunOptions(video, { waitTimeoutMs: VIDEO_WAIT_MS, video: true }).action(
1960
+ async (prompt, opts) => {
1961
+ await runGenerationCommand({
1962
+ model: opts.model ?? atlasDefaultModel("video"),
1963
+ kind: "video",
1964
+ requests: () => videoRequests(prompt, opts),
1965
+ opts,
1966
+ hint: "video",
1967
+ defaultWaitTimeoutMs: VIDEO_WAIT_MS,
1968
+ pollIntervalMs: VIDEO_POLL_MS
1969
+ });
1970
+ }
1971
+ );
1972
+ const i2v = parent.command("image-to-video").description(
1973
+ `Animate reference image(s) via Atlas Cloud (default: ${ATLAS_DEFAULT_MODELS.imageToVideo})`
1974
+ ).option("--prompt <text>", "Motion / scene prompt");
1975
+ addRefOption(i2v, "Reference image; local files are uploaded first; required").option(
1976
+ "-m, --model <id>",
1977
+ "Atlas Cloud image-to-video model id (env: ATLASCLOUD_IMAGE_TO_VIDEO_MODEL)"
1978
+ );
1979
+ addVideoFlags(i2v);
1980
+ addRunOptions(i2v, { waitTimeoutMs: VIDEO_WAIT_MS, video: true }).action(
1981
+ async (opts) => {
1982
+ await runGenerationCommand({
1983
+ model: opts.model ?? atlasDefaultModel("imageToVideo"),
1984
+ kind: "video",
1985
+ refs: requireRefs(opts.ref),
1986
+ requests: (refs) => [
1987
+ ...videoRequests(opts.prompt, opts),
1988
+ { flag: "--ref", value: refs, candidates: FIELD_CANDIDATES.primaryImage }
1989
+ ],
1990
+ opts,
1991
+ hint: "video",
1992
+ defaultWaitTimeoutMs: VIDEO_WAIT_MS,
1993
+ pollIntervalMs: VIDEO_POLL_MS
1994
+ });
1995
+ }
1996
+ );
1997
+ }
1998
+
1999
+ // src/providers/atlascloud/commands/index.ts
2000
+ function registerAtlasCloudCommands(program2) {
2001
+ const atlas = program2.command("atlas").alias("atlascloud").description(
2002
+ "Atlas Cloud: image, image-to-image, video, image-to-video, audio (TTS/STT/music), 3D, generic model runner, async status"
2003
+ );
2004
+ registerAtlasImageCommands(atlas);
2005
+ registerAtlasVideoCommands(atlas);
2006
+ registerAtlasAudioCommand(atlas);
2007
+ registerAtlas3dCommand(atlas);
2008
+ registerAtlasRunCommand(atlas);
2009
+ registerAtlasStatusCommand(atlas);
2010
+ registerAtlasUploadCommand(atlas);
2011
+ }
2012
+
102
2013
  // src/providers/byteplus/commands/generate-3d.ts
103
- import path5 from "path";
2014
+ import path9 from "path";
104
2015
 
105
2016
  // src/core/image-input.ts
106
- import fs from "fs/promises";
107
- import path from "path";
2017
+ import fs10 from "fs/promises";
2018
+ import path8 from "path";
108
2019
  var IMAGE_MIME_BY_EXT = {
109
2020
  ".jpg": "image/jpeg",
110
2021
  ".jpeg": "image/jpeg",
@@ -131,7 +2042,7 @@ function isHttpUrl(input) {
131
2042
  return /^https?:\/\//i.test(input);
132
2043
  }
133
2044
  async function readAsDataUrl(filePath, mimeTable, kindLabel, opts = {}) {
134
- const ext = path.extname(filePath).toLowerCase();
2045
+ const ext = path8.extname(filePath).toLowerCase();
135
2046
  const mime = mimeTable[ext];
136
2047
  if (!mime) {
137
2048
  throw new Error(
@@ -140,7 +2051,7 @@ async function readAsDataUrl(filePath, mimeTable, kindLabel, opts = {}) {
140
2051
  }
141
2052
  const hard = opts.hardLimitBytes ?? DEFAULT_HARD_LIMIT_BYTES;
142
2053
  const soft = opts.softLimitBytes ?? DEFAULT_SOFT_LIMIT_BYTES;
143
- const buf = await fs.readFile(filePath);
2054
+ const buf = await fs10.readFile(filePath);
144
2055
  if (buf.byteLength > hard) {
145
2056
  throw new Error(
146
2057
  `${kindLabel} file ${filePath} is ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 exceeds ${(hard / 1024 / 1024).toFixed(0)}MB limit. Upload to a URL instead.`
@@ -183,65 +2094,8 @@ function normalizeOpts(opts) {
183
2094
  return opts;
184
2095
  }
185
2096
 
186
- // src/core/logger.ts
187
- var ANSI = {
188
- reset: "\x1B[0m",
189
- bold: "\x1B[1m",
190
- green: "\x1B[32m",
191
- yellow: "\x1B[33m",
192
- red: "\x1B[31m",
193
- blue: "\x1B[34m",
194
- cyan: "\x1B[36m",
195
- dim: "\x1B[2m"
196
- };
197
- function c(color, text) {
198
- if (process.env.NO_COLOR || !process.stdout.isTTY) return text;
199
- return `${color}${text}${ANSI.reset}`;
200
- }
201
- function createLogger(opts) {
202
- const { verbose } = opts;
203
- return {
204
- info(msg) {
205
- console.log(c(ANSI.blue, ` ${msg}`));
206
- },
207
- success(msg) {
208
- console.log(c(ANSI.green, `\u2713 ${msg}`));
209
- },
210
- warn(msg) {
211
- console.warn(c(ANSI.yellow, `\u26A0 ${msg}`));
212
- },
213
- error(msg) {
214
- console.error(c(ANSI.red, `\u2717 ${msg}`));
215
- },
216
- debug(msg) {
217
- if (verbose) console.log(c(ANSI.dim, ` [debug] ${msg}`));
218
- },
219
- header(msg) {
220
- const line = "=".repeat(60);
221
- console.log(c(ANSI.bold + ANSI.cyan, `
222
- ${line}
223
- ${msg}
224
- ${line}
225
- `));
226
- }
227
- };
228
- }
229
- var defaultLogger = createLogger({ verbose: false });
230
-
231
- // src/core/output-dir.ts
232
- import fs2 from "fs";
233
- import path2 from "path";
234
- var cachedOutputDir;
235
- function getOutputDir() {
236
- if (cachedOutputDir) return cachedOutputDir;
237
- const dir = process.env.MULTIX_OUTPUT_DIR ? path2.resolve(process.env.MULTIX_OUTPUT_DIR) : path2.resolve(process.cwd(), "multix-output");
238
- fs2.mkdirSync(dir, { recursive: true });
239
- cachedOutputDir = dir;
240
- return dir;
241
- }
242
-
243
2097
  // src/providers/leonardo/poll.ts
244
- var PollTimeoutError = class extends Error {
2098
+ var PollTimeoutError2 = class extends Error {
245
2099
  constructor() {
246
2100
  super("Polling timed out before the operation completed.");
247
2101
  this.name = "PollTimeoutError";
@@ -272,221 +2126,11 @@ async function poll(opts) {
272
2126
  });
273
2127
  });
274
2128
  }
275
- throw new PollTimeoutError();
276
- }
277
-
278
- // src/core/env-loader.ts
279
- init_redact();
280
- import fs3 from "fs";
281
- import os from "os";
282
- import path3 from "path";
283
- import { config as dotenvConfig, parse as dotenvParse } from "dotenv";
284
- var loaded = false;
285
- var loadedFiles = [];
286
- var LEGACY_SKILL_DIRS = ["ai-multimodal", "ak-ai-multimodal"];
287
- function legacyFilesUnder(root) {
288
- const claude = path3.join(root, ".claude");
289
- return [
290
- ...LEGACY_SKILL_DIRS.map((dir) => path3.join(claude, "skills", dir, ".env")),
291
- path3.join(claude, "skills", ".env"),
292
- path3.join(claude, ".env")
293
- ];
294
- }
295
- function samePath(a, b) {
296
- const norm = (p) => {
297
- const r = path3.resolve(p);
298
- return process.platform === "win32" ? r.toLowerCase() : r;
299
- };
300
- return norm(a) === norm(b);
301
- }
302
- function legacyClaudeEnvFiles(cwd, home) {
303
- const files = legacyFilesUnder(cwd);
304
- if (home && !samePath(home, cwd)) files.push(...legacyFilesUnder(home));
305
- return files;
306
- }
307
- function legacyClaudeEnvEnabled(env = process.env) {
308
- const value = env.MULTIX_LEGACY_CLAUDE_ENV;
309
- return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
310
- }
311
- var LEGACY_ALLOWED_NAME = /^[A-Z][A-Z0-9_]*_(API_KEY(_\d+)?|API_TOKEN|ACCOUNT_ID|GATEWAY_ID)$|^(FAL_KEY|ARK_API_KEY)$/;
312
- function isLegacyAllowedName(name) {
313
- return LEGACY_ALLOWED_NAME.test(name);
314
- }
315
- function isReadableFile(file) {
316
- try {
317
- return fs3.statSync(file).isFile();
318
- } catch {
319
- return false;
320
- }
321
- }
322
- function loadFile(file) {
323
- if (!isReadableFile(file)) return;
324
- const result = dotenvConfig({ path: file, override: false });
325
- if (!result.error) loadedFiles.push(file);
326
- }
327
- function loadLegacyFile(file) {
328
- if (!isReadableFile(file)) return;
329
- let parsed;
330
- try {
331
- parsed = dotenvParse(fs3.readFileSync(file));
332
- } catch {
333
- return;
334
- }
335
- for (const [name, value] of Object.entries(parsed)) {
336
- if (isLegacyAllowedName(name) && process.env[name] === void 0) process.env[name] = value;
337
- }
338
- loadedFiles.push(file);
339
- }
340
- function loadEnv() {
341
- if (loaded) return;
342
- loaded = true;
343
- loadedFiles = [];
344
- const cwd = process.cwd();
345
- const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os.homedir();
346
- loadFile(path3.resolve(cwd, ".env"));
347
- if (home) loadFile(path3.join(home, ".multix", ".env"));
348
- if (legacyClaudeEnvEnabled()) {
349
- for (const file of legacyClaudeEnvFiles(cwd, home)) loadLegacyFile(file);
350
- }
351
- }
352
- function getLoadedEnvFiles() {
353
- return [...loadedFiles];
354
- }
355
- function resolveKey(name) {
356
- return process.env[name] || void 0;
2129
+ throw new PollTimeoutError2();
357
2130
  }
358
2131
 
359
2132
  // src/providers/byteplus/client.ts
360
- init_errors();
361
-
362
- // src/core/http-client.ts
363
- init_errors();
364
- import { once } from "events";
365
- import fs4 from "fs";
366
- import path4 from "path";
367
- import { pipeline } from "stream/promises";
368
-
369
- // src/core/fetch-json.ts
370
- init_errors();
371
- var DEFAULT_TIMEOUT_MS = 12e4;
372
- var SNIPPET_MAX = 500;
373
- function parseRetryAfter(value, now = Date.now()) {
374
- if (!value) return void 0;
375
- const trimmed = value.trim();
376
- if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1e3);
377
- const date = Date.parse(trimmed);
378
- if (Number.isNaN(date)) return void 0;
379
- return Math.max(0, date - now);
380
- }
381
- async function httpErrorFromResponse(response, url) {
382
- let snippet = "";
383
- try {
384
- const text = await response.text();
385
- snippet = text.slice(0, SNIPPET_MAX);
386
- } catch {
387
- snippet = "(could not read body)";
388
- }
389
- return new HttpError(
390
- response.status,
391
- snippet,
392
- url,
393
- parseRetryAfter(response.headers.get("retry-after"))
394
- );
395
- }
396
- async function fetchJson(opts) {
397
- const { url, method = "GET", headers: headers2 = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
398
- const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
399
- const init = {
400
- method,
401
- headers: {
402
- "Content-Type": "application/json",
403
- ...headers2
404
- },
405
- signal: AbortSignal.timeout(timeoutMs)
406
- };
407
- if (body !== void 0) {
408
- init.body = JSON.stringify(body);
409
- }
410
- let response;
411
- try {
412
- response = await doFetch(url, init);
413
- } catch (cause) {
414
- const msg = cause instanceof Error ? cause.message : String(cause);
415
- throw new HttpError(0, msg, url);
416
- }
417
- if (!response.ok) {
418
- throw await httpErrorFromResponse(response, url);
419
- }
420
- let text;
421
- try {
422
- text = await response.text();
423
- } catch (cause) {
424
- const msg = cause instanceof Error ? cause.message : String(cause);
425
- throw new HttpError(0, `Failed to read response body: ${msg}`, url);
426
- }
427
- if (text.trim() === "") return void 0;
428
- try {
429
- return JSON.parse(text);
430
- } catch {
431
- throw new HttpError(
432
- response.status,
433
- `Invalid JSON response: ${text.slice(0, SNIPPET_MAX)}`,
434
- url
435
- );
436
- }
437
- }
438
-
439
- // src/core/http-client.ts
440
- async function httpJson(opts) {
441
- return fetchJson(opts);
442
- }
443
- async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers2) {
444
- fs4.mkdirSync(path4.dirname(dest), { recursive: true });
445
- let response;
446
- try {
447
- response = await globalThis.fetch(url, {
448
- signal: AbortSignal.timeout(timeoutMs),
449
- ...headers2 ? { headers: headers2 } : {}
450
- });
451
- } catch (cause) {
452
- const msg = cause instanceof Error ? cause.message : String(cause);
453
- throw new HttpError(0, msg, url);
454
- }
455
- if (!response.ok) {
456
- throw new HttpError(response.status, "(download failed)", url);
457
- }
458
- if (!response.body) {
459
- throw new HttpError(0, "Response body is null", url);
460
- }
461
- const fileStream = fs4.createWriteStream(dest);
462
- try {
463
- await pipeline(response.body, fileStream);
464
- } catch (err2) {
465
- if (!fileStream.closed) await once(fileStream, "close");
466
- throw err2;
467
- }
468
- }
469
- async function fetchBytes(urlOrDataUri) {
470
- if (urlOrDataUri.startsWith("data:")) {
471
- const commaIdx = urlOrDataUri.indexOf(",");
472
- if (commaIdx === -1) throw new Error("Malformed data URI: missing comma");
473
- const encoded = urlOrDataUri.slice(commaIdx + 1);
474
- return Buffer.from(encoded, "base64");
475
- }
476
- let response;
477
- try {
478
- response = await globalThis.fetch(urlOrDataUri, {
479
- signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
480
- });
481
- } catch (cause) {
482
- const msg = cause instanceof Error ? cause.message : String(cause);
483
- throw new HttpError(0, msg, urlOrDataUri);
484
- }
485
- if (!response.ok) {
486
- throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
487
- }
488
- return Buffer.from(await response.arrayBuffer());
489
- }
2133
+ init_errors();
490
2134
 
491
2135
  // src/providers/byteplus/models.ts
492
2136
  var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
@@ -531,9 +2175,9 @@ var BytePlusClient = class {
531
2175
  }
532
2176
  apiKey;
533
2177
  baseUrl;
534
- async request(path49, opts = {}) {
2178
+ async request(path52, opts = {}) {
535
2179
  const base = this.baseUrl.replace(/\/$/, "");
536
- const url = new URL(base + path49);
2180
+ const url = new URL(base + path52);
537
2181
  if (opts.query) {
538
2182
  for (const [k, v] of Object.entries(opts.query)) {
539
2183
  if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
@@ -551,14 +2195,14 @@ var BytePlusClient = class {
551
2195
  timeoutMs: opts.timeoutMs ?? 12e4
552
2196
  });
553
2197
  }
554
- get(path49, query, logger) {
555
- return this.request(path49, { method: "GET", query, logger });
2198
+ get(path52, query, logger) {
2199
+ return this.request(path52, { method: "GET", query, logger });
556
2200
  }
557
- post(path49, body, logger) {
558
- return this.request(path49, { method: "POST", body, logger });
2201
+ post(path52, body, logger) {
2202
+ return this.request(path52, { method: "POST", body, logger });
559
2203
  }
560
- delete(path49, logger) {
561
- return this.request(path49, { method: "DELETE", logger });
2204
+ delete(path52, logger) {
2205
+ return this.request(path52, { method: "DELETE", logger });
562
2206
  }
563
2207
  };
564
2208
  function createBytePlusClient() {
@@ -663,13 +2307,13 @@ function registerBytePlusGenerate3DCommand(parent) {
663
2307
  process.exit(1);
664
2308
  }
665
2309
  const ext = inferModelFileExt(url);
666
- const outPath = opts.output ?? path5.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}-${Date.now()}.${ext}`);
2310
+ const outPath = opts.output ?? path9.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}-${Date.now()}.${ext}`);
667
2311
  await downloadModelFile(url, outPath, logger);
668
2312
  console.log(`
669
2313
  Task: ${taskId}`);
670
2314
  console.log(`Saved: ${outPath}`);
671
2315
  } catch (e) {
672
- if (e instanceof PollTimeoutError) {
2316
+ if (e instanceof PollTimeoutError2) {
673
2317
  logger.error(
674
2318
  `Polling timed out \u2014 keep the task and check later: multix byteplus status ${taskId} --wait --download`
675
2319
  );
@@ -685,196 +2329,9 @@ Task: ${taskId}`);
685
2329
  );
686
2330
  }
687
2331
 
688
- // src/core/image-webp-finalize.ts
689
- init_errors();
690
- import { spawnSync } from "child_process";
691
- import fs6 from "fs";
692
- import path6 from "path";
693
- import { Option } from "commander";
694
-
695
- // src/core/image-header.ts
696
- import fs5 from "fs";
697
- function sniffImageKind(buf) {
698
- if (buf.length >= 8 && buf.readUInt32BE(0) === 2303741511) return "png";
699
- if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpeg";
700
- if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP")
701
- return "webp";
702
- return void 0;
703
- }
704
- function readPng(buf) {
705
- if (buf.length < 33) return void 0;
706
- const width = buf.readUInt32BE(16);
707
- const height = buf.readUInt32BE(20);
708
- const colorType = buf[25];
709
- let hasAlpha = colorType === 4 || colorType === 6;
710
- let offset = 8;
711
- while (!hasAlpha && offset + 8 <= buf.length) {
712
- const len = buf.readUInt32BE(offset);
713
- const type = buf.toString("ascii", offset + 4, offset + 8);
714
- if (type === "tRNS") hasAlpha = true;
715
- if (type === "IDAT" || type === "IEND") break;
716
- offset += 12 + len;
717
- }
718
- return { kind: "png", width, height, hasAlpha };
719
- }
720
- function readJpeg(buf) {
721
- let offset = 2;
722
- while (offset + 9 < buf.length) {
723
- if (buf[offset] !== 255) {
724
- offset++;
725
- continue;
726
- }
727
- const marker = buf[offset + 1] ?? 0;
728
- if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
729
- return {
730
- kind: "jpeg",
731
- height: buf.readUInt16BE(offset + 5),
732
- width: buf.readUInt16BE(offset + 7),
733
- hasAlpha: false
734
- };
735
- }
736
- if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
737
- offset += 2;
738
- continue;
739
- }
740
- offset += 2 + buf.readUInt16BE(offset + 2);
741
- }
742
- return void 0;
743
- }
744
- function readWebp(buf) {
745
- if (buf.length < 30) return void 0;
746
- const chunk = buf.toString("ascii", 12, 16);
747
- if (chunk === "VP8X") {
748
- const flags = buf[20] ?? 0;
749
- return {
750
- kind: "webp",
751
- width: 1 + buf.readUIntLE(24, 3),
752
- height: 1 + buf.readUIntLE(27, 3),
753
- hasAlpha: (flags & 16) !== 0
754
- };
755
- }
756
- if (chunk === "VP8L") {
757
- const bits = buf.readUInt32LE(21);
758
- return {
759
- kind: "webp",
760
- width: 1 + (bits & 16383),
761
- height: 1 + (bits >>> 14 & 16383),
762
- hasAlpha: (bits >>> 28 & 1) === 1
763
- };
764
- }
765
- if (chunk === "VP8 ") {
766
- return {
767
- kind: "webp",
768
- width: buf.readUInt16LE(26) & 16383,
769
- height: buf.readUInt16LE(28) & 16383,
770
- hasAlpha: false
771
- };
772
- }
773
- return void 0;
774
- }
775
- function readImageHeader(buf) {
776
- const kind = sniffImageKind(buf);
777
- if (kind === "png") return readPng(buf);
778
- if (kind === "jpeg") return readJpeg(buf);
779
- if (kind === "webp") return readWebp(buf);
780
- return void 0;
781
- }
782
- function readImageHeaderFromFile(file) {
783
- return readImageHeader(fs5.readFileSync(file));
784
- }
785
-
786
- // src/core/image-webp-finalize.ts
787
- var CWEBP_ARGS = ["-q", "85", "-m", "6", "-metadata", "none"];
788
- var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff"]);
789
- function imageFormatOption() {
790
- return new Option(
791
- "--image-format <mode>",
792
- "Final image format: webp (default, cwebp -q 85) | original (keep provider format/quality)"
793
- );
794
- }
795
- function noWebpOption() {
796
- return new Option("--no-webp", "Keep the provider's original image format and quality");
797
- }
798
- function resolveImageFormatMode(opts, explicitProviderFormat = false) {
799
- if (opts.webp === false && opts.imageFormat === void 0) return "original";
800
- const raw = opts.imageFormat ?? process.env.MULTIX_IMAGE_FORMAT;
801
- if (raw !== void 0 && raw !== "") {
802
- const mode = raw.toLowerCase();
803
- if (mode !== "webp" && mode !== "original") {
804
- throw new ValidationError(`Invalid --image-format: ${raw}. Valid: webp, original`);
805
- }
806
- return mode;
807
- }
808
- if (explicitProviderFormat) return "original";
809
- const ext = opts.output ? path6.extname(opts.output).toLowerCase() : "";
810
- if (ext && ext !== ".webp" && IMAGE_EXTS.has(ext)) return "original";
811
- return "webp";
812
- }
813
- function hasCwebp() {
814
- const res = spawnSync("cwebp", ["-version"], { stdio: "ignore" });
815
- return !res.error && res.status === 0;
816
- }
817
- function webpTarget(file) {
818
- const ext = path6.extname(file);
819
- return ext.toLowerCase() === ".webp" ? file : `${file.slice(0, -ext.length || void 0)}.webp`;
820
- }
821
- function convertToWebp(file, logger, keepPath = false) {
822
- const src = readImageHeaderFromFile(file);
823
- if (!src) {
824
- logger?.warn(`Skipping WebP conversion (unrecognized image): ${file}`);
825
- return file;
826
- }
827
- if (src.kind === "webp") {
828
- const target2 = keepPath ? file : webpTarget(file);
829
- if (target2 !== file) fs6.renameSync(file, target2);
830
- return target2;
831
- }
832
- const target = keepPath ? file : webpTarget(file);
833
- const tmp = `${target}.tmp-${process.pid}.webp`;
834
- const res = spawnSync("cwebp", [...CWEBP_ARGS, file, "-o", tmp], { encoding: "utf8" });
835
- if (res.error || res.status !== 0) {
836
- fs6.rmSync(tmp, { force: true });
837
- logger?.warn(`cwebp failed, keeping original ${file}: ${res.stderr || res.error?.message}`);
838
- return file;
839
- }
840
- const out = fs6.existsSync(tmp) ? readImageHeaderFromFile(tmp) : void 0;
841
- const problem = !out ? "output is not a valid WebP" : out.width !== src.width || out.height !== src.height ? `size changed ${src.width}x${src.height} -> ${out.width}x${out.height}` : src.hasAlpha && !out.hasAlpha ? "transparency lost" : void 0;
842
- if (problem) {
843
- fs6.rmSync(tmp, { force: true });
844
- logger?.warn(`WebP verification failed (${problem}), keeping original ${file}`);
845
- return file;
846
- }
847
- if (target !== file) fs6.rmSync(file, { force: true });
848
- fs6.renameSync(tmp, target);
849
- logger?.debug(`WebP verified ${out?.width}x${out?.height} alpha=${out?.hasAlpha}: ${target}`);
850
- return target;
851
- }
852
- function finalizeGeneratedImages(files, opts) {
853
- const mode = resolveImageFormatMode(opts, opts.explicitProviderFormat);
854
- if (mode === "original" || files.length === 0) return files;
855
- if (!hasCwebp()) {
856
- opts.logger?.warn(
857
- "cwebp not found on PATH; keeping original image format (install libwebp, or pass --image-format original)"
858
- );
859
- return files;
860
- }
861
- const outputAbs = opts.output ? path6.resolve(opts.output) : void 0;
862
- const wroteOutputDirectly = outputAbs !== void 0 && files.some((f) => path6.resolve(f) === outputAbs);
863
- const finals = files.map(
864
- (f) => convertToWebp(f, opts.logger, outputAbs !== void 0 && path6.resolve(f) === outputAbs)
865
- );
866
- if (opts.output && outputAbs && !wroteOutputDirectly && finals[0] && fs6.existsSync(outputAbs)) {
867
- const outExt = path6.extname(outputAbs).toLowerCase();
868
- if ((outExt === ".webp" || outExt === "") && path6.extname(finals[0]).toLowerCase() === ".webp") {
869
- fs6.copyFileSync(finals[0], outputAbs);
870
- }
871
- }
872
- return finals;
873
- }
874
-
875
2332
  // src/providers/byteplus/generators/image.ts
876
- import fs7 from "fs";
877
- import path7 from "path";
2333
+ import fs11 from "fs";
2334
+ import path10 from "path";
878
2335
  var ASPECT_TO_SIZE = {
879
2336
  "1:1": "2048x2048",
880
2337
  "16:9": "2560x1440",
@@ -938,14 +2395,14 @@ async function generateSeedream(client, opts) {
938
2395
  }
939
2396
  const ext = (() => {
940
2397
  try {
941
- const e = path7.extname(new URL(url).pathname);
2398
+ const e = path10.extname(new URL(url).pathname);
942
2399
  return e || ".jpg";
943
2400
  } catch {
944
2401
  return ".jpg";
945
2402
  }
946
2403
  })();
947
- const filename = opts.output && items.length === 1 ? path7.resolve(opts.output) : path7.join(outDir, `byteplus-image-${ts}-${i + 1}${ext}`);
948
- fs7.mkdirSync(path7.dirname(filename), { recursive: true });
2404
+ const filename = opts.output && items.length === 1 ? path10.resolve(opts.output) : path10.join(outDir, `byteplus-image-${ts}-${i + 1}${ext}`);
2405
+ fs11.mkdirSync(path10.dirname(filename), { recursive: true });
949
2406
  await downloadFile(url, filename);
950
2407
  saved.push({ path: filename, url, seed: item.seed });
951
2408
  logger?.success(`Saved ${filename}`);
@@ -1028,97 +2485,13 @@ Generated ${finalImages.length} image(s):`);
1028
2485
  }
1029
2486
  );
1030
2487
  }
1031
- function collect2(value, prev) {
1032
- return [...prev, value];
1033
- }
1034
-
1035
- // src/providers/byteplus/commands/image-to-video.ts
1036
- import path9 from "path";
1037
-
1038
- // src/core/video-thumb.ts
1039
- import fs8 from "fs";
1040
- import path8 from "path";
1041
- var THUMB_KEYS = /* @__PURE__ */ new Set([
1042
- "cover_image_url",
1043
- "coverImageUrl",
1044
- "cover_url",
1045
- "coverUrl",
1046
- "thumbnail_url",
1047
- "thumbnailUrl",
1048
- "thumb_url",
1049
- "thumbUrl",
1050
- "preview_url",
1051
- "previewUrl",
1052
- "first_frame_url",
1053
- "firstFrameUrl",
1054
- "poster_url",
1055
- "posterUrl",
1056
- "image_url",
1057
- "imageUrl"
1058
- ]);
1059
- var IMAGE_EXT_RE = /\.(jpe?g|png|webp|gif|bmp)(\?|$)/i;
1060
- function isImageUrl(value) {
1061
- if (typeof value !== "string") return false;
1062
- if (!/^https?:\/\//i.test(value)) return false;
1063
- return IMAGE_EXT_RE.test(value);
1064
- }
1065
- function detectThumbUrl(obj) {
1066
- if (obj === null || obj === void 0) return null;
1067
- if (Array.isArray(obj)) {
1068
- for (const item of obj) {
1069
- const found = detectThumbUrl(item);
1070
- if (found) return found;
1071
- }
1072
- return null;
1073
- }
1074
- if (typeof obj !== "object") return null;
1075
- const record = obj;
1076
- for (const [key, value] of Object.entries(record)) {
1077
- if (THUMB_KEYS.has(key) && isImageUrl(value)) return value;
1078
- }
1079
- for (const value of Object.values(record)) {
1080
- if (value && typeof value === "object") {
1081
- const found = detectThumbUrl(value);
1082
- if (found) return found;
1083
- }
1084
- }
1085
- return null;
1086
- }
1087
- async function downloadThumbBeside(thumbUrl, videoPath, logger) {
1088
- const match = thumbUrl.match(IMAGE_EXT_RE);
1089
- const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
1090
- const dir = path8.dirname(videoPath);
1091
- const base = path8.basename(videoPath, path8.extname(videoPath));
1092
- const dest = path8.join(dir, `${base}_thumb.${ext}`);
1093
- try {
1094
- await downloadFile(thumbUrl, dest);
1095
- logger?.success(`Saved thumb ${dest}`);
1096
- return dest;
1097
- } catch (e) {
1098
- logger?.warn(`Thumb download failed: ${e instanceof Error ? e.message : String(e)}`);
1099
- return null;
1100
- }
1101
- }
1102
- async function maybeDownloadThumb(source, videoPath, opts = {}) {
1103
- if (opts.skip) return null;
1104
- const url = detectThumbUrl(source);
1105
- if (!url) {
1106
- opts.logger?.debug("No thumbnail URL detected in provider response");
1107
- return null;
1108
- }
1109
- const saved = await downloadThumbBeside(url, videoPath, opts.logger);
1110
- if (saved && opts.copyTo) {
1111
- const copyExt = path8.extname(saved);
1112
- const copyBase = path8.basename(opts.copyTo, path8.extname(opts.copyTo));
1113
- const copyDir = path8.dirname(path8.resolve(opts.copyTo));
1114
- const copyDest = path8.join(copyDir, `${copyBase}_thumb${copyExt}`);
1115
- fs8.mkdirSync(copyDir, { recursive: true });
1116
- fs8.copyFileSync(saved, copyDest);
1117
- opts.logger?.success(`Copied thumb to ${copyDest}`);
1118
- }
1119
- return saved;
2488
+ function collect2(value, prev) {
2489
+ return [...prev, value];
1120
2490
  }
1121
2491
 
2492
+ // src/providers/byteplus/commands/image-to-video.ts
2493
+ import path11 from "path";
2494
+
1122
2495
  // src/providers/byteplus/generators/video.ts
1123
2496
  function buildFlagSuffix(opts) {
1124
2497
  const parts = [];
@@ -1240,7 +2613,7 @@ function registerBytePlusImageToVideoCommand(parent) {
1240
2613
  logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
1241
2614
  process.exit(1);
1242
2615
  }
1243
- const outPath = opts.output ?? path9.join(getOutputDir(), `byteplus-i2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
2616
+ const outPath = opts.output ?? path11.join(getOutputDir(), `byteplus-i2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1244
2617
  await downloadVideo(url, outPath, logger);
1245
2618
  await maybeDownloadThumb(final, outPath, {
1246
2619
  skip: opts.thumb === false,
@@ -1251,7 +2624,7 @@ function registerBytePlusImageToVideoCommand(parent) {
1251
2624
  Task: ${taskId}`);
1252
2625
  console.log(`Saved: ${outPath}`);
1253
2626
  } catch (e) {
1254
- if (e instanceof PollTimeoutError) {
2627
+ if (e instanceof PollTimeoutError2) {
1255
2628
  logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
1256
2629
  process.exit(1);
1257
2630
  }
@@ -1266,7 +2639,7 @@ Task: ${taskId}`);
1266
2639
  }
1267
2640
 
1268
2641
  // src/providers/byteplus/commands/reference-to-video.ts
1269
- import path10 from "path";
2642
+ import path12 from "path";
1270
2643
 
1271
2644
  // src/providers/byteplus/reference-input.ts
1272
2645
  var MAX_TOTAL_REFS = 12;
@@ -1298,10 +2671,10 @@ function toRef(kind, resolved, role) {
1298
2671
  return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
1299
2672
  }
1300
2673
  async function resolveRefSpec(kind, spec, logger) {
1301
- const { path: path49, role } = parseRefSpec(spec);
1302
- if (kind === "image") return toRef(kind, await resolveImageInput(path49, logger), role);
1303
- if (kind === "video") return toRef(kind, await resolveVideoInput(path49, logger), role);
1304
- return toRef(kind, await resolveAudioInput(path49, logger), role);
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);
1305
2678
  }
1306
2679
  async function resolveRefSpecs(specs, logger) {
1307
2680
  const out = [];
@@ -1398,7 +2771,7 @@ function registerBytePlusReferenceToVideoCommand(parent) {
1398
2771
  logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
1399
2772
  process.exit(1);
1400
2773
  }
1401
- const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-r2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
2774
+ const outPath = opts.output ?? path12.join(getOutputDir(), `byteplus-r2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1402
2775
  await downloadVideo(url, outPath, logger);
1403
2776
  await maybeDownloadThumb(final, outPath, {
1404
2777
  skip: opts.thumb === false,
@@ -1409,7 +2782,7 @@ function registerBytePlusReferenceToVideoCommand(parent) {
1409
2782
  Task: ${taskId}`);
1410
2783
  console.log(`Saved: ${outPath}`);
1411
2784
  } catch (e) {
1412
- if (e instanceof PollTimeoutError) {
2785
+ if (e instanceof PollTimeoutError2) {
1413
2786
  logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
1414
2787
  process.exit(1);
1415
2788
  }
@@ -1424,7 +2797,7 @@ Task: ${taskId}`);
1424
2797
  }
1425
2798
 
1426
2799
  // src/providers/byteplus/commands/status.ts
1427
- import path11 from "path";
2800
+ import path13 from "path";
1428
2801
  function registerBytePlusStatusCommand(parent) {
1429
2802
  parent.command("status <taskId>").description("Get a Seedance task's status; --wait + --download to fetch the result MP4").option("--wait", "Poll until terminal status").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download MP4 on success (implies --wait)").option("--output <path>", "Output MP4 path (with --download)").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
1430
2803
  async (taskId, opts) => {
@@ -1442,7 +2815,7 @@ function registerBytePlusStatusCommand(parent) {
1442
2815
  const videoUrl2 = content?.video_url;
1443
2816
  const fileUrl = content?.file_url;
1444
2817
  if (videoUrl2) {
1445
- const outPath = opts.output ?? path11.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
2818
+ const outPath = opts.output ?? path13.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
1446
2819
  await downloadVideo(videoUrl2, outPath, logger);
1447
2820
  await maybeDownloadThumb(status, outPath, {
1448
2821
  skip: opts.thumb === false,
@@ -1452,7 +2825,7 @@ function registerBytePlusStatusCommand(parent) {
1452
2825
  console.log(`Saved: ${outPath}`);
1453
2826
  } else if (fileUrl) {
1454
2827
  const ext = inferModelFileExt(fileUrl);
1455
- const outPath = opts.output ?? path11.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}.${ext}`);
2828
+ const outPath = opts.output ?? path13.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}.${ext}`);
1456
2829
  await downloadModelFile(fileUrl, outPath, logger);
1457
2830
  console.log(`Saved: ${outPath}`);
1458
2831
  } else {
@@ -1461,7 +2834,7 @@ function registerBytePlusStatusCommand(parent) {
1461
2834
  }
1462
2835
  }
1463
2836
  } catch (e) {
1464
- if (e instanceof PollTimeoutError) {
2837
+ if (e instanceof PollTimeoutError2) {
1465
2838
  logger.error(`Timed out waiting for task ${taskId}`);
1466
2839
  process.exit(1);
1467
2840
  }
@@ -1476,7 +2849,7 @@ function registerBytePlusStatusCommand(parent) {
1476
2849
  }
1477
2850
 
1478
2851
  // src/providers/byteplus/commands/video.ts
1479
- import path12 from "path";
2852
+ import path14 from "path";
1480
2853
  function registerBytePlusVideoCommand(parent) {
1481
2854
  parent.command("video").description("Generate a video from a text prompt via BytePlus Seedance 2.0").requiredOption("--prompt <text>", "Text prompt").option("-m, --model <id>", "Model id", BYTEPLUS_DEFAULTS.videoModel).option("--resolution <r>", "480p|720p|1080p|2k", BYTEPLUS_DEFAULTS.videoResolution).option("--duration <n>", "Duration seconds (4-15)", String(BYTEPLUS_DEFAULTS.videoDuration)).option(
1482
2855
  "--aspect-ratio <r>",
@@ -1518,7 +2891,7 @@ function registerBytePlusVideoCommand(parent) {
1518
2891
  logger.error(`Task succeeded but no video_url in response: ${JSON.stringify(final)}`);
1519
2892
  process.exit(1);
1520
2893
  }
1521
- const outPath = opts.output ?? path12.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
2894
+ const outPath = opts.output ?? path14.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1522
2895
  await downloadVideo(url, outPath, logger);
1523
2896
  await maybeDownloadThumb(final, outPath, {
1524
2897
  skip: opts.thumb === false,
@@ -1529,7 +2902,7 @@ function registerBytePlusVideoCommand(parent) {
1529
2902
  Task: ${taskId}`);
1530
2903
  console.log(`Saved: ${outPath}`);
1531
2904
  } catch (e) {
1532
- if (e instanceof PollTimeoutError) {
2905
+ if (e instanceof PollTimeoutError2) {
1533
2906
  logger.error(
1534
2907
  `Polling timed out \u2014 try --async to keep the task and check later: multix byteplus status ${taskId}`
1535
2908
  );
@@ -1736,15 +3109,15 @@ async function downloadVideo2(url) {
1736
3109
 
1737
3110
  // src/providers/cloudflare/media-output.ts
1738
3111
  init_errors();
1739
- import fs9 from "fs";
1740
- import path13 from "path";
3112
+ import fs12 from "fs";
3113
+ import path15 from "path";
1741
3114
  function destination(filename, output) {
1742
- return output ? path13.resolve(output) : path13.join(getOutputDir(), filename);
3115
+ return output ? path15.resolve(output) : path15.join(getOutputDir(), filename);
1743
3116
  }
1744
3117
  function saveBytes(bytes, filename, output) {
1745
3118
  const dest = destination(filename, output);
1746
- fs9.mkdirSync(path13.dirname(dest), { recursive: true });
1747
- fs9.writeFileSync(dest, bytes);
3119
+ fs12.mkdirSync(path15.dirname(dest), { recursive: true });
3120
+ fs12.writeFileSync(dest, bytes);
1748
3121
  return dest;
1749
3122
  }
1750
3123
  function saveBase64Image(image, output) {
@@ -1927,8 +3300,8 @@ function registerCloudflareCommands(program2) {
1927
3300
  }
1928
3301
 
1929
3302
  // src/providers/elevenlabs/client.ts
1930
- import fs10 from "fs";
1931
- import path14 from "path";
3303
+ import fs13 from "fs";
3304
+ import path16 from "path";
1932
3305
  init_errors();
1933
3306
 
1934
3307
  // src/providers/elevenlabs/models.ts
@@ -2089,13 +3462,13 @@ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
2089
3462
  return res.json();
2090
3463
  }
2091
3464
  function readFileAsBlob(filePath, fieldFilename) {
2092
- const abs = path14.resolve(filePath);
2093
- if (!fs10.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
2094
- const buf = fs10.readFileSync(abs);
2095
- const name = fieldFilename ?? path14.basename(abs);
3465
+ const abs = path16.resolve(filePath);
3466
+ if (!fs13.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
3467
+ const buf = fs13.readFileSync(abs);
3468
+ const name = fieldFilename ?? path16.basename(abs);
2096
3469
  return new File([new Uint8Array(buf)], name);
2097
3470
  }
2098
- async function pollUntil(fetchStatus, isDone, isFailed, opts = {}) {
3471
+ async function pollUntil2(fetchStatus, isDone, isFailed, opts = {}) {
2099
3472
  const { intervalMs = 5e3, timeoutMs = 6e5, logger, label = "task" } = opts;
2100
3473
  const deadline = Date.now() + timeoutMs;
2101
3474
  let elapsed = 0;
@@ -2116,12 +3489,12 @@ async function pollUntil(fetchStatus, isDone, isFailed, opts = {}) {
2116
3489
  function saveBytes2(opts) {
2117
3490
  const { bytes, task, ext, outputCopy, logger } = opts;
2118
3491
  const dir = getOutputDir();
2119
- const dest = path14.join(dir, `elevenlabs_${task}_${Date.now()}.${ext}`);
2120
- fs10.writeFileSync(dest, bytes);
3492
+ const dest = path16.join(dir, `elevenlabs_${task}_${Date.now()}.${ext}`);
3493
+ fs13.writeFileSync(dest, bytes);
2121
3494
  logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
2122
3495
  if (outputCopy) {
2123
- fs10.mkdirSync(path14.dirname(path14.resolve(outputCopy)), { recursive: true });
2124
- fs10.copyFileSync(dest, outputCopy);
3496
+ fs13.mkdirSync(path16.dirname(path16.resolve(outputCopy)), { recursive: true });
3497
+ fs13.copyFileSync(dest, outputCopy);
2125
3498
  }
2126
3499
  return dest;
2127
3500
  }
@@ -2160,14 +3533,14 @@ ${models.length} models`);
2160
3533
  }
2161
3534
 
2162
3535
  // src/providers/elevenlabs/commands/align.ts
2163
- import fs11 from "fs";
2164
- import path15 from "path";
3536
+ import fs14 from "fs";
3537
+ import path17 from "path";
2165
3538
  function registerAlignCommand(parent) {
2166
3539
  parent.command("align").description("Forced alignment of audio to a transcript (returns word/char timestamps)").requiredOption("--input <path>", "Source audio file").requiredOption("--text <str>", "Transcript text (or pass --text-file)").option("--text-file <path>", "Read transcript from file").option("--output <path>", "Save JSON to this path (default: multix-output)").option("-v, --verbose", "Verbose logging").action(async (opts) => {
2167
3540
  const logger = createLogger({ verbose: opts.verbose ?? false });
2168
3541
  const apiKey = requireElevenLabsKey();
2169
3542
  let text = opts.text;
2170
- if (opts.textFile) text = fs11.readFileSync(path15.resolve(opts.textFile), "utf8");
3543
+ if (opts.textFile) text = fs14.readFileSync(path17.resolve(opts.textFile), "utf8");
2171
3544
  const form = new FormData();
2172
3545
  form.append("file", readFileAsBlob(opts.input));
2173
3546
  form.append("text", text);
@@ -2175,9 +3548,9 @@ function registerAlignCommand(parent) {
2175
3548
  const data = await apiPostMultipart("forced-alignment", form, apiKey, {
2176
3549
  timeoutMs: 6e5
2177
3550
  });
2178
- const dest = opts.output ? path15.resolve(opts.output) : path15.join(getOutputDir(), `elevenlabs_align_${Date.now()}.json`);
2179
- fs11.mkdirSync(path15.dirname(dest), { recursive: true });
2180
- fs11.writeFileSync(dest, JSON.stringify(data, null, 2));
3551
+ const dest = opts.output ? path17.resolve(opts.output) : path17.join(getOutputDir(), `elevenlabs_align_${Date.now()}.json`);
3552
+ fs14.mkdirSync(path17.dirname(dest), { recursive: true });
3553
+ fs14.writeFileSync(dest, JSON.stringify(data, null, 2));
2181
3554
  logger.success(`Saved alignment: ${dest}`);
2182
3555
  if (data.words) console.log(`Aligned ${data.words.length} words (loss=${data.loss ?? "?"})`);
2183
3556
  });
@@ -2219,8 +3592,8 @@ function registerCloneCommand(parent) {
2219
3592
 
2220
3593
  // src/providers/elevenlabs/commands/dub.ts
2221
3594
  init_errors();
2222
- import fs12 from "fs";
2223
- import path16 from "path";
3595
+ import fs15 from "fs";
3596
+ import path18 from "path";
2224
3597
  function registerDubCommand(parent) {
2225
3598
  parent.command("dub").description("Dub a video/audio file into target languages (async)").option("--input <path>", "Source media file (use --input or --source-url)").option("--source-url <url>", "Public URL of source media").requiredOption("--target-lang <code>", "Target language ISO code (e.g. es, fr, de)").option("--source-lang <code>", "Source language (auto if omitted)").option("--num-speakers <n>", "Speaker count (auto if omitted)").option("--watermark", "Add ElevenLabs watermark", false).option("--start-time <s>", "Start time in seconds").option("--end-time <s>", "End time in seconds").option("--highest-resolution", "Use highest resolution video", false).option("--name <n>", "Project display name").option("--async", "Return job id immediately, do not wait", false).option("--wait", "Wait until status=dubbed (implied by --download)", false).option("--download", "Download dubbed media when ready", false).option("--poll-interval <s>", "Poll interval seconds", "10").option("--wait-timeout <ms>", "Poll timeout", "1800000").option("--output <path>", "Output path for downloaded dub").option("-v, --verbose", "Verbose logging").action(async (opts) => {
2226
3599
  const logger = createLogger({ verbose: opts.verbose ?? false });
@@ -2250,7 +3623,7 @@ function registerDubCommand(parent) {
2250
3623
  console.log("Run `multix elevenlabs dub-status <id>` to check progress.");
2251
3624
  return;
2252
3625
  }
2253
- const final = await pollUntil(
3626
+ const final = await pollUntil2(
2254
3627
  () => apiGet(`dubbing/${start.dubbing_id}`, apiKey),
2255
3628
  (s) => s.status === "dubbed",
2256
3629
  (s) => s.status === "failed",
@@ -2263,7 +3636,7 @@ function registerDubCommand(parent) {
2263
3636
  );
2264
3637
  logger.success(`Dub complete: ${start.dubbing_id}`);
2265
3638
  if (opts.download) {
2266
- const dest = opts.output ? path16.resolve(opts.output) : path16.join(getOutputDir(), `elevenlabs_dub_${start.dubbing_id}_${opts.targetLang}.mp4`);
3639
+ const dest = opts.output ? path18.resolve(opts.output) : path18.join(getOutputDir(), `elevenlabs_dub_${start.dubbing_id}_${opts.targetLang}.mp4`);
2267
3640
  const url = `${ELEVENLABS_BASE_URL}/dubbing/${start.dubbing_id}/audio/${opts.targetLang}`;
2268
3641
  await downloadDub(url, apiKey, dest);
2269
3642
  logger.success(`Saved: ${dest}`);
@@ -2278,7 +3651,7 @@ Dubbed file: ${dest}`);
2278
3651
  const status = await apiGet(`dubbing/${dubbingId}`, apiKey);
2279
3652
  console.log(JSON.stringify(status, null, 2));
2280
3653
  if (opts.download) {
2281
- const dest = opts.output ? path16.resolve(opts.output) : path16.join(getOutputDir(), `elevenlabs_dub_${dubbingId}_${opts.download}.mp4`);
3654
+ const dest = opts.output ? path18.resolve(opts.output) : path18.join(getOutputDir(), `elevenlabs_dub_${dubbingId}_${opts.download}.mp4`);
2282
3655
  const url = `${ELEVENLABS_BASE_URL}/dubbing/${dubbingId}/audio/${opts.download}`;
2283
3656
  await downloadDub(url, apiKey, dest);
2284
3657
  console.log(`Saved: ${dest}`);
@@ -2286,7 +3659,7 @@ Dubbed file: ${dest}`);
2286
3659
  });
2287
3660
  }
2288
3661
  async function downloadDub(url, apiKey, dest) {
2289
- fs12.mkdirSync(path16.dirname(dest), { recursive: true });
3662
+ fs15.mkdirSync(path18.dirname(dest), { recursive: true });
2290
3663
  const res = await globalThis.fetch(url, {
2291
3664
  headers: { "xi-api-key": apiKey },
2292
3665
  signal: AbortSignal.timeout(6e5)
@@ -2295,7 +3668,7 @@ async function downloadDub(url, apiKey, dest) {
2295
3668
  throw new Error(`Failed to download dub: HTTP ${res.status}`);
2296
3669
  }
2297
3670
  const arr = new Uint8Array(await res.arrayBuffer());
2298
- fs12.writeFileSync(dest, arr);
3671
+ fs15.writeFileSync(dest, arr);
2299
3672
  void downloadFile;
2300
3673
  }
2301
3674
 
@@ -2324,8 +3697,8 @@ Isolated audio: ${dest}`);
2324
3697
 
2325
3698
  // src/providers/elevenlabs/commands/music.ts
2326
3699
  init_errors();
2327
- import fs13 from "fs";
2328
- import path17 from "path";
3700
+ import fs16 from "fs";
3701
+ import path19 from "path";
2329
3702
  function registerMusicCommand(parent) {
2330
3703
  parent.command("music").description("Generate music from a text prompt or composition plan").option("--prompt <str>", "Music prompt (mutually exclusive with --plan)").option("--plan <path>", "Path to composition plan JSON file").option("--music-length-ms <n>", "Total length in milliseconds (default 30000)").option("--model <id>", "Music model", "music_v1").option(
2331
3704
  "--format <fmt>",
@@ -2340,9 +3713,9 @@ function registerMusicCommand(parent) {
2340
3713
  const payload = { model_id: opts.model };
2341
3714
  if (opts.prompt) payload.prompt = opts.prompt;
2342
3715
  if (opts.plan) {
2343
- const planPath = path17.resolve(opts.plan);
2344
- if (!fs13.existsSync(planPath)) throw new ValidationError(`Plan file not found: ${planPath}`);
2345
- const planJson = JSON.parse(fs13.readFileSync(planPath, "utf8"));
3716
+ const planPath = path19.resolve(opts.plan);
3717
+ if (!fs16.existsSync(planPath)) throw new ValidationError(`Plan file not found: ${planPath}`);
3718
+ const planJson = JSON.parse(fs16.readFileSync(planPath, "utf8"));
2346
3719
  payload.composition_plan = planJson;
2347
3720
  }
2348
3721
  if (opts.musicLengthMs) payload.music_length_ms = Number.parseInt(opts.musicLengthMs, 10);
@@ -2401,8 +3774,8 @@ Generated SFX: ${dest}`);
2401
3774
  }
2402
3775
 
2403
3776
  // src/providers/elevenlabs/commands/transcribe.ts
2404
- import fs14 from "fs";
2405
- import path18 from "path";
3777
+ import fs17 from "fs";
3778
+ import path20 from "path";
2406
3779
  function registerTranscribeCommand(parent) {
2407
3780
  parent.command("transcribe").description("Speech-to-text transcription with Scribe").requiredOption("--input <path>", "Audio or video file to transcribe").option(
2408
3781
  "--model <id>",
@@ -2425,7 +3798,7 @@ function registerTranscribeCommand(parent) {
2425
3798
  });
2426
3799
  const ext = opts.format === "json" ? "json" : opts.format === "srt" ? "srt" : opts.format === "vtt" ? "vtt" : "txt";
2427
3800
  const dir = getOutputDir();
2428
- const dest = opts.output ? path18.resolve(opts.output) : path18.join(dir, `elevenlabs_transcribe_${Date.now()}.${ext}`);
3801
+ const dest = opts.output ? path20.resolve(opts.output) : path20.join(dir, `elevenlabs_transcribe_${Date.now()}.${ext}`);
2429
3802
  let body;
2430
3803
  switch (opts.format) {
2431
3804
  case "json":
@@ -2440,8 +3813,8 @@ function registerTranscribeCommand(parent) {
2440
3813
  default:
2441
3814
  body = data.text ?? "";
2442
3815
  }
2443
- fs14.mkdirSync(path18.dirname(dest), { recursive: true });
2444
- fs14.writeFileSync(dest, body);
3816
+ fs17.mkdirSync(path20.dirname(dest), { recursive: true });
3817
+ fs17.writeFileSync(dest, body);
2445
3818
  logger.success(`Saved transcript: ${dest}`);
2446
3819
  if (data.language_code) {
2447
3820
  logger.info(
@@ -2687,8 +4060,8 @@ function registerElevenLabsCommands(program2) {
2687
4060
  }
2688
4061
 
2689
4062
  // src/providers/fal/commands/image.ts
2690
- import fs17 from "fs";
2691
- import path20 from "path";
4063
+ import fs19 from "fs";
4064
+ import path22 from "path";
2692
4065
 
2693
4066
  // src/providers/fal/models.ts
2694
4067
  var DEFAULT_FAL_BASE_URL = "https://queue.fal.run";
@@ -2702,37 +4075,8 @@ var FAL_DEFAULTS = {
2702
4075
  };
2703
4076
 
2704
4077
  // src/providers/fal/run-helpers.ts
2705
- import fs16 from "fs";
2706
- import path19 from "path";
2707
-
2708
- // src/core/json-input.ts
2709
- init_errors();
2710
- import fs15 from "fs";
2711
- function parseJsonInput(raw, flag = "--input") {
2712
- let text;
2713
- if (raw.startsWith("@")) {
2714
- try {
2715
- text = fs15.readFileSync(raw.slice(1), "utf8");
2716
- } catch (e) {
2717
- const msg = e instanceof Error ? e.message : String(e);
2718
- throw new ValidationError(`Cannot read ${flag} file: ${msg}`);
2719
- }
2720
- } else {
2721
- text = raw;
2722
- }
2723
- try {
2724
- const parsed = JSON.parse(text);
2725
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2726
- throw new Error("input must be a JSON object");
2727
- }
2728
- return parsed;
2729
- } catch (e) {
2730
- const msg = e instanceof Error ? e.message : String(e);
2731
- throw new ValidationError(`Invalid ${flag} JSON: ${msg}`);
2732
- }
2733
- }
2734
-
2735
- // src/providers/fal/run-helpers.ts
4078
+ import fs18 from "fs";
4079
+ import path21 from "path";
2736
4080
  init_redact();
2737
4081
 
2738
4082
  // src/providers/fal/client.ts
@@ -2757,9 +4101,9 @@ var FalClient = class {
2757
4101
  apiKey;
2758
4102
  baseUrl;
2759
4103
  /** `path` is appended verbatim to the base queue URL, e.g. "/fal-ai/flux/schnell". */
2760
- async request(path49, opts = {}) {
4104
+ async request(path52, opts = {}) {
2761
4105
  const base = this.baseUrl.replace(/\/$/, "");
2762
- const suffix = path49.startsWith("/") ? path49 : `/${path49}`;
4106
+ const suffix = path52.startsWith("/") ? path52 : `/${path52}`;
2763
4107
  return this.requestAbsolute(base + suffix, opts);
2764
4108
  }
2765
4109
  /** Same auth/headers as `request`, but `url` is used verbatim (e.g. a status_url/response_url returned by submit). */
@@ -2776,11 +4120,11 @@ var FalClient = class {
2776
4120
  timeoutMs: opts.timeoutMs ?? 12e4
2777
4121
  });
2778
4122
  }
2779
- get(path49, logger) {
2780
- return this.request(path49, { method: "GET", logger });
4123
+ get(path52, logger) {
4124
+ return this.request(path52, { method: "GET", logger });
2781
4125
  }
2782
- post(path49, body, logger) {
2783
- return this.request(path49, { method: "POST", body, logger });
4126
+ post(path52, body, logger) {
4127
+ return this.request(path52, { method: "POST", body, logger });
2784
4128
  }
2785
4129
  getAbsolute(url, logger) {
2786
4130
  return this.requestAbsolute(url, { method: "GET", logger });
@@ -2922,7 +4266,7 @@ function extractMediaUrls(result) {
2922
4266
  function extFromUrl(url, fallback = ".bin") {
2923
4267
  try {
2924
4268
  const u = new URL(url);
2925
- const e = path19.extname(u.pathname);
4269
+ const e = path21.extname(u.pathname);
2926
4270
  return e.length > 0 ? e : fallback;
2927
4271
  } catch {
2928
4272
  return fallback;
@@ -2935,13 +4279,13 @@ async function downloadResultMedia(urls, outDir, requestId, logger) {
2935
4279
  if (!url) continue;
2936
4280
  const ext = extFromUrl(url);
2937
4281
  const filename = `fal-${requestId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
2938
- const filepath = path19.join(outDir, filename);
4282
+ const filepath = path21.join(outDir, filename);
2939
4283
  try {
2940
4284
  await downloadFile(url, filepath);
2941
4285
  saved.push(filepath);
2942
4286
  logger.success(`Saved ${filepath}`);
2943
4287
  } catch (e) {
2944
- fs16.rmSync(filepath, { force: true });
4288
+ fs18.rmSync(filepath, { force: true });
2945
4289
  logger.warn(`Download failed for ${redactUrl(url)}: ${e instanceof Error ? e.message : e}`);
2946
4290
  }
2947
4291
  }
@@ -2986,9 +4330,9 @@ function registerFalImageCommand(parent) {
2986
4330
  process.exit(1);
2987
4331
  }
2988
4332
  if (opts.output && saved[0]) {
2989
- const dest = path20.resolve(opts.output);
2990
- fs17.mkdirSync(path20.dirname(dest), { recursive: true });
2991
- fs17.copyFileSync(saved[0], dest);
4333
+ const dest = path22.resolve(opts.output);
4334
+ fs19.mkdirSync(path22.dirname(dest), { recursive: true });
4335
+ fs19.copyFileSync(saved[0], dest);
2992
4336
  logger.success(`Copied to ${dest}`);
2993
4337
  }
2994
4338
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -3141,14 +4485,214 @@ function registerFalCommands(program2) {
3141
4485
  registerFalResultCommand(fal);
3142
4486
  }
3143
4487
 
3144
- // src/providers/gemini/commands/analyze.ts
4488
+ // src/core/stdin-file.ts
3145
4489
  init_errors();
3146
4490
  import fs20 from "fs";
4491
+ import os2 from "os";
4492
+ import path23 from "path";
4493
+ var STDIN_ARG = "-";
4494
+ var STDIN_LABEL = "<stdin>";
4495
+ var HEAD_BYTES = 4096;
4496
+ var MAX_STDIN_BYTES = 2 * 1024 ** 3;
4497
+ var WAIT_HINT_MS = 2e3;
4498
+ var ascii = (buf, start, end) => buf.toString("latin1", start, end);
4499
+ var HEIF_BRANDS = /* @__PURE__ */ new Set([
4500
+ "heic",
4501
+ "heix",
4502
+ "hevc",
4503
+ "hevx",
4504
+ "heim",
4505
+ "heis",
4506
+ "hevm",
4507
+ "hevs",
4508
+ "mif1",
4509
+ "msf1"
4510
+ ]);
4511
+ var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
4512
+ function isMpegAudioFrame(head) {
4513
+ if (head.length < 4 || head[0] !== 255) return false;
4514
+ const b1 = head[1] ?? 0;
4515
+ const b2 = head[2] ?? 0;
4516
+ if ((b1 & 224) !== 224) return false;
4517
+ const version = b1 >> 3 & 3;
4518
+ const layer = b1 >> 1 & 3;
4519
+ const bitrate = b2 >> 4;
4520
+ const sampleRate = b2 >> 2 & 3;
4521
+ return version !== 1 && layer !== 0 && bitrate !== 15 && bitrate !== 0 && sampleRate !== 3;
4522
+ }
4523
+ function detectExtension(head) {
4524
+ if (head.length >= 3 && head[0] === 239 && head[1] === 187 && head[2] === 191) return ".txt";
4525
+ if (head.length >= 2 && (head[0] === 255 && head[1] === 254 || head[0] === 254 && head[1] === 255)) {
4526
+ return void 0;
4527
+ }
4528
+ if (head.length >= 8 && head.readUInt32BE(0) === 2303741511) return ".png";
4529
+ if (head.length >= 3 && head[0] === 255 && head[1] === 216 && head[2] === 255) return ".jpg";
4530
+ if (ascii(head, 0, 4) === "GIF8") return ".gif";
4531
+ if (ascii(head, 0, 5) === "%PDF-") return ".pdf";
4532
+ if (ascii(head, 0, 2) === "BM" && head.length >= 26 && head.readUInt32LE(14) >= 12) return ".bmp";
4533
+ if (ascii(head, 0, 4) === "RIFF") {
4534
+ const kind = ascii(head, 8, 12);
4535
+ if (kind === "WEBP") return ".webp";
4536
+ if (kind === "WAVE") return ".wav";
4537
+ if (kind === "AVI ") return ".avi";
4538
+ }
4539
+ if (ascii(head, 0, 4) === "OggS") return ".ogg";
4540
+ if (ascii(head, 0, 4) === "fLaC") return ".flac";
4541
+ if (ascii(head, 0, 4) === "FORM" && ascii(head, 8, 12) === "AIFF") return ".aiff";
4542
+ if (ascii(head, 0, 3) === "ID3" && (head[3] ?? 0) >= 2 && (head[3] ?? 0) <= 4 && head[4] !== 255) {
4543
+ return ".mp3";
4544
+ }
4545
+ if (ascii(head, 4, 8) === "ftyp") {
4546
+ const brand = ascii(head, 8, 12);
4547
+ if (HEIF_BRANDS.has(brand)) return ".heic";
4548
+ if (AVIF_BRANDS.has(brand)) return void 0;
4549
+ if (brand.startsWith("M4A") || brand.startsWith("M4B")) return ".m4a";
4550
+ if (brand === "qt ") return ".mov";
4551
+ if (brand.startsWith("3g")) return ".3gpp";
4552
+ return ".mp4";
4553
+ }
4554
+ if (head.length >= 4 && head.readUInt32BE(0) === 440786851) {
4555
+ return head.includes("webm", 0, "latin1") ? ".webm" : void 0;
4556
+ }
4557
+ if (head.length >= 2 && head[0] === 255 && ((head[1] ?? 0) & 246) === 240) return ".aac";
4558
+ if (isMpegAudioFrame(head)) return ".mp3";
4559
+ if (looksLikeText(head)) return ".txt";
4560
+ return void 0;
4561
+ }
4562
+ function looksLikeText(head) {
4563
+ if (head.length === 0) return false;
4564
+ for (const byte of head) {
4565
+ if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 12 && byte !== 13 && byte !== 27) {
4566
+ return false;
4567
+ }
4568
+ }
4569
+ try {
4570
+ new TextDecoder("utf-8", { fatal: true }).decode(head, { stream: true });
4571
+ return true;
4572
+ } catch {
4573
+ return false;
4574
+ }
4575
+ }
4576
+ function normalizeExt(ext) {
4577
+ const clean = ext.trim().toLowerCase();
4578
+ if (!/^\.?[a-z0-9]{1,10}$/.test(clean)) {
4579
+ throw new ValidationError(`Invalid --stdin-ext: ${ext} (expected e.g. .mp3 or txt)`);
4580
+ }
4581
+ return clean.startsWith(".") ? clean : `.${clean}`;
4582
+ }
4583
+ var SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
4584
+ function trackTempDir(dir) {
4585
+ let done = false;
4586
+ const remove = () => {
4587
+ try {
4588
+ fs20.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
4589
+ } catch {
4590
+ }
4591
+ };
4592
+ const onSignal = (signal) => {
4593
+ dispose();
4594
+ process.kill(process.pid, signal);
4595
+ };
4596
+ const dispose = () => {
4597
+ if (done) return;
4598
+ done = true;
4599
+ process.removeListener("exit", dispose);
4600
+ for (const s of SIGNALS) process.removeListener(s, onSignal);
4601
+ remove();
4602
+ };
4603
+ process.once("exit", dispose);
4604
+ for (const s of SIGNALS) process.once(s, onSignal);
4605
+ return dispose;
4606
+ }
4607
+ async function stdinToTempFile(opts = {}) {
4608
+ const input = opts.input ?? process.stdin;
4609
+ if (input.isTTY) {
4610
+ throw new ValidationError(
4611
+ "`--files -` reads piped input, but stdin is a terminal. Example: cat clip.mp3 | multix gemini transcribe --files -"
4612
+ );
4613
+ }
4614
+ const forcedExt = opts.ext ? normalizeExt(opts.ext) : void 0;
4615
+ const maxBytes = opts.maxBytes ?? MAX_STDIN_BYTES;
4616
+ const dir = fs20.mkdtempSync(path23.join(os2.tmpdir(), "multix-stdin-"));
4617
+ const cleanup = trackTempDir(dir);
4618
+ const rawPath = path23.join(dir, "input");
4619
+ const hint = setTimeout(() => {
4620
+ process.stderr.write("Waiting for data on stdin (`--files -`)... press Ctrl+C to cancel\n");
4621
+ }, WAIT_HINT_MS);
4622
+ hint.unref?.();
4623
+ let head = Buffer.alloc(0);
4624
+ let size = 0;
4625
+ try {
4626
+ const fd = fs20.openSync(rawPath, "w", 384);
4627
+ try {
4628
+ for await (const chunk of input) {
4629
+ clearTimeout(hint);
4630
+ const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
4631
+ if (size + buf.length > maxBytes) {
4632
+ throw new ValidationError(
4633
+ `Piped stdin exceeds ${Math.floor(maxBytes / 1024 ** 2)} MB; save it to a file instead`
4634
+ );
4635
+ }
4636
+ if (head.length < HEAD_BYTES) {
4637
+ head = Buffer.concat([head, buf.subarray(0, HEAD_BYTES - head.length)]);
4638
+ }
4639
+ let offset = 0;
4640
+ while (offset < buf.length) offset += fs20.writeSync(fd, buf, offset);
4641
+ size += buf.length;
4642
+ }
4643
+ } finally {
4644
+ clearTimeout(hint);
4645
+ fs20.closeSync(fd);
4646
+ }
4647
+ if (size === 0) throw new ValidationError("`--files -` was given but stdin was empty");
4648
+ const detectedExt = forcedExt ?? detectExtension(head);
4649
+ const finalPath = path23.join(dir, `stdin${detectedExt ?? ".bin"}`);
4650
+ fs20.renameSync(rawPath, finalPath);
4651
+ return { path: finalPath, size, detected: detectedExt !== void 0, cleanup };
4652
+ } catch (error) {
4653
+ cleanup();
4654
+ throw error;
4655
+ }
4656
+ }
4657
+ function labelError(message, input) {
4658
+ return input.path === input.label ? message : message.split(input.path).join(input.label);
4659
+ }
4660
+ async function resolveFileInputs(files, opts = {}) {
4661
+ const stdinCount = files.filter((f) => f === STDIN_ARG).length;
4662
+ if (stdinCount > 1) throw new ValidationError("`-` (stdin) can only be passed once to --files");
4663
+ if (opts.stdinExt && stdinCount === 0) {
4664
+ throw new ValidationError("--stdin-ext only applies when --files includes `-`");
4665
+ }
4666
+ for (const f of files) {
4667
+ if (f !== STDIN_ARG && !fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
4668
+ }
4669
+ if (stdinCount === 0) {
4670
+ return { inputs: files.map((f) => ({ path: f, label: f })), cleanup: () => {
4671
+ } };
4672
+ }
4673
+ const stdin = await stdinToTempFile({
4674
+ input: opts.input,
4675
+ ext: opts.stdinExt,
4676
+ maxBytes: opts.maxBytes
4677
+ });
4678
+ if (!stdin.detected) {
4679
+ stdin.cleanup();
4680
+ throw new ValidationError(
4681
+ "Could not detect the type of piped stdin; pass --stdin-ext (e.g. --stdin-ext mp3)"
4682
+ );
4683
+ }
4684
+ return {
4685
+ inputs: files.map(
4686
+ (f) => f === STDIN_ARG ? { path: stdin.path, label: STDIN_LABEL } : { path: f, label: f }
4687
+ ),
4688
+ cleanup: stdin.cleanup
4689
+ };
4690
+ }
3147
4691
 
3148
4692
  // src/providers/gemini/client.ts
3149
4693
  init_errors();
3150
- import fs18 from "fs";
3151
- import path22 from "path";
4694
+ import fs21 from "fs";
4695
+ import path25 from "path";
3152
4696
 
3153
4697
  // src/providers/gemini/key-pool.ts
3154
4698
  init_errors();
@@ -3214,7 +4758,7 @@ async function withGeminiKey(fn, opts = {}) {
3214
4758
  }
3215
4759
 
3216
4760
  // src/providers/gemini/task-resolver.ts
3217
- import path21 from "path";
4761
+ import path24 from "path";
3218
4762
  var MIME_MAP = {
3219
4763
  // Audio
3220
4764
  ".mp3": "audio/mp3",
@@ -3255,7 +4799,7 @@ var MIME_MAP = {
3255
4799
  ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
3256
4800
  };
3257
4801
  function getMimeType(filePath) {
3258
- const ext = path21.extname(filePath).toLowerCase();
4802
+ const ext = path24.extname(filePath).toLowerCase();
3259
4803
  return MIME_MAP[ext] ?? "application/octet-stream";
3260
4804
  }
3261
4805
  function requiresProcessingWait(mimeType) {
@@ -3278,8 +4822,8 @@ async function uploadFile(filePath, opts = {}) {
3278
4822
  async function uploadFileWithKey(filePath, apiKey, opts) {
3279
4823
  const { timeoutMs = 3e5, logger } = opts;
3280
4824
  const mimeType = getMimeType(filePath);
3281
- const fileBytes = fs18.readFileSync(filePath);
3282
- const displayName = path22.basename(filePath);
4825
+ const fileBytes = fs21.readFileSync(filePath);
4826
+ const displayName = path25.basename(filePath);
3283
4827
  const boundary = `multix_${Date.now()}`;
3284
4828
  const metaJson = JSON.stringify({ file: { displayName, mimeType } });
3285
4829
  const body = Buffer.concat([
@@ -3396,12 +4940,12 @@ function extractImages(resp) {
3396
4940
  ).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
3397
4941
  }
3398
4942
  function inlinePart(filePath) {
3399
- const data = fs18.readFileSync(filePath).toString("base64");
4943
+ const data = fs21.readFileSync(filePath).toString("base64");
3400
4944
  const mimeType = getMimeType(filePath);
3401
4945
  return { inlineData: { mimeType, data } };
3402
4946
  }
3403
4947
  function shouldUseFileApi(filePath) {
3404
- const stat = fs18.statSync(filePath);
4948
+ const stat = fs21.statSync(filePath);
3405
4949
  return stat.size > 20 * 1024 * 1024;
3406
4950
  }
3407
4951
  function sleep2(ms) {
@@ -3455,13 +4999,13 @@ function getDefaultModel(task) {
3455
4999
  }
3456
5000
 
3457
5001
  // src/providers/gemini/result-formatter.ts
3458
- import fs19 from "fs";
3459
- import path23 from "path";
5002
+ import fs22 from "fs";
5003
+ import path26 from "path";
3460
5004
  function formatResults(results, format, outputPath) {
3461
5005
  const content = renderFormat(results, format);
3462
5006
  if (outputPath) {
3463
- fs19.mkdirSync(path23.dirname(path23.resolve(outputPath)), { recursive: true });
3464
- fs19.writeFileSync(outputPath, content, "utf8");
5007
+ fs22.mkdirSync(path26.dirname(path26.resolve(outputPath)), { recursive: true });
5008
+ fs22.writeFileSync(outputPath, content, "utf8");
3465
5009
  } else {
3466
5010
  process.stdout.write(content);
3467
5011
  }
@@ -3523,17 +5067,17 @@ ${r.response}
3523
5067
 
3524
5068
  // src/providers/gemini/commands/analyze.ts
3525
5069
  function registerAnalyzeCommand(parent) {
3526
- parent.command("analyze").description("Analyze files with Gemini (images, video, audio, documents)").requiredOption("--files <paths...>", "Input file paths to analyze").option("--prompt <text>", "Analysis prompt", "Analyze this content").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file instead of stdout").option("-v, --verbose", "Verbose logging").action(
5070
+ parent.command("analyze").description("Analyze files with Gemini (images, video, audio, documents)").requiredOption("--files <paths...>", "Input file paths to analyze (use - to read piped stdin)").option("--stdin-ext <ext>", "Extension for piped stdin when auto-detection fails (e.g. mp3)").option("--prompt <text>", "Analysis prompt", "Analyze this content").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file instead of stdout").option("-v, --verbose", "Verbose logging").action(
3527
5071
  async (opts) => {
3528
5072
  const logger = createLogger({ verbose: opts.verbose ?? false });
3529
5073
  const model = opts.model ?? getDefaultModel("analyze");
3530
- for (const f of opts.files) {
3531
- if (!fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3532
- }
5074
+ const { inputs, cleanup } = await resolveFileInputs(opts.files, {
5075
+ stdinExt: opts.stdinExt
5076
+ });
3533
5077
  const results = [];
3534
- for (let i = 0; i < opts.files.length; i++) {
3535
- const filePath = opts.files[i];
3536
- logger.debug(`[${i + 1}/${opts.files.length}] Processing: ${filePath}`);
5078
+ for (let i = 0; i < inputs.length; i++) {
5079
+ const { path: filePath, label } = inputs[i];
5080
+ logger.debug(`[${i + 1}/${inputs.length}] Processing: ${label}`);
3537
5081
  try {
3538
5082
  const resp = await withGeminiKey(
3539
5083
  async (apiKey) => {
@@ -3560,17 +5104,21 @@ function registerAnalyzeCommand(parent) {
3560
5104
  { logger }
3561
5105
  );
3562
5106
  results.push({
3563
- file: filePath,
5107
+ file: label,
3564
5108
  status: "success",
3565
5109
  response: extractText(resp)
3566
5110
  });
3567
5111
  } catch (e) {
3568
- const msg = e instanceof Error ? e.message : String(e);
3569
- logger.error(`Failed: ${filePath} \u2014 ${msg}`);
3570
- results.push({ file: filePath, status: "error", error: msg });
5112
+ const msg = labelError(
5113
+ e instanceof Error ? e.message : String(e),
5114
+ inputs[i]
5115
+ );
5116
+ logger.error(`Failed: ${label} \u2014 ${msg}`);
5117
+ results.push({ file: label, status: "error", error: msg });
3571
5118
  }
3572
5119
  }
3573
5120
  formatResults(results, opts.format, opts.output);
5121
+ cleanup();
3574
5122
  logger.debug(
3575
5123
  `Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
3576
5124
  );
@@ -3581,21 +5129,19 @@ function registerAnalyzeCommand(parent) {
3581
5129
  }
3582
5130
 
3583
5131
  // src/providers/gemini/commands/extract.ts
3584
- init_errors();
3585
- import fs21 from "fs";
3586
5132
  function registerExtractCommand(parent) {
3587
- parent.command("extract").description("Extract structured information from files with Gemini").requiredOption("--files <paths...>", "Input file paths").requiredOption("--prompt <text>", "Extraction prompt (required)").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
5133
+ parent.command("extract").description("Extract structured information from files with Gemini").requiredOption("--files <paths...>", "Input file paths (use - to read piped stdin)").option("--stdin-ext <ext>", "Extension for piped stdin when auto-detection fails (e.g. mp3)").requiredOption("--prompt <text>", "Extraction prompt (required)").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
3588
5134
  async (opts) => {
3589
5135
  const logger = createLogger({ verbose: opts.verbose ?? false });
3590
5136
  const model = opts.model ?? getDefaultModel("extract");
3591
- for (const f of opts.files) {
3592
- if (!fs21.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3593
- }
5137
+ const { inputs, cleanup } = await resolveFileInputs(opts.files, {
5138
+ stdinExt: opts.stdinExt
5139
+ });
3594
5140
  const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
3595
5141
  const results = [];
3596
- for (let i = 0; i < opts.files.length; i++) {
3597
- const filePath = opts.files[i];
3598
- logger.debug(`[${i + 1}/${opts.files.length}] Extracting: ${filePath}`);
5142
+ for (let i = 0; i < inputs.length; i++) {
5143
+ const { path: filePath, label } = inputs[i];
5144
+ logger.debug(`[${i + 1}/${inputs.length}] Extracting: ${label}`);
3599
5145
  try {
3600
5146
  const resp = await withGeminiKey(
3601
5147
  async (apiKey) => {
@@ -3621,14 +5167,18 @@ function registerExtractCommand(parent) {
3621
5167
  },
3622
5168
  { logger }
3623
5169
  );
3624
- results.push({ file: filePath, status: "success", response: extractText(resp) });
5170
+ results.push({ file: label, status: "success", response: extractText(resp) });
3625
5171
  } catch (e) {
3626
- const msg = e instanceof Error ? e.message : String(e);
3627
- logger.error(`Failed: ${filePath} \u2014 ${msg}`);
3628
- results.push({ file: filePath, status: "error", error: msg });
5172
+ const msg = labelError(
5173
+ e instanceof Error ? e.message : String(e),
5174
+ inputs[i]
5175
+ );
5176
+ logger.error(`Failed: ${label} \u2014 ${msg}`);
5177
+ results.push({ file: label, status: "error", error: msg });
3629
5178
  }
3630
5179
  }
3631
5180
  formatResults(results, opts.format, opts.output);
5181
+ cleanup();
3632
5182
  const failed = results.filter((r) => r.status === "error").length;
3633
5183
  if (failed > 0) process.exit(1);
3634
5184
  }
@@ -3639,8 +5189,8 @@ function registerExtractCommand(parent) {
3639
5189
  init_errors();
3640
5190
 
3641
5191
  // src/providers/gemini/generators/speech.ts
3642
- import fs22 from "fs";
3643
- import path24 from "path";
5192
+ import fs23 from "fs";
5193
+ import path27 from "path";
3644
5194
 
3645
5195
  // src/providers/gemini/voices.ts
3646
5196
  var GEMINI_TTS_VOICES = [
@@ -3806,12 +5356,12 @@ function extractWavData(wav) {
3806
5356
  }
3807
5357
  function saveSpeech(outBytes, outputFormat, output, logger) {
3808
5358
  const outDir = getOutputDir();
3809
- const dest = path24.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
3810
- fs22.writeFileSync(dest, outBytes);
5359
+ const dest = path27.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
5360
+ fs23.writeFileSync(dest, outBytes);
3811
5361
  logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
3812
5362
  if (output) {
3813
- fs22.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
3814
- fs22.copyFileSync(dest, output);
5363
+ fs23.mkdirSync(path27.dirname(path27.resolve(output)), { recursive: true });
5364
+ fs23.copyFileSync(dest, output);
3815
5365
  logger?.success(`Copied to: ${output}`);
3816
5366
  }
3817
5367
  return dest;
@@ -3924,84 +5474,24 @@ function collectSpeakers(value, prev) {
3924
5474
 
3925
5475
  // src/providers/gemini/commands/generate-video.ts
3926
5476
  init_errors();
3927
- import fs24 from "fs";
3928
- import path25 from "path";
3929
-
3930
- // src/providers/gemini/veo.ts
3931
- import fs23 from "fs";
3932
-
3933
- // src/core/async-poll.ts
3934
- init_errors();
3935
- var PollTimeoutError2 = class extends MultixError {
3936
- constructor(message, lastStatus) {
3937
- super(message, "POLL_TIMEOUT");
3938
- this.lastStatus = lastStatus;
3939
- this.name = "PollTimeoutError";
3940
- }
3941
- lastStatus;
3942
- };
3943
- function isTransientHttpError(error) {
3944
- if (!(error instanceof HttpError)) return false;
3945
- return error.status === 0 || error.status === 429 || error.status >= 500;
3946
- }
3947
- var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3948
- async function pollUntil2(opts) {
3949
- const now = opts.now ?? Date.now;
3950
- const sleep5 = opts.sleep ?? defaultSleep;
3951
- const random = opts.random ?? Math.random;
3952
- const isTransient = opts.isTransientError ?? isTransientHttpError;
3953
- const maxInterval = opts.maxIntervalMs ?? 15e3;
3954
- const minInterval = opts.minIntervalMs ?? 500;
3955
- const factor = opts.backoffFactor ?? 1.5;
3956
- const jitter = opts.jitter ?? true;
3957
- const deadline = now() + opts.timeoutMs;
3958
- let interval = opts.intervalMs ?? 3e3;
3959
- let attempt = 0;
3960
- let last;
3961
- let lastError;
3962
- for (; ; ) {
3963
- attempt++;
3964
- let retryAfterMs;
3965
- try {
3966
- last = await opts.fetchStatus({ remainingMs: Math.max(1, deadline - now()), attempt });
3967
- lastError = void 0;
3968
- opts.onPoll?.(last, attempt);
3969
- if (opts.isTerminal(last)) return last;
3970
- } catch (error) {
3971
- if (!isTransient(error)) throw error;
3972
- lastError = error;
3973
- if (error instanceof HttpError) retryAfterMs = error.retryAfterMs;
3974
- }
3975
- const remaining = deadline - now();
3976
- if (remaining <= 0) {
3977
- const hint = opts.resumeHint ? ` ${opts.resumeHint}` : "";
3978
- const cause = lastError instanceof Error ? ` Last error: ${lastError.message}.` : "";
3979
- throw new PollTimeoutError2(
3980
- `Timed out after ${opts.timeoutMs}ms waiting for job.${cause}${hint}`,
3981
- last
3982
- );
3983
- }
3984
- let delay = retryAfterMs ?? interval;
3985
- if (jitter && retryAfterMs === void 0) delay *= 0.8 + random() * 0.4;
3986
- await sleep5(Math.min(Math.max(delay, minInterval), remaining));
3987
- interval = Math.min(interval * factor, maxInterval);
3988
- }
3989
- }
5477
+ import fs25 from "fs";
5478
+ import path28 from "path";
3990
5479
 
3991
5480
  // src/providers/gemini/veo.ts
5481
+ import fs24 from "fs";
3992
5482
  init_errors();
3993
5483
  var API_HOST = "generativelanguage.googleapis.com";
3994
5484
  var BASE2 = `https://${API_HOST}/v1beta`;
3995
5485
  var VEO_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp"]);
3996
5486
  var VEO_TIMEOUT_MS = 10 * 60 * 1e3;
3997
5487
  function veoImageFromFile(imgPath) {
3998
- if (!fs23.existsSync(imgPath)) throw new ValidationError(`Image not found: ${imgPath}`);
5488
+ if (!fs24.existsSync(imgPath)) throw new ValidationError(`Image not found: ${imgPath}`);
3999
5489
  const detected = getMimeType(imgPath);
4000
5490
  const mimeType = detected === "application/octet-stream" ? "image/jpeg" : detected;
4001
5491
  if (!VEO_IMAGE_TYPES.has(mimeType)) {
4002
5492
  throw new ValidationError(`Veo accepts JPEG, PNG or WebP images: ${imgPath} (${mimeType})`);
4003
5493
  }
4004
- return { bytesBase64Encoded: fs23.readFileSync(imgPath).toString("base64"), mimeType };
5494
+ return { bytesBase64Encoded: fs24.readFileSync(imgPath).toString("base64"), mimeType };
4005
5495
  }
4006
5496
  function veoFramesFromPaths(paths = []) {
4007
5497
  if (paths.length > 2) {
@@ -4050,7 +5540,7 @@ async function submitVeo(apiKey, model, body) {
4050
5540
  }
4051
5541
  async function waitForVeo(apiKey, operation, logger, opts = {}) {
4052
5542
  if (operation.done) return checkVeoResult(operation);
4053
- const finalOp = await pollUntil2({
5543
+ const finalOp = await pollUntil({
4054
5544
  fetchStatus: ({ remainingMs }) => httpJson({
4055
5545
  url: `${BASE2}/${operation.name}`,
4056
5546
  headers: headers(apiKey),
@@ -4126,7 +5616,7 @@ function registerGenerateVideoCommand(parent) {
4126
5616
  logger.debug(`Submitting video generation with ${model}...`);
4127
5617
  logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
4128
5618
  const frames = veoFramesFromPaths(opts.referenceImages);
4129
- const dest = path25.join(getOutputDir(), `veo_generated_${Date.now()}.mp4`);
5619
+ const dest = path28.join(getOutputDir(), `veo_generated_${Date.now()}.mp4`);
4130
5620
  const finalOp = await runVeo(
4131
5621
  model,
4132
5622
  {
@@ -4140,8 +5630,8 @@ function registerGenerateVideoCommand(parent) {
4140
5630
  );
4141
5631
  logger.success(`Saved video: ${dest}`);
4142
5632
  if (opts.output) {
4143
- fs24.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
4144
- fs24.copyFileSync(dest, opts.output);
5633
+ fs25.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
5634
+ fs25.copyFileSync(dest, opts.output);
4145
5635
  logger.success(`Copied to: ${opts.output}`);
4146
5636
  }
4147
5637
  await maybeDownloadThumb(finalOp.response, dest, {
@@ -4157,8 +5647,8 @@ Generated video: ${dest}`);
4157
5647
 
4158
5648
  // src/providers/gemini/commands/generate.ts
4159
5649
  init_errors();
4160
- import fs25 from "fs";
4161
- import path26 from "path";
5650
+ import fs26 from "fs";
5651
+ import path29 from "path";
4162
5652
  function registerGenerateCommand(parent) {
4163
5653
  parent.command("generate").description("Generate images with Gemini (Nano Banana / Imagen 4)").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "Gemini image model to use").option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--num-images <n>", "Number of images to generate (1-4)", "1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Copy first generated image to this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
4164
5654
  async (opts) => {
@@ -4203,8 +5693,8 @@ function registerGenerateCommand(parent) {
4203
5693
  const outDir = getOutputDir();
4204
5694
  const ext = img.mimeType.includes("png") ? "png" : "jpg";
4205
5695
  const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
4206
- const dest = path26.join(outDir, fname);
4207
- fs25.writeFileSync(dest, Buffer.from(img.data, "base64"));
5696
+ const dest = path29.join(outDir, fname);
5697
+ fs26.writeFileSync(dest, Buffer.from(img.data, "base64"));
4208
5698
  savedFiles.push(dest);
4209
5699
  logger.success(`Saved: ${dest}`);
4210
5700
  }
@@ -4219,8 +5709,8 @@ function registerGenerateCommand(parent) {
4219
5709
  process.exit(1);
4220
5710
  }
4221
5711
  if (opts.output && savedFiles[0]) {
4222
- fs25.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
4223
- fs25.copyFileSync(savedFiles[0], opts.output);
5712
+ fs26.mkdirSync(path29.dirname(path29.resolve(opts.output)), { recursive: true });
5713
+ fs26.copyFileSync(savedFiles[0], opts.output);
4224
5714
  logger.success(`Copied to: ${opts.output}`);
4225
5715
  }
4226
5716
  const finalImages = finalizeGeneratedImages(savedFiles, { ...opts, logger });
@@ -4232,8 +5722,8 @@ Generated ${finalImages.length} image(s):`);
4232
5722
  }
4233
5723
 
4234
5724
  // src/providers/gemini/commands/image-to-image.ts
4235
- import fs26 from "fs";
4236
- import path27 from "path";
5725
+ import fs27 from "fs";
5726
+ import path30 from "path";
4237
5727
  var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
4238
5728
  var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
4239
5729
  function registerGeminiImageToImageCommand(parent) {
@@ -4294,14 +5784,14 @@ function registerGeminiImageToImageCommand(parent) {
4294
5784
  const img = images[i];
4295
5785
  if (!img) continue;
4296
5786
  const ext = img.mimeType.includes("png") ? "png" : "jpg";
4297
- const dest = path27.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
4298
- fs26.writeFileSync(dest, Buffer.from(img.data, "base64"));
5787
+ const dest = path30.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
5788
+ fs27.writeFileSync(dest, Buffer.from(img.data, "base64"));
4299
5789
  saved.push(dest);
4300
5790
  logger.success(`Saved: ${dest}`);
4301
5791
  }
4302
5792
  if (opts.output && saved[0]) {
4303
- fs26.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
4304
- fs26.copyFileSync(saved[0], opts.output);
5793
+ fs27.mkdirSync(path30.dirname(path30.resolve(opts.output)), { recursive: true });
5794
+ fs27.copyFileSync(saved[0], opts.output);
4305
5795
  logger.success(`Copied to: ${opts.output}`);
4306
5796
  }
4307
5797
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -4329,7 +5819,7 @@ function sniffImageMime(buf, url) {
4329
5819
  if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
4330
5820
  return "image/webp";
4331
5821
  }
4332
- const ext = path27.extname(new URL(url).pathname).toLowerCase();
5822
+ const ext = path30.extname(new URL(url).pathname).toLowerCase();
4333
5823
  if (ext === ".png") return "image/png";
4334
5824
  if (ext === ".webp") return "image/webp";
4335
5825
  if (ext === ".gif") return "image/gif";
@@ -4338,8 +5828,8 @@ function sniffImageMime(buf, url) {
4338
5828
 
4339
5829
  // src/providers/gemini/commands/image-to-video.ts
4340
5830
  init_errors();
4341
- import fs27 from "fs";
4342
- import path28 from "path";
5831
+ import fs28 from "fs";
5832
+ import path31 from "path";
4343
5833
  function registerGeminiImageToVideoCommand(parent) {
4344
5834
  parent.command("image-to-video <imagePath>").alias("i2v").description("Generate video from a starting image with Veo (image-to-video) [EXPERIMENTAL]").requiredOption("--prompt <text>", "Motion/scene prompt").option("--last-frame <path>", "Optional closing frame image (Veo 3.1)").option("--model <id>", "Veo model id").option("--resolution <res>", "720p|1080p", "1080p").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
4345
5835
  async (imagePath, opts) => {
@@ -4359,7 +5849,7 @@ function registerGeminiImageToVideoCommand(parent) {
4359
5849
  logger.info(
4360
5850
  "Submitting image-to-video. This may take 30s\u20136 minutes. Veo requires billing."
4361
5851
  );
4362
- const dest = path28.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
5852
+ const dest = path31.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
4363
5853
  const finalOp = await runVeo(
4364
5854
  model,
4365
5855
  {
@@ -4374,8 +5864,8 @@ function registerGeminiImageToVideoCommand(parent) {
4374
5864
  );
4375
5865
  logger.success(`Saved video: ${dest}`);
4376
5866
  if (opts.output) {
4377
- fs27.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
4378
- fs27.copyFileSync(dest, opts.output);
5867
+ fs28.mkdirSync(path31.dirname(path31.resolve(opts.output)), { recursive: true });
5868
+ fs28.copyFileSync(dest, opts.output);
4379
5869
  logger.success(`Copied to: ${opts.output}`);
4380
5870
  }
4381
5871
  await maybeDownloadThumb(finalOp.response, dest, {
@@ -4390,20 +5880,21 @@ Generated video: ${dest}`);
4390
5880
  }
4391
5881
 
4392
5882
  // src/providers/gemini/commands/transcribe.ts
4393
- init_errors();
4394
- import fs28 from "fs";
4395
5883
  function registerTranscribeCommand2(parent) {
4396
- parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption("--files <paths...>", "Audio/video file paths to transcribe").option("--prompt <text>", "Transcription prompt", "Generate a transcript with timestamps").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
5884
+ parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption(
5885
+ "--files <paths...>",
5886
+ "Audio/video file paths to transcribe (use - to read piped stdin)"
5887
+ ).option("--stdin-ext <ext>", "Extension for piped stdin when auto-detection fails (e.g. mp3)").option("--prompt <text>", "Transcription prompt", "Generate a transcript with timestamps").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
4397
5888
  async (opts) => {
4398
5889
  const logger = createLogger({ verbose: opts.verbose ?? false });
4399
5890
  const model = opts.model ?? getDefaultModel("transcribe");
4400
- for (const f of opts.files) {
4401
- if (!fs28.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
4402
- }
5891
+ const { inputs, cleanup } = await resolveFileInputs(opts.files, {
5892
+ stdinExt: opts.stdinExt
5893
+ });
4403
5894
  const results = [];
4404
- for (let i = 0; i < opts.files.length; i++) {
4405
- const filePath = opts.files[i];
4406
- logger.debug(`[${i + 1}/${opts.files.length}] Transcribing: ${filePath}`);
5895
+ for (let i = 0; i < inputs.length; i++) {
5896
+ const { path: filePath, label } = inputs[i];
5897
+ logger.debug(`[${i + 1}/${inputs.length}] Transcribing: ${label}`);
4407
5898
  try {
4408
5899
  const resp = await withGeminiKey(
4409
5900
  async (apiKey) => {
@@ -4430,17 +5921,21 @@ function registerTranscribeCommand2(parent) {
4430
5921
  { logger }
4431
5922
  );
4432
5923
  results.push({
4433
- file: filePath,
5924
+ file: label,
4434
5925
  status: "success",
4435
5926
  response: extractText(resp)
4436
5927
  });
4437
5928
  } catch (e) {
4438
- const msg = e instanceof Error ? e.message : String(e);
4439
- logger.error(`Failed: ${filePath} \u2014 ${msg}`);
4440
- results.push({ file: filePath, status: "error", error: msg });
5929
+ const msg = labelError(
5930
+ e instanceof Error ? e.message : String(e),
5931
+ inputs[i]
5932
+ );
5933
+ logger.error(`Failed: ${label} \u2014 ${msg}`);
5934
+ results.push({ file: label, status: "error", error: msg });
4441
5935
  }
4442
5936
  }
4443
5937
  formatResults(results, opts.format, opts.output);
5938
+ cleanup();
4444
5939
  const failed = results.filter((r) => r.status === "error").length;
4445
5940
  if (failed > 0) process.exit(1);
4446
5941
  }
@@ -4464,7 +5959,7 @@ function registerGeminiCommands(program2) {
4464
5959
 
4465
5960
  // src/providers/leonardo/commands/generate.ts
4466
5961
  import fs29 from "fs";
4467
- import path29 from "path";
5962
+ import path32 from "path";
4468
5963
 
4469
5964
  // src/providers/leonardo/client.ts
4470
5965
  init_errors();
@@ -4537,11 +6032,11 @@ var LeonardoClient = class {
4537
6032
  }
4538
6033
  apiKey;
4539
6034
  baseUrl;
4540
- async request(path49, opts = {}) {
6035
+ async request(path52, opts = {}) {
4541
6036
  const base = this.baseUrl.replace(/\/$/, "");
4542
- const v2Override = path49.startsWith("v2:");
6037
+ const v2Override = path52.startsWith("v2:");
4543
6038
  const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
4544
- const finalPath = v2Override ? path49.slice(3) : path49;
6039
+ const finalPath = v2Override ? path52.slice(3) : path52;
4545
6040
  const url = new URL(finalBase + finalPath);
4546
6041
  if (opts.query) {
4547
6042
  for (const [k, v] of Object.entries(opts.query)) {
@@ -4560,14 +6055,14 @@ var LeonardoClient = class {
4560
6055
  timeoutMs: opts.timeoutMs ?? 12e4
4561
6056
  });
4562
6057
  }
4563
- get(path49, query, logger) {
4564
- return this.request(path49, { method: "GET", query, logger });
6058
+ get(path52, query, logger) {
6059
+ return this.request(path52, { method: "GET", query, logger });
4565
6060
  }
4566
- post(path49, body, logger) {
4567
- return this.request(path49, { method: "POST", body, logger });
6061
+ post(path52, body, logger) {
6062
+ return this.request(path52, { method: "POST", body, logger });
4568
6063
  }
4569
- delete(path49, logger) {
4570
- return this.request(path49, { method: "DELETE", logger });
6064
+ delete(path52, logger) {
6065
+ return this.request(path52, { method: "DELETE", logger });
4571
6066
  }
4572
6067
  };
4573
6068
  function createLeonardoClient() {
@@ -4578,7 +6073,7 @@ function createLeonardoClient() {
4578
6073
  function extFromUrl2(url, fallback = ".png") {
4579
6074
  try {
4580
6075
  const u = new URL(url);
4581
- const e = path29.extname(u.pathname);
6076
+ const e = path32.extname(u.pathname);
4582
6077
  return e.length > 0 ? e : fallback;
4583
6078
  } catch {
4584
6079
  return fallback;
@@ -4663,7 +6158,7 @@ function registerLeonardoGenerateCommand(parent) {
4663
6158
  if (!img) continue;
4664
6159
  const ext = extFromUrl2(img.url);
4665
6160
  const filename = autoFilename(generationId, i + 1, ext);
4666
- const filepath = path29.join(outDir, filename);
6161
+ const filepath = path32.join(outDir, filename);
4667
6162
  try {
4668
6163
  await downloadFile(img.url, filepath);
4669
6164
  saved.push(filepath);
@@ -4673,8 +6168,8 @@ function registerLeonardoGenerateCommand(parent) {
4673
6168
  }
4674
6169
  }
4675
6170
  if (opts.output && saved[0]) {
4676
- const dest = path29.resolve(opts.output);
4677
- fs29.mkdirSync(path29.dirname(dest), { recursive: true });
6171
+ const dest = path32.resolve(opts.output);
6172
+ fs29.mkdirSync(path32.dirname(dest), { recursive: true });
4678
6173
  fs29.copyFileSync(saved[0], dest);
4679
6174
  logger.success(`Copied to ${dest}`);
4680
6175
  }
@@ -4688,10 +6183,10 @@ Generated ${finalImages.length} image(s):`);
4688
6183
 
4689
6184
  // src/providers/leonardo/commands/image-to-image.ts
4690
6185
  import fs30 from "fs";
4691
- import path30 from "path";
6186
+ import path33 from "path";
4692
6187
  function extFromUrl3(url, fallback = ".png") {
4693
6188
  try {
4694
- return path30.extname(new URL(url).pathname) || fallback;
6189
+ return path33.extname(new URL(url).pathname) || fallback;
4695
6190
  } catch {
4696
6191
  return fallback;
4697
6192
  }
@@ -4753,7 +6248,7 @@ function registerLeonardoImageToImageCommand(parent) {
4753
6248
  if (!img) continue;
4754
6249
  const ext = extFromUrl3(img.url);
4755
6250
  const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
4756
- const filepath = path30.join(outDir, filename);
6251
+ const filepath = path33.join(outDir, filename);
4757
6252
  try {
4758
6253
  await downloadFile(img.url, filepath);
4759
6254
  saved.push(filepath);
@@ -4763,8 +6258,8 @@ function registerLeonardoImageToImageCommand(parent) {
4763
6258
  }
4764
6259
  }
4765
6260
  if (opts.output && saved[0]) {
4766
- const dest = path30.resolve(opts.output);
4767
- fs30.mkdirSync(path30.dirname(dest), { recursive: true });
6261
+ const dest = path33.resolve(opts.output);
6262
+ fs30.mkdirSync(path33.dirname(dest), { recursive: true });
4768
6263
  fs30.copyFileSync(saved[0], dest);
4769
6264
  logger.success(`Copied to ${dest}`);
4770
6265
  }
@@ -4777,7 +6272,7 @@ Generated ${finalImages.length} image(s):`);
4777
6272
  }
4778
6273
 
4779
6274
  // src/providers/leonardo/commands/video.ts
4780
- import path31 from "path";
6275
+ import path34 from "path";
4781
6276
  function v2DimsFor(resolution) {
4782
6277
  if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
4783
6278
  if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
@@ -4862,7 +6357,7 @@ async function pollAndDownload(client, jobId, opts) {
4862
6357
  onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
4863
6358
  });
4864
6359
  } catch (e) {
4865
- if (e instanceof PollTimeoutError) {
6360
+ if (e instanceof PollTimeoutError2) {
4866
6361
  logger.error(`Timed out \u2014 multix leonardo status ${jobId}`);
4867
6362
  process.exit(1);
4868
6363
  }
@@ -4888,7 +6383,7 @@ async function pollAndDownload(client, jobId, opts) {
4888
6383
  logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
4889
6384
  process.exit(1);
4890
6385
  }
4891
- const outPath = opts.output ?? path31.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
6386
+ const outPath = opts.output ?? path34.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
4892
6387
  await downloadFile(videoUrl2, outPath);
4893
6388
  logger.success(`Saved ${outPath}`);
4894
6389
  await maybeDownloadThumb(gen, outPath, {
@@ -4994,7 +6489,7 @@ function registerLeonardoModelsCommand(parent) {
4994
6489
  }
4995
6490
 
4996
6491
  // src/providers/leonardo/commands/status.ts
4997
- import path32 from "path";
6492
+ import path35 from "path";
4998
6493
  var VIDEO_KEYS = ["motionMP4URL", "url"];
4999
6494
  var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
5000
6495
  var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
@@ -5034,7 +6529,7 @@ function registerLeonardoStatusCommand(parent) {
5034
6529
  onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
5035
6530
  }) : await fetchOnce();
5036
6531
  } catch (e) {
5037
- if (e instanceof PollTimeoutError) {
6532
+ if (e instanceof PollTimeoutError2) {
5038
6533
  logger.error(`Timed out waiting for generation ${generationId}`);
5039
6534
  process.exit(1);
5040
6535
  }
@@ -5059,7 +6554,7 @@ function registerLeonardoStatusCommand(parent) {
5059
6554
  const outDir = getOutputDir();
5060
6555
  let idx = 0;
5061
6556
  for (const url of videos) {
5062
- const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path32.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
6557
+ const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path35.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
5063
6558
  await downloadFile(url, dest);
5064
6559
  logger.success(`Saved ${dest}`);
5065
6560
  await maybeDownloadThumb(gen, dest, {
@@ -5072,7 +6567,7 @@ function registerLeonardoStatusCommand(parent) {
5072
6567
  for (const url of images) {
5073
6568
  const m = url.match(IMAGE_EXT_RE2);
5074
6569
  const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
5075
- const dest = path32.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
6570
+ const dest = path35.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
5076
6571
  await downloadFile(url, dest);
5077
6572
  logger.success(`Saved ${dest}`);
5078
6573
  idx++;
@@ -5230,7 +6725,7 @@ function sleep3(ms) {
5230
6725
 
5231
6726
  // src/providers/minimax/generators/music.ts
5232
6727
  import fs31 from "fs";
5233
- import path33 from "path";
6728
+ import path36 from "path";
5234
6729
  async function generateMinimaxMusic(opts) {
5235
6730
  const {
5236
6731
  apiKey,
@@ -5275,7 +6770,7 @@ async function generateMinimaxMusic(opts) {
5275
6770
  }
5276
6771
  const durationMs = resp.extra_info?.music_duration ?? 0;
5277
6772
  const outDir = getOutputDir();
5278
- const dest = path33.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
6773
+ const dest = path36.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
5279
6774
  try {
5280
6775
  if (audioData.startsWith("http")) {
5281
6776
  const bytes = await fetchBytes(audioData);
@@ -5293,7 +6788,7 @@ async function generateMinimaxMusic(opts) {
5293
6788
  const durSec = durationMs / 1e3;
5294
6789
  logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
5295
6790
  if (output) {
5296
- fs31.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
6791
+ fs31.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
5297
6792
  fs31.copyFileSync(dest, output);
5298
6793
  }
5299
6794
  return { status: "success", generatedAudio: dest, durationMs, model };
@@ -5369,7 +6864,7 @@ init_errors();
5369
6864
 
5370
6865
  // src/providers/minimax/generators/speech.ts
5371
6866
  import fs32 from "fs";
5372
- import path34 from "path";
6867
+ import path37 from "path";
5373
6868
  async function generateMinimaxSpeech(opts) {
5374
6869
  const {
5375
6870
  apiKey,
@@ -5416,11 +6911,11 @@ async function generateMinimaxSpeech(opts) {
5416
6911
  const audioBytes = Buffer.from(audioHex, "hex");
5417
6912
  const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
5418
6913
  const outDir = getOutputDir();
5419
- const dest = path34.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
6914
+ const dest = path37.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
5420
6915
  fs32.writeFileSync(dest, audioBytes);
5421
6916
  logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
5422
6917
  if (output) {
5423
- fs32.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
6918
+ fs32.mkdirSync(path37.dirname(path37.resolve(output)), { recursive: true });
5424
6919
  fs32.copyFileSync(dest, output);
5425
6920
  }
5426
6921
  return { status: "success", generatedAudio: dest, model };
@@ -5469,7 +6964,7 @@ init_errors();
5469
6964
 
5470
6965
  // src/providers/minimax/generators/video.ts
5471
6966
  import fs33 from "fs";
5472
- import path35 from "path";
6967
+ import path38 from "path";
5473
6968
  async function generateMinimaxVideo(opts) {
5474
6969
  const {
5475
6970
  apiKey,
@@ -5515,7 +7010,7 @@ async function generateMinimaxVideo(opts) {
5515
7010
  return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
5516
7011
  }
5517
7012
  const outDir = getOutputDir();
5518
- const dest = path35.join(outDir, `minimax_video_${Date.now()}.mp4`);
7013
+ const dest = path38.join(outDir, `minimax_video_${Date.now()}.mp4`);
5519
7014
  try {
5520
7015
  await downloadMinimaxFile(fileId, apiKey, dest, logger);
5521
7016
  } catch (e) {
@@ -5525,7 +7020,7 @@ async function generateMinimaxVideo(opts) {
5525
7020
  const stat = fs33.statSync(dest);
5526
7021
  const fileSizeMb = stat.size / (1024 * 1024);
5527
7022
  if (output) {
5528
- fs33.mkdirSync(path35.dirname(path35.resolve(output)), { recursive: true });
7023
+ fs33.mkdirSync(path38.dirname(path38.resolve(output)), { recursive: true });
5529
7024
  fs33.copyFileSync(dest, output);
5530
7025
  }
5531
7026
  await maybeDownloadThumb(pollResult, dest, {
@@ -5589,7 +7084,7 @@ Generated video: ${result.generatedVideo}`);
5589
7084
  // src/providers/minimax/generators/image.ts
5590
7085
  init_errors();
5591
7086
  import fs34 from "fs";
5592
- import path36 from "path";
7087
+ import path39 from "path";
5593
7088
  async function generateMinimaxImage(opts) {
5594
7089
  const {
5595
7090
  apiKey,
@@ -5627,7 +7122,7 @@ async function generateMinimaxImage(opts) {
5627
7122
  try {
5628
7123
  const bytes = await fetchBytes(url);
5629
7124
  const fname = `minimax_image_${Date.now()}_${i}.png`;
5630
- const dest = path36.join(outDir, fname);
7125
+ const dest = path39.join(outDir, fname);
5631
7126
  fs34.writeFileSync(dest, bytes);
5632
7127
  savedFiles.push(dest);
5633
7128
  logger?.success(`Saved: ${dest}`);
@@ -5639,7 +7134,7 @@ async function generateMinimaxImage(opts) {
5639
7134
  }
5640
7135
  }
5641
7136
  if (output && savedFiles[0]) {
5642
- fs34.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
7137
+ fs34.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
5643
7138
  fs34.copyFileSync(savedFiles[0], output);
5644
7139
  }
5645
7140
  return { status: "success", generatedImages: savedFiles, model };
@@ -5680,7 +7175,7 @@ Generated ${finalImages.length} image(s):`);
5680
7175
  // src/providers/minimax/commands/image-to-image.ts
5681
7176
  init_errors();
5682
7177
  import fs35 from "fs";
5683
- import path37 from "path";
7178
+ import path40 from "path";
5684
7179
  function registerMinimaxImageToImageCommand(parent) {
5685
7180
  parent.command("image-to-image").alias("i2i").description(
5686
7181
  "MiniMax character/subject preservation. CAVEAT: NOT free-form image editing \u2014 the --ref image is used only as a character/face reference for a NEW scene described by --prompt."
@@ -5735,7 +7230,7 @@ function registerMinimaxImageToImageCommand(parent) {
5735
7230
  if (!url) continue;
5736
7231
  try {
5737
7232
  const bytes = await fetchBytes(url);
5738
- const dest = path37.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
7233
+ const dest = path40.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
5739
7234
  fs35.writeFileSync(dest, bytes);
5740
7235
  saved.push(dest);
5741
7236
  logger.success(`Saved: ${dest}`);
@@ -5747,7 +7242,7 @@ function registerMinimaxImageToImageCommand(parent) {
5747
7242
  }
5748
7243
  }
5749
7244
  if (opts.output && saved[0]) {
5750
- fs35.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
7245
+ fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
5751
7246
  fs35.copyFileSync(saved[0], opts.output);
5752
7247
  logger.success(`Copied to: ${opts.output}`);
5753
7248
  }
@@ -5883,11 +7378,11 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
5883
7378
 
5884
7379
  // src/providers/openai/openai-audio.ts
5885
7380
  import fs37 from "fs";
5886
- import path39 from "path";
7381
+ import path42 from "path";
5887
7382
 
5888
7383
  // src/providers/openai/client.ts
5889
7384
  import fs36 from "fs";
5890
- import path38 from "path";
7385
+ import path41 from "path";
5891
7386
  init_errors();
5892
7387
  var DEFAULT_TIMEOUT_MS3 = 24e4;
5893
7388
  function requireOpenAIKey() {
@@ -5953,10 +7448,10 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
5953
7448
  return Buffer.from(await res.arrayBuffer());
5954
7449
  }
5955
7450
  function fileFromPath(filePath) {
5956
- const abs = path38.resolve(filePath);
7451
+ const abs = path41.resolve(filePath);
5957
7452
  if (!fs36.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
5958
7453
  const bytes = fs36.readFileSync(abs);
5959
- return new File([new Uint8Array(bytes)], path38.basename(abs));
7454
+ return new File([new Uint8Array(bytes)], path41.basename(abs));
5960
7455
  }
5961
7456
 
5962
7457
  // src/providers/openai/openai-audio.ts
@@ -5995,7 +7490,7 @@ async function resolveAudioReferenceDataUrl(ref) {
5995
7490
  }
5996
7491
  function audioMimeFromReference(ref) {
5997
7492
  const pathname = new URL(ref).pathname;
5998
- const ext = path39.extname(pathname).toLowerCase();
7493
+ const ext = path42.extname(pathname).toLowerCase();
5999
7494
  switch (ext) {
6000
7495
  case ".mp3":
6001
7496
  return "audio/mpeg";
@@ -6014,11 +7509,11 @@ function audioMimeFromReference(ref) {
6014
7509
  async function generateOpenAISpeech(opts) {
6015
7510
  const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
6016
7511
  const ext = extFromAudioFormat(opts.outputFormat);
6017
- const dest = path39.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
7512
+ const dest = path42.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
6018
7513
  fs37.writeFileSync(dest, bytes);
6019
7514
  opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
6020
7515
  if (opts.output) {
6021
- fs37.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
7516
+ fs37.mkdirSync(path42.dirname(path42.resolve(opts.output)), { recursive: true });
6022
7517
  fs37.copyFileSync(dest, opts.output);
6023
7518
  }
6024
7519
  return dest;
@@ -6041,8 +7536,8 @@ async function transcribeOpenAIAudio(opts) {
6041
7536
  );
6042
7537
  const text = typeof data.text === "string" ? data.text : void 0;
6043
7538
  const ext = opts.format === "text" ? "txt" : "json";
6044
- const dest = opts.output ? path39.resolve(opts.output) : path39.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
6045
- fs37.mkdirSync(path39.dirname(dest), { recursive: true });
7539
+ const dest = opts.output ? path42.resolve(opts.output) : path42.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
7540
+ fs37.mkdirSync(path42.dirname(dest), { recursive: true });
6046
7541
  fs37.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
6047
7542
  opts.logger?.success(`Saved transcript: ${dest}`);
6048
7543
  return { path: dest, text };
@@ -6071,8 +7566,8 @@ Generated audio: ${dest}`);
6071
7566
 
6072
7567
  // src/providers/openai/codex-image-driver.ts
6073
7568
  import fs38 from "fs";
6074
- import os2 from "os";
6075
- import path40 from "path";
7569
+ import os3 from "os";
7570
+ import path43 from "path";
6076
7571
  import { execa } from "execa";
6077
7572
  init_errors();
6078
7573
  var defaultRunner = async (cmd, args, opts) => {
@@ -6121,11 +7616,11 @@ function sanitizeCodexEnv() {
6121
7616
  async function runCodexImageGeneration(opts) {
6122
7617
  const runner = opts.runner ?? defaultRunner;
6123
7618
  const ext = extFromImageFormat(opts.outputFormat);
6124
- const dest = opts.output ? path40.resolve(opts.output) : path40.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
6125
- fs38.mkdirSync(path40.dirname(dest), { recursive: true });
6126
- const tmp = fs38.mkdtempSync(path40.join(os2.tmpdir(), "multix-codex-image-"));
6127
- const schema = path40.join(tmp, "schema.json");
6128
- const result = path40.join(tmp, "result.json");
7619
+ const dest = opts.output ? path43.resolve(opts.output) : path43.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
7620
+ fs38.mkdirSync(path43.dirname(dest), { recursive: true });
7621
+ const tmp = fs38.mkdtempSync(path43.join(os3.tmpdir(), "multix-codex-image-"));
7622
+ const schema = path43.join(tmp, "schema.json");
7623
+ const result = path43.join(tmp, "result.json");
6129
7624
  fs38.writeFileSync(
6130
7625
  schema,
6131
7626
  JSON.stringify({
@@ -6174,7 +7669,7 @@ function buildCodexPrompt(userPrompt, outputPath) {
6174
7669
  // src/providers/openai/openai-image.ts
6175
7670
  init_errors();
6176
7671
  import fs39 from "fs";
6177
- import path41 from "path";
7672
+ import path44 from "path";
6178
7673
  function buildImageGenerationBody(opts) {
6179
7674
  return {
6180
7675
  model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
@@ -6207,13 +7702,13 @@ function saveOpenAIImages(opts) {
6207
7702
  for (let i = 0; i < items.length; i++) {
6208
7703
  const b64 = items[i]?.b64_json;
6209
7704
  if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
6210
- const dest = path41.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
7705
+ const dest = path44.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
6211
7706
  fs39.writeFileSync(dest, Buffer.from(b64, "base64"));
6212
7707
  saved.push(dest);
6213
7708
  opts.logger?.success(`Saved: ${dest}`);
6214
7709
  }
6215
7710
  if (opts.output && saved[0]) {
6216
- fs39.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
7711
+ fs39.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
6217
7712
  fs39.copyFileSync(saved[0], opts.output);
6218
7713
  opts.logger?.success(`Copied to: ${opts.output}`);
6219
7714
  }
@@ -6223,7 +7718,7 @@ async function resolveImageRefBytes(ref) {
6223
7718
  if (isHttpUrl(ref)) {
6224
7719
  const bytes2 = await fetchBytes(ref);
6225
7720
  const pathname = new URL(ref).pathname;
6226
- const filename = path41.basename(pathname) || "reference.png";
7721
+ const filename = path44.basename(pathname) || "reference.png";
6227
7722
  return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
6228
7723
  }
6229
7724
  const file = fileFromPath(ref);
@@ -6268,7 +7763,7 @@ function sniffImageMime2(buf, name) {
6268
7763
  if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
6269
7764
  if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
6270
7765
  return "image/webp";
6271
- const ext = path41.extname(name).toLowerCase();
7766
+ const ext = path44.extname(name).toLowerCase();
6272
7767
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
6273
7768
  if (ext === ".webp") return "image/webp";
6274
7769
  return "image/png";
@@ -6413,7 +7908,7 @@ function registerOpenAICommands(program2) {
6413
7908
 
6414
7909
  // src/providers/openrouter/client.ts
6415
7910
  import fs40 from "fs";
6416
- import path42 from "path";
7911
+ import path45 from "path";
6417
7912
  init_errors();
6418
7913
 
6419
7914
  // src/providers/openrouter/payload.ts
@@ -6577,7 +8072,7 @@ async function generateOpenRouterImage(opts) {
6577
8072
  if (!url) continue;
6578
8073
  try {
6579
8074
  const bytes = await fetchBytes(url);
6580
- const dest = path42.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
8075
+ const dest = path45.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
6581
8076
  fs40.writeFileSync(dest, bytes);
6582
8077
  savedFiles.push(dest);
6583
8078
  logger?.success(`Saved: ${dest}`);
@@ -6589,7 +8084,7 @@ async function generateOpenRouterImage(opts) {
6589
8084
  }
6590
8085
  }
6591
8086
  if (output && savedFiles[0]) {
6592
- fs40.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
8087
+ fs40.mkdirSync(path45.dirname(path45.resolve(output)), { recursive: true });
6593
8088
  fs40.writeFileSync(output, fs40.readFileSync(savedFiles[0]));
6594
8089
  }
6595
8090
  return { status: "success", generatedImages: savedFiles, model: usedModel };
@@ -6632,7 +8127,7 @@ Generated ${finalImages.length} image(s):`);
6632
8127
 
6633
8128
  // src/providers/openrouter/commands/image-to-image.ts
6634
8129
  import fs41 from "fs";
6635
- import path43 from "path";
8130
+ import path46 from "path";
6636
8131
  init_errors();
6637
8132
  var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
6638
8133
  var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
@@ -6724,13 +8219,13 @@ async function runImageToImage(opts) {
6724
8219
  const url = parsed.urls[i];
6725
8220
  if (!url) continue;
6726
8221
  const bytes = await fetchBytes(url);
6727
- const dest = path43.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
8222
+ const dest = path46.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
6728
8223
  fs41.writeFileSync(dest, bytes);
6729
8224
  saved.push(dest);
6730
8225
  opts.logger.success(`Saved: ${dest}`);
6731
8226
  }
6732
8227
  if (opts.output && saved[0]) {
6733
- fs41.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
8228
+ fs41.mkdirSync(path46.dirname(path46.resolve(opts.output)), { recursive: true });
6734
8229
  fs41.copyFileSync(saved[0], opts.output);
6735
8230
  opts.logger.success(`Copied to: ${opts.output}`);
6736
8231
  }
@@ -6777,12 +8272,12 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
6777
8272
 
6778
8273
  // src/providers/openrouter/commands/video-status.ts
6779
8274
  import fs42 from "fs";
6780
- import path44 from "path";
8275
+ import path47 from "path";
6781
8276
  async function downloadOpenRouterVideo(opts) {
6782
8277
  const { jobId, logger, source } = opts;
6783
8278
  const apiKey = requireOpenRouterKey();
6784
8279
  const url = videoContentUrl(jobId);
6785
- const dest = path44.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
8280
+ const dest = path47.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
6786
8281
  const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
6787
8282
  if (!r.ok) {
6788
8283
  logger.error(`Download HTTP ${r.status}`);
@@ -6792,13 +8287,13 @@ async function downloadOpenRouterVideo(opts) {
6792
8287
  logger.error("Empty response body");
6793
8288
  process.exit(1);
6794
8289
  }
6795
- fs42.mkdirSync(path44.dirname(dest), { recursive: true });
8290
+ fs42.mkdirSync(path47.dirname(dest), { recursive: true });
6796
8291
  const ws = fs42.createWriteStream(dest);
6797
8292
  const { pipeline: pipeline2 } = await import("stream/promises");
6798
8293
  await pipeline2(r.body, ws);
6799
8294
  logger.success(`Saved ${dest}`);
6800
8295
  if (opts.output) {
6801
- fs42.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
8296
+ fs42.mkdirSync(path47.dirname(path47.resolve(opts.output)), { recursive: true });
6802
8297
  fs42.copyFileSync(dest, opts.output);
6803
8298
  logger.success(`Copied to ${opts.output}`);
6804
8299
  }
@@ -6828,7 +8323,7 @@ function registerOpenRouterVideoStatusCommand(parent) {
6828
8323
  onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
6829
8324
  }) : await pollVideoJob(jobId);
6830
8325
  } catch (e) {
6831
- if (e instanceof PollTimeoutError) {
8326
+ if (e instanceof PollTimeoutError2) {
6832
8327
  logger.error(`Timed out waiting for job ${jobId}`);
6833
8328
  process.exit(1);
6834
8329
  }
@@ -6912,7 +8407,7 @@ function registerOpenRouterImageToVideoCommand(parent) {
6912
8407
  onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
6913
8408
  });
6914
8409
  } catch (e) {
6915
- if (e instanceof PollTimeoutError) {
8410
+ if (e instanceof PollTimeoutError2) {
6916
8411
  logger.error(`Timed out \u2014 multix openrouter video-status ${res.id}`);
6917
8412
  process.exit(1);
6918
8413
  }
@@ -6966,7 +8461,7 @@ function registerOpenRouterCommands(program2) {
6966
8461
 
6967
8462
  // src/commands/check.ts
6968
8463
  import fs43 from "fs";
6969
- import os3 from "os";
8464
+ import os4 from "os";
6970
8465
 
6971
8466
  // src/commands/check-helpers/binary-check.ts
6972
8467
  import { execa as execa2 } from "execa";
@@ -7022,6 +8517,7 @@ To configure multix, set at least one provider API key:
7022
8517
  Leonardo.Ai : https://app.leonardo.ai/settings/api-keys
7023
8518
  BytePlus : https://console.byteplus.com/auth/api-keys
7024
8519
  fal.ai : https://fal.ai/dashboard/keys
8520
+ Atlas Cloud : https://www.atlascloud.ai/console/api-keys
7025
8521
 
7026
8522
  Option A \u2014 user global config (recommended):
7027
8523
  echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
@@ -7090,6 +8586,12 @@ var PROVIDERS = [
7090
8586
  envPrimary: "FAL_KEY",
7091
8587
  optional: true,
7092
8588
  link: "https://fal.ai/dashboard/keys"
8589
+ },
8590
+ {
8591
+ name: "Atlas Cloud",
8592
+ envPrimary: "ATLASCLOUD_API_KEY",
8593
+ optional: true,
8594
+ link: "https://www.atlascloud.ai/console/api-keys"
7093
8595
  }
7094
8596
  ];
7095
8597
  function resolveProviderKey(p) {
@@ -7144,7 +8646,7 @@ function registerCheckCommand(program2) {
7144
8646
  logger.info(`Env files loaded: ${files.length > 0 ? files.join(", ") : "none"}`);
7145
8647
  }
7146
8648
  if (!legacyClaudeEnvEnabled()) {
7147
- const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os3.homedir();
8649
+ const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os4.homedir();
7148
8650
  const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) => fs43.existsSync(f));
7149
8651
  if (legacy.length > 0) {
7150
8652
  logger.info(
@@ -7251,7 +8753,7 @@ function registerCheckCommand(program2) {
7251
8753
  // src/commands/doc/convert.ts
7252
8754
  init_errors();
7253
8755
  import fs44 from "fs";
7254
- import path45 from "path";
8756
+ import path48 from "path";
7255
8757
 
7256
8758
  // src/commands/doc/default-prompt.ts
7257
8759
  var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
@@ -7276,14 +8778,14 @@ async function convertDocuments(opts) {
7276
8778
  }
7277
8779
  let outputPath;
7278
8780
  if (output) {
7279
- outputPath = path45.resolve(output);
8781
+ outputPath = path48.resolve(output);
7280
8782
  } else if (autoName && inputs.length === 1 && inputs[0]) {
7281
- const base = path45.basename(inputs[0], path45.extname(inputs[0]));
7282
- outputPath = path45.join(getOutputDir(), `${base}-extraction.md`);
8783
+ const base = path48.basename(inputs[0], path48.extname(inputs[0]));
8784
+ outputPath = path48.join(getOutputDir(), `${base}-extraction.md`);
7283
8785
  } else {
7284
- outputPath = path45.join(getOutputDir(), "document-extraction.md");
8786
+ outputPath = path48.join(getOutputDir(), "document-extraction.md");
7285
8787
  }
7286
- fs44.mkdirSync(path45.dirname(outputPath), { recursive: true });
8788
+ fs44.mkdirSync(path48.dirname(outputPath), { recursive: true });
7287
8789
  const results = [];
7288
8790
  for (let i = 0; i < inputs.length; i++) {
7289
8791
  const filePath = inputs[i];
@@ -7343,7 +8845,7 @@ async function convertDocuments(opts) {
7343
8845
  `);
7344
8846
  for (const r of results) {
7345
8847
  lines.push(`
7346
- ## ${path45.basename(r.file)}
8848
+ ## ${path48.basename(r.file)}
7347
8849
  `);
7348
8850
  if (r.status === "success" && r.markdown) {
7349
8851
  lines.push(`${r.markdown}
@@ -7405,11 +8907,11 @@ function registerDocCommands(program2) {
7405
8907
  // src/commands/media/index.ts
7406
8908
  init_errors();
7407
8909
  import fs47 from "fs";
7408
- import path48 from "path";
8910
+ import path51 from "path";
7409
8911
 
7410
8912
  // src/commands/media/batch.ts
7411
8913
  import fs45 from "fs";
7412
- import path46 from "path";
8914
+ import path49 from "path";
7413
8915
 
7414
8916
  // src/commands/media/detect-type.ts
7415
8917
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
@@ -7608,7 +9110,7 @@ async function batchOptimize(opts) {
7608
9110
  fs45.mkdirSync(outputDir, { recursive: true });
7609
9111
  const entries = fs45.readdirSync(inputDir);
7610
9112
  const mediaFiles = entries.filter((f) => {
7611
- const ext = path46.extname(f).toLowerCase();
9113
+ const ext = path49.extname(f).toLowerCase();
7612
9114
  return ALL_EXTS.has(ext);
7613
9115
  });
7614
9116
  if (mediaFiles.length === 0) {
@@ -7619,9 +9121,9 @@ async function batchOptimize(opts) {
7619
9121
  let succeeded = 0;
7620
9122
  const errors = [];
7621
9123
  for (const file of mediaFiles) {
7622
- const inputPath = path46.join(inputDir, file);
7623
- const outputPath = path46.join(outputDir, file);
7624
- const ext = path46.extname(file).toLowerCase();
9124
+ const inputPath = path49.join(inputDir, file);
9125
+ const outputPath = path49.join(outputDir, file);
9126
+ const ext = path49.extname(file).toLowerCase();
7625
9127
  const kind = extToKind(ext);
7626
9128
  logger?.debug(`Processing: ${file}`);
7627
9129
  try {
@@ -7659,7 +9161,7 @@ async function batchOptimize(opts) {
7659
9161
  // src/commands/media/split-video.ts
7660
9162
  init_errors();
7661
9163
  import fs46 from "fs";
7662
- import path47 from "path";
9164
+ import path50 from "path";
7663
9165
  async function splitVideo(opts) {
7664
9166
  await assertFfmpeg();
7665
9167
  const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
@@ -7671,9 +9173,9 @@ async function splitVideo(opts) {
7671
9173
  return [input];
7672
9174
  }
7673
9175
  fs46.mkdirSync(outputDir, { recursive: true });
7674
- const ext = path47.extname(input) || ".mp4";
7675
- const baseName = path47.basename(input, ext);
7676
- const outputPattern = path47.join(outputDir, `${baseName}_chunk_%03d${ext}`);
9176
+ const ext = path50.extname(input) || ".mp4";
9177
+ const baseName = path50.basename(input, ext);
9178
+ const outputPattern = path50.join(outputDir, `${baseName}_chunk_%03d${ext}`);
7677
9179
  logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
7678
9180
  const args = [
7679
9181
  "-i",
@@ -7692,7 +9194,7 @@ async function splitVideo(opts) {
7692
9194
  await runFfmpeg(args, { verbose, logger });
7693
9195
  const files = [];
7694
9196
  for (let i = 0; i < numChunks + 1; i++) {
7695
- const fname = path47.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
9197
+ const fname = path50.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
7696
9198
  if (fs46.existsSync(fname)) files.push(fname);
7697
9199
  }
7698
9200
  logger?.success(`Created ${files.length} chunks in ${outputDir}`);
@@ -7708,8 +9210,8 @@ function registerMediaCommands(program2) {
7708
9210
  async (opts) => {
7709
9211
  const logger = createLogger({ verbose: opts.verbose ?? false });
7710
9212
  if (!fs47.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7711
- fs47.mkdirSync(path48.dirname(path48.resolve(opts.output)), { recursive: true });
7712
- const ext = path48.extname(opts.input).toLowerCase();
9213
+ fs47.mkdirSync(path51.dirname(path51.resolve(opts.output)), { recursive: true });
9214
+ const ext = path51.extname(opts.input).toLowerCase();
7713
9215
  const kind = extToKind(ext);
7714
9216
  if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
7715
9217
  const quality = Number.parseInt(opts.quality, 10);
@@ -7945,6 +9447,7 @@ function registerCommands(program2) {
7945
9447
  registerCloudflareCommands(program2);
7946
9448
  registerElevenLabsCommands(program2);
7947
9449
  registerFalCommands(program2);
9450
+ registerAtlasCloudCommands(program2);
7948
9451
  registerMediaCommands(program2);
7949
9452
  registerDocCommands(program2);
7950
9453
  }
@@ -7958,7 +9461,7 @@ loadEnv();
7958
9461
  var require3 = createRequire2(import.meta.url);
7959
9462
  var pkg = require3("../package.json");
7960
9463
  var program = new Command().name("multix").description(
7961
- "AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, ffmpeg, and ImageMagick"
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"
7962
9465
  ).version(pkg.version);
7963
9466
  registerCommands(program);
7964
9467
  program.parseAsync(process.argv).catch((err2) => {