@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
@@ -9,6 +9,30 @@ const path = require("path");
9
9
  const os = require("os");
10
10
  const { MANIFEST_FILES, PROBE_DIRS, SUPPORTED_CLIS } = require("./constants");
11
11
 
12
+ const BINDING_IDENTITY_KEYS = new Set([
13
+ "managed",
14
+ "schema",
15
+ "target",
16
+ "current_target",
17
+ "target_aliases",
18
+ "project_id",
19
+ "project_name",
20
+ "display_name",
21
+ "stack",
22
+ "origin",
23
+ "remote_origin",
24
+ "path_hash",
25
+ "device",
26
+ "binding_status",
27
+ "binding_reason",
28
+ "binding_next_action",
29
+ "binding_source",
30
+ "bound_at",
31
+ "updated_at",
32
+ "last_error",
33
+ "server_project",
34
+ ]);
35
+
12
36
  function deviceFingerprint() {
13
37
  const crypto = require("crypto");
14
38
  const parts = [
@@ -93,6 +117,163 @@ function readDiscovery(target) {
93
117
  }
94
118
  }
95
119
 
120
+ function loadProjectBinding(target) {
121
+ try {
122
+ const binding = JSON.parse(fs.readFileSync(path.join(target, ".0dai", "project-binding.json"), "utf8"));
123
+ return binding && typeof binding === "object" && !Array.isArray(binding) ? binding : null;
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ function _stringValue(value) {
130
+ return typeof value === "string" ? value.trim() : "";
131
+ }
132
+
133
+ function validProjectId(value) {
134
+ return /^prj_[A-Za-z0-9_-]{8,}$/.test(_stringValue(value));
135
+ }
136
+
137
+ function validateProjectDisplayName(value) {
138
+ const name = _stringValue(value);
139
+ if (!name) return { ok: false, error: "project name must not be empty" };
140
+ if (name.length > 120) return { ok: false, error: "project name must be 120 characters or fewer" };
141
+ if (/[\x00-\x1f\x7f]/.test(name)) return { ok: false, error: "project name must not contain control characters" };
142
+ return { ok: true, name };
143
+ }
144
+
145
+ function _realpathOrResolved(target) {
146
+ const resolved = path.resolve(target);
147
+ try {
148
+ return fs.realpathSync.native(resolved);
149
+ } catch {
150
+ return resolved;
151
+ }
152
+ }
153
+
154
+ function _bindingPathCandidates(binding) {
155
+ const candidates = [];
156
+ if (binding && typeof binding === "object") {
157
+ for (const key of ["target", "current_target"]) {
158
+ const value = _stringValue(binding[key]);
159
+ if (value) candidates.push(value);
160
+ }
161
+ if (Array.isArray(binding.target_aliases)) {
162
+ for (const alias of binding.target_aliases) {
163
+ const value = _stringValue(alias);
164
+ if (value) candidates.push(value);
165
+ }
166
+ }
167
+ }
168
+ return candidates;
169
+ }
170
+
171
+ function bindingTargetMatches(target, binding) {
172
+ if (!binding || typeof binding !== "object") return false;
173
+ const currentTarget = path.resolve(target);
174
+ const canonicalTarget = _realpathOrResolved(target);
175
+ for (const candidate of _bindingPathCandidates(binding)) {
176
+ const resolved = path.resolve(candidate);
177
+ if (resolved === currentTarget || resolved === canonicalTarget) return true;
178
+ try {
179
+ if (fs.realpathSync.native(resolved) === canonicalTarget) return true;
180
+ } catch {}
181
+ }
182
+ return false;
183
+ }
184
+
185
+ function projectTargetInfo(target, binding = null) {
186
+ const currentTarget = path.resolve(target);
187
+ const canonicalTarget = _realpathOrResolved(target);
188
+ const seen = new Set();
189
+ const aliases = [];
190
+ const addAlias = (value) => {
191
+ const raw = _stringValue(value);
192
+ if (!raw) return;
193
+ const resolved = path.resolve(raw);
194
+ if (resolved === canonicalTarget) return;
195
+ if (seen.has(resolved)) return;
196
+ seen.add(resolved);
197
+ aliases.push(resolved);
198
+ };
199
+ if (currentTarget !== canonicalTarget) addAlias(currentTarget);
200
+ for (const candidate of _bindingPathCandidates(binding)) addAlias(candidate);
201
+ return { current_target: currentTarget, target: canonicalTarget, target_aliases: aliases };
202
+ }
203
+
204
+ function boundProjectIdentityFromBinding(target, binding) {
205
+ if (!binding || binding.binding_status !== "bound") return null;
206
+ if (!bindingTargetMatches(target, binding)) return null;
207
+ const out = {};
208
+ if (validProjectId(binding.project_id)) out.project_id = _stringValue(binding.project_id);
209
+ const displayName = _stringValue(binding.display_name) || _stringValue(binding.project_name);
210
+ if (displayName) out.project_name = displayName;
211
+ return Object.keys(out).length ? out : null;
212
+ }
213
+
214
+ function projectIdForExplicitRebindFromBinding(binding) {
215
+ if (!binding || typeof binding !== "object" || Array.isArray(binding)) return "";
216
+ return validProjectId(binding.project_id) ? _stringValue(binding.project_id) : "";
217
+ }
218
+
219
+ function applyBoundProjectIdentity(target, identity, binding) {
220
+ const bound = boundProjectIdentityFromBinding(target, binding);
221
+ if (!bound) return identity;
222
+ return {
223
+ ...identity,
224
+ ...(bound.project_id ? { project_id: bound.project_id } : {}),
225
+ ...(bound.project_name ? { project_name: bound.project_name } : {}),
226
+ };
227
+ }
228
+
229
+ function writeProjectBinding(target, options = {}) {
230
+ const existing = loadProjectBinding(target) || {};
231
+ const preserved = {};
232
+ for (const [key, value] of Object.entries(existing)) {
233
+ if (!BINDING_IDENTITY_KEYS.has(key)) preserved[key] = value;
234
+ }
235
+
236
+ const identity = options.identity || {};
237
+ const serverProject = options.server_project || options.serverProject || {};
238
+ const targetInfo = projectTargetInfo(target, bindingTargetMatches(target, existing) ? existing : null);
239
+ const projectID = _stringValue(options.project_id) || _stringValue(serverProject.project_id) || _stringValue(identity.project_id);
240
+ const projectName =
241
+ _stringValue(options.project_name) ||
242
+ _stringValue(options.display_name) ||
243
+ _stringValue(serverProject.name) ||
244
+ _stringValue(identity.project_name);
245
+ const now = new Date().toISOString();
246
+ const binding = {
247
+ ...preserved,
248
+ managed: true,
249
+ schema: 1,
250
+ target: targetInfo.target,
251
+ current_target: targetInfo.current_target,
252
+ target_aliases: targetInfo.target_aliases,
253
+ project_id: projectID,
254
+ project_name: projectName,
255
+ display_name: projectName,
256
+ stack: _stringValue(options.stack) || _stringValue(serverProject.stack) || _stringValue(identity.stack) || "unknown",
257
+ origin: _stringValue(identity.origin) || _stringValue(existing.origin) || "local",
258
+ remote_origin: _stringValue(identity.remote_origin) || _stringValue(existing.remote_origin),
259
+ binding_status: _stringValue(options.binding_status) || _stringValue(serverProject.binding_status) || "bound",
260
+ binding_source: _stringValue(options.binding_source) || "project bind",
261
+ bound_at: _stringValue(options.bound_at) || _stringValue(existing.bound_at) || now,
262
+ updated_at: now,
263
+ server_project: serverProject,
264
+ last_error: _stringValue(options.last_error),
265
+ };
266
+ if (options.account_email) binding.account_email = String(options.account_email);
267
+ if (options.account_plan) binding.account_plan = String(options.account_plan);
268
+ if (options.activation_status) binding.activation_status = String(options.activation_status);
269
+ if (options.activation_id) binding.activation_id = String(options.activation_id);
270
+
271
+ const file = path.join(target, ".0dai", "project-binding.json");
272
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
273
+ fs.writeFileSync(file, JSON.stringify(binding, null, 2) + "\n", { mode: 0o600 });
274
+ return binding;
275
+ }
276
+
96
277
  function projectIdFor(target, projectName, remoteOrigin) {
97
278
  const crypto = require("crypto");
98
279
  const seed = projectIdSeed(projectName, remoteOrigin || path.resolve(target));
@@ -148,6 +329,19 @@ function detectStackHint(projectFiles, manifestContents) {
148
329
  return "unknown";
149
330
  }
150
331
 
332
+ /**
333
+ * Return a {filename: sha256hex} map for the given manifest contents.
334
+ * The raw content never leaves the machine — only the hash is exported.
335
+ */
336
+ function hashManifestFiles(manifestContents) {
337
+ const crypto = require("crypto");
338
+ const hashes = {};
339
+ for (const [name, content] of Object.entries(manifestContents)) {
340
+ hashes[name] = crypto.createHash("sha256").update(content, "utf8").digest("hex");
341
+ }
342
+ return hashes;
343
+ }
344
+
151
345
  function collectMetadata(target) {
152
346
  const projectFiles = [];
153
347
  const manifestContents = {};
@@ -200,6 +394,9 @@ module.exports = {
200
394
  MANIFEST_FILES, PROBE_DIRS,
201
395
  deviceFingerprint, registerProject, projectIdFor, projectIdSeed,
202
396
  scrubRemoteUrl, readProjectManifest, readDiscovery,
397
+ loadProjectBinding, validProjectId, validateProjectDisplayName,
398
+ bindingTargetMatches, projectTargetInfo, boundProjectIdentityFromBinding,
399
+ projectIdForExplicitRebindFromBinding, applyBoundProjectIdentity, writeProjectBinding,
203
400
  getGitRemoteOrigin, inferProjectName, detectStackHint,
204
- collectMetadata, buildProjectIdentity,
401
+ collectMetadata, buildProjectIdentity, hashManifestFiles,
205
402
  };
@@ -24,7 +24,7 @@ const EMBEDDED_TEMPLATE = `{
24
24
  "command": "npx",
25
25
  "args": ["-y", "@modelcontextprotocol/server-filesystem", "{{PROJECT_PATH}}"]
26
26
  },
27
- "claude.ai 0dai": {
27
+ "claude_ai_0dai": {
28
28
  "type": "http",
29
29
  "url": "{{MCP_HOST}}/mcp"
30
30
  }
@@ -71,8 +71,8 @@ function renderTemplate(template, values) {
71
71
  });
72
72
  }
73
73
 
74
- function repoRootFromPackage() {
75
- return path.resolve(__dirname, "..", "..", "..", "..");
74
+ function repoRootFromPackage(options = {}) {
75
+ return options.repoRoot ? path.resolve(options.repoRoot) : path.resolve(__dirname, "..", "..", "..", "..");
76
76
  }
77
77
 
78
78
  function resolveTemplatePath() {
@@ -101,17 +101,22 @@ function loadTemplate() {
101
101
  return { text: EMBEDDED_TEMPLATE, source: "embedded" };
102
102
  }
103
103
 
104
- function resolveMcpServerScript(target) {
105
- const repoRoot = repoRootFromPackage();
104
+ function resolveMcpServerScript(target, options = {}) {
105
+ const explicit = String((options.env || process.env).ODAI_MCP_SERVER_SCRIPT || "").trim();
106
+ if (explicit) {
107
+ return { path: explicit, found: true, explicit: true };
108
+ }
109
+ const repoRoot = repoRootFromPackage(options);
110
+ const cwd = options.cwd ? path.resolve(options.cwd) : process.cwd();
106
111
  const candidates = [
107
112
  path.join(target, "scripts", "mcp_server.py"),
108
- path.join(process.cwd(), "scripts", "mcp_server.py"),
113
+ path.join(cwd, "scripts", "mcp_server.py"),
109
114
  path.join(repoRoot, "scripts", "mcp_server.py"),
110
115
  ];
111
116
  for (const candidate of candidates) {
112
- if (fs.existsSync(candidate)) return candidate;
117
+ if (fs.existsSync(candidate)) return { path: candidate, found: true, explicit: false };
113
118
  }
114
- return "scripts/mcp_server.py";
119
+ return { path: "", found: false, explicit: false };
115
120
  }
116
121
 
117
122
  function readJsonFile(filePath) {
@@ -190,7 +195,7 @@ function shouldUpdateManagedServer(name, existingConfig, incomingConfig, options
190
195
  const incomingTarget = filesystemTarget(incomingConfig);
191
196
  return (incomingTarget && !existingTarget) || pathsDiffer(existingTarget, incomingTarget, base);
192
197
  }
193
- if (name === DEFAULT_CLOUD_SERVER_NAME) {
198
+ if (name === DEFAULT_CLOUD_SERVER_ID) {
194
199
  const existingUrl = existingConfig && typeof existingConfig.url === "string" ? existingConfig.url : "";
195
200
  const incomingUrl = incomingConfig && typeof incomingConfig.url === "string" ? incomingConfig.url : "";
196
201
  return !!incomingUrl && existingUrl !== incomingUrl;
@@ -198,16 +203,48 @@ function shouldUpdateManagedServer(name, existingConfig, incomingConfig, options
198
203
  return false;
199
204
  }
200
205
 
206
+ function normalizeCloudServerIds(servers) {
207
+ const out = servers && typeof servers === "object" && !Array.isArray(servers) ? { ...servers } : {};
208
+ if (Object.prototype.hasOwnProperty.call(out, DEFAULT_CLOUD_SERVER_NAME)) {
209
+ if (!Object.prototype.hasOwnProperty.call(out, DEFAULT_CLOUD_SERVER_ID)) {
210
+ out[DEFAULT_CLOUD_SERVER_ID] = out[DEFAULT_CLOUD_SERVER_NAME];
211
+ }
212
+ delete out[DEFAULT_CLOUD_SERVER_NAME];
213
+ }
214
+ return out;
215
+ }
216
+
217
+ function mcpServerScriptArg(config) {
218
+ const args = Array.isArray(config && config.args) ? config.args : [];
219
+ for (const arg of args) {
220
+ const value = String(arg || "");
221
+ if (/(^|[/\\])mcp_server\.py$/.test(value)) return value;
222
+ }
223
+ const command = typeof (config && config.command) === "string" ? config.command : "";
224
+ return /(^|[/\\])mcp_server\.py$/.test(command) ? command : "";
225
+ }
226
+
227
+ function looksLikeManagedLocalOdaiServer(name, config, options = {}) {
228
+ if (name !== "0dai") return false;
229
+ if (options.reset) return true;
230
+ const args = Array.isArray(config && config.args) ? config.args.map((arg) => String(arg || "")) : [];
231
+ const command = typeof (config && config.command) === "string" ? config.command : "";
232
+ if (mcpServerScriptArg(config)) return true;
233
+ if (envTarget(config)) return true;
234
+ return command === "python3" && args.includes("--target");
235
+ }
236
+
201
237
  function mergeMcpConfig(existing, incoming, options = {}) {
202
238
  const out = existing && typeof existing === "object" && !Array.isArray(existing) ? { ...existing } : {};
203
239
  const existingServers = out.mcpServers && typeof out.mcpServers === "object" && !Array.isArray(out.mcpServers)
204
- ? { ...out.mcpServers }
240
+ ? normalizeCloudServerIds(out.mcpServers)
205
241
  : {};
206
242
  const incomingServers = incoming && incoming.mcpServers && typeof incoming.mcpServers === "object"
207
- ? incoming.mcpServers
243
+ ? normalizeCloudServerIds(incoming.mcpServers)
208
244
  : {};
209
245
  const added = [];
210
246
  const updated = [];
247
+ const removed = [];
211
248
 
212
249
  for (const [name, config] of Object.entries(incomingServers)) {
213
250
  if (!(name in existingServers)) {
@@ -221,17 +258,24 @@ function mergeMcpConfig(existing, incoming, options = {}) {
221
258
  }
222
259
  }
223
260
 
261
+ if (options.removeLocalOdai && Object.prototype.hasOwnProperty.call(existingServers, "0dai")
262
+ && looksLikeManagedLocalOdaiServer("0dai", existingServers["0dai"], options)) {
263
+ delete existingServers["0dai"];
264
+ removed.push("0dai");
265
+ }
266
+
224
267
  out.mcpServers = existingServers;
225
- return { config: out, added, updated };
268
+ return { config: out, added, updated, removed };
226
269
  }
227
270
 
228
271
  function ensureMcpConfig(target, options = {}) {
229
272
  const resolvedTarget = path.resolve(target);
230
273
  const { text, source } = loadTemplate();
274
+ const mcpServerScript = resolveMcpServerScript(resolvedTarget, options);
231
275
  const rendered = renderTemplate(text, {
232
276
  PROJECT_PATH: resolvedTarget,
233
277
  MCP_HOST: options.host || DEFAULT_MCP_HOST,
234
- ODAI_MCP_SERVER_SCRIPT: resolveMcpServerScript(resolvedTarget),
278
+ ODAI_MCP_SERVER_SCRIPT: mcpServerScript.path,
235
279
  });
236
280
  let incoming;
237
281
  try {
@@ -239,12 +283,32 @@ function ensureMcpConfig(target, options = {}) {
239
283
  } catch (err) {
240
284
  throw new Error(`MCP template rendered invalid JSON: ${err.message}`);
241
285
  }
286
+ const warnings = [];
287
+ if (!mcpServerScript.found && incoming && incoming.mcpServers && incoming.mcpServers["0dai"]) {
288
+ delete incoming.mcpServers["0dai"];
289
+ warnings.push(
290
+ "local 0dai MCP skipped: mcp_server.py not found; cloud claude_ai_0dai remains configured. Set ODAI_MCP_SERVER_SCRIPT to enable local stdio MCP.",
291
+ );
292
+ }
242
293
 
243
294
  const mcpPath = path.join(resolvedTarget, ".mcp.json");
244
295
  const existing = readJsonFile(mcpPath);
245
- const merged = mergeMcpConfig(existing, incoming, { reset: !!options.reset, target: resolvedTarget });
296
+ const merged = mergeMcpConfig(existing, incoming, {
297
+ reset: !!options.reset,
298
+ target: resolvedTarget,
299
+ removeLocalOdai: !mcpServerScript.found,
300
+ });
246
301
  const changed = writeJsonIfChanged(mcpPath, merged.config);
247
- return { path: mcpPath, changed, template: source, added: merged.added, updated: merged.updated };
302
+ return {
303
+ path: mcpPath,
304
+ changed,
305
+ template: source,
306
+ added: merged.added,
307
+ updated: merged.updated,
308
+ removed: merged.removed,
309
+ localServer: mcpServerScript,
310
+ warnings,
311
+ };
248
312
  }
249
313
 
250
314
  function ensureClaudeSettings(target) {
@@ -551,6 +615,7 @@ async function bootstrapMcp(target, args = [], logger = console.log) {
551
615
 
552
616
  try {
553
617
  result.config = ensureMcpConfig(target, { host: options.host, reset: options.reset });
618
+ result.warnings.push(...(result.config.warnings || []));
554
619
  } catch (err) {
555
620
  result.ok = false;
556
621
  result.warnings.push(err.message);
@@ -602,6 +667,7 @@ module.exports = {
602
667
  normalizeMcpHost,
603
668
  parseMcpArgs,
604
669
  renderTemplate,
670
+ resolveMcpServerScript,
605
671
  runOAuthHandshake,
606
672
  writeMcpAuthTokenFromAccount,
607
673
  };
package/lib/utils/plan.js CHANGED
@@ -92,7 +92,7 @@ function requirePlan(requiredPlan, featureName, target) {
92
92
 
93
93
  function getSwarmQuotaLocal(target) {
94
94
  const plan = _detectPlanLocal(target);
95
- const limits = { free: 0, pro: 50, team: 200, enterprise: 999999 };
95
+ const limits = { free: 1, pro: 50, team: 200, enterprise: 999999 };
96
96
  const dailyLimit = limits[plan] || 0;
97
97
  const budgetPath = path.join(target, "ai", "swarm", "budget.json");
98
98
  let usedToday = 0;
@@ -78,10 +78,10 @@ const cipher = require("./cipher");
78
78
  const NAME_RE = /^[a-zA-Z0-9._-]{1,64}$/;
79
79
 
80
80
  function _validateName(name) {
81
- if (typeof name !== "string" || !NAME_RE.test(name)) {
81
+ if (typeof name !== "string" || name === "." || name === ".." || !NAME_RE.test(name)) {
82
82
  const err = new Error(
83
83
  `invalid secret name: ${JSON.stringify(name)} — ` +
84
- "must match /^[a-zA-Z0-9._-]{1,64}$/",
84
+ "must match /^[a-zA-Z0-9._-]{1,64}$/ and not be '.' or '..'",
85
85
  );
86
86
  err.code = "VAULT_INVALID_NAME";
87
87
  throw err;
@@ -135,9 +135,25 @@ function add(scope, name, value, options = {}) {
135
135
  throw err;
136
136
  }
137
137
  const sdir = storage.scopeDir(vdir, scope);
138
+ const outPath = _secretPath(vdir, scope, name);
139
+ // Defense in depth (#4358): even if scope/name validation were bypassed,
140
+ // never write a secret over the identity key/public key or outside the
141
+ // vault dir.
142
+ const vroot = path.resolve(vdir);
143
+ const resolvedOut = path.resolve(outPath);
144
+ if (
145
+ !resolvedOut.startsWith(vroot + path.sep) ||
146
+ resolvedOut === path.resolve(storage.identityPath(vdir)) ||
147
+ resolvedOut === path.resolve(storage.publicKeyPath(vdir))
148
+ ) {
149
+ const err = new Error(
150
+ `refusing to write secret to a protected vault path: ${outPath}`,
151
+ );
152
+ err.code = "VAULT_PROTECTED_PATH";
153
+ throw err;
154
+ }
138
155
  fs.mkdirSync(sdir, { recursive: true, mode: 0o700 });
139
156
  try { fs.chmodSync(sdir, 0o700); } catch (_) { /* best effort */ }
140
- const outPath = _secretPath(vdir, scope, name);
141
157
  const overwritten = fs.existsSync(outPath);
142
158
  cipher.encryptToFile(value, recipient, outPath);
143
159
  const stat = fs.statSync(outPath);
@@ -66,11 +66,30 @@ function publicKeyPath(dir) {
66
66
  * Phase 2 helper: per-scope dir under the vault. Stubbed so callers can
67
67
  * import it today; not exercised by Phase 1 tests.
68
68
  */
69
+ // A scope must be a single, safe path segment. Critically this rejects
70
+ // "." and ".." (which path.join collapses into the vault root or its
71
+ // parent), preventing `vault add . identity <v>` from overwriting the
72
+ // master key and `vault add .. <name> <v>` from escaping the vault (#4358).
73
+ const SCOPE_RE = /^[a-zA-Z0-9._-]{1,64}$/;
74
+
69
75
  function scopeDir(dir, scope) {
70
- if (!scope || typeof scope !== "string" || scope.includes("/") || scope.includes("\\")) {
76
+ if (
77
+ !scope ||
78
+ typeof scope !== "string" ||
79
+ scope === "." ||
80
+ scope === ".." ||
81
+ !SCOPE_RE.test(scope) ||
82
+ scope.includes("/") ||
83
+ scope.includes("\\")
84
+ ) {
85
+ throw new Error(`invalid vault scope: ${JSON.stringify(scope)}`);
86
+ }
87
+ const base = path.resolve(dir);
88
+ const resolved = path.resolve(base, scope);
89
+ if (resolved === base || path.dirname(resolved) !== base) {
71
90
  throw new Error(`invalid vault scope: ${JSON.stringify(scope)}`);
72
91
  }
73
- return path.join(dir, scope);
92
+ return resolved;
74
93
  }
75
94
 
76
95
  module.exports = {
package/lib/wizard.js CHANGED
@@ -9,7 +9,7 @@
9
9
  const fs = require("fs");
10
10
  const path = require("path");
11
11
  const readline = require("readline");
12
- const { writeFiles } = require("./shared");
12
+ const { writeFiles, ensureLiveManifestDefaults } = require("./shared");
13
13
  const { loadCanonicalCounts, mcpToolsLabel } = require("./utils/canonical-counts");
14
14
 
15
15
  // ---------------------------------------------------------------------------
@@ -182,6 +182,7 @@ function stepGenerate(target, agent, stack) {
182
182
  path.join(manifestDir, "project.yaml"),
183
183
  `plan: free\nname: ${discovery.project_name}\nstack: ${discovery.stack}\n`,
184
184
  );
185
+ ensureLiveManifestDefaults(target);
185
186
 
186
187
  // CLAUDE.md
187
188
  const claudeMd = `# ${discovery.project_name}\n\nStack: ${stack.join(", ") || "unknown"}\nGenerated by 0dai wizard.\n\n## Commands\n\nSee ai/manifest/ for full configuration.\n`;
@@ -198,9 +199,11 @@ function stepGenerate(target, agent, stack) {
198
199
  // ai/manifest files
199
200
  console.log(" ✓ ai/manifest/discovery.json");
200
201
  console.log(" ✓ ai/manifest/project.yaml");
202
+ console.log(" ✓ ai/manifest/current_state.json");
203
+ console.log(" ✓ ai/manifest/current_task.json");
201
204
  console.log(" ✓ ai/VERSION");
202
205
 
203
- const count = 5; // CLAUDE.md, AGENTS.md, discovery.json, project.yaml, VERSION
206
+ const count = 7; // CLAUDE.md, AGENTS.md, discovery.json, project.yaml, live manifests, VERSION
204
207
  console.log("");
205
208
  console.log(` ${count} configs generated in ai/ directory.`);
206
209
  return count;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@0dai-dev/cli",
3
- "version": "4.3.5",
4
- "description": "One config layer for seven AI coding agents Claude Code, Codex, OpenCode, Gemini, Aider, Qoder",
3
+ "version": "4.3.7",
4
+ "description": "One config layer for seven AI coding agents \u2014 Claude Code, Codex, OpenCode, Gemini, Aider, Qoder, Cursor",
5
5
  "bin": {
6
6
  "0dai": "./bin/0dai.js"
7
7
  },
@@ -29,18 +29,24 @@
29
29
  "url": "https://github.com/0dai-dev/0dai/issues"
30
30
  },
31
31
  "homepage": "https://0dai.dev",
32
+ "privacy": "https://0dai.dev/legal/privacy",
32
33
  "engines": {
33
34
  "node": ">=20"
34
35
  },
35
36
  "scripts": {
36
37
  "postinstall": "node scripts/postinstall.js || true",
37
38
  "build:tui": "node scripts/build-tui.js",
38
- "prepack": "node scripts/build-tui.js"
39
+ "build:python": "node scripts/build-python-bundle.js",
40
+ "prepack": "node scripts/build-tui.js && node scripts/build-python-bundle.js"
39
41
  },
40
42
  "files": [
41
43
  "bin/",
42
44
  "lib/",
43
45
  "scripts/",
46
+ "!scripts/__pycache__/",
47
+ "!scripts/**/*.pyc",
48
+ "!lib/**/__pycache__/",
49
+ "!lib/**/*.pyc",
44
50
  "README.md",
45
51
  "!lib/tui/src/**"
46
52
  ],
@@ -0,0 +1,106 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ const ROOT = path.resolve(__dirname, '..');
5
+ const REPO = path.resolve(ROOT, '..', '..');
6
+ const SRC_DIR = path.join(REPO, 'scripts');
7
+ const LIB_DIR = path.join(ROOT, 'lib');
8
+ const OUT_DIR = path.join(ROOT, 'lib', 'python');
9
+ const FILES = [
10
+ 'experience_pipeline.py',
11
+ 'anti_pattern_detector.py',
12
+ 'auth.py',
13
+ 'project_manager.py',
14
+ 'json_utils.py',
15
+ // bundle-quickwins (#4363 triage): pure-stdlib single-file commands.
16
+ // drift_detector.py imports json_utils (above) — both land in lib/python/.
17
+ 'heatmap.py',
18
+ 'model_router.py',
19
+ 'agent_quotas.py',
20
+ 'receipt_png.py',
21
+ 'drift_detector.py',
22
+ // bundle mid-wave (#4363): pure-stdlib closures, no pip deps.
23
+ // usage_ledger imports swarm_cost; compliance_report imports json_utils (above).
24
+ 'usage_ledger.py',
25
+ 'swarm_cost.py',
26
+ 'compliance_report.py',
27
+ // bundle heavy-wave (#4363): doctor + graph closures, all pure-stdlib, no pip.
28
+ // doctor: provider check (provider_profiles -> capi_profile_guard) + mcp
29
+ // exposure check (mcp_exposure_check -> anomaly_alert); drift already above.
30
+ 'provider_profiles.py',
31
+ 'capi_profile_guard.py',
32
+ 'mcp_exposure_check.py',
33
+ 'anomaly_alert.py',
34
+ // provider registry: npm list/switch/clear wrapper + BYOK registry module.
35
+ 'provider_registry.py',
36
+ 'provider_registry_cli.py',
37
+ // graph: graph_slicer_cli + structural_memory pull the split graph_* modules
38
+ // via the graph.py compatibility facade.
39
+ 'graph.py',
40
+ 'graph_core.py',
41
+ 'graph_io.py',
42
+ 'graph_legacy.py',
43
+ 'graph_legacy_helpers.py',
44
+ 'graph_outcomes_core.py',
45
+ 'graph_queries.py',
46
+ 'graph_slice.py',
47
+ 'graph_slicer.py',
48
+ 'graph_slicer_cli.py',
49
+ 'graph_validation.py',
50
+ 'structural_memory.py'
51
+ ];
52
+ const DATA_FILES = [
53
+ {
54
+ src: path.join(REPO, 'ai', 'manifest', 'mcp-exposure-contract.json'),
55
+ dest: path.join(LIB_DIR, 'ai', 'manifest', 'mcp-exposure-contract.json'),
56
+ },
57
+ {
58
+ src: path.join(REPO, 'ai', 'meta', 'manifest', 'mcp-tool-tiers.json'),
59
+ dest: path.join(LIB_DIR, 'ai', 'meta', 'manifest', 'mcp-tool-tiers.json'),
60
+ },
61
+ {
62
+ src: path.join(REPO, 'templates', 'layer', 'ai', 'registry', 'mcp-catalog.json'),
63
+ dest: path.join(LIB_DIR, 'ai', 'registry', 'mcp-catalog.json'),
64
+ },
65
+ {
66
+ src: path.join(SRC_DIR, 'mcp_tier_config.py'),
67
+ dest: path.join(LIB_DIR, 'scripts', 'mcp_tier_config.py'),
68
+ },
69
+ ];
70
+
71
+ try {
72
+ if (!fs.existsSync(SRC_DIR)) {
73
+ console.error(`[py-bundle] missing required scripts directory: ${SRC_DIR}`);
74
+ process.exit(1);
75
+ }
76
+
77
+ fs.mkdirSync(OUT_DIR, { recursive: true });
78
+
79
+ for (const file of FILES) {
80
+ const srcPath = path.join(SRC_DIR, file);
81
+ const outPath = path.join(OUT_DIR, file);
82
+
83
+ if (fs.existsSync(srcPath)) {
84
+ fs.copyFileSync(srcPath, outPath);
85
+ } else {
86
+ console.error(`[py-bundle] missing required source file: ${file}`);
87
+ process.exit(1);
88
+ }
89
+ }
90
+
91
+ for (const { src, dest } of DATA_FILES) {
92
+ if (!fs.existsSync(src)) {
93
+ console.error(`[py-bundle] missing required data file: ${path.relative(REPO, src)}`);
94
+ process.exit(1);
95
+ }
96
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
97
+ fs.copyFileSync(src, dest);
98
+ }
99
+
100
+ fs.writeFileSync(path.join(OUT_DIR, '__init__.py'), '');
101
+
102
+ console.log(`[py-bundle] wrote ${FILES.length} python files and ${DATA_FILES.length} data files to lib`);
103
+ } catch (e) {
104
+ console.error('[py-bundle] failed:', e.message);
105
+ process.exit(1);
106
+ }