@0dai-dev/cli 4.3.6 → 4.3.7
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/README.md +12 -11
- package/bin/0dai.js +127 -30
- package/lib/ai/manifest/mcp-exposure-contract.json +121 -0
- package/lib/ai/meta/manifest/mcp-tool-tiers.json +435 -0
- package/lib/ai/registry/mcp-catalog.json +98 -0
- package/lib/commands/auth.js +2 -1
- package/lib/commands/compliance.js +1 -1
- package/lib/commands/doctor.js +506 -12
- package/lib/commands/experience.js +40 -5
- package/lib/commands/feedback.js +157 -15
- package/lib/commands/gh.js +26 -0
- package/lib/commands/graph.js +9 -4
- package/lib/commands/heatmap.js +1 -1
- package/lib/commands/init.js +209 -27
- package/lib/commands/mcp.js +111 -33
- package/lib/commands/models.js +138 -41
- package/lib/commands/provider.js +30 -59
- package/lib/commands/quota.js +1 -1
- package/lib/commands/receipt.js +1 -1
- package/lib/commands/run.js +14 -6
- package/lib/commands/runner.js +31 -1
- package/lib/commands/status.js +38 -10
- package/lib/commands/swarm.js +130 -12
- package/lib/commands/update.js +184 -38
- package/lib/commands/usage.js +1 -1
- package/lib/commands/validate.js +32 -3
- package/lib/commands/vault.js +43 -8
- package/lib/python/__init__.py +0 -0
- package/lib/python/agent_quotas.py +525 -0
- package/lib/python/anomaly_alert.py +397 -0
- package/lib/python/anti_pattern_detector.py +799 -0
- package/lib/python/auth.py +443 -0
- package/lib/python/capi_profile_guard.py +477 -0
- package/lib/python/compliance_report.py +581 -0
- package/lib/python/drift_detector.py +388 -0
- package/lib/python/experience_pipeline.py +1130 -0
- package/lib/python/graph.py +19 -0
- package/lib/python/graph_core.py +293 -0
- package/lib/python/graph_io.py +179 -0
- package/lib/python/graph_legacy.py +2052 -0
- package/lib/python/graph_legacy_helpers.py +221 -0
- package/lib/python/graph_outcomes_core.py +85 -0
- package/lib/python/graph_queries.py +171 -0
- package/lib/python/graph_slice.py +198 -0
- package/lib/python/graph_slicer.py +576 -0
- package/lib/python/graph_slicer_cli.py +60 -0
- package/lib/python/graph_validation.py +64 -0
- package/lib/python/heatmap.py +934 -0
- package/lib/python/json_utils.py +193 -0
- package/lib/python/mcp_exposure_check.py +247 -0
- package/lib/python/model_router.py +1434 -0
- package/lib/python/project_manager.py +621 -0
- package/lib/python/provider_profiles.py +1618 -0
- package/lib/python/provider_registry.py +1211 -0
- package/lib/python/provider_registry_cli.py +125 -0
- package/lib/python/receipt_png.py +727 -0
- package/lib/python/structural_memory.py +325 -0
- package/lib/python/swarm_cost.py +177 -0
- package/lib/python/usage_ledger.py +569 -0
- package/lib/scripts/mcp_tier_config.py +240 -0
- package/lib/shared.js +95 -12
- package/lib/tui/index.mjs +35174 -0
- package/lib/utils/activation_telemetry.js +1 -4
- package/lib/utils/constants.js +7 -1
- package/lib/utils/identity.js +184 -0
- package/lib/utils/mcp-auth.js +81 -15
- package/lib/utils/plan.js +1 -1
- package/lib/vault/index.js +19 -3
- package/lib/vault/storage.js +21 -2
- package/lib/wizard.js +5 -2
- package/package.json +9 -3
- package/scripts/build-python-bundle.js +106 -0
- package/scripts/build-tui.js +14 -1
- package/scripts/harvest_experience.py +523 -0
- package/scripts/postinstall.js +15 -9
package/lib/commands/doctor.js
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const shared = require("../shared");
|
|
3
|
-
const {
|
|
3
|
+
const {
|
|
4
|
+
log,
|
|
5
|
+
T,
|
|
6
|
+
R,
|
|
7
|
+
D,
|
|
8
|
+
fs,
|
|
9
|
+
path,
|
|
10
|
+
spawnSync,
|
|
11
|
+
findRepoScript,
|
|
12
|
+
repoScriptCandidates,
|
|
13
|
+
resolvePythonScript,
|
|
14
|
+
SUPPORTED_CLIS,
|
|
15
|
+
cliDisplayName,
|
|
16
|
+
recordExperienceEvent,
|
|
17
|
+
} = shared;
|
|
4
18
|
|
|
5
19
|
const LOCAL_LAYER_CHECKS = Object.freeze([
|
|
6
20
|
["ai/VERSION", "ai/VERSION", "error"],
|
|
7
21
|
["ai/manifest/project.yaml", "ai/manifest/project.yaml", "error"],
|
|
8
22
|
["ai/manifest/discovery.json", "ai/manifest/discovery.json", "warn"],
|
|
9
23
|
["ai/manifest/commands.yaml", "ai/manifest/commands.yaml", "warn"],
|
|
24
|
+
["ai/manifest/current_state.json", "ai/manifest/current_state.json", "warn"],
|
|
25
|
+
["ai/manifest/current_task.json", "ai/manifest/current_task.json", "warn"],
|
|
10
26
|
[".claude/settings.json", ".claude/settings.json", "warn"],
|
|
11
27
|
["AGENTS.md", "AGENTS.md", "warn"],
|
|
12
28
|
]);
|
|
@@ -25,7 +41,7 @@ function nodePtyProbe() {
|
|
|
25
41
|
name: "node-pty",
|
|
26
42
|
present: false,
|
|
27
43
|
sev: "warn",
|
|
28
|
-
hint: "install node-pty
|
|
44
|
+
hint: "install node-pty in the 0dai CLI package for terminal sessions",
|
|
29
45
|
};
|
|
30
46
|
}
|
|
31
47
|
}
|
|
@@ -48,6 +64,149 @@ function ghostAuthStatus(home = process.env.HOME || process.env.USERPROFILE || "
|
|
|
48
64
|
};
|
|
49
65
|
}
|
|
50
66
|
|
|
67
|
+
function _ghProbeEnv(env, configDir = "", unsetConfigDir = false) {
|
|
68
|
+
const next = { ...process.env, ...(env || {}) };
|
|
69
|
+
if (unsetConfigDir) delete next.GH_CONFIG_DIR;
|
|
70
|
+
else if (configDir) next.GH_CONFIG_DIR = configDir;
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _probeGhIdentity(spec, options = {}) {
|
|
75
|
+
const runner = options.spawnSync || spawnSync;
|
|
76
|
+
const env = options.env || process.env;
|
|
77
|
+
const timeout = options.timeout || 5000;
|
|
78
|
+
const configDir = spec.configDir || "";
|
|
79
|
+
const expectedLogin = spec.expectedLogin || "";
|
|
80
|
+
const required = !!spec.required;
|
|
81
|
+
const check = {
|
|
82
|
+
name: spec.name,
|
|
83
|
+
role: spec.role,
|
|
84
|
+
ok: false,
|
|
85
|
+
sev: "info",
|
|
86
|
+
status: "unknown",
|
|
87
|
+
login: "",
|
|
88
|
+
expected_login: expectedLogin,
|
|
89
|
+
config_dir: configDir,
|
|
90
|
+
configured: true,
|
|
91
|
+
hint: "",
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (configDir && !fs.existsSync(configDir)) {
|
|
95
|
+
check.configured = false;
|
|
96
|
+
check.status = "missing_config";
|
|
97
|
+
check.sev = required ? "warn" : "info";
|
|
98
|
+
check.hint = required
|
|
99
|
+
? `configured gh identity dir is missing: ${configDir}`
|
|
100
|
+
: `not configured: ${configDir}`;
|
|
101
|
+
return check;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
result = runner("gh", ["api", "user", "--jq", ".login"], {
|
|
107
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
108
|
+
encoding: "utf8",
|
|
109
|
+
timeout,
|
|
110
|
+
env: _ghProbeEnv(env, configDir, !!spec.unsetConfigDir),
|
|
111
|
+
});
|
|
112
|
+
} catch (error) {
|
|
113
|
+
check.status = "probe_failed";
|
|
114
|
+
check.sev = configDir || required ? "warn" : "info";
|
|
115
|
+
check.hint = String(error && error.message ? error.message : error).split("\n").slice(-1)[0] || "gh probe failed";
|
|
116
|
+
return check;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (result && result.error && result.error.code === "ENOENT") {
|
|
120
|
+
check.status = "gh_missing";
|
|
121
|
+
check.sev = configDir || required ? "warn" : "info";
|
|
122
|
+
check.hint = "gh CLI not installed";
|
|
123
|
+
return check;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const exitCode = result && typeof result.status === "number" ? result.status : null;
|
|
127
|
+
const login = String((result && result.stdout) || "").trim();
|
|
128
|
+
if (exitCode !== 0 || !login) {
|
|
129
|
+
check.status = "not_authenticated";
|
|
130
|
+
check.sev = configDir || required ? "warn" : "info";
|
|
131
|
+
check.hint = configDir
|
|
132
|
+
? `token invalid or expired for ${configDir}; re-auth ${expectedLogin || "the configured review identity"} with gh`
|
|
133
|
+
: "default gh CLI identity is not authenticated; run: gh auth login";
|
|
134
|
+
if (exitCode !== null) check.exit_code = exitCode;
|
|
135
|
+
return check;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
check.login = login;
|
|
139
|
+
if (expectedLogin && login !== expectedLogin) {
|
|
140
|
+
check.status = "login_mismatch";
|
|
141
|
+
check.sev = "warn";
|
|
142
|
+
check.hint = `resolves to '${login}', expected '${expectedLogin}'; re-auth the fenced gh config dir`;
|
|
143
|
+
return check;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
check.ok = true;
|
|
147
|
+
check.sev = "ok";
|
|
148
|
+
check.status = "ok";
|
|
149
|
+
check.hint = expectedLogin
|
|
150
|
+
? `authenticated as expected ${expectedLogin}`
|
|
151
|
+
: `authenticated as ${login}`;
|
|
152
|
+
return check;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function collectGitHubIdentityPayload(target, options = {}) {
|
|
156
|
+
const env = options.env || process.env;
|
|
157
|
+
const primaryDir = env.OPERATOR_GH_REVIEW_CONFIG_DIR || path.join(target, ".0dai", "gh-lead-0dai-review");
|
|
158
|
+
const primaryLogin = env.OPERATOR_GH_REVIEW_EXPECTED_LOGIN || "pq1-dev";
|
|
159
|
+
const fallbackDir = env.OPERATOR_GH_REVIEW_FALLBACK_CONFIG_DIR || "";
|
|
160
|
+
const fallbackLogin = env.OPERATOR_GH_REVIEW_FALLBACK_LOGIN || "";
|
|
161
|
+
const legacyPq1Dir = "/root/.config/gh-pq1";
|
|
162
|
+
const specs = [
|
|
163
|
+
{
|
|
164
|
+
role: "default",
|
|
165
|
+
name: "default gh identity",
|
|
166
|
+
unsetConfigDir: true,
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
const seenDirs = new Set();
|
|
170
|
+
|
|
171
|
+
function addConfig(role, name, configDir, expectedLogin, required = false) {
|
|
172
|
+
if (!configDir || seenDirs.has(configDir)) return;
|
|
173
|
+
seenDirs.add(configDir);
|
|
174
|
+
specs.push({ role, name, configDir, expectedLogin, required });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
addConfig(
|
|
178
|
+
"env-gh-config",
|
|
179
|
+
"GH_CONFIG_DIR identity",
|
|
180
|
+
env.GH_CONFIG_DIR || "",
|
|
181
|
+
env.GH_CONFIG_DIR === legacyPq1Dir ? "pq1-dev" : "",
|
|
182
|
+
true,
|
|
183
|
+
);
|
|
184
|
+
addConfig(
|
|
185
|
+
"review-primary",
|
|
186
|
+
"pq1-dev review identity",
|
|
187
|
+
primaryDir,
|
|
188
|
+
primaryLogin,
|
|
189
|
+
Boolean(env.OPERATOR_GH_REVIEW_CONFIG_DIR),
|
|
190
|
+
);
|
|
191
|
+
if (fallbackDir || fallbackLogin) {
|
|
192
|
+
addConfig("review-fallback", "fallback review identity", fallbackDir, fallbackLogin, true);
|
|
193
|
+
}
|
|
194
|
+
if (legacyPq1Dir !== primaryDir && (env.GH_CONFIG_DIR === legacyPq1Dir || fs.existsSync(legacyPq1Dir))) {
|
|
195
|
+
addConfig("review-legacy-pq1", "legacy pq1-dev review identity", legacyPq1Dir, "pq1-dev", true);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const checks = specs.map((spec) => _probeGhIdentity(spec, options));
|
|
199
|
+
const warnings = checks.filter((check) => check.sev === "warn" && !check.ok).length;
|
|
200
|
+
return {
|
|
201
|
+
status: warnings ? "warn" : "ok",
|
|
202
|
+
warnings,
|
|
203
|
+
checks,
|
|
204
|
+
next_step: warnings
|
|
205
|
+
? "Rotate or remove stale fenced gh review credentials; default gh identity is separate from pq1-dev review identity."
|
|
206
|
+
: "GitHub identity diagnostics are non-blocking.",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
51
210
|
function collectLayerChecks(target) {
|
|
52
211
|
return LOCAL_LAYER_CHECKS.map(([name, relPath, missingSev]) => {
|
|
53
212
|
const fullPath = path.join(target, relPath);
|
|
@@ -61,19 +220,290 @@ function collectLayerChecks(target) {
|
|
|
61
220
|
});
|
|
62
221
|
}
|
|
63
222
|
|
|
64
|
-
function
|
|
223
|
+
function parseReleaseVersion(value) {
|
|
224
|
+
const match = String(value || "").trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
225
|
+
if (!match) return null;
|
|
226
|
+
return match.slice(1, 4).map((part) => Number(part));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function compareReleaseVersions(a, b) {
|
|
230
|
+
const left = parseReleaseVersion(a);
|
|
231
|
+
const right = parseReleaseVersion(b);
|
|
232
|
+
if (!left || !right) return null;
|
|
233
|
+
return left[0] - right[0] || left[1] - right[1] || left[2] - right[2];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function collectLayerVersionFreshness(target, cliVersion = shared.VERSION) {
|
|
237
|
+
const versionPath = path.join(target, "ai", "VERSION");
|
|
238
|
+
let current = "";
|
|
239
|
+
try { current = fs.readFileSync(versionPath, "utf8").trim(); } catch {}
|
|
240
|
+
const cmp = compareReleaseVersions(current, cliVersion);
|
|
241
|
+
if (!current) {
|
|
242
|
+
return {
|
|
243
|
+
status: "missing",
|
|
244
|
+
ok: false,
|
|
245
|
+
sev: "error",
|
|
246
|
+
current: "",
|
|
247
|
+
expected: cliVersion,
|
|
248
|
+
hint: `missing: ${versionPath}`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (cmp === null) {
|
|
252
|
+
return {
|
|
253
|
+
status: "unknown",
|
|
254
|
+
ok: true,
|
|
255
|
+
sev: "info",
|
|
256
|
+
current,
|
|
257
|
+
expected: cliVersion,
|
|
258
|
+
hint: "ai/VERSION is not a release semver; skipping freshness check",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (cmp < 0) {
|
|
262
|
+
return {
|
|
263
|
+
status: "stale",
|
|
264
|
+
ok: false,
|
|
265
|
+
sev: "warn",
|
|
266
|
+
current,
|
|
267
|
+
expected: cliVersion,
|
|
268
|
+
hint: `ai/VERSION stale: ${current} -> ${cliVersion}; run: 0dai sync`,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (cmp > 0) {
|
|
272
|
+
return {
|
|
273
|
+
status: "newer",
|
|
274
|
+
ok: true,
|
|
275
|
+
sev: "info",
|
|
276
|
+
current,
|
|
277
|
+
expected: cliVersion,
|
|
278
|
+
hint: `project layer ${current} is newer than CLI ${cliVersion}; not recommending sync`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
65
281
|
return {
|
|
282
|
+
status: "ok",
|
|
283
|
+
ok: true,
|
|
284
|
+
sev: "ok",
|
|
285
|
+
current,
|
|
286
|
+
expected: cliVersion,
|
|
287
|
+
hint: "ai/VERSION matches installed CLI",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function collectDriftPayload(target, options = {}) {
|
|
292
|
+
const scriptName = "drift_detector.py";
|
|
293
|
+
const candidates = options.candidates || repoScriptCandidates(target, scriptName);
|
|
294
|
+
const findScript = options.findRepoScript || resolvePythonScript;
|
|
295
|
+
const script = Object.prototype.hasOwnProperty.call(options, "script")
|
|
296
|
+
? options.script
|
|
297
|
+
: findScript(target, scriptName);
|
|
298
|
+
|
|
299
|
+
if (!script) {
|
|
300
|
+
return {
|
|
301
|
+
status: "unavailable",
|
|
302
|
+
available: false,
|
|
303
|
+
helper: scriptName,
|
|
304
|
+
reason: "helper_not_found",
|
|
305
|
+
message: "drift detector unavailable in this environment",
|
|
306
|
+
checked_paths: candidates,
|
|
307
|
+
next_step: "Run from a 0dai repo checkout or sync/install the managed project helper scripts, then rerun: 0dai doctor --drift --json",
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const runner = options.spawnSync || spawnSync;
|
|
312
|
+
const result = runner("python3", [script, "report", "--target", target], {
|
|
313
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
314
|
+
encoding: "utf8",
|
|
315
|
+
timeout: options.timeout || 15000,
|
|
316
|
+
});
|
|
317
|
+
const exitCode = typeof result.status === "number" ? result.status : null;
|
|
318
|
+
const stdout = String(result.stdout || "").trim();
|
|
319
|
+
const stderr = String(result.stderr || "").trim();
|
|
320
|
+
return {
|
|
321
|
+
status: exitCode === 0 ? "ok" : "error",
|
|
322
|
+
available: true,
|
|
323
|
+
helper: scriptName,
|
|
324
|
+
script,
|
|
325
|
+
command: `python3 ${script} report --target ${target}`,
|
|
326
|
+
exit_code: exitCode,
|
|
327
|
+
report: stdout,
|
|
328
|
+
...(stderr ? { stderr } : {}),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function _detectAgentKind(env = process.env) {
|
|
333
|
+
return env.ODAI_AGENT_KIND || env.ODAI_AGENT || env.CLAUDE_AGENT_KIND || "default";
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function _readMcpConfigSummary(target) {
|
|
337
|
+
const configPath = path.join(target, ".mcp.json");
|
|
338
|
+
const summary = {
|
|
339
|
+
path: "",
|
|
340
|
+
configured: false,
|
|
341
|
+
servers: [],
|
|
342
|
+
odai_servers: [],
|
|
343
|
+
};
|
|
344
|
+
if (!fs.existsSync(configPath)) return summary;
|
|
345
|
+
summary.path = configPath;
|
|
346
|
+
try {
|
|
347
|
+
const data = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
348
|
+
const servers = data && data.mcpServers && typeof data.mcpServers === "object"
|
|
349
|
+
? Object.keys(data.mcpServers)
|
|
350
|
+
: [];
|
|
351
|
+
summary.servers = servers;
|
|
352
|
+
summary.odai_servers = servers.filter((name) => name.toLowerCase().includes("0dai"));
|
|
353
|
+
summary.configured = summary.odai_servers.length > 0;
|
|
354
|
+
} catch {
|
|
355
|
+
summary.configured = false;
|
|
356
|
+
}
|
|
357
|
+
return summary;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function _exposureNextStep(payload) {
|
|
361
|
+
if (!payload.configured) return "run: 0dai sync --target .";
|
|
362
|
+
if (payload.status === "green") return "0dai MCP tools exposed";
|
|
363
|
+
const agent = String(payload.agent || "agent").toLowerCase();
|
|
364
|
+
const reconnect = agent.includes("codex")
|
|
365
|
+
? "restart/reconnect Codex CLI"
|
|
366
|
+
: agent.includes("claude")
|
|
367
|
+
? "restart/reconnect Claude Code"
|
|
368
|
+
: "restart/reconnect active agent CLI";
|
|
369
|
+
if (payload.status === "yellow" || payload.status === "red") {
|
|
370
|
+
return `${reconnect}; fallback: 0dai mcp doctor --json`;
|
|
371
|
+
}
|
|
372
|
+
return `set ODAI_EXPOSED_MCP_TOOLS; fallback: 0dai mcp doctor --json`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function collectMcpExposurePayload(target, options = {}) {
|
|
376
|
+
const env = options.env || process.env;
|
|
377
|
+
const agent = options.agent || _detectAgentKind(env);
|
|
378
|
+
const config = _readMcpConfigSummary(target);
|
|
379
|
+
const scriptName = "mcp_exposure_check.py";
|
|
380
|
+
const script = Object.prototype.hasOwnProperty.call(options, "script")
|
|
381
|
+
? options.script
|
|
382
|
+
: resolvePythonScript(target, scriptName);
|
|
383
|
+
const observedRaw = String(env.ODAI_EXPOSED_MCP_TOOLS || "").trim();
|
|
384
|
+
const payload = {
|
|
385
|
+
agent,
|
|
386
|
+
status: "unknown",
|
|
387
|
+
configured: config.configured,
|
|
388
|
+
config_path: config.path,
|
|
389
|
+
servers: config.servers,
|
|
390
|
+
odai_servers: config.odai_servers,
|
|
391
|
+
observed_count: 0,
|
|
392
|
+
expected_tool_count: 0,
|
|
393
|
+
missing_required: [],
|
|
394
|
+
missing_recommended: [],
|
|
395
|
+
helper: script || "",
|
|
396
|
+
reason: observedRaw ? "" : "runtime_tool_list_not_reported",
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
if (!script) {
|
|
400
|
+
payload.reason = "helper_not_found";
|
|
401
|
+
payload.next_step = _exposureNextStep(payload);
|
|
402
|
+
return payload;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const runner = options.spawnSync || spawnSync;
|
|
406
|
+
let result;
|
|
407
|
+
try {
|
|
408
|
+
result = runner("python3", [script, "--agent", agent], {
|
|
409
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
410
|
+
encoding: "utf8",
|
|
411
|
+
timeout: options.timeout || 5000,
|
|
412
|
+
env: { ...process.env, ...env },
|
|
413
|
+
});
|
|
414
|
+
} catch (error) {
|
|
415
|
+
payload.reason = "helper_failed";
|
|
416
|
+
payload.stderr = String(error && error.message ? error.message : error).split("\n").slice(-1)[0] || "";
|
|
417
|
+
payload.next_step = _exposureNextStep(payload);
|
|
418
|
+
return payload;
|
|
419
|
+
}
|
|
420
|
+
const stdout = String(result.stdout || "").trim();
|
|
421
|
+
const stderr = String(result.stderr || "").trim();
|
|
422
|
+
if (result.status !== 0 && !stdout) {
|
|
423
|
+
payload.reason = "helper_failed";
|
|
424
|
+
payload.stderr = stderr.split("\n").slice(-1)[0] || "";
|
|
425
|
+
payload.next_step = _exposureNextStep(payload);
|
|
426
|
+
return payload;
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
const checked = JSON.parse(stdout);
|
|
430
|
+
Object.assign(payload, {
|
|
431
|
+
status: checked.status || "unknown",
|
|
432
|
+
blocking: !!checked.blocking,
|
|
433
|
+
anomaly_type: checked.anomaly_type || "",
|
|
434
|
+
observed_count: Number(checked.observed_count || 0),
|
|
435
|
+
expected_tool_count: Number(checked.expected_tool_count || 0),
|
|
436
|
+
required: checked.required || [],
|
|
437
|
+
recommended: checked.recommended || [],
|
|
438
|
+
missing_required: checked.missing_required || [],
|
|
439
|
+
missing_recommended: checked.missing_recommended || [],
|
|
440
|
+
observed_required: checked.observed_required || [],
|
|
441
|
+
observed_recommended: checked.observed_recommended || [],
|
|
442
|
+
contract_tool_count: Number(checked.contract_tool_count || 0),
|
|
443
|
+
tier_config_tool_count: Number(checked.tier_config_tool_count || 0),
|
|
444
|
+
contract_tier_count_match: checked.contract_tier_count_match,
|
|
445
|
+
reason: checked.status === "unknown" && !observedRaw ? "runtime_tool_list_not_reported" : "",
|
|
446
|
+
});
|
|
447
|
+
} catch {
|
|
448
|
+
payload.reason = "helper_output_unparseable";
|
|
449
|
+
}
|
|
450
|
+
payload.next_step = _exposureNextStep(payload);
|
|
451
|
+
return payload;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function formatMcpExposureLine(payload) {
|
|
455
|
+
const serverText = payload.configured
|
|
456
|
+
? `configured: ${payload.odai_servers.join(", ")}`
|
|
457
|
+
: "not configured";
|
|
458
|
+
const observed = Number(payload.observed_count || 0);
|
|
459
|
+
const expected = Number(payload.expected_tool_count || 0);
|
|
460
|
+
const observedText = observed ? `${observed}/${expected || "?"} observed` : "not reported by runtime";
|
|
461
|
+
return `${payload.status} (${payload.agent}) — ${serverText}; ${observedText}`;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function printDriftReport(target, options = {}) {
|
|
465
|
+
const payload = collectDriftPayload(target, options);
|
|
466
|
+
console.log("\n drift report:");
|
|
467
|
+
if (!payload.available) {
|
|
468
|
+
console.log(` ${D}${payload.message}${R}`);
|
|
469
|
+
console.log(` ${D}reason: ${payload.helper} was not found in the script lookup paths${R}`);
|
|
470
|
+
console.log(` ${D}checked:${R}`);
|
|
471
|
+
for (const candidate of payload.checked_paths) console.log(` ${D}- ${candidate}${R}`);
|
|
472
|
+
console.log(` ${D}next: ${payload.next_step}${R}`);
|
|
473
|
+
return payload;
|
|
474
|
+
}
|
|
475
|
+
if (payload.report) console.log(payload.report);
|
|
476
|
+
else console.log(` ${D}drift detector returned no output${R}`);
|
|
477
|
+
if (payload.status === "error") {
|
|
478
|
+
console.log(` ${D}drift detector exited with status ${payload.exit_code ?? "unknown"}${R}`);
|
|
479
|
+
if (payload.stderr) console.log(` ${D}${payload.stderr}${R}`);
|
|
480
|
+
}
|
|
481
|
+
return payload;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function collectDoctorPayload(target, options = {}) {
|
|
485
|
+
const payload = {
|
|
66
486
|
binary: "node",
|
|
67
487
|
binary_version: shared.VERSION,
|
|
68
488
|
checks: collectLayerChecks(target),
|
|
489
|
+
layer_version: collectLayerVersionFreshness(target),
|
|
490
|
+
mcp_exposure: collectMcpExposurePayload(target, options.mcpExposureOptions || {}),
|
|
491
|
+
node_pty: nodePtyProbe(),
|
|
69
492
|
};
|
|
493
|
+
if (options.drift) {
|
|
494
|
+
payload.drift = collectDriftPayload(target, options.driftOptions || {});
|
|
495
|
+
}
|
|
496
|
+
return payload;
|
|
70
497
|
}
|
|
71
498
|
|
|
72
499
|
function cmdDoctor(target, options = {}) {
|
|
73
500
|
const ai = path.join(target, "ai");
|
|
74
501
|
if (!fs.existsSync(ai)) { log("No 0dai config found. Run: 0dai init"); return; }
|
|
75
502
|
if (options.json) {
|
|
76
|
-
const payload = collectDoctorPayload(target
|
|
503
|
+
const payload = collectDoctorPayload(target, {
|
|
504
|
+
drift: options.drift,
|
|
505
|
+
driftOptions: options.driftOptions,
|
|
506
|
+
});
|
|
77
507
|
console.log(JSON.stringify(payload, null, 2));
|
|
78
508
|
return payload;
|
|
79
509
|
}
|
|
@@ -152,6 +582,12 @@ function cmdDoctor(target, options = {}) {
|
|
|
152
582
|
const mark = check.ok ? `${G}ok${R2}` : check.sev === "error" ? `${E}MISSING${R2}` : `${W}missing${R2}`;
|
|
153
583
|
console.log(` ${mark.padEnd(22)} ${check.name}`);
|
|
154
584
|
}
|
|
585
|
+
const layerVersion = collectLayerVersionFreshness(target);
|
|
586
|
+
if (layerVersion.status === "stale") {
|
|
587
|
+
warnings++;
|
|
588
|
+
console.log(` ${W}warn${R2}`.padEnd(22) + ` ai/VERSION stale ${D}${layerVersion.current} → ${layerVersion.expected}${R2}`);
|
|
589
|
+
console.log(` ${D}→ run: 0dai sync${R2}`);
|
|
590
|
+
}
|
|
155
591
|
// Explain WHY native configs are missing and what to do
|
|
156
592
|
if (missingConfigs.length > 0) {
|
|
157
593
|
const hasDiscovery = fs.existsSync(path.join(ai, "manifest", "discovery.json"));
|
|
@@ -171,9 +607,48 @@ function cmdDoctor(target, options = {}) {
|
|
|
171
607
|
console.log(` ${mark.padEnd(22)} ${c.name}${hint}`);
|
|
172
608
|
}
|
|
173
609
|
|
|
610
|
+
const githubIdentities = collectGitHubIdentityPayload(target, options.githubIdentityOptions || {});
|
|
611
|
+
console.log("\n GitHub identities:");
|
|
612
|
+
for (const check of githubIdentities.checks) {
|
|
613
|
+
if (!check.ok && check.sev === "warn") warnings++;
|
|
614
|
+
const mark = check.ok
|
|
615
|
+
? `${G}ok${R2}`
|
|
616
|
+
: check.sev === "warn"
|
|
617
|
+
? `${W}warn${R2}`
|
|
618
|
+
: `${D}not set${R2}`;
|
|
619
|
+
const expected = check.expected_login ? ` ${D}(expected ${check.expected_login})${R2}` : "";
|
|
620
|
+
const detail = check.ok && check.login ? ` ${D}(${check.login})${R2}` : "";
|
|
621
|
+
const hint = check.ok ? "" : `\n ${D}→ ${check.hint}${R2}`;
|
|
622
|
+
console.log(` ${mark.padEnd(22)} ${check.name}${expected}${detail}${hint}`);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const mcpExposure = collectMcpExposurePayload(target);
|
|
626
|
+
console.log("\n MCP exposure:");
|
|
627
|
+
const mcpMark = mcpExposure.status === "green"
|
|
628
|
+
? `${G}ok${R2}`
|
|
629
|
+
: mcpExposure.status === "red"
|
|
630
|
+
? `${E}missing${R2}`
|
|
631
|
+
: mcpExposure.status === "yellow"
|
|
632
|
+
? `${W}partial${R2}`
|
|
633
|
+
: `${D}unknown${R2}`;
|
|
634
|
+
console.log(` ${mcpMark.padEnd(22)} ${formatMcpExposureLine(mcpExposure)}`);
|
|
635
|
+
if (mcpExposure.next_step && mcpExposure.status !== "green") {
|
|
636
|
+
console.log(` ${D}→ ${mcpExposure.next_step}${R2}`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const nodePty = nodePtyProbe();
|
|
640
|
+
if (!nodePty.present && nodePty.sev === "warn") warnings++;
|
|
641
|
+
console.log("\n terminal sessions:");
|
|
642
|
+
const terminalMark = nodePty.sev === "ok"
|
|
643
|
+
? `${G}ok${R2}`
|
|
644
|
+
: nodePty.sev === "warn"
|
|
645
|
+
? `${W}warn${R2}`
|
|
646
|
+
: `${D}${nodePty.sev}${R2}`;
|
|
647
|
+
console.log(` ${terminalMark.padEnd(22)} ${nodePty.name} ${D}(${nodePty.hint})${R2}`);
|
|
648
|
+
|
|
174
649
|
console.log("\n provider profiles:");
|
|
175
650
|
try {
|
|
176
|
-
const providerScript =
|
|
651
|
+
const providerScript = resolvePythonScript(target, "provider_profiles.py");
|
|
177
652
|
if (!providerScript) {
|
|
178
653
|
console.log(` ${D}—${R2} unavailable in this environment`);
|
|
179
654
|
} else {
|
|
@@ -206,6 +681,7 @@ function cmdDoctor(target, options = {}) {
|
|
|
206
681
|
let updatesAvailable = 0;
|
|
207
682
|
console.log("\n agent CLIs:");
|
|
208
683
|
for (const cli of SUPPORTED_CLIS) {
|
|
684
|
+
const label = cliDisplayName(cli);
|
|
209
685
|
let installed = false, ver = null;
|
|
210
686
|
try {
|
|
211
687
|
const out = _ef2(cli.bin, ["--version"], { timeout: 8000 }).toString().trim();
|
|
@@ -224,13 +700,13 @@ function cmdDoctor(target, options = {}) {
|
|
|
224
700
|
}
|
|
225
701
|
if (latest && ver && latest !== ver) {
|
|
226
702
|
updatesAvailable++;
|
|
227
|
-
console.log(` ${W}update${R2} ${
|
|
703
|
+
console.log(` ${W}update${R2} ${label} ${D}${ver} → ${latest}${R2}`);
|
|
228
704
|
console.log(` ${D}→ ${cli.install}${R2}`);
|
|
229
705
|
} else {
|
|
230
|
-
console.log(` ${G}ok${R2} ${
|
|
706
|
+
console.log(` ${G}ok${R2} ${label}${ver ? ` ${D}v${ver}${R2}` : ""}`);
|
|
231
707
|
}
|
|
232
708
|
} else {
|
|
233
|
-
console.log(` ${D}—${R2} ${
|
|
709
|
+
console.log(` ${D}—${R2} ${label} ${D}not installed${R2}`);
|
|
234
710
|
console.log(` ${D}→ ${cli.install}${cli.altAuth ? ` (or ${cli.altAuth})` : ""}${R2}`);
|
|
235
711
|
}
|
|
236
712
|
}
|
|
@@ -267,10 +743,15 @@ function cmdDoctor(target, options = {}) {
|
|
|
267
743
|
});
|
|
268
744
|
if (errors && !options.suppressExitCode) process.exitCode = 1;
|
|
269
745
|
|
|
270
|
-
|
|
271
|
-
|
|
746
|
+
if (options.drift) {
|
|
747
|
+
const drift = printDriftReport(target, options.driftOptions || {});
|
|
748
|
+
if (drift.status === "error" && typeof drift.exit_code === "number" && drift.exit_code !== 0) {
|
|
749
|
+
process.exitCode = drift.exit_code;
|
|
750
|
+
}
|
|
751
|
+
} else {
|
|
752
|
+
// Drift summary (lightweight — full report via --drift flag)
|
|
272
753
|
try {
|
|
273
|
-
const ds =
|
|
754
|
+
const ds = resolvePythonScript(target, "drift_detector.py");
|
|
274
755
|
if (ds) {
|
|
275
756
|
const dr = spawnSync("python3", [ds, "report", "--target", target],
|
|
276
757
|
{ stdio: ["ignore", "pipe", "ignore"], encoding: "utf8", timeout: 5000 });
|
|
@@ -300,4 +781,17 @@ function cmdDoctor(target, options = {}) {
|
|
|
300
781
|
}
|
|
301
782
|
}
|
|
302
783
|
|
|
303
|
-
module.exports = {
|
|
784
|
+
module.exports = {
|
|
785
|
+
cmdDoctor,
|
|
786
|
+
ghostAuthStatus,
|
|
787
|
+
nodePtyProbe,
|
|
788
|
+
collectDoctorPayload,
|
|
789
|
+
collectLayerChecks,
|
|
790
|
+
collectLayerVersionFreshness,
|
|
791
|
+
compareReleaseVersions,
|
|
792
|
+
collectDriftPayload,
|
|
793
|
+
printDriftReport,
|
|
794
|
+
collectGitHubIdentityPayload,
|
|
795
|
+
collectMcpExposurePayload,
|
|
796
|
+
formatMcpExposureLine,
|
|
797
|
+
};
|
|
@@ -1,12 +1,43 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
2
4
|
const shared = require("../shared");
|
|
3
5
|
const { log, D, R, spawnSync, findRepoScript } = shared;
|
|
4
6
|
|
|
7
|
+
// npm-installed users have no repo checkout. Fall back to the self-contained
|
|
8
|
+
// python experience closure bundled into the package at lib/python/ by
|
|
9
|
+
// scripts/build-python-bundle.js (#4360).
|
|
10
|
+
function resolveBundledScript(name) {
|
|
11
|
+
const bundled = path.join(__dirname, "..", "python", name);
|
|
12
|
+
return fs.existsSync(bundled) ? bundled : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function resolvePythonScript(target, name) {
|
|
16
|
+
return findRepoScript(target, name) || resolveBundledScript(name);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function printExperienceHelp() {
|
|
20
|
+
console.log("Usage: 0dai experience [list|stats|record-json|sync|warnings|dismiss]");
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log("Commands:");
|
|
23
|
+
console.log(" list Show recent local experience events");
|
|
24
|
+
console.log(" stats Summarize local experience events");
|
|
25
|
+
console.log(" record-json Record one JSON payload through the repo experience helper");
|
|
26
|
+
console.log(" sync Sync eligible events when the repo helper permits it");
|
|
27
|
+
console.log(" warnings Show anti-pattern warnings");
|
|
28
|
+
console.log(" dismiss Dismiss an anti-pattern warning");
|
|
29
|
+
}
|
|
30
|
+
|
|
5
31
|
function cmdExperience(target, sub, args) {
|
|
6
|
-
|
|
32
|
+
if (sub === "--help" || sub === "-h" || args.includes("--help") || args.includes("-h")) {
|
|
33
|
+
printExperienceHelp();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const experienceScript = resolvePythonScript(target, "experience_pipeline.py");
|
|
7
38
|
if (!experienceScript) {
|
|
8
39
|
log("experience pipeline unavailable in this environment");
|
|
9
|
-
console.log(` ${D}
|
|
40
|
+
console.log(` ${D}Needs scripts/experience_pipeline.py (repo) or the bundled lib/python copy; requires python3${R}`);
|
|
10
41
|
process.exit(1);
|
|
11
42
|
}
|
|
12
43
|
|
|
@@ -34,8 +65,12 @@ function cmdExperience(target, sub, args) {
|
|
|
34
65
|
if (args.includes("--json")) forwarded.push("--json");
|
|
35
66
|
const limitIdx = args.indexOf("--limit");
|
|
36
67
|
if (limitIdx >= 0 && args[limitIdx + 1]) forwarded.push("--limit", args[limitIdx + 1]);
|
|
68
|
+
} else if (command === "record-json") {
|
|
69
|
+
if (args.includes("--json")) forwarded.push("--json");
|
|
70
|
+
const payloadIdx = args.indexOf("--payload");
|
|
71
|
+
if (payloadIdx >= 0 && args[payloadIdx + 1]) forwarded.push("--payload", args[payloadIdx + 1]);
|
|
37
72
|
} else if (command === "warnings") {
|
|
38
|
-
const detectorScript =
|
|
73
|
+
const detectorScript = resolvePythonScript(target, "anti_pattern_detector.py");
|
|
39
74
|
if (!detectorScript) { log("anti-pattern detector unavailable"); process.exit(1); }
|
|
40
75
|
const fwd = [detectorScript, "warnings", "--target", target];
|
|
41
76
|
if (args.includes("--json")) fwd.push("--json");
|
|
@@ -47,7 +82,7 @@ function cmdExperience(target, sub, args) {
|
|
|
47
82
|
if (typeof wr.status === "number") process.exit(wr.status);
|
|
48
83
|
process.exit(1);
|
|
49
84
|
} else if (command === "dismiss") {
|
|
50
|
-
const detectorScript =
|
|
85
|
+
const detectorScript = resolvePythonScript(target, "anti_pattern_detector.py");
|
|
51
86
|
if (!detectorScript) { log("anti-pattern detector unavailable"); process.exit(1); }
|
|
52
87
|
const patternId = args.find(a => a && !a.startsWith("-")) || "";
|
|
53
88
|
if (!patternId) { console.log("Usage: 0dai experience dismiss <pattern_id>"); process.exit(1); }
|
|
@@ -57,7 +92,7 @@ function cmdExperience(target, sub, args) {
|
|
|
57
92
|
if (typeof dr.status === "number") process.exit(dr.status);
|
|
58
93
|
process.exit(1);
|
|
59
94
|
} else {
|
|
60
|
-
|
|
95
|
+
printExperienceHelp();
|
|
61
96
|
process.exit(1);
|
|
62
97
|
}
|
|
63
98
|
|