@0dai-dev/cli 2.4.0 → 2.5.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 +127 -1
  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.5.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; }
@@ -824,6 +948,7 @@ async function main() {
824
948
  checkVersion();
825
949
 
826
950
  switch (cmd) {
951
+ case "audit": cmdAudit(target); break;
827
952
  case "init": await cmdInit(target); break;
828
953
  case "sync": await cmdSync(target); break;
829
954
  case "detect": await cmdDetect(target); break;
@@ -867,6 +992,7 @@ async function main() {
867
992
  case "help": case "--help": case "-h":
868
993
  console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
869
994
  console.log("Commands:");
995
+ console.log(" audit Scan ai/ and agent configs for leaked secrets");
870
996
  console.log(" init Initialize ai/ layer (via API)");
871
997
  console.log(" sync Update ai/ layer (via API)");
872
998
  console.log(" detect Show detected stack");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "2.4.0",
3
+ "version": "2.5.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"