@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 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
- const cwdEnv = path3.resolve(process.cwd(), ".env");
289
- if (fs3.existsSync(cwdEnv)) {
290
- dotenvConfig({ path: cwdEnv, override: false });
291
- }
292
- if (!process.env.MULTIX_DISABLE_HOME_ENV) {
293
- const homeEnv = path3.join(os.homedir(), ".multix", ".env");
294
- if (fs3.existsSync(homeEnv)) {
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 fs43 from "fs";
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 (!fs43.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
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
- fs43.mkdirSync(path45.dirname(outputPath), { recursive: true });
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
- fs43.writeFileSync(outputPath, lines.join("\n"), "utf8");
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 fs46 from "fs";
7407
+ import fs47 from "fs";
7335
7408
  import path48 from "path";
7336
7409
 
7337
7410
  // src/commands/media/batch.ts
7338
- import fs44 from "fs";
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
- fs44.mkdirSync(outputDir, { recursive: true });
7536
- const entries = fs44.readdirSync(inputDir);
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 fs45 from "fs";
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
- fs45.mkdirSync(outputDir, { recursive: true });
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 (fs45.existsSync(fname)) files.push(fname);
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 (!fs46.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7638
- fs46.mkdirSync(path48.dirname(path48.resolve(opts.output)), { recursive: true });
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 (!fs46.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
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 (!fs46.existsSync(opts.inputDir))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrgoonie/multix",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "AI multimodal CLI — OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, ffmpeg, ImageMagick, doc-to-md",
5
5
  "type": "module",
6
6
  "bin": {
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.8.0 # x-release-please-version
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