@0dai-dev/cli 4.3.5 → 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 (79) hide show
  1. package/README.md +12 -11
  2. package/bin/0dai.js +214 -40
  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 +55 -1
  7. package/lib/commands/compliance.js +1 -1
  8. package/lib/commands/detect.js +10 -4
  9. package/lib/commands/doctor.js +545 -26
  10. package/lib/commands/experience.js +40 -5
  11. package/lib/commands/export.js +73 -0
  12. package/lib/commands/feedback.js +157 -15
  13. package/lib/commands/gh.js +26 -0
  14. package/lib/commands/graph.js +9 -4
  15. package/lib/commands/heatmap.js +1 -1
  16. package/lib/commands/init.js +222 -30
  17. package/lib/commands/mcp.js +129 -21
  18. package/lib/commands/models.js +138 -41
  19. package/lib/commands/provider.js +30 -59
  20. package/lib/commands/quota.js +1 -1
  21. package/lib/commands/receipt.js +1 -1
  22. package/lib/commands/run.js +18 -7
  23. package/lib/commands/runner.js +31 -1
  24. package/lib/commands/status.js +44 -11
  25. package/lib/commands/swarm.js +130 -12
  26. package/lib/commands/trust.js +286 -0
  27. package/lib/commands/update.js +184 -38
  28. package/lib/commands/usage.js +1 -1
  29. package/lib/commands/validate.js +32 -3
  30. package/lib/commands/vault.js +46 -9
  31. package/lib/python/__init__.py +0 -0
  32. package/lib/python/agent_quotas.py +525 -0
  33. package/lib/python/anomaly_alert.py +397 -0
  34. package/lib/python/anti_pattern_detector.py +799 -0
  35. package/lib/python/auth.py +443 -0
  36. package/lib/python/capi_profile_guard.py +477 -0
  37. package/lib/python/compliance_report.py +581 -0
  38. package/lib/python/drift_detector.py +388 -0
  39. package/lib/python/experience_pipeline.py +1130 -0
  40. package/lib/python/graph.py +19 -0
  41. package/lib/python/graph_core.py +293 -0
  42. package/lib/python/graph_io.py +179 -0
  43. package/lib/python/graph_legacy.py +2052 -0
  44. package/lib/python/graph_legacy_helpers.py +221 -0
  45. package/lib/python/graph_outcomes_core.py +85 -0
  46. package/lib/python/graph_queries.py +171 -0
  47. package/lib/python/graph_slice.py +198 -0
  48. package/lib/python/graph_slicer.py +576 -0
  49. package/lib/python/graph_slicer_cli.py +60 -0
  50. package/lib/python/graph_validation.py +64 -0
  51. package/lib/python/heatmap.py +934 -0
  52. package/lib/python/json_utils.py +193 -0
  53. package/lib/python/mcp_exposure_check.py +247 -0
  54. package/lib/python/model_router.py +1434 -0
  55. package/lib/python/project_manager.py +621 -0
  56. package/lib/python/provider_profiles.py +1618 -0
  57. package/lib/python/provider_registry.py +1211 -0
  58. package/lib/python/provider_registry_cli.py +125 -0
  59. package/lib/python/receipt_png.py +727 -0
  60. package/lib/python/structural_memory.py +325 -0
  61. package/lib/python/swarm_cost.py +177 -0
  62. package/lib/python/usage_ledger.py +569 -0
  63. package/lib/scripts/mcp_tier_config.py +240 -0
  64. package/lib/shared.js +97 -14
  65. package/lib/tui/index.mjs +35174 -0
  66. package/lib/utils/activation_telemetry.js +230 -11
  67. package/lib/utils/constants.js +7 -1
  68. package/lib/utils/export-bundler.js +285 -0
  69. package/lib/utils/identity.js +198 -1
  70. package/lib/utils/mcp-auth.js +81 -15
  71. package/lib/utils/plan.js +1 -1
  72. package/lib/vault/index.js +19 -3
  73. package/lib/vault/storage.js +21 -2
  74. package/lib/wizard.js +5 -2
  75. package/package.json +9 -3
  76. package/scripts/build-python-bundle.js +106 -0
  77. package/scripts/build-tui.js +14 -1
  78. package/scripts/harvest_experience.py +523 -0
  79. package/scripts/postinstall.js +15 -9
@@ -0,0 +1,240 @@
1
+ """MCP tool tier configuration - single source of truth for Free vs Pro tool access.
2
+
3
+ FREE_TOOLS (49): Basic read-only tools and Mode A baseline helpers.
4
+ PRO_TOOLS (64): Advanced tools requiring Pro/Team plan ($15/mo).
5
+
6
+ Total: 113 unique tools. Must match all @mcp.tool registrations in mcp_server.py + mcp_tools/.
7
+ """
8
+
9
+ PLAN_LEVELS = {"free": 0, "pro": 1, "team": 2, "enterprise": 3}
10
+
11
+ FREE_TOOLS: list[str] = [
12
+ # Core read tools — project context
13
+ "get_project_manifest",
14
+ "get_project_context",
15
+ "get_codebase_map",
16
+ "get_commands",
17
+ "get_environment",
18
+ "get_ai_version",
19
+ "get_project_health",
20
+ "mcp_tier_load",
21
+ "get_bulletins",
22
+ "get_applied_lock",
23
+ "get_custom_stacks",
24
+ "get_discovery",
25
+ "get_personas",
26
+ "get_registry",
27
+ "get_telemetry_summary",
28
+ "citation_audit",
29
+ "get_specs",
30
+ "master_plan_read",
31
+ # Knowledge tools
32
+ "search_experience",
33
+ "record_experience",
34
+ "score_candidates",
35
+ # Federation & graph
36
+ "get_federation",
37
+ "get_project_graph",
38
+ # Model selection
39
+ "get_model_ratings",
40
+ # Agent anomaly observability
41
+ "get_anomaly_taxonomy",
42
+ "get_anomaly_events",
43
+ "get_anomaly_summary",
44
+ "get_agent_activity",
45
+ "get_ghost_critiques",
46
+ "get_token_telemetry",
47
+ "verdict_count",
48
+ "idle_watchdog_status",
49
+ "get_pool_state",
50
+ "pool_query_free",
51
+ "pool_health",
52
+ # Solution utility tools (read-only, local-safe)
53
+ "debug_probe",
54
+ "parse_content",
55
+ # Memory tools — core feature, free tier (makes product sticky)
56
+ "memory_add",
57
+ "memory_search",
58
+ "memory_list",
59
+ "memory_delete",
60
+ "memory_verify",
61
+ "memory_inject",
62
+ # GitHub helpers (Tier 1, Mode A) - added 2026-04-26 per #1132.
63
+ "gh_label_create",
64
+ "gh_issue_edit_body",
65
+ "gh_pr_diff",
66
+ "propose_file_change",
67
+ # Session productivity scoring - added 2026-01-XX per #1727.
68
+ "session_productivity",
69
+ "session_productivity_pool",
70
+ ]
71
+
72
+ CORE_DISCOVERY_TOOLS: list[str] = [
73
+ "mcp_tier_load",
74
+ "memory_search",
75
+ "get_project_health",
76
+ "get_session",
77
+ "get_bulletins",
78
+ "get_project_manifest",
79
+ "get_codebase_map",
80
+ "get_commands",
81
+ "get_discovery",
82
+ "get_specs",
83
+ "search_experience",
84
+ "record_experience",
85
+ "get_activity_feed",
86
+ "check_approval",
87
+ "get_role_policy",
88
+ ]
89
+
90
+ HIDDEN_DISCOVERY_TOOLS: list[str] = [
91
+ "memory_delete",
92
+ "memory_inject",
93
+ "gh_label_create",
94
+ "gh_issue_edit_body",
95
+ "propose_file_change",
96
+ "create_spec",
97
+ "update_decision",
98
+ "submit_feedback",
99
+ "undo_mutation",
100
+ "record_anomaly",
101
+ "pool_checkin",
102
+ "pool_checkout",
103
+ "task_cancel",
104
+ "idle_watchdog_pause",
105
+ "gh_pr_merge_safe",
106
+ "rebase_orchestrator",
107
+ "fallback_chain_resolver",
108
+ ]
109
+
110
+ PRO_TOOLS: list[str] = [
111
+ # Swarm tools (8)
112
+ "get_swarm_status",
113
+ "swarm_delegate",
114
+ "peer_delegate_request",
115
+ "peer_delegate_list",
116
+ "peer_delegate_status",
117
+ "peer_delegate_accept",
118
+ "peer_delegate_decline",
119
+ "peer_delegate_complete",
120
+ "peer_delegate_sessions",
121
+ "swarm_run",
122
+ "run_task",
123
+ "watch_tasks",
124
+ "get_portfolio",
125
+ "estimate_task_cost",
126
+ "recommend_model",
127
+ # Working group tools (4)
128
+ "get_working_group_profiles",
129
+ "working_group_deliberate",
130
+ "get_working_group_history",
131
+ "working_group_research",
132
+ # Audit tools (4)
133
+ "get_audit_log",
134
+ "scan_secrets",
135
+ "get_compliance_report",
136
+ "get_role_policy",
137
+ # Specs write tool
138
+ "create_spec",
139
+ # Write / mutation tools
140
+ "update_decision",
141
+ "save_session",
142
+ "submit_feedback",
143
+ "undo_mutation",
144
+ "record_anomaly",
145
+ "pool_checkin",
146
+ "pool_checkout",
147
+ "loop_kick",
148
+ "task_cancel",
149
+ "idle_watchdog_pause",
150
+ # Solution utility tools (external/browser providers)
151
+ "web_search",
152
+ "capture_screenshot",
153
+ # OpenRouter BYOK tools (SPEC-024 Phase 1)
154
+ "mcp__openrouter__list_models",
155
+ "mcp__openrouter__invoke",
156
+ # Advanced read tools
157
+ "get_org_policy",
158
+ "get_maturity_score",
159
+ "get_mcp_catalog",
160
+ "get_agent_teams",
161
+ "get_plugins",
162
+ "check_approval",
163
+ "list_projects",
164
+ "get_project_health_multi",
165
+ "get_daily_digest",
166
+ "get_session",
167
+ "check_conflicts",
168
+ "get_activity_feed",
169
+ "get_knowledge_base",
170
+ "get_wal",
171
+ "get_prompt_history",
172
+ "get_observability",
173
+ "get_orchestration",
174
+ "get_graph_context",
175
+ "trace_task",
176
+ "get_distilled_rules",
177
+ "get_substrate_health",
178
+ # Ops-wrappers suite (3) — added 2026-04-28 per ops-wrappers-suite task.
179
+ "gh_pr_merge_safe",
180
+ "rebase_orchestrator",
181
+ "fallback_chain_resolver",
182
+ # Graph memory (Phase D) — read-only Cypher over AGE memory_graph (#2104).
183
+ "memory_traverse",
184
+ # Hybrid pgvector+AGE search (#3638) — Pro; v1 short-circuit when flag off.
185
+ "memory_search_v2",
186
+ ]
187
+
188
+ # Sentinel value for unknown/local-only plan (treated as Free)
189
+ UNKNOWN_PLAN = "free"
190
+
191
+ # Minimum plan level required for Pro tools
192
+ PRO_MIN_LEVEL = 1 # pro
193
+
194
+ UPGRADE_MESSAGE = "This tool requires Pro plan ($15/mo). Run: 0dai upgrade"
195
+
196
+
197
+ def validate_completeness(all_tool_names: set[str]) -> list[str]:
198
+ """Validate that FREE_TOOLS + PRO_TOOLS covers all registered tools.
199
+
200
+ Returns list of error messages (empty = valid).
201
+ """
202
+ errors: list[str] = []
203
+ free_set = set(FREE_TOOLS)
204
+ pro_set = set(PRO_TOOLS)
205
+
206
+ # Check for duplicates between lists
207
+ overlap = free_set & pro_set
208
+ if overlap:
209
+ errors.append(f"Tools in both FREE and PRO lists: {sorted(overlap)}")
210
+
211
+ # Check for duplicates within lists
212
+ if len(FREE_TOOLS) != len(free_set):
213
+ from collections import Counter
214
+ dupes = [k for k, v in Counter(FREE_TOOLS).items() if v > 1]
215
+ errors.append(f"Duplicate tools in FREE_TOOLS: {dupes}")
216
+
217
+ if len(PRO_TOOLS) != len(pro_set):
218
+ from collections import Counter
219
+ dupes = [k for k, v in Counter(PRO_TOOLS).items() if v > 1]
220
+ errors.append(f"Duplicate tools in PRO_TOOLS: {dupes}")
221
+
222
+ # Check completeness
223
+ all_declared = free_set | pro_set
224
+ missing = all_tool_names - all_declared
225
+ extra = all_declared - all_tool_names
226
+
227
+ if missing:
228
+ errors.append(f"Tools not in any tier list: {sorted(missing)}")
229
+ if extra:
230
+ errors.append(f"Tools in tier list but not registered: {sorted(extra)}")
231
+
232
+ # Check counts (49 free / 64 pro / 113 total after memory_search_v2 #3718).
233
+ if len(free_set) != 49:
234
+ errors.append(f"FREE_TOOLS has {len(free_set)} items, expected 49")
235
+ if len(pro_set) != 64:
236
+ errors.append(f"PRO_TOOLS has {len(pro_set)} items, expected 64")
237
+ if len(free_set) + len(pro_set) != 113:
238
+ errors.append(f"Total tools: {len(free_set) + len(pro_set)}, expected 113")
239
+
240
+ return errors
package/lib/shared.js CHANGED
@@ -17,11 +17,14 @@ const { spawnSync } = require("child_process");
17
17
  const VERSION = require("../package.json").version;
18
18
 
19
19
  // --- Re-export from extracted modules ---
20
- const { SUPPORTED_CLIS, MANIFEST_FILES, PROBE_DIRS, SETTINGS_PRESERVE_FIELDS } = require("./utils/constants");
20
+ const { SUPPORTED_CLIS, MANIFEST_FILES, PROBE_DIRS, SETTINGS_PRESERVE_FIELDS, cliDisplayName } = require("./utils/constants");
21
21
  const {
22
22
  deviceFingerprint, registerProject, projectIdFor,
23
23
  getGitRemoteOrigin, inferProjectName, detectStackHint,
24
- collectMetadata, buildProjectIdentity,
24
+ collectMetadata, buildProjectIdentity, hashManifestFiles,
25
+ loadProjectBinding, validProjectId, validateProjectDisplayName,
26
+ bindingTargetMatches, projectTargetInfo, boundProjectIdentityFromBinding,
27
+ applyBoundProjectIdentity, writeProjectBinding,
25
28
  } = require("./utils/identity");
26
29
  const { PLAN_LEVELS, _detectPlanLocal, requirePlan, getSwarmQuotaLocal } = require("./utils/plan");
27
30
 
@@ -58,6 +61,32 @@ const DRIFT_TRACKED_CONFIGS = [
58
61
  ".windsurfrules",
59
62
  ".aider.conf.yml",
60
63
  ];
64
+ const LIVE_MANIFEST_DEFAULTS = Object.freeze({
65
+ "ai/manifest/current_state.json": JSON.stringify({
66
+ managed: true,
67
+ schema: "0dai.current_state.v1",
68
+ status: "initialized",
69
+ phase: "bootstrap",
70
+ current_milestone: "initialization",
71
+ current_sprint: "bootstrap",
72
+ release_posture: "local",
73
+ notes: "Initial 0dai live-state scaffold. Update this file as the project execution state changes.",
74
+ }, null, 2) + "\n",
75
+ "ai/manifest/current_task.json": JSON.stringify({
76
+ managed: true,
77
+ schema: "0dai.current_task.v1",
78
+ task_id: "bootstrap",
79
+ title: "Initialize 0dai project context",
80
+ status: "active",
81
+ phase: "bootstrap",
82
+ acceptance: [
83
+ "ai/manifest/project.yaml reflects project identity",
84
+ "ai/manifest/commands.yaml reflects safe project commands",
85
+ "ai/docs/roadmap.md captures the next work",
86
+ ],
87
+ linked: [],
88
+ }, null, 2) + "\n",
89
+ });
61
90
 
62
91
  // --- API ---
63
92
  function apiCall(endpoint, data, options = {}) {
@@ -84,10 +113,10 @@ function apiCall(endpoint, data, options = {}) {
84
113
  const opts = {
85
114
  hostname: url.hostname,
86
115
  port: url.port || (url.protocol === "https:" ? 443 : 80),
87
- path: url.pathname,
116
+ path: url.pathname + url.search,
88
117
  method: body ? "POST" : "GET",
89
118
  headers,
90
- timeout: 60000,
119
+ timeout: Number(options && options.timeoutMs) > 0 ? Number(options.timeoutMs) : 60000,
91
120
  };
92
121
  if (body) opts.headers["Content-Length"] = Buffer.byteLength(body);
93
122
  const req = mod.request(opts, (res) => {
@@ -98,6 +127,11 @@ function apiCall(endpoint, data, options = {}) {
98
127
  catch { resolve({ error: `HTTP ${res.statusCode}` }); }
99
128
  });
100
129
  });
130
+ if (options && options.background) {
131
+ req.on("socket", (socket) => {
132
+ if (socket && typeof socket.unref === "function") socket.unref();
133
+ });
134
+ }
101
135
  req.on("error", (e) => resolve({ error: `${e.message}. Is ${API_URL} reachable?` }));
102
136
  req.on("timeout", () => { req.destroy(); resolve({ error: "request timed out after 60s. Check your internet connection or try again." }); });
103
137
  if (body) req.write(body);
@@ -345,33 +379,78 @@ function writeFiles(target, files) {
345
379
  return created + updated;
346
380
  }
347
381
 
382
+ function missingLiveManifestDefaults(target, pendingFiles = {}) {
383
+ const missing = {};
384
+ for (const [rel, content] of Object.entries(LIVE_MANIFEST_DEFAULTS)) {
385
+ if (Object.prototype.hasOwnProperty.call(pendingFiles, rel)) continue;
386
+ if (!fs.existsSync(path.join(target, rel))) missing[rel] = content;
387
+ }
388
+ return missing;
389
+ }
390
+
391
+ function ensureLiveManifestDefaults(target) {
392
+ const missing = missingLiveManifestDefaults(target);
393
+ for (const [rel, content] of Object.entries(missing)) {
394
+ const p = path.join(target, rel);
395
+ fs.mkdirSync(path.dirname(p), { recursive: true });
396
+ fs.writeFileSync(p, content, "utf8");
397
+ }
398
+ return Object.keys(missing);
399
+ }
400
+
348
401
  // --- Repo Script Lookup ---
349
- function findRepoScript(target, scriptName) {
350
- const candidates = [
402
+ function repoScriptCandidates(target, scriptName) {
403
+ return [
351
404
  path.join(target, "scripts", scriptName),
352
405
  path.join(process.cwd(), "scripts", scriptName),
406
+ path.join(__dirname, "..", "scripts", scriptName),
353
407
  path.join(__dirname, "..", "..", "..", "scripts", scriptName),
354
408
  path.join(__dirname, "..", "..", "..", "..", "scripts", scriptName),
355
409
  ];
410
+ }
411
+
412
+ function findRepoScript(target, scriptName) {
413
+ const candidates = repoScriptCandidates(target, scriptName);
356
414
  for (const c of candidates) { if (fs.existsSync(c)) return c; }
357
415
  return null;
358
416
  }
359
417
 
418
+ // Python scripts copied into the package at lib/python/ on prepack
419
+ // (scripts/build-python-bundle.js). Lets pure-stdlib commands work for npm
420
+ // end-users who have no repo checkout of scripts/. Mirrors experience.js (#4360).
421
+ function resolveBundledScript(scriptName) {
422
+ const bundled = path.join(__dirname, "python", scriptName);
423
+ return fs.existsSync(bundled) ? bundled : null;
424
+ }
425
+
426
+ // Prefer a repo copy (operator/dev checkout); fall back to the bundled copy.
427
+ function resolvePythonScript(target, scriptName) {
428
+ return findRepoScript(target, scriptName) || resolveBundledScript(scriptName);
429
+ }
430
+
360
431
  // --- Version Check ---
361
- async function checkVersion() {
432
+ async function checkVersion(options = {}) {
362
433
  try {
434
+ const force = Boolean(options && options.force);
435
+ const background = options && Object.prototype.hasOwnProperty.call(options, "background")
436
+ ? Boolean(options.background)
437
+ : true;
363
438
  const intervalSec = parseInt(process.env.ODAI_UPDATE_CHECK_INTERVAL || "3600");
364
439
  let lastCheck = 0;
365
440
  try { lastCheck = parseFloat(fs.readFileSync(VERSION_CHECK_FILE, "utf8")); } catch {}
366
- if (Date.now() / 1000 - lastCheck < intervalSec) return;
441
+ if (!force && Date.now() / 1000 - lastCheck < intervalSec) return;
367
442
  fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
368
443
  fs.writeFileSync(VERSION_CHECK_FILE, String(Date.now() / 1000));
369
- const result = await apiCall("/v1/version");
444
+ const result = await apiCall("/v1/version", null, {
445
+ background,
446
+ timeoutMs: options && options.timeoutMs,
447
+ });
370
448
  if (result.version && result.version !== VERSION) {
371
449
  const cmp = (a, b) => { const [a1,a2,a3] = a.split(".").map(Number); const [b1,b2,b3] = b.split(".").map(Number); return a1 - b1 || a2 - b2 || a3 - b3; };
372
450
  if (cmp(result.version, VERSION) > 0) {
373
451
  log(`Update available: ${VERSION} → ${result.version}`);
374
- console.log(` Run: npm update -g @0dai-dev/cli\n`);
452
+ const suffix = options && options.initHint ? ", then rerun 0dai init" : "";
453
+ console.log(` Run: npm update -g @0dai-dev/cli${suffix}\n`);
375
454
  }
376
455
  }
377
456
  } catch {}
@@ -379,7 +458,7 @@ async function checkVersion() {
379
458
 
380
459
  // --- Experience ---
381
460
  function recordExperienceEvent(target, payload) {
382
- const script = findRepoScript(target, "experience_pipeline.py");
461
+ const script = resolvePythonScript(target, "experience_pipeline.py");
383
462
  if (!script) return;
384
463
  try {
385
464
  spawnSync("python3", [script, "record-json", "--target", target], {
@@ -394,7 +473,7 @@ module.exports = {
394
473
  VERSION, API_URL, T, R, D, E, G, W,
395
474
  log, CONFIG_DIR, AUTH_FILE, VERSION_CHECK_FILE, PROJECTS_FILE,
396
475
  // Constants
397
- PLAN_LEVELS, MANIFEST_FILES, PROBE_DIRS, SUPPORTED_CLIS, SETTINGS_PRESERVE_FIELDS,
476
+ PLAN_LEVELS, MANIFEST_FILES, PROBE_DIRS, SUPPORTED_CLIS, SETTINGS_PRESERVE_FIELDS, cliDisplayName,
398
477
  // API
399
478
  apiCall,
400
479
  // Auth
@@ -402,14 +481,18 @@ module.exports = {
402
481
  fetchAuthStatus, makeEnsureAuthenticated, ensureLicenseActivation,
403
482
  // Identity
404
483
  deviceFingerprint, registerProject, projectIdFor,
484
+ loadProjectBinding, validProjectId, validateProjectDisplayName,
485
+ bindingTargetMatches, projectTargetInfo, boundProjectIdentityFromBinding,
486
+ applyBoundProjectIdentity, writeProjectBinding,
405
487
  getGitRemoteOrigin, inferProjectName, detectStackHint,
406
- collectMetadata, buildProjectIdentity,
488
+ collectMetadata, buildProjectIdentity, hashManifestFiles,
407
489
  // Plan / Tier
408
490
  _detectPlanLocal, requirePlan, getSwarmQuotaLocal,
409
491
  // Project
410
492
  sendProjectHeartbeat, recordExperienceEvent, logFirstRunSuccess,
411
493
  // Files
412
- mergeSettingsJson, writeFiles, findRepoScript,
494
+ mergeSettingsJson, writeFiles, missingLiveManifestDefaults, ensureLiveManifestDefaults,
495
+ findRepoScript, repoScriptCandidates, resolveBundledScript, resolvePythonScript,
413
496
  // Version
414
497
  checkVersion,
415
498
  // Re-exports for convenience