@0dai-dev/cli 2.3.1 → 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 +294 -13
  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.3.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,22 +259,299 @@ 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; }
265
- let v = "?";
389
+ let v = "?", stack = "generic";
266
390
  try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
267
- const checks = {
268
- "ai/VERSION": fs.existsSync(path.join(ai, "VERSION")),
269
- "ai/manifest/project.yaml": fs.existsSync(path.join(ai, "manifest", "project.yaml")),
270
- "ai/manifest/commands.yaml": fs.existsSync(path.join(ai, "manifest", "commands.yaml")),
271
- "ai/manifest/discovery.json": fs.existsSync(path.join(ai, "manifest", "discovery.json")),
272
- ".claude/settings.json": fs.existsSync(path.join(target, ".claude", "settings.json")),
273
- "AGENTS.md": fs.existsSync(path.join(target, "AGENTS.md")),
391
+ try { stack = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).stack || "generic"; } catch {}
392
+
393
+ const W = process.stdout.isTTY ? "\x1b[33m" : ""; // yellow
394
+ const E = process.stdout.isTTY ? "\x1b[31m" : ""; // red
395
+ const G = process.stdout.isTTY ? "\x1b[32m" : ""; // green
396
+ const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
397
+
398
+ // --- ai/ layer checks ---
399
+ const layerChecks = {
400
+ "ai/VERSION": { path: path.join(ai, "VERSION"), sev: "error" },
401
+ "ai/manifest/project.yaml": { path: path.join(ai, "manifest", "project.yaml"), sev: "error" },
402
+ "ai/manifest/commands.yaml": { path: path.join(ai, "manifest", "commands.yaml"), sev: "warn" },
403
+ "ai/manifest/discovery.json": { path: path.join(ai, "manifest", "discovery.json"),sev: "warn" },
404
+ ".claude/settings.json": { path: path.join(target, ".claude", "settings.json"), sev: "warn" },
405
+ "AGENTS.md": { path: path.join(target, "AGENTS.md"), sev: "warn" },
274
406
  };
275
- const ok = Object.values(checks).every(Boolean);
276
- log(`v${v} ${ok ? "healthy" : "issues found"}`);
277
- for (const [name, exists] of Object.entries(checks)) console.log(` ${exists ? "ok" : "MISSING"}: ${name}`);
407
+
408
+ // --- credentials checklist ---
409
+ const credChecks = [
410
+ { name: "ANTHROPIC_API_KEY", env: "ANTHROPIC_API_KEY", needed: true, sev: "error", hint: "Required for Claude Code — get at console.anthropic.com" },
411
+ { name: "OPENAI_API_KEY", env: "OPENAI_API_KEY", needed: false, sev: "warn", hint: "Required for Codex CLI — get at platform.openai.com" },
412
+ { name: "GITHUB_TOKEN", env: "GITHUB_TOKEN", needed: false, sev: "warn", hint: "Needed for gh CLI, PR creation, swarm delegation" },
413
+ ];
414
+
415
+ // Stack-specific creds
416
+ if (stack.includes("vercel") || stack.includes("next")) {
417
+ credChecks.push({ name: "VERCEL_TOKEN", env: "VERCEL_TOKEN", needed: false, sev: "warn", hint: "Required for Vercel deployments — get at vercel.com/account/tokens" });
418
+ }
419
+ if (stack.includes("aws") || stack.includes("lambda") || stack.includes("cdk")) {
420
+ credChecks.push({ name: "AWS_ACCESS_KEY_ID", env: "AWS_ACCESS_KEY_ID", needed: false, sev: "warn", hint: "Required for AWS deployments" });
421
+ }
422
+ if (stack.includes("gcp") || stack.includes("firebase") || stack.includes("flutter")) {
423
+ credChecks.push({ name: "GOOGLE_APPLICATION_CREDENTIALS", env: "GOOGLE_APPLICATION_CREDENTIALS", needed: false, sev: "warn", hint: "Required for GCP/Firebase deployments" });
424
+ }
425
+
426
+ // --- run checks ---
427
+ let errors = 0, warnings = 0;
428
+ log(`v${v} | stack: ${stack}\n`);
429
+
430
+ console.log(" ai/ layer:");
431
+ for (const [name, { path: p, sev }] of Object.entries(layerChecks)) {
432
+ const exists = fs.existsSync(p);
433
+ if (!exists) sev === "error" ? errors++ : warnings++;
434
+ const mark = exists ? `${G}ok${R2}` : sev === "error" ? `${E}MISSING${R2}` : `${W}missing${R2}`;
435
+ console.log(` ${mark.padEnd(22)} ${name}`);
436
+ }
437
+
438
+ console.log("\n credentials:");
439
+ for (const c of credChecks) {
440
+ const present = !!process.env[c.env];
441
+ if (!present) c.sev === "error" ? errors++ : warnings++;
442
+ const mark = present ? `${G}set${R2}` : c.sev === "error" ? `${E}NOT SET${R2}` : `${W}not set${R2}`;
443
+ console.log(` ${mark.padEnd(22)} ${c.name}${!present ? `\n ${D}→ ${c.hint}${R2}` : ""}`);
444
+ }
445
+
446
+ // --- swarm check ---
447
+ const swarmDir = path.join(ai, "swarm");
448
+ const countDir = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
449
+ const qCount = countDir(path.join(swarmDir, "queue"));
450
+ const dCount = countDir(path.join(swarmDir, "done"));
451
+ if (qCount || dCount) {
452
+ console.log(`\n swarm: ${qCount} queued, ${dCount} done`);
453
+ if (qCount) console.log(` ${W}→ run '0dai reflect' to review pending tasks${R2}`);
454
+ }
455
+
456
+ const summary = errors ? `${E}${errors} error(s)${R2}` : warnings ? `${W}${warnings} warning(s)${R2}` : `${G}healthy${R2}`;
457
+ console.log(`\n status: ${summary}`);
458
+ if (errors) process.exitCode = 1;
459
+ }
460
+
461
+ // --- Session reflection --- (dogfood feedback #36)
462
+ function cmdReflect(target, args) {
463
+ const ai = path.join(target, "ai");
464
+ const W = process.stdout.isTTY ? "\x1b[33m" : "";
465
+ const G = process.stdout.isTTY ? "\x1b[32m" : "";
466
+ const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
467
+ const B = process.stdout.isTTY ? "\x1b[1m" : "";
468
+
469
+ // Collect swarm done tasks
470
+ const doneDir = path.join(ai, "swarm", "done");
471
+ const queueDir = path.join(ai, "swarm", "queue");
472
+ const activeDir = path.join(ai, "swarm", "active");
473
+ const doneTasks = [], queueTasks = [];
474
+
475
+ // -- how many days to look back (default 7)
476
+ const daysArg = args.find((_, i) => args[i - 1] === "--days");
477
+ const days = parseInt(daysArg || "7");
478
+ const since = Date.now() - days * 24 * 60 * 60 * 1000;
479
+
480
+ const readJsonDir = (dir) => {
481
+ const out = [];
482
+ try {
483
+ for (const f of fs.readdirSync(dir).filter(f => f.endsWith(".json"))) {
484
+ try { out.push(JSON.parse(fs.readFileSync(path.join(dir, f), "utf8"))); } catch {}
485
+ }
486
+ } catch {}
487
+ return out;
488
+ };
489
+
490
+ const allDone = readJsonDir(doneDir).filter(t => {
491
+ const ts = t.completed_at || t.created_at;
492
+ return !ts || new Date(ts).getTime() >= since;
493
+ });
494
+ const allQueue = readJsonDir(queueDir);
495
+ const allActive = readJsonDir(activeDir);
496
+
497
+ // Aggregate by agent
498
+ const byAgent = {};
499
+ for (const t of allDone) {
500
+ const a = t.assigned_to || t.agent || "unknown";
501
+ if (!byAgent[a]) byAgent[a] = { done: 0, tasks: [] };
502
+ byAgent[a].done++;
503
+ byAgent[a].tasks.push(t.title || t.id || "?");
504
+ }
505
+
506
+ // Session data
507
+ let sessionGoal = "?";
508
+ try {
509
+ const active = JSON.parse(fs.readFileSync(path.join(ai, "sessions", "active.json"), "utf8"));
510
+ sessionGoal = (active.task || {}).goal || "?";
511
+ } catch {}
512
+
513
+ console.log(`\n ${B}${T}0dai reflect${R2}${R} — last ${days} days\n`);
514
+
515
+ // Goal
516
+ if (sessionGoal !== "?") console.log(` ${B}Goal${R2} ${sessionGoal}`);
517
+
518
+ // Delegation stats
519
+ const totalDone = allDone.length;
520
+ const totalPending = allQueue.length + allActive.length;
521
+ const successRate = totalDone + totalPending > 0
522
+ ? Math.round((totalDone / (totalDone + totalPending)) * 100)
523
+ : null;
524
+
525
+ console.log(` ${B}Delivered${R2} ${G}${totalDone}${R2} tasks completed`);
526
+ if (totalPending) console.log(` ${B}Remaining${R2} ${W}${totalPending}${R2} tasks still pending`);
527
+ if (successRate !== null) console.log(` ${B}Rate${R2} ${successRate >= 80 ? G : W}${successRate}%${R2} delegation success rate`);
528
+
529
+ // By agent breakdown
530
+ if (Object.keys(byAgent).length) {
531
+ console.log(`\n ${B}By agent:${R2}`);
532
+ for (const [agent, data] of Object.entries(byAgent).sort((a, b) => b[1].done - a[1].done)) {
533
+ const bar = "█".repeat(Math.min(data.done, 20));
534
+ console.log(` ${(agent + " ").padEnd(14)} ${G}${bar}${R2} ${data.done}`);
535
+ }
536
+ }
537
+
538
+ // Remaining blockers
539
+ if (allQueue.length) {
540
+ console.log(`\n ${B}Blockers / queue:${R2}`);
541
+ for (const t of allQueue.slice(0, 8)) {
542
+ const agent = t.assigned_to ? ` → ${t.assigned_to}` : "";
543
+ console.log(` ${W}•${R2} ${(t.title || t.id || "?").slice(0, 60)}${agent}`);
544
+ }
545
+ if (allQueue.length > 8) console.log(` ${D}… and ${allQueue.length - 8} more${R2}`);
546
+ }
547
+
548
+ // No data
549
+ if (!totalDone && !totalPending) {
550
+ console.log(` ${D}No swarm tasks found in the last ${days} days.${R2}`);
551
+ console.log(` ${D}Use '0dai swarm add --task "..." --to codex' to delegate tasks.${R2}`);
552
+ }
553
+
554
+ console.log();
278
555
  }
279
556
 
280
557
  function cmdStatus(target) {
@@ -671,10 +948,12 @@ async function main() {
671
948
  checkVersion();
672
949
 
673
950
  switch (cmd) {
951
+ case "audit": cmdAudit(target); break;
674
952
  case "init": await cmdInit(target); break;
675
953
  case "sync": await cmdSync(target); break;
676
954
  case "detect": await cmdDetect(target); break;
677
955
  case "doctor": cmdDoctor(target); break;
956
+ case "reflect": cmdReflect(target, args); break;
678
957
  case "status": cmdStatus(target); break;
679
958
  case "auth":
680
959
  if (sub === "login") await cmdAuthLogin();
@@ -713,10 +992,12 @@ async function main() {
713
992
  case "help": case "--help": case "-h":
714
993
  console.log(`\n ${T}0dai${R} v${VERSION} — One config for 5 AI agent CLIs\n`);
715
994
  console.log("Commands:");
995
+ console.log(" audit Scan ai/ and agent configs for leaked secrets");
716
996
  console.log(" init Initialize ai/ layer (via API)");
717
997
  console.log(" sync Update ai/ layer (via API)");
718
998
  console.log(" detect Show detected stack");
719
- console.log(" doctor Check health");
999
+ console.log(" doctor Check health + credentials checklist");
1000
+ console.log(" reflect Session reflection: delivered, delegation rate, blockers");
720
1001
  console.log(" status Show maturity, swarm, session");
721
1002
  console.log(" session save Save session for roaming");
722
1003
  console.log(" swarm status Task queue & delegation");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "2.3.1",
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"