@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.
- package/bin/0dai.js +294 -13
- 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.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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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");
|