@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
|
@@ -350,13 +350,10 @@ function printActivationStats(target) {
|
|
|
350
350
|
|
|
351
351
|
const mrStats = getActivationMergedPrStats(target);
|
|
352
352
|
if (mrStats.count > 0) {
|
|
353
|
-
const cohortNote = mrStats.cohorts.external > 0
|
|
354
|
-
? ` [dogfood: ${mrStats.cohorts.dogfood}, external: ${mrStats.cohorts.external}]`
|
|
355
|
-
: ` [dogfood only — no external activations yet]`;
|
|
356
353
|
console.log(
|
|
357
354
|
` activation first-PR: p50 ${formatDurationMs(mrStats.p50_ms)}`
|
|
358
355
|
+ ` / p90 ${formatDurationMs(mrStats.p90_ms)}`
|
|
359
|
-
+ ` (${mrStats.count} sample${mrStats.count === 1 ? "" : "s"})
|
|
356
|
+
+ ` (${mrStats.count} sample${mrStats.count === 1 ? "" : "s"})`,
|
|
360
357
|
);
|
|
361
358
|
}
|
|
362
359
|
|
package/lib/utils/constants.js
CHANGED
|
@@ -80,4 +80,10 @@ const PROBE_DIRS = [
|
|
|
80
80
|
|
|
81
81
|
const SETTINGS_PRESERVE_FIELDS = ["model", "permissionMode", "effortLevel"];
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
function cliDisplayName(cli) {
|
|
84
|
+
const name = String((cli && cli.name) || "").trim() || "unknown";
|
|
85
|
+
const bin = String((cli && cli.bin) || "").trim();
|
|
86
|
+
return bin && bin !== name ? `${name} (${bin})` : name;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { SUPPORTED_CLIS, MANIFEST_FILES, PROBE_DIRS, SETTINGS_PRESERVE_FIELDS, cliDisplayName };
|
package/lib/utils/identity.js
CHANGED
|
@@ -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));
|
|
@@ -213,6 +394,9 @@ module.exports = {
|
|
|
213
394
|
MANIFEST_FILES, PROBE_DIRS,
|
|
214
395
|
deviceFingerprint, registerProject, projectIdFor, projectIdSeed,
|
|
215
396
|
scrubRemoteUrl, readProjectManifest, readDiscovery,
|
|
397
|
+
loadProjectBinding, validProjectId, validateProjectDisplayName,
|
|
398
|
+
bindingTargetMatches, projectTargetInfo, boundProjectIdentityFromBinding,
|
|
399
|
+
projectIdForExplicitRebindFromBinding, applyBoundProjectIdentity, writeProjectBinding,
|
|
216
400
|
getGitRemoteOrigin, inferProjectName, detectStackHint,
|
|
217
401
|
collectMetadata, buildProjectIdentity, hashManifestFiles,
|
|
218
402
|
};
|
package/lib/utils/mcp-auth.js
CHANGED
|
@@ -24,7 +24,7 @@ const EMBEDDED_TEMPLATE = `{
|
|
|
24
24
|
"command": "npx",
|
|
25
25
|
"args": ["-y", "@modelcontextprotocol/server-filesystem", "{{PROJECT_PATH}}"]
|
|
26
26
|
},
|
|
27
|
-
"
|
|
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
|
|
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(
|
|
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 "
|
|
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 ===
|
|
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
|
-
?
|
|
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:
|
|
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, {
|
|
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 {
|
|
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:
|
|
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;
|
package/lib/vault/index.js
CHANGED
|
@@ -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);
|
package/lib/vault/storage.js
CHANGED
|
@@ -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 (
|
|
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
|
|
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 =
|
|
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.
|
|
4
|
-
"description": "One config layer for seven AI coding agents \u2014 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
|
-
"
|
|
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
|
+
}
|