@0dai-dev/cli 3.10.1 → 4.0.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.
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { T, R, D, fs, path } = shared;
4
+
5
+ function cmdAudit(target) {
6
+ const W = process.stdout.isTTY ? "\x1b[33m" : ""; // yellow
7
+ const RE = process.stdout.isTTY ? "\x1b[31m" : ""; // red
8
+ const G = process.stdout.isTTY ? "\x1b[32m" : ""; // green
9
+
10
+ // Secret patterns: [label, regex, severity]
11
+ const PATTERNS = [
12
+ ["Anthropic API key", /sk-ant-api[0-9A-Za-z_-]{20,}/g, "critical"],
13
+ ["OpenAI API key", /sk-[A-Za-z0-9]{20,}/g, "critical"],
14
+ ["GitHub PAT (ghp)", /ghp_[A-Za-z0-9]{36}/g, "critical"],
15
+ ["GitHub PAT (gho)", /gho_[A-Za-z0-9]{36}/g, "critical"],
16
+ ["GitHub fine-grained",/github_pat_[A-Za-z0-9_]{59}/g, "critical"],
17
+ ["AWS access key", /AKIA[0-9A-Z]{16}/g, "critical"],
18
+ ["AWS secret key", /aws_secret_access_key\s*[=:]\s*\S{20,}/gi,"critical"],
19
+ ["Google API key", /AIza[0-9A-Za-z_-]{35}/g, "high"],
20
+ ["Bearer token", /Bearer\s+[A-Za-z0-9_-]{32,}/g, "high"],
21
+ ["Private key block", /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY/g, "critical"],
22
+ ["Generic secret var", /(?:secret|password|passwd|pwd)\s*[=:]\s*["']?[A-Za-z0-9+/=_-]{12,}["']?/gi, "medium"],
23
+ ];
24
+
25
+ // Files to scan
26
+ const SCAN_FILES = [
27
+ "CLAUDE.md", "AGENTS.md", "GEMINI.md", ".cursorrules",
28
+ ".codex/config.md", ".codex/instructions.md",
29
+ "opencode.json", ".mcp.json",
30
+ ];
31
+
32
+ // Walk a directory recursively, return all file paths
33
+ function walk(dir, maxDepth = 6, _depth = 0) {
34
+ if (_depth > maxDepth) return [];
35
+ let results = [];
36
+ try {
37
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
38
+ if (entry.name.startsWith(".git") || entry.name === "node_modules") continue;
39
+ const full = path.join(dir, entry.name);
40
+ if (entry.isDirectory()) results = results.concat(walk(full, maxDepth, _depth + 1));
41
+ else results.push(full);
42
+ }
43
+ } catch { /* skip unreadable */ }
44
+ return results;
45
+ }
46
+
47
+ const findings = []; // {file, line, label, severity, excerpt}
48
+
49
+ function scanContent(filePath, content) {
50
+ const lines = content.split("\n");
51
+ for (const [label, regex, severity] of PATTERNS) {
52
+ regex.lastIndex = 0;
53
+ for (let i = 0; i < lines.length; i++) {
54
+ let m;
55
+ regex.lastIndex = 0;
56
+ while ((m = regex.exec(lines[i])) !== null) {
57
+ const val = m[0];
58
+ // Redact: show first 6 + ... + last 4 chars
59
+ const excerpt = val.length > 14 ? val.slice(0, 6) + "..." + val.slice(-4) : val.slice(0, 4) + "...";
60
+ findings.push({ file: filePath, line: i + 1, label, severity, excerpt });
61
+ }
62
+ }
63
+ }
64
+ }
65
+
66
+ // Collect files to scan
67
+ const toScan = new Set();
68
+
69
+ for (const rel of SCAN_FILES) {
70
+ const p = path.join(target, rel);
71
+ if (fs.existsSync(p)) toScan.add(p);
72
+ }
73
+
74
+ const aiDir = path.join(target, "ai");
75
+ if (fs.existsSync(aiDir)) {
76
+ for (const f of walk(aiDir)) {
77
+ if (/\.(md|json|yaml|yml|txt|toml)$/.test(f)) toScan.add(f);
78
+ }
79
+ }
80
+
81
+ // Warn about .env files (don't scan content, just flag existence)
82
+ const envFiles = [".env", ".env.local", ".env.production", ".env.development"];
83
+ const foundEnv = envFiles.filter(e => fs.existsSync(path.join(target, e)));
84
+
85
+ console.log(`\n ${T}0dai audit${R} — scanning for leaked secrets\n`);
86
+ console.log(` ${D}target: ${target}${R}`);
87
+ console.log(` ${D}files: ${toScan.size} scanned${R}\n`);
88
+
89
+ for (const filePath of toScan) {
90
+ try {
91
+ const content = fs.readFileSync(filePath, "utf8");
92
+ scanContent(filePath, content);
93
+ } catch { /* skip */ }
94
+ }
95
+
96
+ if (foundEnv.length > 0) {
97
+ console.log(` ${W}WARN${R} .env files detected — ensure they are in .gitignore`);
98
+ for (const e of foundEnv) console.log(` ${D}${e}${R}`);
99
+ console.log();
100
+ }
101
+
102
+ if (findings.length === 0) {
103
+ console.log(` ${G}✓ No secrets found${R} in scanned files\n`);
104
+ return;
105
+ }
106
+
107
+ const critical = findings.filter(f => f.severity === "critical");
108
+ const high = findings.filter(f => f.severity === "high");
109
+ const medium = findings.filter(f => f.severity === "medium");
110
+
111
+ const colorFor = (s) => s === "critical" ? RE : s === "high" ? W : D;
112
+
113
+ for (const f of findings) {
114
+ const c = colorFor(f.severity);
115
+ const rel = path.relative(target, f.file);
116
+ console.log(` ${c}${f.severity.toUpperCase().padEnd(8)}${R} ${rel}:${f.line}`);
117
+ console.log(` ${D}${f.label}: ${f.excerpt}${R}`);
118
+ }
119
+
120
+ console.log();
121
+ if (critical.length > 0) console.log(` ${RE}${critical.length} critical${R} · ${W}${high.length} high${R} · ${D}${medium.length} medium${R}\n`);
122
+ else console.log(` ${W}${high.length} high${R} · ${D}${medium.length} medium${R}\n`);
123
+
124
+ console.log(` ${D}Tip: add secrets to .gitignore or use env vars, not plaintext files${R}\n`);
125
+
126
+ if (critical.length > 0) process.exit(1);
127
+ }
128
+
129
+ module.exports = { cmdAudit };
@@ -0,0 +1,241 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const {
4
+ T, R, D, log,
5
+ fs, os,
6
+ CONFIG_DIR, AUTH_FILE, API_URL,
7
+ apiCall, loadAuthState, fetchAuthStatus, updateAuthState,
8
+ makeEnsureAuthenticated, ensureLicenseActivation,
9
+ } = shared;
10
+
11
+ async function cmdAuthLogin() {
12
+ const isTTY = process.stdout.isTTY && process.stdin.isTTY;
13
+
14
+ // Check if already authenticated
15
+ try {
16
+ const existing = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
17
+ if (existing.access_token || existing.email) {
18
+ if (isTTY) {
19
+ const p = require("@clack/prompts");
20
+ p.intro(`${T}0dai${R} authentication`);
21
+ p.log.success(`Already logged in as ${T}${existing.email || "unknown"}${R} (${existing.plan || "free"} plan)`);
22
+ const reauth = await p.confirm({ message: "Sign in with a different account?" });
23
+ if (p.isCancel(reauth) || !reauth) {
24
+ p.outro("Current session kept");
25
+ return;
26
+ }
27
+ } else {
28
+ log(`Already logged in as ${existing.email || "unknown"} (${existing.plan || "free"} plan)`);
29
+ log("To switch accounts, delete ~/.0dai/auth.json and run again");
30
+ return;
31
+ }
32
+ }
33
+ } catch {}
34
+
35
+ if (isTTY) {
36
+ // Interactive TUI flow
37
+ const p = require("@clack/prompts");
38
+ if (!p._intro_shown) p.intro(`${T}0dai${R} authentication`);
39
+
40
+ p.note(
41
+ "0dai auth is separate from agent CLIs (Claude Code, Codex).\n" +
42
+ "It tracks your projects, usage limits, and team features.\n" +
43
+ "Your agent CLIs keep their own auth (subscription/API key).",
44
+ "Why sign in?"
45
+ );
46
+
47
+ const method = await p.select({
48
+ message: "How would you like to sign in?",
49
+ options: [
50
+ { value: "github", label: "GitHub", hint: "recommended" },
51
+ { value: "google", label: "Google" },
52
+ { value: "device", label: "Device code", hint: "no browser needed" },
53
+ ],
54
+ });
55
+ if (p.isCancel(method)) { p.cancel("Cancelled"); process.exit(0); }
56
+
57
+ if (method === "github" || method === "google") {
58
+ const url = `${API_URL}/v1/auth/${method}?cli=true`;
59
+ p.log.info(`Opening browser: ${url}`);
60
+ try {
61
+ const { execFileSync } = require("child_process");
62
+ const cmd = os.platform() === "darwin" ? "open" : os.platform() === "win32" ? "start" : "xdg-open";
63
+ // MED: use execFileSync to avoid shell injection via URL metacharacters
64
+ execFileSync(cmd, [url], { stdio: "ignore" });
65
+ } catch {
66
+ p.log.warn(`Could not open browser. Visit manually:\n ${url}`);
67
+ }
68
+
69
+ const s = p.spinner();
70
+ s.start("Waiting for browser confirmation...");
71
+
72
+ // Poll auth/status until we get a new token (check every 3s, 5min timeout)
73
+ // For now, ask user to paste token from success page
74
+ s.stop("Browser opened");
75
+ const token = await p.text({
76
+ message: "Paste your token from the success page (or press Enter to skip):",
77
+ placeholder: "0dai_at_...",
78
+ });
79
+ if (token && !p.isCancel(token) && token.startsWith("0dai_at_")) {
80
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
81
+ fs.writeFileSync(AUTH_FILE, JSON.stringify({
82
+ access_token: token,
83
+ authenticated_at: new Date().toISOString(),
84
+ }, null, 2) + "\n", { mode: 0o600 });
85
+ // Fetch profile
86
+ const status = await apiCall("/v1/auth/status");
87
+ if (status.email) {
88
+ const auth = JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
89
+ auth.email = status.email;
90
+ auth.plan = status.plan;
91
+ auth.name = status.name;
92
+ fs.writeFileSync(AUTH_FILE, JSON.stringify(auth, null, 2) + "\n", { mode: 0o600 });
93
+ p.outro(`${T}Logged in${R} as ${status.email} (${status.plan} plan)`);
94
+ } else {
95
+ p.outro(`${T}Token saved${R}`);
96
+ }
97
+ return;
98
+ }
99
+ p.log.info("Skipped. You can also use device code flow:");
100
+ }
101
+
102
+ // Device code fallback
103
+ const result = await apiCall("/v1/auth/device", { client_id: "cli" });
104
+ if (result.error) { p.log.error(result.error); process.exit(1); }
105
+
106
+ p.log.step(`Open: ${result.verification_uri}`);
107
+ p.log.step(`Code: ${T}${result.user_code}${R}`);
108
+
109
+ const s = p.spinner();
110
+ s.start("Waiting for confirmation...");
111
+
112
+ const interval = (result.interval || 5) * 1000;
113
+ const deadline = Date.now() + (result.expires_in || 600) * 1000;
114
+ while (Date.now() < deadline) {
115
+ await new Promise(r => setTimeout(r, interval));
116
+ const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
117
+ if (poll.access_token) {
118
+ s.stop("Authorized!");
119
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
120
+ fs.writeFileSync(AUTH_FILE, JSON.stringify({
121
+ access_token: poll.access_token, email: poll.email,
122
+ plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
123
+ expires_at: poll.expires_at,
124
+ }, null, 2) + "\n", { mode: 0o600 });
125
+ p.outro(`${T}Logged in${R} as ${poll.email} (${poll.plan} plan)`);
126
+ return;
127
+ }
128
+ if (poll.error && poll.error !== "authorization_pending") {
129
+ s.stop("Failed");
130
+ p.log.error(poll.error);
131
+ process.exit(1);
132
+ }
133
+ }
134
+ s.stop("Timed out");
135
+ p.log.error("Try again.");
136
+ process.exit(1);
137
+
138
+ } else {
139
+ // Non-interactive: device code only
140
+ log("0dai auth is separate from agent CLIs. It tracks projects, limits, and team features.");
141
+ const result = await apiCall("/v1/auth/device", { client_id: "cli" });
142
+ if (result.error) { log(`error: ${result.error}`); process.exit(1); }
143
+ log(`Open: ${result.verification_uri}`);
144
+ log(`Code: ${result.user_code}`);
145
+ log("Waiting...");
146
+ const interval = (result.interval || 5) * 1000;
147
+ const deadline = Date.now() + (result.expires_in || 600) * 1000;
148
+ while (Date.now() < deadline) {
149
+ await new Promise(r => setTimeout(r, interval));
150
+ const poll = await apiCall("/v1/auth/token", { device_code: result.device_code });
151
+ if (poll.access_token) {
152
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
153
+ fs.writeFileSync(AUTH_FILE, JSON.stringify({
154
+ access_token: poll.access_token, email: poll.email,
155
+ plan: poll.plan || "free", authenticated_at: new Date().toISOString(),
156
+ }, null, 2) + "\n", { mode: 0o600 });
157
+ log(`Logged in as ${poll.email}`);
158
+ return;
159
+ }
160
+ }
161
+ log("Timed out");
162
+ process.exit(1);
163
+ }
164
+ }
165
+
166
+ function cmdAuthLogout() {
167
+ try { fs.unlinkSync(AUTH_FILE); } catch {}
168
+ log("Logged out");
169
+ }
170
+
171
+ async function cmdRedeem(code) {
172
+ if (!code) {
173
+ console.log("Usage: 0dai redeem <CODE>");
174
+ console.log("Example: 0dai redeem ESSE-ABCD-1234");
175
+ process.exit(1);
176
+ }
177
+ try {
178
+ JSON.parse(fs.readFileSync(AUTH_FILE, "utf8"));
179
+ } catch {
180
+ log("Not logged in. Run: 0dai auth login");
181
+ process.exit(1);
182
+ }
183
+ log(`Redeeming code ${T}${code.toUpperCase()}${R}...`);
184
+ const result = await apiCall("/v1/redeem", { code: code.toUpperCase().trim() });
185
+ if (result.ok) {
186
+ log(`${T}✓${R} ${result.message}`);
187
+ if (result.duration_days) {
188
+ log(` Plan active for ${result.duration_days} days`);
189
+ }
190
+ log(` Run ${D}0dai auth status${R} to see updated limits`);
191
+ } else {
192
+ log(`error: ${result.error || "unknown"}`);
193
+ if (result.hint) log(`hint: ${result.hint}`);
194
+ process.exit(1);
195
+ }
196
+ }
197
+
198
+ async function cmdAuthStatus() {
199
+ try {
200
+ const auth = loadAuthState();
201
+ if (!auth) throw new Error("missing auth");
202
+ // Backwards compat: old auth.json used `user`, new uses `email`
203
+ const email = auth.email || auth.user || "unknown";
204
+ log(`${email} (${auth.plan || "free"} plan)`);
205
+ // Get usage from API
206
+ const status = await fetchAuthStatus();
207
+ if (status.usage_today) {
208
+ console.log(" Usage today:");
209
+ for (const [k, v] of Object.entries(status.usage_today))
210
+ console.log(` ${k}: ${v} / ${status.limits[k]}`);
211
+ }
212
+ const license = status.license || auth.license || { status: "inactive" };
213
+ console.log(` Activation: ${license.status || "inactive"}${license.activation_id ? ` (${license.activation_id})` : ""}`);
214
+ if (status.projects && status.projects.length) {
215
+ console.log(` Projects bound: ${status.projects.length} / ${status.project_limit || "?"}`);
216
+ }
217
+ } catch {
218
+ log("Not logged in. Run: 0dai auth login");
219
+ }
220
+ }
221
+
222
+ async function cmdActivateFree() {
223
+ const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
224
+ await ensureAuthenticated("activation");
225
+ const license = await ensureLicenseActivation();
226
+ log(`license ${license.status}`);
227
+ console.log(` activation id: ${license.activation_id}`);
228
+ console.log(` plan: ${license.plan || "free"}`);
229
+ }
230
+
231
+ async function cmdActivateStatus() {
232
+ const ensureAuthenticated = makeEnsureAuthenticated(cmdAuthLogin);
233
+ const status = await ensureAuthenticated("activation status");
234
+ const license = status.license || (await apiCall("/v1/licenses/status")).license || { status: "inactive" };
235
+ updateAuthState({ license });
236
+ log(`license ${license.status || "inactive"}`);
237
+ if (license.activation_id) console.log(` activation id: ${license.activation_id}`);
238
+ console.log(` plan: ${license.plan || status.plan || "free"}`);
239
+ }
240
+
241
+ module.exports = { cmdAuthLogin, cmdAuthLogout, cmdRedeem, cmdAuthStatus, cmdActivateFree, cmdActivateStatus };
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { D, R, log, apiCall, collectMetadata } = shared;
4
+
5
+ async function cmdDetect(target) {
6
+ const OPTIONAL_CLIS = ["gemini", "aider", "opencode"];
7
+ const { projectFiles, manifestContents, clis: localClis } = collectMetadata(target);
8
+ // Send file contents AND local CLI inventory so server can do content-based detection
9
+ const result = await apiCall("/v1/detect", {
10
+ project_files: projectFiles,
11
+ manifest_contents: manifestContents,
12
+ available_clis: localClis,
13
+ });
14
+ if (result.error) { log(`error: ${result.error}`); return; }
15
+ console.log(`stack: ${result.stack || "?"}`);
16
+ // Use local CLIs if server didn't return any (server can't detect locally installed binaries)
17
+ const clis = (result.available_clis && result.available_clis.length && result.available_clis[0]) ? result.available_clis : localClis;
18
+ if (clis.length) {
19
+ console.log(`clis: ${clis.join(", ")}`);
20
+ } else {
21
+ console.log(`clis: none detected`);
22
+ console.log(` ${D}install claude, codex, or opencode to use 0dai${R}`);
23
+ }
24
+ // Explain optional CLIs so missing doesn't alarm users
25
+ const missing = OPTIONAL_CLIS.filter(c => !clis.includes(c));
26
+ if (missing.length && clis.length) {
27
+ console.log(` ${D}optional (not installed): ${missing.join(", ")}${R}`);
28
+ }
29
+ }
30
+
31
+ module.exports = { cmdDetect };
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { log, T, R, D, fs, path, spawnSync, findRepoScript, SUPPORTED_CLIS, recordExperienceEvent } = shared;
4
+
5
+ function cmdDoctor(target) {
6
+ const ai = path.join(target, "ai");
7
+ if (!fs.existsSync(ai)) { log("No 0dai config found. Run: 0dai init"); return; }
8
+ let v = "?", stack = "generic";
9
+ try { v = fs.readFileSync(path.join(ai, "VERSION"), "utf8").trim(); } catch {}
10
+ try { stack = JSON.parse(fs.readFileSync(path.join(ai, "manifest", "discovery.json"), "utf8")).stack || "generic"; } catch {}
11
+
12
+ const W = process.stdout.isTTY ? "\x1b[33m" : ""; // yellow
13
+ const E = process.stdout.isTTY ? "\x1b[31m" : ""; // red
14
+ const G = process.stdout.isTTY ? "\x1b[32m" : ""; // green
15
+ const R2 = process.stdout.isTTY ? "\x1b[0m" : "";
16
+
17
+ // --- ai/ layer checks ---
18
+ const layerChecks = {
19
+ "ai/VERSION": { path: path.join(ai, "VERSION"), sev: "error" },
20
+ "ai/manifest/project.yaml": { path: path.join(ai, "manifest", "project.yaml"), sev: "error" },
21
+ "ai/manifest/commands.yaml": { path: path.join(ai, "manifest", "commands.yaml"), sev: "warn" },
22
+ "ai/manifest/discovery.json": { path: path.join(ai, "manifest", "discovery.json"),sev: "warn" },
23
+ ".claude/settings.json": { path: path.join(target, ".claude", "settings.json"), sev: "warn" },
24
+ "AGENTS.md": { path: path.join(target, "AGENTS.md"), sev: "warn" },
25
+ };
26
+
27
+ // --- credentials checklist ---
28
+ // Detect subscription-based auth (not just env API keys)
29
+ const { execFileSync: _execFile } = require("child_process");
30
+ function cliAuthed(cli) {
31
+ try {
32
+ if (cli === "claude") {
33
+ const out = _execFile("claude", ["auth", "status"], { timeout: 5000 }).toString();
34
+ try { return JSON.parse(out).loggedIn === true; } catch {}
35
+ return out.includes("loggedIn");
36
+ }
37
+ _execFile("which", [cli], { timeout: 2000 });
38
+ return true;
39
+ } catch { return false; }
40
+ }
41
+
42
+ const claudeAuth = cliAuthed("claude");
43
+ const codexAuth = cliAuthed("codex");
44
+
45
+ const credChecks = [
46
+ {
47
+ name: "Claude Code",
48
+ present: claudeAuth || !!process.env.ANTHROPIC_API_KEY,
49
+ sev: (claudeAuth || process.env.ANTHROPIC_API_KEY) ? "ok" : "warn",
50
+ hint: claudeAuth ? "authenticated via subscription" : "run: claude auth login (or set ANTHROPIC_API_KEY)",
51
+ },
52
+ {
53
+ name: "Codex CLI",
54
+ present: codexAuth || !!process.env.OPENAI_API_KEY,
55
+ sev: (codexAuth || process.env.OPENAI_API_KEY) ? "ok" : "warn",
56
+ hint: codexAuth ? "installed (uses ChatGPT subscription)" : "run: npm i -g @openai/codex (or set OPENAI_API_KEY)",
57
+ },
58
+ {
59
+ name: "GITHUB_TOKEN",
60
+ present: !!process.env.GITHUB_TOKEN,
61
+ sev: process.env.GITHUB_TOKEN ? "ok" : "info",
62
+ hint: "Optional — for gh CLI, PR creation",
63
+ },
64
+ ];
65
+
66
+ // Stack-specific creds
67
+ if (stack.includes("vercel") || stack.includes("next")) {
68
+ credChecks.push({ name: "VERCEL_TOKEN", present: !!process.env.VERCEL_TOKEN, sev: process.env.VERCEL_TOKEN ? "ok" : "info", hint: "Optional — for Vercel deployments" });
69
+ }
70
+ if (stack.includes("aws") || stack.includes("lambda") || stack.includes("cdk")) {
71
+ credChecks.push({ name: "AWS_ACCESS_KEY_ID", present: !!process.env.AWS_ACCESS_KEY_ID, sev: process.env.AWS_ACCESS_KEY_ID ? "ok" : "info", hint: "Optional — for AWS deployments" });
72
+ }
73
+ if (stack.includes("gcp") || stack.includes("firebase") || stack.includes("flutter")) {
74
+ credChecks.push({ name: "GCP_CREDENTIALS", present: !!process.env.GOOGLE_APPLICATION_CREDENTIALS, sev: process.env.GOOGLE_APPLICATION_CREDENTIALS ? "ok" : "info", hint: "Optional — for GCP/Firebase" });
75
+ }
76
+
77
+ // --- run checks ---
78
+ let errors = 0, warnings = 0;
79
+ log(`v${v} | stack: ${stack}\n`);
80
+
81
+ const missingConfigs = [];
82
+ console.log(" ai/ layer:");
83
+ for (const [name, { path: p, sev }] of Object.entries(layerChecks)) {
84
+ const exists = fs.existsSync(p);
85
+ if (!exists) {
86
+ sev === "error" ? errors++ : warnings++;
87
+ if (sev === "warn") missingConfigs.push(name);
88
+ }
89
+ const mark = exists ? `${G}ok${R2}` : sev === "error" ? `${E}MISSING${R2}` : `${W}missing${R2}`;
90
+ console.log(` ${mark.padEnd(22)} ${name}`);
91
+ }
92
+ // Explain WHY native configs are missing and what to do
93
+ if (missingConfigs.length > 0) {
94
+ const hasDiscovery = fs.existsSync(path.join(ai, "manifest", "discovery.json"));
95
+ if (hasDiscovery) {
96
+ console.log(`\n ${W}→ Native configs not generated yet.${R2}`);
97
+ console.log(` ${D}Run: 0dai sync --target .${R2}`);
98
+ } else {
99
+ console.log(`\n ${W}→ ai/ layer incomplete — run '0dai init' first.${R2}`);
100
+ }
101
+ }
102
+
103
+ console.log("\n credentials:");
104
+ for (const c of credChecks) {
105
+ if (!c.present && c.sev === "warn") warnings++;
106
+ const mark = c.present ? `${G}ok${R2}` : c.sev === "warn" ? `${W}not set${R2}` : `${D}not set${R2}`;
107
+ const hint = c.present && c.hint.includes("subscription") ? ` ${D}(${c.hint})${R2}` : (!c.present ? `\n ${D}→ ${c.hint}${R2}` : "");
108
+ console.log(` ${mark.padEnd(22)} ${c.name}${hint}`);
109
+ }
110
+
111
+ // --- agent CLIs check ---
112
+ const { execFileSync: _ef2 } = require("child_process");
113
+ let updatesAvailable = 0;
114
+ console.log("\n agent CLIs:");
115
+ for (const cli of SUPPORTED_CLIS) {
116
+ let installed = false, ver = null;
117
+ try {
118
+ const out = _ef2(cli.bin, ["--version"], { timeout: 8000 }).toString().trim();
119
+ installed = true;
120
+ const m = out.match(/(\d+\.\d+\.\d+)/);
121
+ if (m) ver = m[1];
122
+ } catch {}
123
+
124
+ if (installed) {
125
+ let latest = null;
126
+ if (cli.pkg) {
127
+ try {
128
+ const npmOut = _ef2("npm", ["view", cli.pkg, "version"], { timeout: 5000 }).toString().trim();
129
+ if (npmOut.match(/^\d+\.\d+\.\d+$/)) latest = npmOut;
130
+ } catch {}
131
+ }
132
+ if (latest && ver && latest !== ver) {
133
+ updatesAvailable++;
134
+ console.log(` ${W}update${R2} ${cli.name} ${D}${ver} → ${latest}${R2}`);
135
+ console.log(` ${D}→ ${cli.install}${R2}`);
136
+ } else {
137
+ console.log(` ${G}ok${R2} ${cli.name}${ver ? ` ${D}v${ver}${R2}` : ""}`);
138
+ }
139
+ } else {
140
+ console.log(` ${D}—${R2} ${cli.name} ${D}not installed${R2}`);
141
+ console.log(` ${D}→ ${cli.install}${cli.altAuth ? ` (or ${cli.altAuth})` : ""}${R2}`);
142
+ }
143
+ }
144
+ if (updatesAvailable) {
145
+ console.log(`\n ${D}Run: 0dai update${R2} to update all`);
146
+ }
147
+
148
+ // --- swarm check ---
149
+ const swarmDir = path.join(ai, "swarm");
150
+ const countDir = (d) => { try { return fs.readdirSync(d).filter(f => f.endsWith(".json")).length; } catch { return 0; } };
151
+ const qCount = countDir(path.join(swarmDir, "queue"));
152
+ const dCount = countDir(path.join(swarmDir, "done"));
153
+ if (qCount || dCount) {
154
+ console.log(`\n swarm: ${qCount} queued, ${dCount} done`);
155
+ if (qCount) console.log(` ${W}→ run '0dai reflect' to review pending tasks${R2}`);
156
+ }
157
+
158
+ const summary = errors ? `${E}${errors} error(s)${R2}` : warnings ? `${W}${warnings} warning(s)${R2}` : `${G}healthy${R2}`;
159
+ console.log(`\n status: ${summary}`);
160
+ recordExperienceEvent(target, {
161
+ event_type: "doctor_run",
162
+ agent: "cli",
163
+ model: "0dai-cli",
164
+ effort: "low",
165
+ task: { goal: "doctor health check", task_type: "review", result: errors ? "failure" : "success", elapsed_seconds: 0, cost_usd: 0 },
166
+ context: { stack, files_touched: 0, tests_passed: errors === 0 },
167
+ quality: {
168
+ lint_clean: errors === 0,
169
+ no_secrets: true,
170
+ commit_message_valid: true,
171
+ acceptance_criteria_met: errors === 0,
172
+ review_needed: warnings > 0,
173
+ },
174
+ });
175
+ if (errors) process.exitCode = 1;
176
+
177
+ // Drift summary (lightweight — full report via --drift flag)
178
+ try {
179
+ const ds = findRepoScript(target, "drift_detector.py");
180
+ if (ds) {
181
+ const dr = spawnSync("python3", [ds, "report", "--target", target],
182
+ { stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 });
183
+ if (dr.stdout && dr.stdout.includes("MODIFIED")) {
184
+ const lines = dr.stdout.trim().split("\n");
185
+ const driftCount = lines.filter(l => l.includes("MODIFIED") || l.includes("CONTRADICTS")).length;
186
+ if (driftCount > 0) {
187
+ console.log(`\n config drift: ${driftCount} issue(s) — run: 0dai doctor --drift`);
188
+ }
189
+ }
190
+ }
191
+ } catch {}
192
+ }
193
+
194
+ module.exports = { cmdDoctor };
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ const shared = require("../shared");
3
+ const { log, D, R, spawnSync, findRepoScript } = shared;
4
+
5
+ function cmdExperience(target, sub, args) {
6
+ const experienceScript = findRepoScript(target, "experience_pipeline.py");
7
+ if (!experienceScript) {
8
+ log("experience pipeline unavailable in this environment");
9
+ console.log(` ${D}Expected scripts/experience_pipeline.py in repo checkout${R}`);
10
+ process.exit(1);
11
+ }
12
+
13
+ const command = sub || "list";
14
+ const forwarded = [experienceScript, command, "--target", target];
15
+ if (command === "list") {
16
+ if (args.includes("--json")) forwarded.push("--json");
17
+ const sinceIdx = args.indexOf("--since");
18
+ if (sinceIdx >= 0 && args[sinceIdx + 1]) forwarded.push("--since", args[sinceIdx + 1]);
19
+ const agentIdx = args.indexOf("--agent");
20
+ if (agentIdx >= 0 && args[agentIdx + 1]) forwarded.push("--agent", args[agentIdx + 1]);
21
+ const typeIdx = args.indexOf("--type");
22
+ if (typeIdx >= 0 && args[typeIdx + 1]) forwarded.push("--type", args[typeIdx + 1]);
23
+ const resultIdx = args.indexOf("--result");
24
+ if (resultIdx >= 0 && args[resultIdx + 1]) forwarded.push("--result", args[resultIdx + 1]);
25
+ const limitIdx = args.indexOf("--limit");
26
+ if (limitIdx >= 0 && args[limitIdx + 1]) forwarded.push("--limit", args[limitIdx + 1]);
27
+ } else if (command === "stats") {
28
+ if (args.includes("--json")) forwarded.push("--json");
29
+ const periodIdx = args.indexOf("--period");
30
+ if (periodIdx >= 0 && args[periodIdx + 1]) forwarded.push("--period", args[periodIdx + 1]);
31
+ const byIdx = args.indexOf("--by");
32
+ if (byIdx >= 0 && args[byIdx + 1]) forwarded.push("--by", args[byIdx + 1]);
33
+ } else if (command === "warnings") {
34
+ const detectorScript = findRepoScript(target, "anti_pattern_detector.py");
35
+ if (!detectorScript) { log("anti-pattern detector unavailable"); process.exit(1); }
36
+ const fwd = [detectorScript, "warnings", "--target", target];
37
+ if (args.includes("--json")) fwd.push("--json");
38
+ if (args.includes("--refresh")) fwd.push("--refresh");
39
+ if (args.includes("--verbose")) fwd.push("--verbose");
40
+ const sevIdx = args.indexOf("--severity");
41
+ if (sevIdx >= 0 && args[sevIdx + 1]) fwd.push("--severity", args[sevIdx + 1]);
42
+ const wr = spawnSync("python3", fwd, { stdio: "inherit" });
43
+ if (typeof wr.status === "number") process.exit(wr.status);
44
+ process.exit(1);
45
+ } else if (command === "dismiss") {
46
+ const detectorScript = findRepoScript(target, "anti_pattern_detector.py");
47
+ if (!detectorScript) { log("anti-pattern detector unavailable"); process.exit(1); }
48
+ const patternId = args.find(a => a && !a.startsWith("-")) || "";
49
+ if (!patternId) { console.log("Usage: 0dai experience dismiss <pattern_id>"); process.exit(1); }
50
+ const fwd = [detectorScript, "dismiss", patternId, "--target", target];
51
+ if (args.includes("--json")) fwd.push("--json");
52
+ const dr = spawnSync("python3", fwd, { stdio: "inherit" });
53
+ if (typeof dr.status === "number") process.exit(dr.status);
54
+ process.exit(1);
55
+ } else {
56
+ console.log("Usage: 0dai experience [list|stats|warnings|dismiss]");
57
+ process.exit(1);
58
+ }
59
+
60
+ const result = spawnSync("python3", forwarded, { stdio: "inherit" });
61
+ if (typeof result.status === "number") process.exit(result.status);
62
+ process.exit(1);
63
+ }
64
+
65
+ module.exports = { cmdExperience };