@mrgoonie/multix 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/cli.js +263 -165
- package/package.json +1 -1
- package/skill/SKILL.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.7.2](https://github.com/mrgoonie/multix-cli/compare/v0.7.1...v0.7.2) (2026-10-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* add AGENTS.md contributor and issue-label workflow ([#57](https://github.com/mrgoonie/multix-cli/issues/57)) ([2cffb46](https://github.com/mrgoonie/multix-cli/commit/2cffb46be060664539df62eaf262e8cb41bd367b))
|
|
14
|
+
|
|
15
|
+
## [0.7.1](https://github.com/mrgoonie/multix-cli/compare/v0.7.0...v0.7.1) (2026-10-05)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
* **check:** probe ffmpeg with -version so ffmpeg 8.x is detected ([#51](https://github.com/mrgoonie/multix-cli/issues/51)) ([ca26541](https://github.com/mrgoonie/multix-cli/commit/ca265416075a0df6eecf21f3e359f7aab74bce43))
|
|
21
|
+
|
|
8
22
|
## [0.7.0](https://github.com/mrgoonie/multix-cli/compare/v0.6.0...v0.7.0) (2026-09-24)
|
|
9
23
|
|
|
10
24
|
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,28 @@ var __export = (target, all) => {
|
|
|
9
9
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
+
// src/core/redact.ts
|
|
13
|
+
function redact(key) {
|
|
14
|
+
if (key.length <= 10) return "***";
|
|
15
|
+
return `${key.slice(0, 6)}...${key.slice(-4)}`;
|
|
16
|
+
}
|
|
17
|
+
function redactUrl(url) {
|
|
18
|
+
try {
|
|
19
|
+
const u = new URL(url);
|
|
20
|
+
if (u.origin === "null") return `${u.protocol}[redacted]`;
|
|
21
|
+
const suffix = u.search || u.hash ? "?[redacted]" : "";
|
|
22
|
+
return `${u.origin}${u.pathname}${suffix}`;
|
|
23
|
+
} catch {
|
|
24
|
+
const cut = url.search(/[?#]/);
|
|
25
|
+
return cut === -1 ? url : `${url.slice(0, cut)}?[redacted]`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
var init_redact = __esm({
|
|
29
|
+
"src/core/redact.ts"() {
|
|
30
|
+
"use strict";
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
12
34
|
// src/core/errors.ts
|
|
13
35
|
var errors_exports = {};
|
|
14
36
|
__export(errors_exports, {
|
|
@@ -22,6 +44,7 @@ var MultixError, ConfigError, ProviderError, ValidationError, HttpError;
|
|
|
22
44
|
var init_errors = __esm({
|
|
23
45
|
"src/core/errors.ts"() {
|
|
24
46
|
"use strict";
|
|
47
|
+
init_redact();
|
|
25
48
|
MultixError = class extends Error {
|
|
26
49
|
constructor(message, code) {
|
|
27
50
|
super(message);
|
|
@@ -54,33 +77,51 @@ var init_errors = __esm({
|
|
|
54
77
|
}
|
|
55
78
|
};
|
|
56
79
|
HttpError = class extends MultixError {
|
|
57
|
-
constructor(status, snippet, url) {
|
|
58
|
-
super(`HTTP ${status} from ${url}: ${snippet}`, "HTTP_ERROR");
|
|
80
|
+
constructor(status, snippet, url, retryAfterMs) {
|
|
81
|
+
super(`HTTP ${status} from ${redactUrl(url)}: ${snippet}`, "HTTP_ERROR");
|
|
59
82
|
this.status = status;
|
|
60
83
|
this.snippet = snippet;
|
|
61
|
-
this.
|
|
84
|
+
this.retryAfterMs = retryAfterMs;
|
|
85
|
+
this.url = redactUrl(url);
|
|
86
|
+
Object.defineProperty(this, "rawUrl", { value: url, enumerable: false });
|
|
62
87
|
this.name = "HttpError";
|
|
63
88
|
}
|
|
64
89
|
status;
|
|
65
90
|
snippet;
|
|
91
|
+
retryAfterMs;
|
|
92
|
+
/** Request URL with query string and fragment redacted — safe to log or serialize. */
|
|
66
93
|
url;
|
|
67
94
|
};
|
|
68
95
|
}
|
|
69
96
|
});
|
|
70
97
|
|
|
71
|
-
// src/core/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
98
|
+
// src/core/fetch-json.ts
|
|
99
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
100
|
+
if (!value) return void 0;
|
|
101
|
+
const trimmed = value.trim();
|
|
102
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1e3);
|
|
103
|
+
const date = Date.parse(trimmed);
|
|
104
|
+
if (Number.isNaN(date)) return void 0;
|
|
105
|
+
return Math.max(0, date - now);
|
|
106
|
+
}
|
|
107
|
+
async function httpErrorFromResponse(response, url) {
|
|
108
|
+
let snippet = "";
|
|
109
|
+
try {
|
|
110
|
+
const text = await response.text();
|
|
111
|
+
snippet = text.slice(0, SNIPPET_MAX);
|
|
112
|
+
} catch {
|
|
113
|
+
snippet = "(could not read body)";
|
|
114
|
+
}
|
|
115
|
+
return new HttpError(
|
|
116
|
+
response.status,
|
|
117
|
+
snippet,
|
|
118
|
+
url,
|
|
119
|
+
parseRetryAfter(response.headers.get("retry-after"))
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
async function fetchJson(opts) {
|
|
83
123
|
const { url, method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
124
|
+
const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
|
|
84
125
|
const init = {
|
|
85
126
|
method,
|
|
86
127
|
headers: {
|
|
@@ -94,28 +135,64 @@ async function httpJson(opts) {
|
|
|
94
135
|
}
|
|
95
136
|
let response;
|
|
96
137
|
try {
|
|
97
|
-
response = await
|
|
138
|
+
response = await doFetch(url, init);
|
|
98
139
|
} catch (cause) {
|
|
99
140
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
100
141
|
throw new HttpError(0, msg, url);
|
|
101
142
|
}
|
|
102
143
|
if (!response.ok) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
throw new HttpError(
|
|
144
|
+
throw await httpErrorFromResponse(response, url);
|
|
145
|
+
}
|
|
146
|
+
let text;
|
|
147
|
+
try {
|
|
148
|
+
text = await response.text();
|
|
149
|
+
} catch (cause) {
|
|
150
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
151
|
+
throw new HttpError(0, `Failed to read response body: ${msg}`, url);
|
|
152
|
+
}
|
|
153
|
+
if (text.trim() === "") return void 0;
|
|
154
|
+
try {
|
|
155
|
+
return JSON.parse(text);
|
|
156
|
+
} catch {
|
|
157
|
+
throw new HttpError(
|
|
158
|
+
response.status,
|
|
159
|
+
`Invalid JSON response: ${text.slice(0, SNIPPET_MAX)}`,
|
|
160
|
+
url
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
var DEFAULT_TIMEOUT_MS, SNIPPET_MAX;
|
|
165
|
+
var init_fetch_json = __esm({
|
|
166
|
+
"src/core/fetch-json.ts"() {
|
|
167
|
+
"use strict";
|
|
168
|
+
init_errors();
|
|
169
|
+
DEFAULT_TIMEOUT_MS = 12e4;
|
|
170
|
+
SNIPPET_MAX = 500;
|
|
111
171
|
}
|
|
112
|
-
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// src/core/http-client.ts
|
|
175
|
+
var http_client_exports = {};
|
|
176
|
+
__export(http_client_exports, {
|
|
177
|
+
downloadFile: () => downloadFile,
|
|
178
|
+
fetchBytes: () => fetchBytes,
|
|
179
|
+
httpJson: () => httpJson
|
|
180
|
+
});
|
|
181
|
+
import { once } from "events";
|
|
182
|
+
import fs4 from "fs";
|
|
183
|
+
import path4 from "path";
|
|
184
|
+
import { pipeline } from "stream/promises";
|
|
185
|
+
async function httpJson(opts) {
|
|
186
|
+
return fetchJson(opts);
|
|
113
187
|
}
|
|
114
|
-
async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
188
|
+
async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers) {
|
|
115
189
|
fs4.mkdirSync(path4.dirname(dest), { recursive: true });
|
|
116
190
|
let response;
|
|
117
191
|
try {
|
|
118
|
-
response = await globalThis.fetch(url, {
|
|
192
|
+
response = await globalThis.fetch(url, {
|
|
193
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
194
|
+
...headers ? { headers } : {}
|
|
195
|
+
});
|
|
119
196
|
} catch (cause) {
|
|
120
197
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
121
198
|
throw new HttpError(0, msg, url);
|
|
@@ -155,13 +232,11 @@ async function fetchBytes(urlOrDataUri) {
|
|
|
155
232
|
}
|
|
156
233
|
return Buffer.from(await response.arrayBuffer());
|
|
157
234
|
}
|
|
158
|
-
var DEFAULT_TIMEOUT_MS, SNIPPET_MAX;
|
|
159
235
|
var init_http_client = __esm({
|
|
160
236
|
"src/core/http-client.ts"() {
|
|
161
237
|
"use strict";
|
|
162
238
|
init_errors();
|
|
163
|
-
|
|
164
|
-
SNIPPET_MAX = 500;
|
|
239
|
+
init_fetch_json();
|
|
165
240
|
}
|
|
166
241
|
});
|
|
167
242
|
|
|
@@ -346,6 +421,7 @@ async function poll(opts) {
|
|
|
346
421
|
}
|
|
347
422
|
|
|
348
423
|
// src/core/env-loader.ts
|
|
424
|
+
init_redact();
|
|
349
425
|
import fs3 from "fs";
|
|
350
426
|
import os from "os";
|
|
351
427
|
import path3 from "path";
|
|
@@ -368,10 +444,6 @@ function loadEnv() {
|
|
|
368
444
|
function resolveKey(name) {
|
|
369
445
|
return process.env[name] || void 0;
|
|
370
446
|
}
|
|
371
|
-
function redact(key) {
|
|
372
|
-
if (key.length <= 10) return "***";
|
|
373
|
-
return `${key.slice(0, 6)}...${key.slice(-4)}`;
|
|
374
|
-
}
|
|
375
447
|
|
|
376
448
|
// src/providers/byteplus/client.ts
|
|
377
449
|
init_errors();
|
|
@@ -2582,7 +2654,7 @@ function registerElevenLabsCommands(program2) {
|
|
|
2582
2654
|
}
|
|
2583
2655
|
|
|
2584
2656
|
// src/providers/fal/commands/image.ts
|
|
2585
|
-
import
|
|
2657
|
+
import fs17 from "fs";
|
|
2586
2658
|
import path20 from "path";
|
|
2587
2659
|
|
|
2588
2660
|
// src/providers/fal/models.ts
|
|
@@ -2597,15 +2669,45 @@ var FAL_DEFAULTS = {
|
|
|
2597
2669
|
};
|
|
2598
2670
|
|
|
2599
2671
|
// src/providers/fal/run-helpers.ts
|
|
2600
|
-
init_errors();
|
|
2601
2672
|
init_http_client();
|
|
2602
|
-
import
|
|
2673
|
+
import fs16 from "fs";
|
|
2603
2674
|
import path19 from "path";
|
|
2604
2675
|
|
|
2676
|
+
// src/core/json-input.ts
|
|
2677
|
+
init_errors();
|
|
2678
|
+
import fs15 from "fs";
|
|
2679
|
+
function parseJsonInput(raw, flag = "--input") {
|
|
2680
|
+
let text;
|
|
2681
|
+
if (raw.startsWith("@")) {
|
|
2682
|
+
try {
|
|
2683
|
+
text = fs15.readFileSync(raw.slice(1), "utf8");
|
|
2684
|
+
} catch (e) {
|
|
2685
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2686
|
+
throw new ValidationError(`Cannot read ${flag} file: ${msg}`);
|
|
2687
|
+
}
|
|
2688
|
+
} else {
|
|
2689
|
+
text = raw;
|
|
2690
|
+
}
|
|
2691
|
+
try {
|
|
2692
|
+
const parsed = JSON.parse(text);
|
|
2693
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2694
|
+
throw new Error("input must be a JSON object");
|
|
2695
|
+
}
|
|
2696
|
+
return parsed;
|
|
2697
|
+
} catch (e) {
|
|
2698
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
2699
|
+
throw new ValidationError(`Invalid ${flag} JSON: ${msg}`);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
// src/providers/fal/run-helpers.ts
|
|
2704
|
+
init_redact();
|
|
2705
|
+
|
|
2605
2706
|
// src/providers/fal/client.ts
|
|
2606
2707
|
init_errors();
|
|
2607
2708
|
init_errors();
|
|
2608
2709
|
init_http_client();
|
|
2710
|
+
init_redact();
|
|
2609
2711
|
function requireFalKey() {
|
|
2610
2712
|
const key = resolveKey("FAL_KEY");
|
|
2611
2713
|
if (!key) {
|
|
@@ -2631,7 +2733,7 @@ var FalClient = class {
|
|
|
2631
2733
|
}
|
|
2632
2734
|
/** Same auth/headers as `request`, but `url` is used verbatim (e.g. a status_url/response_url returned by submit). */
|
|
2633
2735
|
async requestAbsolute(url, opts = {}) {
|
|
2634
|
-
opts.logger?.debug(`${opts.method ?? "GET"} ${url}`);
|
|
2736
|
+
opts.logger?.debug(`${opts.method ?? "GET"} ${redactUrl(url)}`);
|
|
2635
2737
|
return httpJson({
|
|
2636
2738
|
url,
|
|
2637
2739
|
method: opts.method ?? "GET",
|
|
@@ -2677,17 +2779,7 @@ function resultPath(model, requestId) {
|
|
|
2677
2779
|
|
|
2678
2780
|
// src/providers/fal/run-helpers.ts
|
|
2679
2781
|
function parseInputArg(raw) {
|
|
2680
|
-
|
|
2681
|
-
try {
|
|
2682
|
-
const parsed = JSON.parse(text);
|
|
2683
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2684
|
-
throw new Error("input must be a JSON object");
|
|
2685
|
-
}
|
|
2686
|
-
return parsed;
|
|
2687
|
-
} catch (e) {
|
|
2688
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
2689
|
-
throw new ValidationError(`Invalid --input JSON: ${msg}`);
|
|
2690
|
-
}
|
|
2782
|
+
return parseJsonInput(raw);
|
|
2691
2783
|
}
|
|
2692
2784
|
function isTransientError(e) {
|
|
2693
2785
|
if (e instanceof HttpError) return e.status === 0 || e.status >= 500;
|
|
@@ -2818,8 +2910,8 @@ async function downloadResultMedia(urls, outDir, requestId, logger) {
|
|
|
2818
2910
|
saved.push(filepath);
|
|
2819
2911
|
logger.success(`Saved ${filepath}`);
|
|
2820
2912
|
} catch (e) {
|
|
2821
|
-
|
|
2822
|
-
logger.warn(`Download failed for ${url}: ${e instanceof Error ? e.message : e}`);
|
|
2913
|
+
fs16.rmSync(filepath, { force: true });
|
|
2914
|
+
logger.warn(`Download failed for ${redactUrl(url)}: ${e instanceof Error ? e.message : e}`);
|
|
2823
2915
|
}
|
|
2824
2916
|
}
|
|
2825
2917
|
return saved;
|
|
@@ -2864,8 +2956,8 @@ function registerFalImageCommand(parent) {
|
|
|
2864
2956
|
}
|
|
2865
2957
|
if (opts.output && saved[0]) {
|
|
2866
2958
|
const dest = path20.resolve(opts.output);
|
|
2867
|
-
|
|
2868
|
-
|
|
2959
|
+
fs17.mkdirSync(path20.dirname(dest), { recursive: true });
|
|
2960
|
+
fs17.copyFileSync(saved[0], dest);
|
|
2869
2961
|
logger.success(`Copied to ${dest}`);
|
|
2870
2962
|
}
|
|
2871
2963
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -3020,10 +3112,10 @@ function registerFalCommands(program2) {
|
|
|
3020
3112
|
|
|
3021
3113
|
// src/providers/gemini/commands/analyze.ts
|
|
3022
3114
|
init_errors();
|
|
3023
|
-
import
|
|
3115
|
+
import fs20 from "fs";
|
|
3024
3116
|
|
|
3025
3117
|
// src/providers/gemini/client.ts
|
|
3026
|
-
import
|
|
3118
|
+
import fs18 from "fs";
|
|
3027
3119
|
import path22 from "path";
|
|
3028
3120
|
init_errors();
|
|
3029
3121
|
init_http_client();
|
|
@@ -3096,7 +3188,7 @@ async function uploadFile(filePath, opts = {}) {
|
|
|
3096
3188
|
const apiKey = requireApiKey();
|
|
3097
3189
|
const { timeoutMs = 3e5, logger } = opts;
|
|
3098
3190
|
const mimeType = getMimeType(filePath);
|
|
3099
|
-
const fileBytes =
|
|
3191
|
+
const fileBytes = fs18.readFileSync(filePath);
|
|
3100
3192
|
const displayName = path22.basename(filePath);
|
|
3101
3193
|
const boundary = `multix_${Date.now()}`;
|
|
3102
3194
|
const metaJson = JSON.stringify({ file: { displayName, mimeType } });
|
|
@@ -3213,12 +3305,12 @@ function extractImages(resp) {
|
|
|
3213
3305
|
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
3214
3306
|
}
|
|
3215
3307
|
function inlinePart(filePath) {
|
|
3216
|
-
const data =
|
|
3308
|
+
const data = fs18.readFileSync(filePath).toString("base64");
|
|
3217
3309
|
const mimeType = getMimeType(filePath);
|
|
3218
3310
|
return { inlineData: { mimeType, data } };
|
|
3219
3311
|
}
|
|
3220
3312
|
function shouldUseFileApi(filePath) {
|
|
3221
|
-
const stat =
|
|
3313
|
+
const stat = fs18.statSync(filePath);
|
|
3222
3314
|
return stat.size > 20 * 1024 * 1024;
|
|
3223
3315
|
}
|
|
3224
3316
|
function sleep2(ms) {
|
|
@@ -3272,13 +3364,13 @@ function getDefaultModel(task) {
|
|
|
3272
3364
|
}
|
|
3273
3365
|
|
|
3274
3366
|
// src/providers/gemini/result-formatter.ts
|
|
3275
|
-
import
|
|
3367
|
+
import fs19 from "fs";
|
|
3276
3368
|
import path23 from "path";
|
|
3277
3369
|
function formatResults(results, format, outputPath) {
|
|
3278
3370
|
const content = renderFormat(results, format);
|
|
3279
3371
|
if (outputPath) {
|
|
3280
|
-
|
|
3281
|
-
|
|
3372
|
+
fs19.mkdirSync(path23.dirname(path23.resolve(outputPath)), { recursive: true });
|
|
3373
|
+
fs19.writeFileSync(outputPath, content, "utf8");
|
|
3282
3374
|
} else {
|
|
3283
3375
|
process.stdout.write(content);
|
|
3284
3376
|
}
|
|
@@ -3345,7 +3437,7 @@ function registerAnalyzeCommand(parent) {
|
|
|
3345
3437
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3346
3438
|
const model = opts.model ?? getDefaultModel("analyze");
|
|
3347
3439
|
for (const f of opts.files) {
|
|
3348
|
-
if (!
|
|
3440
|
+
if (!fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
3349
3441
|
}
|
|
3350
3442
|
const results = [];
|
|
3351
3443
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -3391,14 +3483,14 @@ function registerAnalyzeCommand(parent) {
|
|
|
3391
3483
|
|
|
3392
3484
|
// src/providers/gemini/commands/extract.ts
|
|
3393
3485
|
init_errors();
|
|
3394
|
-
import
|
|
3486
|
+
import fs21 from "fs";
|
|
3395
3487
|
function registerExtractCommand(parent) {
|
|
3396
3488
|
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(
|
|
3397
3489
|
async (opts) => {
|
|
3398
3490
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3399
3491
|
const model = opts.model ?? getDefaultModel("extract");
|
|
3400
3492
|
for (const f of opts.files) {
|
|
3401
|
-
if (!
|
|
3493
|
+
if (!fs21.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
3402
3494
|
}
|
|
3403
3495
|
const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
|
|
3404
3496
|
const results = [];
|
|
@@ -3440,7 +3532,7 @@ function registerExtractCommand(parent) {
|
|
|
3440
3532
|
init_errors();
|
|
3441
3533
|
|
|
3442
3534
|
// src/providers/gemini/generators/speech.ts
|
|
3443
|
-
import
|
|
3535
|
+
import fs22 from "fs";
|
|
3444
3536
|
import path24 from "path";
|
|
3445
3537
|
|
|
3446
3538
|
// src/providers/gemini/voices.ts
|
|
@@ -3605,11 +3697,11 @@ function extractWavData(wav) {
|
|
|
3605
3697
|
function saveSpeech(outBytes, outputFormat, output, logger) {
|
|
3606
3698
|
const outDir = getOutputDir();
|
|
3607
3699
|
const dest = path24.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
|
|
3608
|
-
|
|
3700
|
+
fs22.writeFileSync(dest, outBytes);
|
|
3609
3701
|
logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
|
|
3610
3702
|
if (output) {
|
|
3611
|
-
|
|
3612
|
-
|
|
3703
|
+
fs22.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
|
|
3704
|
+
fs22.copyFileSync(dest, output);
|
|
3613
3705
|
logger?.success(`Copied to: ${output}`);
|
|
3614
3706
|
}
|
|
3615
3707
|
return dest;
|
|
@@ -3721,7 +3813,7 @@ function collectSpeakers(value, prev) {
|
|
|
3721
3813
|
}
|
|
3722
3814
|
|
|
3723
3815
|
// src/providers/gemini/commands/generate-video.ts
|
|
3724
|
-
import
|
|
3816
|
+
import fs23 from "fs";
|
|
3725
3817
|
import path25 from "path";
|
|
3726
3818
|
init_errors();
|
|
3727
3819
|
init_http_client();
|
|
@@ -3756,9 +3848,9 @@ function registerGenerateVideoCommand(parent) {
|
|
|
3756
3848
|
const contentParts = [{ text: opts.prompt }];
|
|
3757
3849
|
if (opts.referenceImages?.length) {
|
|
3758
3850
|
for (const imgPath of opts.referenceImages.slice(0, 2)) {
|
|
3759
|
-
if (!
|
|
3851
|
+
if (!fs23.existsSync(imgPath))
|
|
3760
3852
|
throw new ValidationError(`Reference image not found: ${imgPath}`);
|
|
3761
|
-
const data =
|
|
3853
|
+
const data = fs23.readFileSync(imgPath).toString("base64");
|
|
3762
3854
|
const ext = path25.extname(imgPath).toLowerCase();
|
|
3763
3855
|
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
3764
3856
|
contentParts.push({ inlineData: { mimeType, data } });
|
|
@@ -3813,8 +3905,8 @@ function registerGenerateVideoCommand(parent) {
|
|
|
3813
3905
|
await downloadFile2(videoUri, dest);
|
|
3814
3906
|
logger.success(`Saved video: ${dest}`);
|
|
3815
3907
|
if (opts.output) {
|
|
3816
|
-
|
|
3817
|
-
|
|
3908
|
+
fs23.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
|
|
3909
|
+
fs23.copyFileSync(dest, opts.output);
|
|
3818
3910
|
logger.success(`Copied to: ${opts.output}`);
|
|
3819
3911
|
}
|
|
3820
3912
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -3833,7 +3925,7 @@ function sleep3(ms) {
|
|
|
3833
3925
|
|
|
3834
3926
|
// src/providers/gemini/commands/generate.ts
|
|
3835
3927
|
init_errors();
|
|
3836
|
-
import
|
|
3928
|
+
import fs24 from "fs";
|
|
3837
3929
|
import path26 from "path";
|
|
3838
3930
|
function registerGenerateCommand(parent) {
|
|
3839
3931
|
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(
|
|
@@ -3877,7 +3969,7 @@ function registerGenerateCommand(parent) {
|
|
|
3877
3969
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
3878
3970
|
const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
|
|
3879
3971
|
const dest = path26.join(outDir, fname);
|
|
3880
|
-
|
|
3972
|
+
fs24.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
3881
3973
|
savedFiles.push(dest);
|
|
3882
3974
|
logger.success(`Saved: ${dest}`);
|
|
3883
3975
|
}
|
|
@@ -3892,8 +3984,8 @@ function registerGenerateCommand(parent) {
|
|
|
3892
3984
|
process.exit(1);
|
|
3893
3985
|
}
|
|
3894
3986
|
if (opts.output && savedFiles[0]) {
|
|
3895
|
-
|
|
3896
|
-
|
|
3987
|
+
fs24.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
|
|
3988
|
+
fs24.copyFileSync(savedFiles[0], opts.output);
|
|
3897
3989
|
logger.success(`Copied to: ${opts.output}`);
|
|
3898
3990
|
}
|
|
3899
3991
|
const finalImages = finalizeGeneratedImages(savedFiles, { ...opts, logger });
|
|
@@ -3905,7 +3997,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
3905
3997
|
}
|
|
3906
3998
|
|
|
3907
3999
|
// src/providers/gemini/commands/image-to-image.ts
|
|
3908
|
-
import
|
|
4000
|
+
import fs25 from "fs";
|
|
3909
4001
|
import path27 from "path";
|
|
3910
4002
|
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
3911
4003
|
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
@@ -3968,13 +4060,13 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
3968
4060
|
if (!img) continue;
|
|
3969
4061
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
3970
4062
|
const dest = path27.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
|
|
3971
|
-
|
|
4063
|
+
fs25.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
3972
4064
|
saved.push(dest);
|
|
3973
4065
|
logger.success(`Saved: ${dest}`);
|
|
3974
4066
|
}
|
|
3975
4067
|
if (opts.output && saved[0]) {
|
|
3976
|
-
|
|
3977
|
-
|
|
4068
|
+
fs25.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
|
|
4069
|
+
fs25.copyFileSync(saved[0], opts.output);
|
|
3978
4070
|
logger.success(`Copied to: ${opts.output}`);
|
|
3979
4071
|
}
|
|
3980
4072
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -4010,16 +4102,16 @@ function sniffImageMime(buf, url) {
|
|
|
4010
4102
|
}
|
|
4011
4103
|
|
|
4012
4104
|
// src/providers/gemini/commands/image-to-video.ts
|
|
4013
|
-
import
|
|
4105
|
+
import fs26 from "fs";
|
|
4014
4106
|
import path28 from "path";
|
|
4015
4107
|
init_errors();
|
|
4016
4108
|
init_http_client();
|
|
4017
4109
|
var BASE3 = "https://generativelanguage.googleapis.com";
|
|
4018
4110
|
function imagePart(imgPath) {
|
|
4019
|
-
if (!
|
|
4111
|
+
if (!fs26.existsSync(imgPath)) {
|
|
4020
4112
|
throw new ValidationError(`Image not found: ${imgPath}`);
|
|
4021
4113
|
}
|
|
4022
|
-
const data =
|
|
4114
|
+
const data = fs26.readFileSync(imgPath).toString("base64");
|
|
4023
4115
|
const ext = path28.extname(imgPath).toLowerCase();
|
|
4024
4116
|
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
4025
4117
|
return { inlineData: { mimeType, data } };
|
|
@@ -4097,8 +4189,8 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
4097
4189
|
await downloadFile(videoUri, dest);
|
|
4098
4190
|
logger.success(`Saved video: ${dest}`);
|
|
4099
4191
|
if (opts.output) {
|
|
4100
|
-
|
|
4101
|
-
|
|
4192
|
+
fs26.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
|
|
4193
|
+
fs26.copyFileSync(dest, opts.output);
|
|
4102
4194
|
logger.success(`Copied to: ${opts.output}`);
|
|
4103
4195
|
}
|
|
4104
4196
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -4114,14 +4206,14 @@ Generated video: ${dest}`);
|
|
|
4114
4206
|
|
|
4115
4207
|
// src/providers/gemini/commands/transcribe.ts
|
|
4116
4208
|
init_errors();
|
|
4117
|
-
import
|
|
4209
|
+
import fs27 from "fs";
|
|
4118
4210
|
function registerTranscribeCommand2(parent) {
|
|
4119
4211
|
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(
|
|
4120
4212
|
async (opts) => {
|
|
4121
4213
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4122
4214
|
const model = opts.model ?? getDefaultModel("transcribe");
|
|
4123
4215
|
for (const f of opts.files) {
|
|
4124
|
-
if (!
|
|
4216
|
+
if (!fs27.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
4125
4217
|
}
|
|
4126
4218
|
const results = [];
|
|
4127
4219
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -4179,7 +4271,7 @@ function registerGeminiCommands(program2) {
|
|
|
4179
4271
|
|
|
4180
4272
|
// src/providers/leonardo/commands/generate.ts
|
|
4181
4273
|
init_http_client();
|
|
4182
|
-
import
|
|
4274
|
+
import fs28 from "fs";
|
|
4183
4275
|
import path29 from "path";
|
|
4184
4276
|
|
|
4185
4277
|
// src/providers/leonardo/client.ts
|
|
@@ -4391,8 +4483,8 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
4391
4483
|
}
|
|
4392
4484
|
if (opts.output && saved[0]) {
|
|
4393
4485
|
const dest = path29.resolve(opts.output);
|
|
4394
|
-
|
|
4395
|
-
|
|
4486
|
+
fs28.mkdirSync(path29.dirname(dest), { recursive: true });
|
|
4487
|
+
fs28.copyFileSync(saved[0], dest);
|
|
4396
4488
|
logger.success(`Copied to ${dest}`);
|
|
4397
4489
|
}
|
|
4398
4490
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -4405,7 +4497,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
4405
4497
|
|
|
4406
4498
|
// src/providers/leonardo/commands/image-to-image.ts
|
|
4407
4499
|
init_http_client();
|
|
4408
|
-
import
|
|
4500
|
+
import fs29 from "fs";
|
|
4409
4501
|
import path30 from "path";
|
|
4410
4502
|
function extFromUrl3(url, fallback = ".png") {
|
|
4411
4503
|
try {
|
|
@@ -4482,8 +4574,8 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
4482
4574
|
}
|
|
4483
4575
|
if (opts.output && saved[0]) {
|
|
4484
4576
|
const dest = path30.resolve(opts.output);
|
|
4485
|
-
|
|
4486
|
-
|
|
4577
|
+
fs29.mkdirSync(path30.dirname(dest), { recursive: true });
|
|
4578
|
+
fs29.copyFileSync(saved[0], dest);
|
|
4487
4579
|
logger.success(`Copied to ${dest}`);
|
|
4488
4580
|
}
|
|
4489
4581
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -4951,7 +5043,7 @@ function sleep5(ms) {
|
|
|
4951
5043
|
|
|
4952
5044
|
// src/providers/minimax/generators/music.ts
|
|
4953
5045
|
init_http_client();
|
|
4954
|
-
import
|
|
5046
|
+
import fs30 from "fs";
|
|
4955
5047
|
import path33 from "path";
|
|
4956
5048
|
async function generateMinimaxMusic(opts) {
|
|
4957
5049
|
const {
|
|
@@ -5001,10 +5093,10 @@ async function generateMinimaxMusic(opts) {
|
|
|
5001
5093
|
try {
|
|
5002
5094
|
if (audioData.startsWith("http")) {
|
|
5003
5095
|
const bytes = await fetchBytes(audioData);
|
|
5004
|
-
|
|
5096
|
+
fs30.writeFileSync(dest, bytes);
|
|
5005
5097
|
} else {
|
|
5006
5098
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
5007
|
-
|
|
5099
|
+
fs30.writeFileSync(dest, audioBytes);
|
|
5008
5100
|
}
|
|
5009
5101
|
} catch (e) {
|
|
5010
5102
|
return {
|
|
@@ -5015,8 +5107,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
5015
5107
|
const durSec = durationMs / 1e3;
|
|
5016
5108
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
5017
5109
|
if (output) {
|
|
5018
|
-
|
|
5019
|
-
|
|
5110
|
+
fs30.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
|
|
5111
|
+
fs30.copyFileSync(dest, output);
|
|
5020
5112
|
}
|
|
5021
5113
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
5022
5114
|
}
|
|
@@ -5090,7 +5182,7 @@ Generated music: ${result.generatedAudio}`);
|
|
|
5090
5182
|
init_errors();
|
|
5091
5183
|
|
|
5092
5184
|
// src/providers/minimax/generators/speech.ts
|
|
5093
|
-
import
|
|
5185
|
+
import fs31 from "fs";
|
|
5094
5186
|
import path34 from "path";
|
|
5095
5187
|
async function generateMinimaxSpeech(opts) {
|
|
5096
5188
|
const {
|
|
@@ -5139,11 +5231,11 @@ async function generateMinimaxSpeech(opts) {
|
|
|
5139
5231
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
5140
5232
|
const outDir = getOutputDir();
|
|
5141
5233
|
const dest = path34.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
5142
|
-
|
|
5234
|
+
fs31.writeFileSync(dest, audioBytes);
|
|
5143
5235
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
5144
5236
|
if (output) {
|
|
5145
|
-
|
|
5146
|
-
|
|
5237
|
+
fs31.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
|
|
5238
|
+
fs31.copyFileSync(dest, output);
|
|
5147
5239
|
}
|
|
5148
5240
|
return { status: "success", generatedAudio: dest, model };
|
|
5149
5241
|
}
|
|
@@ -5190,7 +5282,7 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
5190
5282
|
init_errors();
|
|
5191
5283
|
|
|
5192
5284
|
// src/providers/minimax/generators/video.ts
|
|
5193
|
-
import
|
|
5285
|
+
import fs32 from "fs";
|
|
5194
5286
|
import path35 from "path";
|
|
5195
5287
|
async function generateMinimaxVideo(opts) {
|
|
5196
5288
|
const {
|
|
@@ -5244,11 +5336,11 @@ async function generateMinimaxVideo(opts) {
|
|
|
5244
5336
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
5245
5337
|
}
|
|
5246
5338
|
const elapsed = (Date.now() - start) / 1e3;
|
|
5247
|
-
const stat =
|
|
5339
|
+
const stat = fs32.statSync(dest);
|
|
5248
5340
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
5249
5341
|
if (output) {
|
|
5250
|
-
|
|
5251
|
-
|
|
5342
|
+
fs32.mkdirSync(path35.dirname(path35.resolve(output)), { recursive: true });
|
|
5343
|
+
fs32.copyFileSync(dest, output);
|
|
5252
5344
|
}
|
|
5253
5345
|
await maybeDownloadThumb(pollResult, dest, {
|
|
5254
5346
|
skip: thumb === false,
|
|
@@ -5311,7 +5403,7 @@ Generated video: ${result.generatedVideo}`);
|
|
|
5311
5403
|
// src/providers/minimax/generators/image.ts
|
|
5312
5404
|
init_errors();
|
|
5313
5405
|
init_http_client();
|
|
5314
|
-
import
|
|
5406
|
+
import fs33 from "fs";
|
|
5315
5407
|
import path36 from "path";
|
|
5316
5408
|
async function generateMinimaxImage(opts) {
|
|
5317
5409
|
const {
|
|
@@ -5351,7 +5443,7 @@ async function generateMinimaxImage(opts) {
|
|
|
5351
5443
|
const bytes = await fetchBytes(url);
|
|
5352
5444
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
5353
5445
|
const dest = path36.join(outDir, fname);
|
|
5354
|
-
|
|
5446
|
+
fs33.writeFileSync(dest, bytes);
|
|
5355
5447
|
savedFiles.push(dest);
|
|
5356
5448
|
logger?.success(`Saved: ${dest}`);
|
|
5357
5449
|
} catch (e) {
|
|
@@ -5362,8 +5454,8 @@ async function generateMinimaxImage(opts) {
|
|
|
5362
5454
|
}
|
|
5363
5455
|
}
|
|
5364
5456
|
if (output && savedFiles[0]) {
|
|
5365
|
-
|
|
5366
|
-
|
|
5457
|
+
fs33.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
|
|
5458
|
+
fs33.copyFileSync(savedFiles[0], output);
|
|
5367
5459
|
}
|
|
5368
5460
|
return { status: "success", generatedImages: savedFiles, model };
|
|
5369
5461
|
}
|
|
@@ -5403,7 +5495,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
5403
5495
|
// src/providers/minimax/commands/image-to-image.ts
|
|
5404
5496
|
init_errors();
|
|
5405
5497
|
init_http_client();
|
|
5406
|
-
import
|
|
5498
|
+
import fs34 from "fs";
|
|
5407
5499
|
import path37 from "path";
|
|
5408
5500
|
function registerMinimaxImageToImageCommand(parent) {
|
|
5409
5501
|
parent.command("image-to-image").alias("i2i").description(
|
|
@@ -5460,7 +5552,7 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
5460
5552
|
try {
|
|
5461
5553
|
const bytes = await fetchBytes(url);
|
|
5462
5554
|
const dest = path37.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
5463
|
-
|
|
5555
|
+
fs34.writeFileSync(dest, bytes);
|
|
5464
5556
|
saved.push(dest);
|
|
5465
5557
|
logger.success(`Saved: ${dest}`);
|
|
5466
5558
|
} catch (e) {
|
|
@@ -5471,8 +5563,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
5471
5563
|
}
|
|
5472
5564
|
}
|
|
5473
5565
|
if (opts.output && saved[0]) {
|
|
5474
|
-
|
|
5475
|
-
|
|
5566
|
+
fs34.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
|
|
5567
|
+
fs34.copyFileSync(saved[0], opts.output);
|
|
5476
5568
|
logger.success(`Copied to: ${opts.output}`);
|
|
5477
5569
|
}
|
|
5478
5570
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -5607,11 +5699,11 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
|
|
|
5607
5699
|
|
|
5608
5700
|
// src/providers/openai/openai-audio.ts
|
|
5609
5701
|
init_http_client();
|
|
5610
|
-
import
|
|
5702
|
+
import fs36 from "fs";
|
|
5611
5703
|
import path39 from "path";
|
|
5612
5704
|
|
|
5613
5705
|
// src/providers/openai/client.ts
|
|
5614
|
-
import
|
|
5706
|
+
import fs35 from "fs";
|
|
5615
5707
|
import path38 from "path";
|
|
5616
5708
|
init_errors();
|
|
5617
5709
|
init_http_client();
|
|
@@ -5680,8 +5772,8 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
5680
5772
|
}
|
|
5681
5773
|
function fileFromPath(filePath) {
|
|
5682
5774
|
const abs = path38.resolve(filePath);
|
|
5683
|
-
if (!
|
|
5684
|
-
const bytes =
|
|
5775
|
+
if (!fs35.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
5776
|
+
const bytes = fs35.readFileSync(abs);
|
|
5685
5777
|
return new File([new Uint8Array(bytes)], path38.basename(abs));
|
|
5686
5778
|
}
|
|
5687
5779
|
|
|
@@ -5741,11 +5833,11 @@ async function generateOpenAISpeech(opts) {
|
|
|
5741
5833
|
const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
|
|
5742
5834
|
const ext = extFromAudioFormat(opts.outputFormat);
|
|
5743
5835
|
const dest = path39.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
|
|
5744
|
-
|
|
5836
|
+
fs36.writeFileSync(dest, bytes);
|
|
5745
5837
|
opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
5746
5838
|
if (opts.output) {
|
|
5747
|
-
|
|
5748
|
-
|
|
5839
|
+
fs36.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
|
|
5840
|
+
fs36.copyFileSync(dest, opts.output);
|
|
5749
5841
|
}
|
|
5750
5842
|
return dest;
|
|
5751
5843
|
}
|
|
@@ -5768,8 +5860,8 @@ async function transcribeOpenAIAudio(opts) {
|
|
|
5768
5860
|
const text = typeof data.text === "string" ? data.text : void 0;
|
|
5769
5861
|
const ext = opts.format === "text" ? "txt" : "json";
|
|
5770
5862
|
const dest = opts.output ? path39.resolve(opts.output) : path39.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
|
|
5771
|
-
|
|
5772
|
-
|
|
5863
|
+
fs36.mkdirSync(path39.dirname(dest), { recursive: true });
|
|
5864
|
+
fs36.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
|
|
5773
5865
|
opts.logger?.success(`Saved transcript: ${dest}`);
|
|
5774
5866
|
return { path: dest, text };
|
|
5775
5867
|
}
|
|
@@ -5796,7 +5888,7 @@ Generated audio: ${dest}`);
|
|
|
5796
5888
|
}
|
|
5797
5889
|
|
|
5798
5890
|
// src/providers/openai/codex-image-driver.ts
|
|
5799
|
-
import
|
|
5891
|
+
import fs37 from "fs";
|
|
5800
5892
|
import os2 from "os";
|
|
5801
5893
|
import path40 from "path";
|
|
5802
5894
|
import { execa } from "execa";
|
|
@@ -5848,11 +5940,11 @@ async function runCodexImageGeneration(opts) {
|
|
|
5848
5940
|
const runner = opts.runner ?? defaultRunner;
|
|
5849
5941
|
const ext = extFromImageFormat(opts.outputFormat);
|
|
5850
5942
|
const dest = opts.output ? path40.resolve(opts.output) : path40.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
|
|
5851
|
-
|
|
5852
|
-
const tmp =
|
|
5943
|
+
fs37.mkdirSync(path40.dirname(dest), { recursive: true });
|
|
5944
|
+
const tmp = fs37.mkdtempSync(path40.join(os2.tmpdir(), "multix-codex-image-"));
|
|
5853
5945
|
const schema = path40.join(tmp, "schema.json");
|
|
5854
5946
|
const result = path40.join(tmp, "result.json");
|
|
5855
|
-
|
|
5947
|
+
fs37.writeFileSync(
|
|
5856
5948
|
schema,
|
|
5857
5949
|
JSON.stringify({
|
|
5858
5950
|
type: "object",
|
|
@@ -5882,7 +5974,7 @@ async function runCodexImageGeneration(opts) {
|
|
|
5882
5974
|
if (res.exitCode !== 0) {
|
|
5883
5975
|
throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
|
|
5884
5976
|
}
|
|
5885
|
-
if (!
|
|
5977
|
+
if (!fs37.existsSync(dest) || fs37.statSync(dest).size === 0) {
|
|
5886
5978
|
throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
|
|
5887
5979
|
}
|
|
5888
5980
|
return { path: dest };
|
|
@@ -5900,7 +5992,7 @@ function buildCodexPrompt(userPrompt, outputPath) {
|
|
|
5900
5992
|
// src/providers/openai/openai-image.ts
|
|
5901
5993
|
init_errors();
|
|
5902
5994
|
init_http_client();
|
|
5903
|
-
import
|
|
5995
|
+
import fs38 from "fs";
|
|
5904
5996
|
import path41 from "path";
|
|
5905
5997
|
function buildImageGenerationBody(opts) {
|
|
5906
5998
|
return {
|
|
@@ -5935,13 +6027,13 @@ function saveOpenAIImages(opts) {
|
|
|
5935
6027
|
const b64 = items[i]?.b64_json;
|
|
5936
6028
|
if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
|
|
5937
6029
|
const dest = path41.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
|
|
5938
|
-
|
|
6030
|
+
fs38.writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
5939
6031
|
saved.push(dest);
|
|
5940
6032
|
opts.logger?.success(`Saved: ${dest}`);
|
|
5941
6033
|
}
|
|
5942
6034
|
if (opts.output && saved[0]) {
|
|
5943
|
-
|
|
5944
|
-
|
|
6035
|
+
fs38.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
|
|
6036
|
+
fs38.copyFileSync(saved[0], opts.output);
|
|
5945
6037
|
opts.logger?.success(`Copied to: ${opts.output}`);
|
|
5946
6038
|
}
|
|
5947
6039
|
return saved;
|
|
@@ -6139,7 +6231,7 @@ function registerOpenAICommands(program2) {
|
|
|
6139
6231
|
}
|
|
6140
6232
|
|
|
6141
6233
|
// src/providers/openrouter/client.ts
|
|
6142
|
-
import
|
|
6234
|
+
import fs39 from "fs";
|
|
6143
6235
|
import path42 from "path";
|
|
6144
6236
|
init_errors();
|
|
6145
6237
|
init_http_client();
|
|
@@ -6306,7 +6398,7 @@ async function generateOpenRouterImage(opts) {
|
|
|
6306
6398
|
try {
|
|
6307
6399
|
const bytes = await fetchBytes(url);
|
|
6308
6400
|
const dest = path42.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
6309
|
-
|
|
6401
|
+
fs39.writeFileSync(dest, bytes);
|
|
6310
6402
|
savedFiles.push(dest);
|
|
6311
6403
|
logger?.success(`Saved: ${dest}`);
|
|
6312
6404
|
} catch (e) {
|
|
@@ -6317,8 +6409,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
6317
6409
|
}
|
|
6318
6410
|
}
|
|
6319
6411
|
if (output && savedFiles[0]) {
|
|
6320
|
-
|
|
6321
|
-
|
|
6412
|
+
fs39.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
|
|
6413
|
+
fs39.writeFileSync(output, fs39.readFileSync(savedFiles[0]));
|
|
6322
6414
|
}
|
|
6323
6415
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
6324
6416
|
}
|
|
@@ -6359,7 +6451,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
6359
6451
|
}
|
|
6360
6452
|
|
|
6361
6453
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
6362
|
-
import
|
|
6454
|
+
import fs40 from "fs";
|
|
6363
6455
|
import path43 from "path";
|
|
6364
6456
|
init_errors();
|
|
6365
6457
|
init_http_client();
|
|
@@ -6454,13 +6546,13 @@ async function runImageToImage(opts) {
|
|
|
6454
6546
|
if (!url) continue;
|
|
6455
6547
|
const bytes = await fetchBytes(url);
|
|
6456
6548
|
const dest = path43.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
6457
|
-
|
|
6549
|
+
fs40.writeFileSync(dest, bytes);
|
|
6458
6550
|
saved.push(dest);
|
|
6459
6551
|
opts.logger.success(`Saved: ${dest}`);
|
|
6460
6552
|
}
|
|
6461
6553
|
if (opts.output && saved[0]) {
|
|
6462
|
-
|
|
6463
|
-
|
|
6554
|
+
fs40.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
|
|
6555
|
+
fs40.copyFileSync(saved[0], opts.output);
|
|
6464
6556
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
6465
6557
|
}
|
|
6466
6558
|
return saved;
|
|
@@ -6506,7 +6598,7 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
|
6506
6598
|
}
|
|
6507
6599
|
|
|
6508
6600
|
// src/providers/openrouter/commands/video-status.ts
|
|
6509
|
-
import
|
|
6601
|
+
import fs41 from "fs";
|
|
6510
6602
|
import path44 from "path";
|
|
6511
6603
|
async function downloadOpenRouterVideo(opts) {
|
|
6512
6604
|
const { jobId, logger, source } = opts;
|
|
@@ -6522,14 +6614,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
6522
6614
|
logger.error("Empty response body");
|
|
6523
6615
|
process.exit(1);
|
|
6524
6616
|
}
|
|
6525
|
-
|
|
6526
|
-
const ws =
|
|
6617
|
+
fs41.mkdirSync(path44.dirname(dest), { recursive: true });
|
|
6618
|
+
const ws = fs41.createWriteStream(dest);
|
|
6527
6619
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
6528
6620
|
await pipeline2(r.body, ws);
|
|
6529
6621
|
logger.success(`Saved ${dest}`);
|
|
6530
6622
|
if (opts.output) {
|
|
6531
|
-
|
|
6532
|
-
|
|
6623
|
+
fs41.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
|
|
6624
|
+
fs41.copyFileSync(dest, opts.output);
|
|
6533
6625
|
logger.success(`Copied to ${opts.output}`);
|
|
6534
6626
|
}
|
|
6535
6627
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -6696,9 +6788,9 @@ function registerOpenRouterCommands(program2) {
|
|
|
6696
6788
|
|
|
6697
6789
|
// src/commands/check-helpers/binary-check.ts
|
|
6698
6790
|
import { execa as execa2 } from "execa";
|
|
6699
|
-
async function checkBinary(binary) {
|
|
6791
|
+
async function checkBinary(binary, versionFlag = "--version") {
|
|
6700
6792
|
try {
|
|
6701
|
-
const result = await execa2(binary, [
|
|
6793
|
+
const result = await execa2(binary, [versionFlag], { reject: true, all: true });
|
|
6702
6794
|
const version = (result.stdout || result.stderr || "").split("\n")[0]?.trim();
|
|
6703
6795
|
return { available: true, version, binary };
|
|
6704
6796
|
} catch (e) {
|
|
@@ -6720,7 +6812,8 @@ var MODELS_URL = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
|
6720
6812
|
async function pingGemini(apiKey) {
|
|
6721
6813
|
try {
|
|
6722
6814
|
const resp = await httpJson({
|
|
6723
|
-
url:
|
|
6815
|
+
url: MODELS_URL,
|
|
6816
|
+
headers: { "x-goog-api-key": apiKey },
|
|
6724
6817
|
timeoutMs: 1e4
|
|
6725
6818
|
});
|
|
6726
6819
|
const modelCount = Array.isArray(resp.models) ? resp.models.length : 0;
|
|
@@ -6846,7 +6939,7 @@ function registerCheckCommand(program2) {
|
|
|
6846
6939
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
6847
6940
|
let exitCode = 0;
|
|
6848
6941
|
logger.header("Tooling");
|
|
6849
|
-
const ffmpegStatus = await checkBinary("ffmpeg");
|
|
6942
|
+
const ffmpegStatus = await checkBinary("ffmpeg", "-version");
|
|
6850
6943
|
const magickStatus = await checkBinary("magick");
|
|
6851
6944
|
if (ffmpegStatus.available) {
|
|
6852
6945
|
logger.success(`ffmpeg found${ffmpegStatus.version ? ` \u2014 ${ffmpegStatus.version}` : ""}`);
|
|
@@ -6947,7 +7040,7 @@ function registerCheckCommand(program2) {
|
|
|
6947
7040
|
|
|
6948
7041
|
// src/commands/doc/convert.ts
|
|
6949
7042
|
init_errors();
|
|
6950
|
-
import
|
|
7043
|
+
import fs42 from "fs";
|
|
6951
7044
|
import path45 from "path";
|
|
6952
7045
|
|
|
6953
7046
|
// src/commands/doc/default-prompt.ts
|
|
@@ -6969,7 +7062,7 @@ async function convertDocuments(opts) {
|
|
|
6969
7062
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
6970
7063
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
6971
7064
|
for (const f of inputs) {
|
|
6972
|
-
if (!
|
|
7065
|
+
if (!fs42.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
6973
7066
|
}
|
|
6974
7067
|
let outputPath;
|
|
6975
7068
|
if (output) {
|
|
@@ -6980,7 +7073,7 @@ async function convertDocuments(opts) {
|
|
|
6980
7073
|
} else {
|
|
6981
7074
|
outputPath = path45.join(getOutputDir(), "document-extraction.md");
|
|
6982
7075
|
}
|
|
6983
|
-
|
|
7076
|
+
fs42.mkdirSync(path45.dirname(outputPath), { recursive: true });
|
|
6984
7077
|
const results = [];
|
|
6985
7078
|
for (let i = 0; i < inputs.length; i++) {
|
|
6986
7079
|
const filePath = inputs[i];
|
|
@@ -7044,7 +7137,7 @@ async function convertDocuments(opts) {
|
|
|
7044
7137
|
}
|
|
7045
7138
|
lines.push("\n---\n");
|
|
7046
7139
|
}
|
|
7047
|
-
|
|
7140
|
+
fs42.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
7048
7141
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
7049
7142
|
console.log(`
|
|
7050
7143
|
${"=".repeat(50)}`);
|
|
@@ -7092,11 +7185,11 @@ function registerDocCommands(program2) {
|
|
|
7092
7185
|
|
|
7093
7186
|
// src/commands/media/index.ts
|
|
7094
7187
|
init_errors();
|
|
7095
|
-
import
|
|
7188
|
+
import fs45 from "fs";
|
|
7096
7189
|
import path48 from "path";
|
|
7097
7190
|
|
|
7098
7191
|
// src/commands/media/batch.ts
|
|
7099
|
-
import
|
|
7192
|
+
import fs43 from "fs";
|
|
7100
7193
|
import path46 from "path";
|
|
7101
7194
|
|
|
7102
7195
|
// src/commands/media/detect-type.ts
|
|
@@ -7293,8 +7386,8 @@ async function batchOptimize(opts) {
|
|
|
7293
7386
|
verbose,
|
|
7294
7387
|
logger
|
|
7295
7388
|
} = opts;
|
|
7296
|
-
|
|
7297
|
-
const entries =
|
|
7389
|
+
fs43.mkdirSync(outputDir, { recursive: true });
|
|
7390
|
+
const entries = fs43.readdirSync(inputDir);
|
|
7298
7391
|
const mediaFiles = entries.filter((f) => {
|
|
7299
7392
|
const ext = path46.extname(f).toLowerCase();
|
|
7300
7393
|
return ALL_EXTS.has(ext);
|
|
@@ -7346,7 +7439,7 @@ async function batchOptimize(opts) {
|
|
|
7346
7439
|
|
|
7347
7440
|
// src/commands/media/split-video.ts
|
|
7348
7441
|
init_errors();
|
|
7349
|
-
import
|
|
7442
|
+
import fs44 from "fs";
|
|
7350
7443
|
import path47 from "path";
|
|
7351
7444
|
async function splitVideo(opts) {
|
|
7352
7445
|
await assertFfmpeg();
|
|
@@ -7358,7 +7451,7 @@ async function splitVideo(opts) {
|
|
|
7358
7451
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
7359
7452
|
return [input];
|
|
7360
7453
|
}
|
|
7361
|
-
|
|
7454
|
+
fs44.mkdirSync(outputDir, { recursive: true });
|
|
7362
7455
|
const ext = path47.extname(input) || ".mp4";
|
|
7363
7456
|
const baseName = path47.basename(input, ext);
|
|
7364
7457
|
const outputPattern = path47.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
@@ -7381,7 +7474,7 @@ async function splitVideo(opts) {
|
|
|
7381
7474
|
const files = [];
|
|
7382
7475
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
7383
7476
|
const fname = path47.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
7384
|
-
if (
|
|
7477
|
+
if (fs44.existsSync(fname)) files.push(fname);
|
|
7385
7478
|
}
|
|
7386
7479
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
7387
7480
|
return files;
|
|
@@ -7395,8 +7488,8 @@ function registerMediaCommands(program2) {
|
|
|
7395
7488
|
media.command("optimize").description("Optimize a single media file (video, audio, or image)").requiredOption("--input <file>", "Input media file").requiredOption("--output <file>", "Output file path").option("--target-size <MB>", "Target file size in MB").option("--quality <n>", "Quality: video CRF (0-51) or image (1-100)", "85").option("--max-width <px>", "Max image/video width in pixels", "1920").option("--bitrate <rate>", "Audio bitrate (e.g. 64k)", "64k").option("--resolution <WxH>", "Force video resolution (e.g. 1920x1080)").option("-v, --verbose", "Verbose logging").action(
|
|
7396
7489
|
async (opts) => {
|
|
7397
7490
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7398
|
-
if (!
|
|
7399
|
-
|
|
7491
|
+
if (!fs45.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
7492
|
+
fs45.mkdirSync(path48.dirname(path48.resolve(opts.output)), { recursive: true });
|
|
7400
7493
|
const ext = path48.extname(opts.input).toLowerCase();
|
|
7401
7494
|
const kind = extToKind(ext);
|
|
7402
7495
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
@@ -7437,7 +7530,7 @@ function registerMediaCommands(program2) {
|
|
|
7437
7530
|
media.command("split").description("Split a long video into fixed-duration chunks").requiredOption("--input <file>", "Input video file").option("--output-dir <dir>", "Output directory for chunks", "./chunks").option("--chunk-duration <sec>", "Chunk duration in seconds", "3600").option("-v, --verbose", "Verbose logging").action(
|
|
7438
7531
|
async (opts) => {
|
|
7439
7532
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7440
|
-
if (!
|
|
7533
|
+
if (!fs45.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
7441
7534
|
const chunks = await splitVideo({
|
|
7442
7535
|
input: opts.input,
|
|
7443
7536
|
outputDir: opts.outputDir,
|
|
@@ -7453,7 +7546,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
7453
7546
|
media.command("batch").description("Batch optimize all media files in a directory").requiredOption("--input-dir <dir>", "Input directory").requiredOption("--output-dir <dir>", "Output directory").option("--quality <n>", "Quality setting", "85").option("--max-width <px>", "Max image/video width", "1920").option("--bitrate <rate>", "Audio bitrate", "64k").option("-v, --verbose", "Verbose logging").action(
|
|
7454
7547
|
async (opts) => {
|
|
7455
7548
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7456
|
-
if (!
|
|
7549
|
+
if (!fs45.existsSync(opts.inputDir))
|
|
7457
7550
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
7458
7551
|
const result = await batchOptimize({
|
|
7459
7552
|
inputDir: opts.inputDir,
|
|
@@ -7640,7 +7733,12 @@ function registerCommands(program2) {
|
|
|
7640
7733
|
|
|
7641
7734
|
// src/core/index.ts
|
|
7642
7735
|
init_errors();
|
|
7736
|
+
init_redact();
|
|
7643
7737
|
init_http_client();
|
|
7738
|
+
init_fetch_json();
|
|
7739
|
+
|
|
7740
|
+
// src/core/async-poll.ts
|
|
7741
|
+
init_errors();
|
|
7644
7742
|
|
|
7645
7743
|
// src/cli.ts
|
|
7646
7744
|
loadEnv();
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: multix
|
|
3
3
|
description: AI multimodal CLI — generate and edit images (OpenAI, Codex driver, Gemini Nano Banana, Imagen, MiniMax, OpenRouter, Leonardo, BytePlus Seedream, Cloudflare Workers AI FLUX.1 Schnell, fal.ai FLUX), generate video (Gemini Veo, MiniMax Hailuo, OpenRouter via Veo/Seedance/Kling, Leonardo via Motion/Veo/Kling/Hailuo/Seedance/LTX, BytePlus Seedance, Cloudflare AI Gateway via Replicate, fal.ai queue models), TTS/STT/music (OpenAI, Gemini Flash TTS incl. Gemini 3.8 with delivery-style control, MiniMax, Cloudflare Workers AI MeloTTS, ElevenLabs), 3D models (BytePlus Hyper3D/Hitem3d), analyze/transcribe media, convert documents to Markdown, optimize media via ffmpeg/ImageMagick, and self-update the CLI. Use whenever the user wants to create, edit, or transform images/video/audio/3D/documents from a CLI, mentions any of: OpenAI, Codex image driver, image-to-image, i2i, image edit, watercolor/cyberpunk style transfer, reference image, OpenRouter, Nano Banana, Flux, Seedream, Veo, Hailuo, Seedance, Kling, Cloudflare Workers AI, AI Gateway, fal.ai, video generation, text-to-video, image-to-video, i2v, ElevenLabs voice cloning, updating/upgrading multix, or asks for batch media optimization.
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.2 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|