@0dai-dev/cli 2.4.0 → 2.6.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/bin/0dai.js +184 -2
- package/package.json +1 -1
package/bin/0dai.js
CHANGED
|
@@ -7,7 +7,7 @@ const fs = require("fs");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const os = require("os");
|
|
9
9
|
|
|
10
|
-
const VERSION = "2.
|
|
10
|
+
const VERSION = "2.6.0";
|
|
11
11
|
const API_URL = process.env.ODAI_API_URL || "https://api.0dai.dev";
|
|
12
12
|
const T = process.stdout.isTTY ? "\x1b[38;2;45;212;168m" : ""; // teal
|
|
13
13
|
const R = process.stdout.isTTY ? "\x1b[0m" : ""; // reset
|
|
@@ -259,6 +259,130 @@ async function cmdDetect(target) {
|
|
|
259
259
|
console.log(`clis: ${(result.available_clis || []).join(",")}`);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
function cmdAudit(target) {
|
|
263
|
+
const W = process.stdout.isTTY ? "\x1b[33m" : ""; // yellow
|
|
264
|
+
const RE = process.stdout.isTTY ? "\x1b[31m" : ""; // red
|
|
265
|
+
const G = process.stdout.isTTY ? "\x1b[32m" : ""; // green
|
|
266
|
+
|
|
267
|
+
// Secret patterns: [label, regex, severity]
|
|
268
|
+
const PATTERNS = [
|
|
269
|
+
["Anthropic API key", /sk-ant-api[0-9A-Za-z_-]{20,}/g, "critical"],
|
|
270
|
+
["OpenAI API key", /sk-[A-Za-z0-9]{20,}/g, "critical"],
|
|
271
|
+
["GitHub PAT (ghp)", /ghp_[A-Za-z0-9]{36}/g, "critical"],
|
|
272
|
+
["GitHub PAT (gho)", /gho_[A-Za-z0-9]{36}/g, "critical"],
|
|
273
|
+
["GitHub fine-grained",/github_pat_[A-Za-z0-9_]{59}/g, "critical"],
|
|
274
|
+
["AWS access key", /AKIA[0-9A-Z]{16}/g, "critical"],
|
|
275
|
+
["AWS secret key", /aws_secret_access_key\s*[=:]\s*\S{20,}/gi,"critical"],
|
|
276
|
+
["Google API key", /AIza[0-9A-Za-z_-]{35}/g, "high"],
|
|
277
|
+
["Bearer token", /Bearer\s+[A-Za-z0-9_-]{32,}/g, "high"],
|
|
278
|
+
["Private key block", /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY/g, "critical"],
|
|
279
|
+
["Generic secret var", /(?:secret|password|passwd|pwd)\s*[=:]\s*["']?[A-Za-z0-9+/=_-]{12,}["']?/gi, "medium"],
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
// Files to scan
|
|
283
|
+
const SCAN_FILES = [
|
|
284
|
+
"CLAUDE.md", "AGENTS.md", "GEMINI.md", ".cursorrules",
|
|
285
|
+
".codex/config.md", ".codex/instructions.md",
|
|
286
|
+
"opencode.json", ".mcp.json",
|
|
287
|
+
];
|
|
288
|
+
|
|
289
|
+
// Walk a directory recursively, return all file paths
|
|
290
|
+
function walk(dir, maxDepth = 6, _depth = 0) {
|
|
291
|
+
if (_depth > maxDepth) return [];
|
|
292
|
+
let results = [];
|
|
293
|
+
try {
|
|
294
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
295
|
+
if (entry.name.startsWith(".git") || entry.name === "node_modules") continue;
|
|
296
|
+
const full = path.join(dir, entry.name);
|
|
297
|
+
if (entry.isDirectory()) results = results.concat(walk(full, maxDepth, _depth + 1));
|
|
298
|
+
else results.push(full);
|
|
299
|
+
}
|
|
300
|
+
} catch { /* skip unreadable */ }
|
|
301
|
+
return results;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const findings = []; // {file, line, label, severity, excerpt}
|
|
305
|
+
|
|
306
|
+
function scanContent(filePath, content) {
|
|
307
|
+
const lines = content.split("\n");
|
|
308
|
+
for (const [label, regex, severity] of PATTERNS) {
|
|
309
|
+
regex.lastIndex = 0;
|
|
310
|
+
for (let i = 0; i < lines.length; i++) {
|
|
311
|
+
let m;
|
|
312
|
+
regex.lastIndex = 0;
|
|
313
|
+
while ((m = regex.exec(lines[i])) !== null) {
|
|
314
|
+
const val = m[0];
|
|
315
|
+
// Redact: show first 6 + ... + last 4 chars
|
|
316
|
+
const excerpt = val.length > 14 ? val.slice(0, 6) + "..." + val.slice(-4) : val.slice(0, 4) + "...";
|
|
317
|
+
findings.push({ file: filePath, line: i + 1, label, severity, excerpt });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Collect files to scan
|
|
324
|
+
const toScan = new Set();
|
|
325
|
+
|
|
326
|
+
for (const rel of SCAN_FILES) {
|
|
327
|
+
const p = path.join(target, rel);
|
|
328
|
+
if (fs.existsSync(p)) toScan.add(p);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const aiDir = path.join(target, "ai");
|
|
332
|
+
if (fs.existsSync(aiDir)) {
|
|
333
|
+
for (const f of walk(aiDir)) {
|
|
334
|
+
if (/\.(md|json|yaml|yml|txt|toml)$/.test(f)) toScan.add(f);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Warn about .env files (don't scan content, just flag existence)
|
|
339
|
+
const envFiles = [".env", ".env.local", ".env.production", ".env.development"];
|
|
340
|
+
const foundEnv = envFiles.filter(e => fs.existsSync(path.join(target, e)));
|
|
341
|
+
|
|
342
|
+
console.log(`\n ${T}0dai audit${R} — scanning for leaked secrets\n`);
|
|
343
|
+
console.log(` ${D}target: ${target}${R}`);
|
|
344
|
+
console.log(` ${D}files: ${toScan.size} scanned${R}\n`);
|
|
345
|
+
|
|
346
|
+
for (const filePath of toScan) {
|
|
347
|
+
try {
|
|
348
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
349
|
+
scanContent(filePath, content);
|
|
350
|
+
} catch { /* skip */ }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (foundEnv.length > 0) {
|
|
354
|
+
console.log(` ${W}WARN${R} .env files detected — ensure they are in .gitignore`);
|
|
355
|
+
for (const e of foundEnv) console.log(` ${D}${e}${R}`);
|
|
356
|
+
console.log();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (findings.length === 0) {
|
|
360
|
+
console.log(` ${G}✓ No secrets found${R} in scanned files\n`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const critical = findings.filter(f => f.severity === "critical");
|
|
365
|
+
const high = findings.filter(f => f.severity === "high");
|
|
366
|
+
const medium = findings.filter(f => f.severity === "medium");
|
|
367
|
+
|
|
368
|
+
const colorFor = (s) => s === "critical" ? RE : s === "high" ? W : D;
|
|
369
|
+
|
|
370
|
+
for (const f of findings) {
|
|
371
|
+
const c = colorFor(f.severity);
|
|
372
|
+
const rel = path.relative(target, f.file);
|
|
373
|
+
console.log(` ${c}${f.severity.toUpperCase().padEnd(8)}${R} ${rel}:${f.line}`);
|
|
374
|
+
console.log(` ${D}${f.label}: ${f.excerpt}${R}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
console.log();
|
|
378
|
+
if (critical.length > 0) console.log(` ${RE}${critical.length} critical${R} · ${W}${high.length} high${R} · ${D}${medium.length} medium${R}\n`);
|
|
379
|
+
else console.log(` ${W}${high.length} high${R} · ${D}${medium.length} medium${R}\n`);
|
|
380
|
+
|
|
381
|
+
console.log(` ${D}Tip: add secrets to .gitignore or use env vars, not plaintext files${R}\n`);
|
|
382
|
+
|
|
383
|
+
if (critical.length > 0) process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
|
|
262
386
|
function cmdDoctor(target) {
|
|
263
387
|
const ai = path.join(target, "ai");
|
|
264
388
|
if (!fs.existsSync(ai)) { log("no ai/ layer. Run '0dai init' first."); return; }
|
|
@@ -768,6 +892,59 @@ function cmdSwarm(target, sub, args) {
|
|
|
768
892
|
log(`task created: ${id} → ${forAgent}`);
|
|
769
893
|
return;
|
|
770
894
|
}
|
|
895
|
+
if (sub === "webhook") {
|
|
896
|
+
const webhooksFile = path.join(swarmDir, "webhooks.json");
|
|
897
|
+
const loadHooks = () => { try { return JSON.parse(fs.readFileSync(webhooksFile, "utf8")); } catch { return []; } };
|
|
898
|
+
const saveHooks = (h) => { fs.mkdirSync(swarmDir, { recursive: true }); fs.writeFileSync(webhooksFile, JSON.stringify(h, null, 2)); };
|
|
899
|
+
const action = args[2] || "";
|
|
900
|
+
|
|
901
|
+
if (action === "add") {
|
|
902
|
+
const url = args[3] || args.find((_, i) => args[i-1] === "--url");
|
|
903
|
+
const event = args.find((_, i) => args[i-1] === "--event") || "all";
|
|
904
|
+
const secret = args.find((_, i) => args[i-1] === "--secret") || "";
|
|
905
|
+
if (!url || !url.startsWith("http")) { log("Usage: 0dai swarm webhook add <url> [--event task_done|task_failed|all] [--secret TOKEN]"); return; }
|
|
906
|
+
const hooks = loadHooks();
|
|
907
|
+
if (hooks.find(h => h.url === url)) { log(`already registered: ${url}`); return; }
|
|
908
|
+
hooks.push({ url, event, secret: secret || undefined, added_at: new Date().toISOString() });
|
|
909
|
+
saveHooks(hooks);
|
|
910
|
+
log(`webhook added: ${url} (event: ${event})`);
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
if (action === "list") {
|
|
914
|
+
const hooks = loadHooks();
|
|
915
|
+
if (hooks.length === 0) { log("no webhooks registered. Use: 0dai swarm webhook add <url>"); return; }
|
|
916
|
+
console.log(`\n ${T}Registered webhooks${R}\n`);
|
|
917
|
+
hooks.forEach((h, i) => {
|
|
918
|
+
console.log(` ${i+1}. ${h.url}`);
|
|
919
|
+
console.log(` ${D}event: ${h.event} added: ${h.added_at?.slice(0,10)}${R}`);
|
|
920
|
+
});
|
|
921
|
+
console.log();
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (action === "remove") {
|
|
925
|
+
const url = args[3] || "";
|
|
926
|
+
if (!url) { log("Usage: 0dai swarm webhook remove <url>"); return; }
|
|
927
|
+
const hooks = loadHooks().filter(h => h.url !== url);
|
|
928
|
+
saveHooks(hooks);
|
|
929
|
+
log(`removed: ${url}`);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (action === "test") {
|
|
933
|
+
const url = args[3] || loadHooks()[0]?.url;
|
|
934
|
+
if (!url) { log("Usage: 0dai swarm webhook test <url>"); return; }
|
|
935
|
+
const payload = JSON.stringify({ event: "test", task_id: "test-ping", title: "Webhook test from 0dai", status: "done", timestamp: new Date().toISOString() });
|
|
936
|
+
const req = https.request(url, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": "0dai-swarm/1.0", "Content-Length": Buffer.byteLength(payload) } }, (res) => {
|
|
937
|
+
log(`test sent to ${url} → HTTP ${res.statusCode}`);
|
|
938
|
+
});
|
|
939
|
+
req.on("error", (e) => log(`test failed: ${e.message}`));
|
|
940
|
+
req.setTimeout(5000, () => { req.destroy(); log("test timed out"); });
|
|
941
|
+
req.write(payload);
|
|
942
|
+
req.end();
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
console.log("Usage: 0dai swarm webhook [add|list|remove|test] <url> [--event all|task_done|task_failed] [--secret TOKEN]");
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
771
948
|
if (sub === "budget") {
|
|
772
949
|
const budgetFile = path.join(swarmDir, "budget.json");
|
|
773
950
|
if (!fs.existsSync(budgetFile)) { log("no budget data yet"); return; }
|
|
@@ -824,6 +1001,7 @@ async function main() {
|
|
|
824
1001
|
checkVersion();
|
|
825
1002
|
|
|
826
1003
|
switch (cmd) {
|
|
1004
|
+
case "audit": cmdAudit(target); break;
|
|
827
1005
|
case "init": await cmdInit(target); break;
|
|
828
1006
|
case "sync": await cmdSync(target); break;
|
|
829
1007
|
case "detect": await cmdDetect(target); break;
|
|
@@ -867,6 +1045,7 @@ async function main() {
|
|
|
867
1045
|
case "help": case "--help": case "-h":
|
|
868
1046
|
console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
|
|
869
1047
|
console.log("Commands:");
|
|
1048
|
+
console.log(" audit Scan ai/ and agent configs for leaked secrets");
|
|
870
1049
|
console.log(" init Initialize ai/ layer (via API)");
|
|
871
1050
|
console.log(" sync Update ai/ layer (via API)");
|
|
872
1051
|
console.log(" detect Show detected stack");
|
|
@@ -874,7 +1053,10 @@ async function main() {
|
|
|
874
1053
|
console.log(" reflect Session reflection: delivered, delegation rate, blockers");
|
|
875
1054
|
console.log(" status Show maturity, swarm, session");
|
|
876
1055
|
console.log(" session save Save session for roaming");
|
|
877
|
-
console.log(" swarm status
|
|
1056
|
+
console.log(" swarm status Task queue & delegation");
|
|
1057
|
+
console.log(" swarm webhook add Register webhook (fires on task done/failed)");
|
|
1058
|
+
console.log(" swarm webhook list Show registered webhooks");
|
|
1059
|
+
console.log(" swarm webhook test Send test ping to a webhook URL");
|
|
878
1060
|
console.log(" feedback push Send feedback to 0dai");
|
|
879
1061
|
console.log(" models Show model ratings (--fast/--balanced/--deep/--available)");
|
|
880
1062
|
console.log(" terminal Launch interactive agent session");
|