@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.
Files changed (2) hide show
  1. package/bin/0dai.js +184 -2
  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.4.0";
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 Task queue & delegation");
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");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
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"