@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.
- package/bin/0dai.js +66 -26
- 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
|
-
|
|
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
|
-
|
|
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 {
|
|
1472
|
+
const { execFileSync } = require("child_process");
|
|
1450
1473
|
const cmd = os.platform() === "darwin" ? "open" : os.platform() === "win32" ? "start" : "xdg-open";
|
|
1451
|
-
|
|
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: "
|
|
1662
|
-
{ name: "
|
|
1663
|
-
{ name: "
|
|
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",
|
|
1671
|
-
{ name: "
|
|
1672
|
-
{ name: "
|
|
1673
|
-
{ name: "MiniMax M2.5", tier: "
|
|
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() });
|