@0dai-dev/cli 3.3.0 → 3.3.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.
Files changed (2) hide show
  1. package/bin/0dai.js +66 -26
  2. package/package.json +1 -1
package/bin/0dai.js CHANGED
@@ -8,7 +8,26 @@ const path = require("path");
8
8
  const os = require("os");
9
9
 
10
10
  const VERSION = require("../package.json").version;
11
- const API_URL = process.env.ODAI_API_URL || "https://api.0dai.dev";
11
+
12
+ // CRITICAL: validate API_URL to prevent credential exfiltration to attacker hosts
13
+ function _validateApiUrl(url) {
14
+ const DEFAULT = "https://api.0dai.dev";
15
+ if (!url) return DEFAULT;
16
+ try {
17
+ const u = new URL(url);
18
+ // Only allow https (or http for localhost during dev)
19
+ if (u.protocol === "https:") return url;
20
+ if (u.protocol === "http:" && (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "::1")) {
21
+ return url;
22
+ }
23
+ console.error(`[0dai] warning: ODAI_API_URL must use https (or http://localhost). Got: ${u.protocol}//${u.hostname}. Ignoring.`);
24
+ return DEFAULT;
25
+ } catch {
26
+ console.error(`[0dai] warning: ODAI_API_URL is not a valid URL, ignoring`);
27
+ return DEFAULT;
28
+ }
29
+ }
30
+ const API_URL = _validateApiUrl(process.env.ODAI_API_URL);
12
31
  const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : ""; // teal
13
32
  const R = process.stdout.isTTY ? "\x1b[0m" : ""; // reset
14
33
  const D = process.stdout.isTTY ? "\x1b[2m" : ""; // dim
@@ -321,17 +340,20 @@ function mergeSettingsJson(existing, incoming) {
321
340
  } catch { return incoming; }
322
341
  }
323
342
 
324
- function mergeAgentsMd(existing, incoming) {
325
- // If user marked as unmanaged, don't touch
326
- if (existing.includes("managed: false")) return existing;
327
- // If managed, update with new content but preserve user additions after managed block
328
- return incoming;
329
- }
330
-
331
343
  function writeFiles(target, files) {
332
- let created = 0, updated = 0, unchanged = 0, merged = 0;
344
+ let created = 0, updated = 0, unchanged = 0, merged = 0, skipped = 0;
345
+ const targetResolved = path.resolve(target);
333
346
  for (const [rel, content] of Object.entries(files)) {
334
- const p = path.join(target, rel);
347
+ // HIGH: path traversal protection — reject absolute paths and `..` escapes
348
+ if (typeof rel !== "string" || !rel || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
349
+ skipped++;
350
+ continue;
351
+ }
352
+ const p = path.resolve(targetResolved, rel);
353
+ if (!p.startsWith(targetResolved + path.sep) && p !== targetResolved) {
354
+ skipped++;
355
+ continue;
356
+ }
335
357
  fs.mkdirSync(path.dirname(p), { recursive: true });
336
358
 
337
359
  let finalContent = content;
@@ -365,6 +387,7 @@ function writeFiles(target, files) {
365
387
  }
366
388
  const parts = [`${created} created`, `${updated} updated`, `${unchanged} unchanged`];
367
389
  if (merged) parts.push(`${merged} merged`);
390
+ if (skipped) parts.push(`${skipped} skipped (unsafe path)`);
368
391
  log(parts.join(", "));
369
392
  return created + updated;
370
393
  }
@@ -1446,9 +1469,10 @@ async function cmdAuthLogin() {
1446
1469
  const url = `${API_URL}/v1/auth/${method}?cli=true`;
1447
1470
  p.log.info(`Opening browser: ${url}`);
1448
1471
  try {
1449
- const { execSync } = require("child_process");
1472
+ const { execFileSync } = require("child_process");
1450
1473
  const cmd = os.platform() === "darwin" ? "open" : os.platform() === "win32" ? "start" : "xdg-open";
1451
- execSync(`${cmd} "${url}"`, { stdio: "ignore" });
1474
+ // MED: use execFileSync to avoid shell injection via URL metacharacters
1475
+ execFileSync(cmd, [url], { stdio: "ignore" });
1452
1476
  } catch {
1453
1477
  p.log.warn(`Could not open browser. Visit manually:\n ${url}`);
1454
1478
  }
@@ -1655,25 +1679,24 @@ async function cmdFeedbackPush(target) {
1655
1679
 
1656
1680
  // --- Models ---
1657
1681
  function cmdModels(filter) {
1682
+ // Scores from benchmark_models.py (3-task: read/count/review, 2026-04-06)
1658
1683
  const MODELS = [
1659
1684
  { name: "Claude Opus 4.6", tier: "deep", score: 95, cli: "claude", flag: "--model opus" },
1685
+ { name: "GPT-5.4-mini", tier: "fast", score: 93, cli: "codex", flag: "-m gpt-5.4-mini", tested: true },
1686
+ { name: "MiniMax M2.7", tier: "balanced", score: 93, cli: "opencode", flag: "-m opencode-go/minimax-m2.7", tested: true },
1660
1687
  { name: "Claude Sonnet 4.6", tier: "balanced", score: 90, cli: "claude", flag: "--model sonnet" },
1661
- { name: "Claude Haiku 4.5", tier: "fast", score: 78, cli: "claude", flag: "--model haiku" },
1662
- { name: "GPT-5.3 Codex", tier: "deep", score: 91, cli: "codex", flag: "-m gpt-5.3-codex", tested: true },
1663
- { name: "GPT-5.3 Spark", tier: "fast", score: 82, cli: "codex", flag: "-m gpt-5.3-codex-spark",tested: true },
1664
- { name: "GPT-5.4", tier: "balanced", score: 89, cli: "codex", flag: "-m gpt-5.4", tested: true },
1665
- { name: "GPT-5.4-mini", tier: "fast", score: 76, cli: "codex", flag: "-m gpt-5.4-mini", tested: true },
1666
- { name: "GPT-5.2", tier: "balanced", score: 87, cli: "codex", flag: "-m gpt-5.2", tested: true },
1667
- { name: "GPT-5.1", tier: "fast", score: 75, cli: "codex", flag: "-m gpt-5.1", tested: true },
1688
+ { name: "GPT-5.4", tier: "balanced", score: 90, cli: "codex", flag: "-m gpt-5.4", tested: true },
1689
+ { name: "Kimi K2.5", tier: "balanced", score: 88, cli: "opencode", flag: "-m opencode-go/kimi-k2.5", tested: true },
1690
+ { name: "Qwen 3.6+ Free", tier: "free", score: 88, cli: "opencode", flag: "-m opencode/qwen3.6-plus-free", tested: true },
1668
1691
  { name: "Gemini 3.1 Pro", tier: "balanced", score: 85, cli: "gemini", flag: "-m gemini-3.1-pro" },
1692
+ { name: "GPT-5.3 Codex", tier: "deep", score: 83, cli: "codex", flag: "-m gpt-5.3-codex", tested: true },
1693
+ { name: "GPT-5.3 Spark", tier: "fast", score: 82, cli: "codex", flag: "-m gpt-5.3-codex-spark" },
1694
+ { name: "Claude Haiku 4.5", tier: "fast", score: 78, cli: "claude", flag: "--model haiku" },
1669
1695
  { name: "Gemini 3 Flash", tier: "fast", score: 77, cli: "gemini", flag: "-m gemini-3-flash" },
1670
- { name: "Mimo v2 Pro", tier: "balanced", score: 80, cli: "opencode", flag: "-m opencode-go/mimo-v2-pro", tested: true },
1671
- { name: "Mimo v2 Omni", tier: "fast", score: 78, cli: "opencode", flag: "-m opencode-go/mimo-v2-omni", tested: true },
1672
- { name: "Kimi K2.5", tier: "balanced", score: 72, cli: "opencode", flag: "-m opencode-go/kimi-k2.5", tested: true },
1673
- { name: "MiniMax M2.5", tier: "fast", score: 70, cli: "opencode", flag: "-m opencode-go/minimax-m2.5", tested: true },
1674
- { name: "MiniMax M2.7", tier: "balanced", score: 68, cli: "opencode", flag: "-m opencode-go/minimax-m2.7", tested: true },
1675
- { name: "Qwen 3.6+ Free", tier: "fast", score: 64, cli: "opencode", flag: "-m opencode/qwen3.6-plus-free", tested: true },
1676
- { name: "GLM-5", tier: "fast", score: 62, cli: "opencode", flag: "-m opencode-go/glm-5", tested: true },
1696
+ { name: "Mimo v2 Pro", tier: "fast", score: 74, cli: "opencode", flag: "-m opencode-go/mimo-v2-pro", tested: true },
1697
+ { name: "GPT-5.4 (opencode)",tier: "fast", score: 74, cli: "opencode", flag: "-m openai/gpt-5.4", tested: true },
1698
+ { name: "GPT-5.2", tier: "balanced", score: 87, cli: "codex", flag: "-m gpt-5.2", tested: true },
1699
+ { name: "MiniMax M2.5", tier: "slow", score: 57, cli: "opencode", flag: "-m opencode-go/minimax-m2.5", tested: true },
1677
1700
  ];
1678
1701
 
1679
1702
  const { execFileSync } = require("child_process");
@@ -1792,6 +1815,23 @@ function cmdSwarm(target, sub, args) {
1792
1815
  const event = args.find((_, i) => args[i-1] === "--event") || "all";
1793
1816
  const secret = args.find((_, i) => args[i-1] === "--secret") || "";
1794
1817
  if (!url || !url.startsWith("http")) { log("Usage: 0dai swarm webhook add <url> [--event task_done|task_failed|all] [--secret TOKEN]"); return; }
1818
+ // MED: SSRF protection — block internal/metadata endpoints
1819
+ try {
1820
+ const u = new URL(url);
1821
+ const host = u.hostname;
1822
+ const BLOCKED = /^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.|::1$|fc00:|fe80:|localhost$|0\.0\.0\.0$)/i;
1823
+ if (BLOCKED.test(host) || host === "metadata.google.internal") {
1824
+ log(`rejected: ${host} is a private/internal address (SSRF protection)`);
1825
+ return;
1826
+ }
1827
+ if (u.protocol !== "https:" && u.protocol !== "http:") {
1828
+ log(`rejected: only http/https allowed, got ${u.protocol}`);
1829
+ return;
1830
+ }
1831
+ } catch {
1832
+ log(`invalid URL: ${url}`);
1833
+ return;
1834
+ }
1795
1835
  const hooks = loadHooks();
1796
1836
  if (hooks.find(h => h.url === url)) { log(`already registered: ${url}`); return; }
1797
1837
  hooks.push({ url, event, secret: secret || undefined, added_at: new Date().toISOString() });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "One config layer for 5 AI agent CLIs — Claude Code, Codex, OpenCode, Gemini, Aider",
5
5
  "bin": {
6
6
  "0dai": "./bin/0dai.js"