@mrgoonie/multix 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +1 -1
- package/dist/cli.js +98 -25
- package/package.json +1 -1
- package/skill/SKILL.md +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ 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.9.0](https://github.com/mrgoonie/multix-cli/compare/v0.8.0...v0.9.0) (2026-10-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* **core:** opt-in loading of legacy .claude env files ([#70](https://github.com/mrgoonie/multix-cli/issues/70)) ([48d9420](https://github.com/mrgoonie/multix-cli/commit/48d9420012a36ada0b5a35e2e51d90c1daa5042c))
|
|
14
|
+
|
|
8
15
|
## [0.8.0](https://github.com/mrgoonie/multix-cli/compare/v0.7.3...v0.8.0) (2026-10-11)
|
|
9
16
|
|
|
10
17
|
|
package/README.md
CHANGED
|
@@ -100,7 +100,7 @@ Set at least one provider key. Add to `.env` in your project root or `~/.multix/
|
|
|
100
100
|
|
|
101
101
|
Set extra keys as `GEMINI_API_KEY_2`, `GEMINI_API_KEY_3`, … (any numeric suffix; numbered keys alone are enough). `multix` tries `GEMINI_API_KEY` first, then the numbered keys in ascending order, skipping duplicates. When a key returns HTTP 429 (or 400/403 with `RESOURCE_EXHAUSTED`), it cools down for the server's `Retry-After`/`retryDelay` (default 60s) and the same request is retried with the next ready key; other errors are not rotated. A Files API upload is repeated on the new key if the following generate call is rate-limited. Veo rotates on submit only, so a rate limit while polling or downloading never starts a second billed job. `doc convert` stops its own retries on a rate limit. Cooldowns last for the current process only, and logs name the env var, never the key. `multix check` lists the keys in rotation and pings each one. Quotas are per Google Cloud project: keys from the same project share one quota, so rotation only helps across projects.
|
|
102
102
|
|
|
103
|
-
Priority: `process.env` > `cwd/.env` > `~/.multix/.env`.
|
|
103
|
+
Priority: `process.env` > `cwd/.env` > `~/.multix/.env`. Set `MULTIX_LEGACY_CLAUDE_ENV=1` to also read legacy AgentKit files (`.claude/skills/{ai-multimodal,ak-ai-multimodal}/.env`, `.claude/skills/.env`, `.claude/.env`, project first, then home) with the lowest priority (credential names only, never endpoint/proxy settings); `multix check -v` lists the env files that were loaded.
|
|
104
104
|
|
|
105
105
|
## Commands
|
|
106
106
|
|
package/dist/cli.js
CHANGED
|
@@ -280,22 +280,78 @@ init_redact();
|
|
|
280
280
|
import fs3 from "fs";
|
|
281
281
|
import os from "os";
|
|
282
282
|
import path3 from "path";
|
|
283
|
-
import { config as dotenvConfig } from "dotenv";
|
|
283
|
+
import { config as dotenvConfig, parse as dotenvParse } from "dotenv";
|
|
284
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
|
+
}
|
|
285
340
|
function loadEnv() {
|
|
286
341
|
if (loaded) return;
|
|
287
342
|
loaded = true;
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
dotenvConfig({ path: homeEnv, override: false });
|
|
296
|
-
}
|
|
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);
|
|
297
350
|
}
|
|
298
351
|
}
|
|
352
|
+
function getLoadedEnvFiles() {
|
|
353
|
+
return [...loadedFiles];
|
|
354
|
+
}
|
|
299
355
|
function resolveKey(name) {
|
|
300
356
|
return process.env[name] || void 0;
|
|
301
357
|
}
|
|
@@ -6908,6 +6964,10 @@ function registerOpenRouterCommands(program2) {
|
|
|
6908
6964
|
registerOpenRouterVideoModelsCommand(openrouter);
|
|
6909
6965
|
}
|
|
6910
6966
|
|
|
6967
|
+
// src/commands/check.ts
|
|
6968
|
+
import fs43 from "fs";
|
|
6969
|
+
import os3 from "os";
|
|
6970
|
+
|
|
6911
6971
|
// src/commands/check-helpers/binary-check.ts
|
|
6912
6972
|
import { execa as execa2 } from "execa";
|
|
6913
6973
|
async function checkBinary(binary, versionFlag = "--version") {
|
|
@@ -7079,6 +7139,19 @@ function registerCheckCommand(program2) {
|
|
|
7079
7139
|
if (opts.verbose) console.log(` ${MAGICK_HINT}`);
|
|
7080
7140
|
}
|
|
7081
7141
|
logger.header("API Keys");
|
|
7142
|
+
if (opts.verbose) {
|
|
7143
|
+
const files = getLoadedEnvFiles();
|
|
7144
|
+
logger.info(`Env files loaded: ${files.length > 0 ? files.join(", ") : "none"}`);
|
|
7145
|
+
}
|
|
7146
|
+
if (!legacyClaudeEnvEnabled()) {
|
|
7147
|
+
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os3.homedir();
|
|
7148
|
+
const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) => fs43.existsSync(f));
|
|
7149
|
+
if (legacy.length > 0) {
|
|
7150
|
+
logger.info(
|
|
7151
|
+
`Found ${legacy.length} legacy .claude env file(s); set MULTIX_LEGACY_CLAUDE_ENV=1 to load them`
|
|
7152
|
+
);
|
|
7153
|
+
}
|
|
7154
|
+
}
|
|
7082
7155
|
let anyKey = false;
|
|
7083
7156
|
const resolved = /* @__PURE__ */ new Map();
|
|
7084
7157
|
for (const p of PROVIDERS) {
|
|
@@ -7177,7 +7250,7 @@ function registerCheckCommand(program2) {
|
|
|
7177
7250
|
|
|
7178
7251
|
// src/commands/doc/convert.ts
|
|
7179
7252
|
init_errors();
|
|
7180
|
-
import
|
|
7253
|
+
import fs44 from "fs";
|
|
7181
7254
|
import path45 from "path";
|
|
7182
7255
|
|
|
7183
7256
|
// src/commands/doc/default-prompt.ts
|
|
@@ -7199,7 +7272,7 @@ async function convertDocuments(opts) {
|
|
|
7199
7272
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
7200
7273
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
7201
7274
|
for (const f of inputs) {
|
|
7202
|
-
if (!
|
|
7275
|
+
if (!fs44.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
7203
7276
|
}
|
|
7204
7277
|
let outputPath;
|
|
7205
7278
|
if (output) {
|
|
@@ -7210,7 +7283,7 @@ async function convertDocuments(opts) {
|
|
|
7210
7283
|
} else {
|
|
7211
7284
|
outputPath = path45.join(getOutputDir(), "document-extraction.md");
|
|
7212
7285
|
}
|
|
7213
|
-
|
|
7286
|
+
fs44.mkdirSync(path45.dirname(outputPath), { recursive: true });
|
|
7214
7287
|
const results = [];
|
|
7215
7288
|
for (let i = 0; i < inputs.length; i++) {
|
|
7216
7289
|
const filePath = inputs[i];
|
|
@@ -7283,7 +7356,7 @@ async function convertDocuments(opts) {
|
|
|
7283
7356
|
}
|
|
7284
7357
|
lines.push("\n---\n");
|
|
7285
7358
|
}
|
|
7286
|
-
|
|
7359
|
+
fs44.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
7287
7360
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
7288
7361
|
console.log(`
|
|
7289
7362
|
${"=".repeat(50)}`);
|
|
@@ -7331,11 +7404,11 @@ function registerDocCommands(program2) {
|
|
|
7331
7404
|
|
|
7332
7405
|
// src/commands/media/index.ts
|
|
7333
7406
|
init_errors();
|
|
7334
|
-
import
|
|
7407
|
+
import fs47 from "fs";
|
|
7335
7408
|
import path48 from "path";
|
|
7336
7409
|
|
|
7337
7410
|
// src/commands/media/batch.ts
|
|
7338
|
-
import
|
|
7411
|
+
import fs45 from "fs";
|
|
7339
7412
|
import path46 from "path";
|
|
7340
7413
|
|
|
7341
7414
|
// src/commands/media/detect-type.ts
|
|
@@ -7532,8 +7605,8 @@ async function batchOptimize(opts) {
|
|
|
7532
7605
|
verbose,
|
|
7533
7606
|
logger
|
|
7534
7607
|
} = opts;
|
|
7535
|
-
|
|
7536
|
-
const entries =
|
|
7608
|
+
fs45.mkdirSync(outputDir, { recursive: true });
|
|
7609
|
+
const entries = fs45.readdirSync(inputDir);
|
|
7537
7610
|
const mediaFiles = entries.filter((f) => {
|
|
7538
7611
|
const ext = path46.extname(f).toLowerCase();
|
|
7539
7612
|
return ALL_EXTS.has(ext);
|
|
@@ -7585,7 +7658,7 @@ async function batchOptimize(opts) {
|
|
|
7585
7658
|
|
|
7586
7659
|
// src/commands/media/split-video.ts
|
|
7587
7660
|
init_errors();
|
|
7588
|
-
import
|
|
7661
|
+
import fs46 from "fs";
|
|
7589
7662
|
import path47 from "path";
|
|
7590
7663
|
async function splitVideo(opts) {
|
|
7591
7664
|
await assertFfmpeg();
|
|
@@ -7597,7 +7670,7 @@ async function splitVideo(opts) {
|
|
|
7597
7670
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
7598
7671
|
return [input];
|
|
7599
7672
|
}
|
|
7600
|
-
|
|
7673
|
+
fs46.mkdirSync(outputDir, { recursive: true });
|
|
7601
7674
|
const ext = path47.extname(input) || ".mp4";
|
|
7602
7675
|
const baseName = path47.basename(input, ext);
|
|
7603
7676
|
const outputPattern = path47.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
@@ -7620,7 +7693,7 @@ async function splitVideo(opts) {
|
|
|
7620
7693
|
const files = [];
|
|
7621
7694
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
7622
7695
|
const fname = path47.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
7623
|
-
if (
|
|
7696
|
+
if (fs46.existsSync(fname)) files.push(fname);
|
|
7624
7697
|
}
|
|
7625
7698
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
7626
7699
|
return files;
|
|
@@ -7634,8 +7707,8 @@ function registerMediaCommands(program2) {
|
|
|
7634
7707
|
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(
|
|
7635
7708
|
async (opts) => {
|
|
7636
7709
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7637
|
-
if (!
|
|
7638
|
-
|
|
7710
|
+
if (!fs47.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
7711
|
+
fs47.mkdirSync(path48.dirname(path48.resolve(opts.output)), { recursive: true });
|
|
7639
7712
|
const ext = path48.extname(opts.input).toLowerCase();
|
|
7640
7713
|
const kind = extToKind(ext);
|
|
7641
7714
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
@@ -7676,7 +7749,7 @@ function registerMediaCommands(program2) {
|
|
|
7676
7749
|
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(
|
|
7677
7750
|
async (opts) => {
|
|
7678
7751
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7679
|
-
if (!
|
|
7752
|
+
if (!fs47.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
7680
7753
|
const chunks = await splitVideo({
|
|
7681
7754
|
input: opts.input,
|
|
7682
7755
|
outputDir: opts.outputDir,
|
|
@@ -7692,7 +7765,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
7692
7765
|
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(
|
|
7693
7766
|
async (opts) => {
|
|
7694
7767
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7695
|
-
if (!
|
|
7768
|
+
if (!fs47.existsSync(opts.inputDir))
|
|
7696
7769
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
7697
7770
|
const result = await batchOptimize({
|
|
7698
7771
|
inputDir: opts.inputDir,
|
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.
|
|
4
|
+
version: 0.9.0 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|
|
@@ -48,7 +48,7 @@ CLOUDFLARE_API_TOKEN # Cloudflare Workers AI / AI Gateway token
|
|
|
48
48
|
# Video additionally requires CLOUDFLARE_AI_GATEWAY_ID and REPLICATE_API_TOKEN.
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
Priority: `process.env` > `cwd/.env` > `~/.multix/.env`.
|
|
51
|
+
Priority: `process.env` > `cwd/.env` > `~/.multix/.env`. Set `MULTIX_LEGACY_CLAUDE_ENV=1` to also read legacy AgentKit `.claude` env files (credential names only, lowest priority); `multix check -v` lists loaded env files.
|
|
52
52
|
|
|
53
53
|
## Gemini text-output tasks
|
|
54
54
|
|
|
@@ -323,7 +323,8 @@ Requires `cwebp` (libwebp: `brew install webp`, `apt install webp`) on `PATH`.
|
|
|
323
323
|
| `CLOUDFLARE_AI_GATEWAY_COLLECT_LOG_PAYLOAD` | Set `true` to explicitly opt in to AI Gateway request-payload collection; defaults to `false` |
|
|
324
324
|
| `REPLICATE_API_TOKEN` | Required for Cloudflare AI Gateway video |
|
|
325
325
|
| `CLOUDFLARE_AI_IMAGE_MODEL` / `CLOUDFLARE_AI_TTS_MODEL` | Fixed-model settings: only `@cf/black-forest-labs/flux-1-schnell` and `@cf/myshell-ai/melotts` respectively are accepted |
|
|
326
|
-
| `MULTIX_DISABLE_HOME_ENV` | `1` to skip `~/.multix/.env
|
|
326
|
+
| `MULTIX_DISABLE_HOME_ENV` | `1` to skip every home-directory env file (`~/.multix/.env`, `~/.claude/...`) |
|
|
327
|
+
| `MULTIX_LEGACY_CLAUDE_ENV` | `1` to also read legacy `.claude` env files (credential names only) |
|
|
327
328
|
|
|
328
329
|
## Quick examples for agents
|
|
329
330
|
|