@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.
Files changed (75) hide show
  1. package/README.md +12 -11
  2. package/bin/0dai.js +127 -30
  3. package/lib/ai/manifest/mcp-exposure-contract.json +121 -0
  4. package/lib/ai/meta/manifest/mcp-tool-tiers.json +435 -0
  5. package/lib/ai/registry/mcp-catalog.json +98 -0
  6. package/lib/commands/auth.js +2 -1
  7. package/lib/commands/compliance.js +1 -1
  8. package/lib/commands/doctor.js +506 -12
  9. package/lib/commands/experience.js +40 -5
  10. package/lib/commands/feedback.js +157 -15
  11. package/lib/commands/gh.js +26 -0
  12. package/lib/commands/graph.js +9 -4
  13. package/lib/commands/heatmap.js +1 -1
  14. package/lib/commands/init.js +209 -27
  15. package/lib/commands/mcp.js +111 -33
  16. package/lib/commands/models.js +138 -41
  17. package/lib/commands/provider.js +30 -59
  18. package/lib/commands/quota.js +1 -1
  19. package/lib/commands/receipt.js +1 -1
  20. package/lib/commands/run.js +14 -6
  21. package/lib/commands/runner.js +31 -1
  22. package/lib/commands/status.js +38 -10
  23. package/lib/commands/swarm.js +130 -12
  24. package/lib/commands/update.js +184 -38
  25. package/lib/commands/usage.js +1 -1
  26. package/lib/commands/validate.js +32 -3
  27. package/lib/commands/vault.js +43 -8
  28. package/lib/python/__init__.py +0 -0
  29. package/lib/python/agent_quotas.py +525 -0
  30. package/lib/python/anomaly_alert.py +397 -0
  31. package/lib/python/anti_pattern_detector.py +799 -0
  32. package/lib/python/auth.py +443 -0
  33. package/lib/python/capi_profile_guard.py +477 -0
  34. package/lib/python/compliance_report.py +581 -0
  35. package/lib/python/drift_detector.py +388 -0
  36. package/lib/python/experience_pipeline.py +1130 -0
  37. package/lib/python/graph.py +19 -0
  38. package/lib/python/graph_core.py +293 -0
  39. package/lib/python/graph_io.py +179 -0
  40. package/lib/python/graph_legacy.py +2052 -0
  41. package/lib/python/graph_legacy_helpers.py +221 -0
  42. package/lib/python/graph_outcomes_core.py +85 -0
  43. package/lib/python/graph_queries.py +171 -0
  44. package/lib/python/graph_slice.py +198 -0
  45. package/lib/python/graph_slicer.py +576 -0
  46. package/lib/python/graph_slicer_cli.py +60 -0
  47. package/lib/python/graph_validation.py +64 -0
  48. package/lib/python/heatmap.py +934 -0
  49. package/lib/python/json_utils.py +193 -0
  50. package/lib/python/mcp_exposure_check.py +247 -0
  51. package/lib/python/model_router.py +1434 -0
  52. package/lib/python/project_manager.py +621 -0
  53. package/lib/python/provider_profiles.py +1618 -0
  54. package/lib/python/provider_registry.py +1211 -0
  55. package/lib/python/provider_registry_cli.py +125 -0
  56. package/lib/python/receipt_png.py +727 -0
  57. package/lib/python/structural_memory.py +325 -0
  58. package/lib/python/swarm_cost.py +177 -0
  59. package/lib/python/usage_ledger.py +569 -0
  60. package/lib/scripts/mcp_tier_config.py +240 -0
  61. package/lib/shared.js +95 -12
  62. package/lib/tui/index.mjs +35174 -0
  63. package/lib/utils/activation_telemetry.js +1 -4
  64. package/lib/utils/constants.js +7 -1
  65. package/lib/utils/identity.js +184 -0
  66. package/lib/utils/mcp-auth.js +81 -15
  67. package/lib/utils/plan.js +1 -1
  68. package/lib/vault/index.js +19 -3
  69. package/lib/vault/storage.js +21 -2
  70. package/lib/wizard.js +5 -2
  71. package/package.json +9 -3
  72. package/scripts/build-python-bundle.js +106 -0
  73. package/scripts/build-tui.js +14 -1
  74. package/scripts/harvest_experience.py +523 -0
  75. package/scripts/postinstall.js +15 -9
@@ -11,7 +11,9 @@ function writeFeedbackOutbox(target, report, result) {
11
11
  queued_at: new Date().toISOString(),
12
12
  project: path.basename(target),
13
13
  report,
14
+ error_type: "feedback_endpoint_error",
14
15
  error: result && result.error ? result.error : "feedback endpoint did not acknowledge receipt",
16
+ retry_command: "0dai feedback retry",
15
17
  response: result || null,
16
18
  }, null, 2) + "\n", { mode: 0o600 });
17
19
  return outboxPath;
@@ -33,36 +35,147 @@ function feedbackAccepted(result) {
33
35
  return Boolean(result && (result.received || result.ok));
34
36
  }
35
37
 
36
- async function cmdFeedbackPush(target) {
38
+ function hasHelp(args = []) {
39
+ return args.includes("--help") || args.includes("-h");
40
+ }
41
+
42
+ function printFeedbackHelp() {
43
+ console.log("Usage: 0dai feedback [push|submit|retry|log|list] [--target PATH] [options]");
44
+ console.log("");
45
+ console.log("Commands:");
46
+ console.log(" push Send local feedback reports and operational log entries");
47
+ console.log(" push Send accepted feedback reports from ai/feedback/");
48
+ console.log(" retry Retry queued feedback from a failed push");
49
+ console.log(" log Append one feedback item to ai/feedback/operational.jsonl");
50
+ console.log(" list List local feedback report files");
51
+ }
52
+
53
+ function printFeedbackPushHelp() {
54
+ console.log("Usage: 0dai feedback push --target PATH");
55
+ console.log("");
56
+ console.log("Sends accepted feedback reports from ai/feedback/ and operational.jsonl entries.");
57
+ console.log("Accepted reports: ai/feedback/*-report.json and ai/feedback/YYYYMMDD*.json.");
58
+ console.log("Accepted log: ai/feedback/operational.jsonl.");
59
+ }
60
+
61
+ function printFeedbackSubmitHelp() {
62
+ console.log("Usage: 0dai feedback push --target PATH");
63
+ console.log("");
64
+ console.log("Sends one accepted feedback report file. Use feedback push to send every accepted local report.");
65
+ }
66
+
67
+ function printFeedbackRetryHelp() {
68
+ console.log("Usage: 0dai feedback retry [--json]");
69
+ console.log("");
70
+ console.log("Retries queued feedback submissions from ~/.0dai/feedback-outbox/.");
71
+ }
72
+
73
+ function printFeedbackLogHelp() {
74
+ console.log("Usage: 0dai feedback log --type bug|suggestion|friction|positive --detail '...'");
75
+ console.log("");
76
+ console.log("Records one local feedback entry without sending it.");
77
+ }
78
+
79
+ function printFeedbackListHelp() {
80
+ console.log("Usage: 0dai feedback list");
81
+ console.log("");
82
+ console.log("Lists local *-report.json feedback files.");
83
+ }
84
+
85
+ function resolveFeedbackSubmitFile(target, fileArg) {
86
+ if (!fileArg) return { error: "missing --file" };
87
+ const fbDir = path.resolve(target, "ai", "feedback");
88
+ const fullPath = path.resolve(target, fileArg);
89
+ if (fullPath !== fbDir && !fullPath.startsWith(`${fbDir}${path.sep}`)) {
90
+ return { error: "file must be under ai/feedback/" };
91
+ }
92
+ if (!fs.existsSync(fullPath)) return { error: `file not found: ${fileArg}` };
93
+ return { fullPath };
94
+ }
95
+
96
+ async function cmdFeedbackPush(target, options = {}) {
37
97
  const fbDir = path.join(target, "ai", "feedback");
98
+ const onlyFile = options.onlyFile ? path.resolve(options.onlyFile) : "";
38
99
  const items = [];
100
+ const ignored = [];
101
+ let feedbackDirExists = false;
102
+ let jsonlStatus = "missing";
103
+ let includedOperationalLog = false;
104
+ const acceptedFormats = [
105
+ "ai/feedback/*-report.json",
106
+ "ai/feedback/YYYYMMDD*.json",
107
+ "ai/feedback/operational.jsonl",
108
+ ];
39
109
 
40
110
  // Collect from report JSON files
41
111
  try {
112
+ feedbackDirExists = fs.existsSync(fbDir) && fs.statSync(fbDir).isDirectory();
42
113
  for (const f of fs.readdirSync(fbDir)) {
43
- if (f.endsWith("-report.json") || (f.endsWith(".json") && f.match(/^\d{8}/))) {
44
- try {
45
- const d = JSON.parse(fs.readFileSync(path.join(fbDir, f), "utf8"));
46
- if (d.project || d.verdict) items.push({ type: "report", data: d, file: f });
47
- } catch {}
114
+ const fullPath = path.join(fbDir, f);
115
+ if (onlyFile && path.resolve(fullPath) !== onlyFile) continue;
116
+ let stat = null;
117
+ try { stat = fs.statSync(fullPath); } catch {}
118
+ if (!stat || !stat.isFile()) {
119
+ ignored.push({ file: f, reason: "not a regular file" });
120
+ continue;
121
+ }
122
+ if (f === "operational.jsonl") continue;
123
+ if (!(f.endsWith("-report.json") || (f.endsWith(".json") && f.match(/^\d{8}/)))) {
124
+ ignored.push({ file: f, reason: "unsupported report name" });
125
+ continue;
126
+ }
127
+ try {
128
+ const d = JSON.parse(fs.readFileSync(fullPath, "utf8"));
129
+ if (d.project || d.verdict) items.push({ type: "report", data: d, file: f });
130
+ else ignored.push({ file: f, reason: "missing project or verdict" });
131
+ } catch (err) {
132
+ ignored.push({ file: f, reason: `invalid JSON: ${err.message || err}` });
48
133
  }
49
134
  }
50
- } catch {}
135
+ } catch (err) {
136
+ if (!feedbackDirExists) ignored.push({ file: "ai/feedback/", reason: "directory does not exist" });
137
+ else ignored.push({ file: "ai/feedback/", reason: `could not scan directory: ${err.message || err}` });
138
+ }
51
139
 
52
140
  // Collect from operational.jsonl (feedback log entries)
53
141
  const jsonlPath = path.join(fbDir, "operational.jsonl");
54
142
  try {
55
- if (fs.existsSync(jsonlPath)) {
56
- const lines = fs.readFileSync(jsonlPath, "utf8").trim().split("\n").filter(Boolean);
57
- for (const line of lines) {
58
- try { items.push({ type: "log", data: JSON.parse(line) }); } catch {}
143
+ if (fs.existsSync(jsonlPath) && (!onlyFile || path.resolve(jsonlPath) === onlyFile)) {
144
+ const raw = fs.readFileSync(jsonlPath, "utf8");
145
+ const lines = raw.trim().split("\n").filter(Boolean);
146
+ jsonlStatus = lines.length ? `${lines.length} line(s)` : "empty";
147
+ if (!lines.length) ignored.push({ file: "operational.jsonl", reason: "empty log" });
148
+ for (const [idx, line] of lines.entries()) {
149
+ try {
150
+ items.push({ type: "log", data: JSON.parse(line) });
151
+ includedOperationalLog = true;
152
+ }
153
+ catch (err) {
154
+ ignored.push({ file: `operational.jsonl:${idx + 1}`, reason: `invalid JSON: ${err.message || err}` });
155
+ }
59
156
  }
60
157
  }
61
- } catch {}
158
+ } catch (err) {
159
+ jsonlStatus = `unreadable: ${err.message || err}`;
160
+ ignored.push({ file: "operational.jsonl", reason: jsonlStatus });
161
+ }
62
162
 
63
163
  if (!items.length) {
64
164
  log("no feedback found");
165
+ console.log(` ${D}scanned: ${fbDir}${R}`);
166
+ console.log(` ${D}accepted inputs: ${acceptedFormats.join(", ")}${R}`);
167
+ console.log(` ${D}operational log: ${jsonlStatus}${R}`);
168
+ const shownIgnored = ignored.slice(0, 8);
169
+ for (const item of shownIgnored) {
170
+ console.log(` ${D}ignored ${item.file}: ${item.reason}${R}`);
171
+ }
172
+ if (ignored.length > shownIgnored.length) {
173
+ console.log(` ${D}ignored ${ignored.length - shownIgnored.length} more item(s)${R}`);
174
+ }
65
175
  console.log(` ${D}Log feedback first: 0dai feedback log --type suggestion --detail '...'${R}`);
176
+ console.log(` ${D}Report files must include project or verdict before push.${R}`);
177
+ console.log(` ${D}Use 0dai feedback push --target . to send accepted reports from ai/feedback/.${R}`);
178
+ if (onlyFile) process.exitCode = 1;
66
179
  return;
67
180
  }
68
181
 
@@ -81,14 +194,17 @@ async function cmdFeedbackPush(target) {
81
194
  if (result.warning) console.log(` ${D}${result.warning}${R}`);
82
195
  if (result.issue_error) console.log(` ${D}issue warning: ${result.issue_error}${R}`);
83
196
  // Archive pushed entries
84
- if (fs.existsSync(jsonlPath)) {
197
+ if (includedOperationalLog && fs.existsSync(jsonlPath)) {
85
198
  const archivePath = path.join(fbDir, `pushed-${Date.now()}.jsonl`);
86
199
  fs.renameSync(jsonlPath, archivePath);
87
200
  }
88
201
  } else {
89
202
  const outboxPath = writeFeedbackOutbox(target, report, result);
90
203
  log(`error: ${(result && result.error) || "unknown"}`);
204
+ console.log(` ${D}type: feedback_endpoint_error${R}`);
205
+ console.log(` ${D}endpoint: /v1/feedback${R}`);
91
206
  console.log(` ${D}queued for retry: ${outboxPath}${R}`);
207
+ console.log(` ${D}retry: 0dai feedback retry${R}`);
92
208
  console.log(` ${D}source feedback was left in place${R}`);
93
209
  process.exitCode = 1;
94
210
  }
@@ -144,9 +260,35 @@ async function cmdFeedbackRetry(args = []) {
144
260
  async function cmdFeedback(target, sub, args) {
145
261
  const fbDir = path.join(target, "ai", "feedback");
146
262
 
263
+ if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
264
+ printFeedbackHelp();
265
+ return;
266
+ }
267
+
268
+ if (hasHelp(args)) {
269
+ if (sub === "push") printFeedbackPushHelp();
270
+ else if (sub === "submit") printFeedbackSubmitHelp();
271
+ else if (sub === "retry") printFeedbackRetryHelp();
272
+ else if (sub === "log") printFeedbackLogHelp();
273
+ else if (sub === "list") printFeedbackListHelp();
274
+ else printFeedbackHelp();
275
+ return;
276
+ }
277
+
147
278
  if (sub === "push") {
148
279
  return cmdFeedbackPush(target);
149
280
  }
281
+ if (sub === "submit") {
282
+ const fileArg = args.find((_, i) => args[i - 1] === "--file") || "";
283
+ const resolved = resolveFeedbackSubmitFile(target, fileArg);
284
+ if (resolved.error) {
285
+ log(`feedback submit error: ${resolved.error}`);
286
+ printFeedbackSubmitHelp();
287
+ process.exitCode = 1;
288
+ return;
289
+ }
290
+ return cmdFeedbackPush(target, { onlyFile: resolved.fullPath });
291
+ }
150
292
  if (sub === "retry") {
151
293
  return cmdFeedbackRetry(args);
152
294
  }
@@ -173,7 +315,7 @@ async function cmdFeedback(target, sub, args) {
173
315
  } catch { log("no feedback directory"); }
174
316
  return;
175
317
  }
176
- console.log("Usage: 0dai feedback [push|retry|log|list] [--type ...] [--detail '...']");
318
+ printFeedbackHelp();
177
319
  }
178
320
 
179
- module.exports = { cmdFeedbackPush, cmdFeedbackRetry, cmdFeedback, writeFeedbackOutbox, listFeedbackOutbox, feedbackAccepted };
321
+ module.exports = { cmdFeedbackPush, cmdFeedbackRetry, cmdFeedback, writeFeedbackOutbox, listFeedbackOutbox, feedbackAccepted, resolveFeedbackSubmitFile };
@@ -330,12 +330,38 @@ function hasGithubRemote(target) {
330
330
  return /github\.com[:/]/.test(String(result.stdout || ""));
331
331
  }
332
332
 
333
+ // An existing hook setup that pointing core.hooksPath at .githooks would
334
+ // silently bypass (Husky, or non-sample committed .git/hooks).
335
+ function existingHookSetup(target) {
336
+ try {
337
+ if (fs.existsSync(path.join(target, ".husky"))) return ".husky";
338
+ const hooksDir = path.join(target, ".git", "hooks");
339
+ if (fs.existsSync(hooksDir)) {
340
+ const real = fs.readdirSync(hooksDir).filter((f) => !f.endsWith(".sample"));
341
+ if (real.length) return ".git/hooks";
342
+ }
343
+ } catch {
344
+ /* ignore */
345
+ }
346
+ return null;
347
+ }
348
+
333
349
  function ensureHooksPath(target) {
334
350
  if (!isGitRepo(target)) return false;
351
+ const current = getHooksPath(target);
352
+ if (current === ".githooks") return true; // already ours — nothing to do
353
+ // Warn (don't silently clobber) when the user already has hooks we'd bypass.
354
+ const conflict = current && current !== ".githooks" ? `core.hooksPath=${current}` : existingHookSetup(target);
335
355
  const result = spawnSync("git", ["-C", target, "config", "core.hooksPath", ".githooks"], {
336
356
  encoding: "utf8",
337
357
  stdio: ["ignore", "pipe", "pipe"],
338
358
  });
359
+ if (conflict && result.status === 0) {
360
+ log(`warning: redirected git hooks to .githooks — your existing hooks (${conflict}) are now bypassed`);
361
+ console.log(
362
+ ` ${D}Keep yours: git config core.hooksPath ${current || "<your-path>"} (or chain them from .githooks). Skip 0dai's policy: ODAI_GIT_POLICY_SKIP=1${R}`,
363
+ );
364
+ }
339
365
  return result.status === 0;
340
366
  }
341
367
 
@@ -23,8 +23,12 @@ function graphPayloadForPush(localGraph, auth) {
23
23
  };
24
24
  }
25
25
 
26
+ function missingGraphHint() {
27
+ return "No local graph found. Create ai/manifest/project_graph.json, or from a repo checkout run: python3 scripts/generate_project_graph.py --target .";
28
+ }
29
+
26
30
  function readStructuralMemorySummary(target) {
27
- const script = shared.findRepoScript(target, "structural_memory.py");
31
+ const script = shared.resolvePythonScript(target, "structural_memory.py");
28
32
  if (!script) return null;
29
33
  const result = shared.spawnSync("python3", [script, "--target", target, "--json"], {
30
34
  encoding: "utf8",
@@ -72,7 +76,7 @@ async function cmdGraph(target, sub, args) {
72
76
  }
73
77
 
74
78
  if (!fs.existsSync(graphFile)) {
75
- log("No local graph found. Run: 0dai graph init or create ai/manifest/project_graph.json");
79
+ log(missingGraphHint());
76
80
  return;
77
81
  }
78
82
 
@@ -264,10 +268,10 @@ async function cmdGraph(target, sub, args) {
264
268
 
265
269
  if (sub === "context") {
266
270
  if (!fs.existsSync(graphFile)) {
267
- log("No local graph found. Run: 0dai graph init or create ai/manifest/project_graph.json");
271
+ log(missingGraphHint());
268
272
  return;
269
273
  }
270
- const slicerScript = shared.findRepoScript(target, "graph_slicer_cli.py");
274
+ const slicerScript = shared.resolvePythonScript(target, "graph_slicer_cli.py");
271
275
  if (!slicerScript) {
272
276
  log("graph slicer unavailable in this environment");
273
277
  console.log(` ${D}Expected scripts/graph_slicer_cli.py in repo checkout${R}`);
@@ -333,6 +337,7 @@ module.exports = {
333
337
  graphAuthToken,
334
338
  graphPayloadForPush,
335
339
  isPaidGraphPlan,
340
+ missingGraphHint,
336
341
  readStructuralMemorySummary,
337
342
  formatStructuralMemorySummary,
338
343
  };
@@ -3,7 +3,7 @@ const shared = require("../shared");
3
3
  const { log, spawnSync, findRepoScript } = shared;
4
4
 
5
5
  function cmdHeatmap(target, args) {
6
- const script = findRepoScript(target, "heatmap.py");
6
+ const script = shared.resolvePythonScript(target, "heatmap.py");
7
7
  if (!script) {
8
8
  log("heatmap script unavailable in this environment");
9
9
  return;