0xray 4.0.27 → 4.0.29
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/.gitignore.default +1 -0
- package/.grok-plugin/plugin.json +1 -1
- package/.xray/features.json +1 -1
- package/README.md +4 -2
- package/dist/.gitignore.default +1 -0
- package/dist/CHANGELOG.md +16 -0
- package/dist/README.md +4 -2
- package/dist/cli/index.js +52 -44
- package/dist/integrations/cursor/README.md +21 -2
- package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +62 -5
- package/dist/integrations/hooks/plates.cjs +7 -0
- package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/dist/memory-routing/plates.d.ts +1 -1
- package/dist/memory-routing/plates.js +1 -0
- package/docs-site/docs/plates/house.md +50 -0
- package/docs-site/docs/plates/index.md +1 -1
- package/llms.txt +2 -0
- package/package.json +1 -1
- package/scripts/foundry/inspect.mjs +47 -2
- package/scripts/node/bridge-mcp-wiring.cjs +321 -29
- package/scripts/node/consumer-gitignore.cjs +1 -0
- package/scripts/node/consumer-install-smoke.mjs +9 -38
- package/scripts/node/install-bridges.cjs +1049 -33
- package/scripts/node/mcp-launch.cjs +70 -0
- package/scripts/node/postinstall.cjs +46 -70
- package/scripts/node/unwear-cursor-hooks.cjs +21 -0
- package/scripts/node/wear-cursor-hooks.cjs +17 -0
- package/scripts/node/wear-vendored-repertoire.cjs +35 -0
- package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
- package/xray/features.json +1 -1
|
@@ -144,20 +144,25 @@ function detectConsumerExtraMcpServers(targetDir) {
|
|
|
144
144
|
try {
|
|
145
145
|
const repertoireMcp = resolveRepertoireMcp(targetDir);
|
|
146
146
|
if (!repertoireMcp) return extras;
|
|
147
|
+
const launch = wrapScopedLaunch(
|
|
148
|
+
{ command: "node", args: [repertoireMcp] },
|
|
149
|
+
baseScopedEnv(targetDir),
|
|
150
|
+
);
|
|
147
151
|
extras.hermes.repertoire = {
|
|
148
|
-
command:
|
|
149
|
-
args:
|
|
150
|
-
env:
|
|
152
|
+
command: launch.command,
|
|
153
|
+
args: launch.args,
|
|
154
|
+
env: launch.env,
|
|
151
155
|
};
|
|
152
156
|
extras.opencode.repertoire = {
|
|
153
157
|
type: "local",
|
|
154
|
-
command:
|
|
158
|
+
command: launch.commandList,
|
|
155
159
|
enabled: true,
|
|
160
|
+
environment: launch.env,
|
|
156
161
|
};
|
|
157
162
|
extras.openclaw.repertoire = {
|
|
158
|
-
command:
|
|
159
|
-
args:
|
|
160
|
-
env:
|
|
163
|
+
command: launch.command,
|
|
164
|
+
args: launch.args,
|
|
165
|
+
env: launch.env,
|
|
161
166
|
};
|
|
162
167
|
} catch {
|
|
163
168
|
// best-effort
|
|
@@ -165,11 +170,284 @@ function detectConsumerExtraMcpServers(targetDir) {
|
|
|
165
170
|
return extras;
|
|
166
171
|
}
|
|
167
172
|
|
|
168
|
-
function
|
|
173
|
+
function readInstalledXrayVersion() {
|
|
174
|
+
const pkgPath = path.join(__dirname, "..", "..", "package.json");
|
|
175
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
176
|
+
if (typeof pkg.version !== "string" || !pkg.version) {
|
|
177
|
+
throw new Error("0xray package.json version missing; refusing unpinned MCP launch");
|
|
178
|
+
}
|
|
179
|
+
return pkg.version;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function localXrayCli(targetDir) {
|
|
183
|
+
if (!targetDir) return null;
|
|
184
|
+
const cli = path.join(targetDir, "node_modules", "0xray", "dist", "cli", "index.js");
|
|
185
|
+
return fs.existsSync(cli) ? cli : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Prefer the installed CLI. Else pin npx to this package version. Never bare `0xray`. */
|
|
189
|
+
function pinnedMcpLaunch(targetDir, mcpCmd) {
|
|
190
|
+
const cli = localXrayCli(targetDir);
|
|
191
|
+
if (cli) {
|
|
192
|
+
return {
|
|
193
|
+
command: "node",
|
|
194
|
+
args: [cli, "mcp", mcpCmd],
|
|
195
|
+
commandList: ["node", cli, "mcp", mcpCmd],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const spec = `0xray@${readInstalledXrayVersion()}`;
|
|
169
199
|
return {
|
|
170
200
|
command: "npx",
|
|
171
|
-
args: ["-y",
|
|
172
|
-
|
|
201
|
+
args: ["-y", spec, "mcp", mcpCmd],
|
|
202
|
+
commandList: ["npx", "-y", spec, "mcp", mcpCmd],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Stdio MCP hosts merge an `env` object onto the parent process. They cannot
|
|
208
|
+
* drop inherited secrets. argv is world-readable, so values never go on the
|
|
209
|
+
* command line. Every launch is:
|
|
210
|
+
* node <mcp-launch.cjs> --keep PATH,HOME,XRAY_ROOT[,flag][,user names] -- <pinned command>
|
|
211
|
+
* `scripts/node/mcp-launch.cjs` copies those names from its own environ (the
|
|
212
|
+
* live host, after the host merged this entry's `env` block) and spawns the
|
|
213
|
+
* server. PATH and HOME are names only; their values are whatever the host
|
|
214
|
+
* has at launch. XRAY_ROOT and non-secret server flags stay in the `env`
|
|
215
|
+
* block. User keys stay in that block too, and their names are appended to
|
|
216
|
+
* --keep. NODE_OPTIONS is omitted: none of the seven stdio servers read it.
|
|
217
|
+
*/
|
|
218
|
+
const LIVE_ENV_NAMES = ["PATH", "HOME"];
|
|
219
|
+
|
|
220
|
+
function mcpLauncherPath() {
|
|
221
|
+
return path.join(__dirname, "mcp-launch.cjs");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function baseScopedEnv(targetDir) {
|
|
225
|
+
const env = {};
|
|
226
|
+
if (targetDir) env.XRAY_ROOT = targetDir;
|
|
227
|
+
return env;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function orderedEnv(envMap) {
|
|
231
|
+
const preferred = ["PATH", "HOME", "XRAY_ROOT"];
|
|
232
|
+
const ordered = {};
|
|
233
|
+
for (const key of preferred) {
|
|
234
|
+
if (Object.prototype.hasOwnProperty.call(envMap, key)) ordered[key] = String(envMap[key]);
|
|
235
|
+
}
|
|
236
|
+
for (const key of Object.keys(envMap)) {
|
|
237
|
+
if (!Object.prototype.hasOwnProperty.call(ordered, key)) ordered[key] = String(envMap[key]);
|
|
238
|
+
}
|
|
239
|
+
return ordered;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function mergeScopedEnv(layers) {
|
|
243
|
+
const merged = {};
|
|
244
|
+
for (const layer of layers) {
|
|
245
|
+
if (!layer || typeof layer !== "object" || Array.isArray(layer)) continue;
|
|
246
|
+
for (const key of Object.keys(layer)) {
|
|
247
|
+
if (layer[key] == null) continue;
|
|
248
|
+
merged[key] = String(layer[key]);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return orderedEnv(merged);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function configEnvFromMap(envMap) {
|
|
255
|
+
const ordered = orderedEnv(envMap || {});
|
|
256
|
+
const config = {};
|
|
257
|
+
for (const key of Object.keys(ordered)) {
|
|
258
|
+
if (LIVE_ENV_NAMES.includes(key)) continue;
|
|
259
|
+
config[key] = ordered[key];
|
|
260
|
+
}
|
|
261
|
+
return config;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function keepList(config) {
|
|
265
|
+
return [...LIVE_ENV_NAMES, ...Object.keys(config)];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function wrapScopedLaunch(inner, envMap) {
|
|
269
|
+
const env = configEnvFromMap(envMap);
|
|
270
|
+
const tail = [mcpLauncherPath(), "--keep", keepList(env).join(","), "--", inner.command, ...inner.args];
|
|
271
|
+
return {
|
|
272
|
+
command: "node",
|
|
273
|
+
args: tail,
|
|
274
|
+
commandList: ["node", ...tail],
|
|
275
|
+
env,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function scopedMcpLaunch(targetDir, mcpCmd, serverEnv) {
|
|
280
|
+
return wrapScopedLaunch(pinnedMcpLaunch(targetDir, mcpCmd), mergeScopedEnv([baseScopedEnv(targetDir), serverEnv]));
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function commandTokens(server) {
|
|
284
|
+
if (!server || typeof server !== "object") return [];
|
|
285
|
+
if (Array.isArray(server.command)) return server.command.map(String);
|
|
286
|
+
const tokens = [];
|
|
287
|
+
if (typeof server.command === "string") tokens.push(server.command);
|
|
288
|
+
if (Array.isArray(server.args)) tokens.push(...server.args.map(String));
|
|
289
|
+
return tokens;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function isMcpLauncherToken(token) {
|
|
293
|
+
return token === "mcp-launch.cjs" || token.endsWith(`${path.sep}mcp-launch.cjs`) || token.endsWith("/mcp-launch.cjs");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function peelScopedLaunch(tokens) {
|
|
297
|
+
if (tokens[0] === "env") {
|
|
298
|
+
let i = 1;
|
|
299
|
+
if (tokens[i] === "-i" || tokens[i] === "--ignore-environment") i += 1;
|
|
300
|
+
else return tokens.slice();
|
|
301
|
+
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i += 1;
|
|
302
|
+
return tokens.slice(i);
|
|
303
|
+
}
|
|
304
|
+
if (tokens[0] === "node" && isMcpLauncherToken(tokens[1] || "")) {
|
|
305
|
+
const dash = tokens.indexOf("--");
|
|
306
|
+
if (dash >= 0) return tokens.slice(dash + 1);
|
|
307
|
+
}
|
|
308
|
+
return tokens.slice();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function launchTokens(server) {
|
|
312
|
+
return peelScopedLaunch(commandTokens(server));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function innerFromTokens(tokens) {
|
|
316
|
+
if (!tokens.length) return null;
|
|
317
|
+
return { command: tokens[0], args: tokens.slice(1) };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function usesCommandArray(server) {
|
|
321
|
+
return Array.isArray(server && server.command);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function hasVersionPin(server) {
|
|
325
|
+
return launchTokens(server).some((token) => /^0xray@.+/.test(token));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function isUnpinnedXrayLaunch(server) {
|
|
329
|
+
return launchTokens(server).some((token) => token === "0xray");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function jsonDeepEqual(a, b) {
|
|
333
|
+
if (a === b) return true;
|
|
334
|
+
if (typeof a !== typeof b) return false;
|
|
335
|
+
if (a === null || b === null || typeof a !== "object") return a === b;
|
|
336
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
337
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false;
|
|
338
|
+
return a.every((item, i) => jsonDeepEqual(item, b[i]));
|
|
339
|
+
}
|
|
340
|
+
const aKeys = Object.keys(a);
|
|
341
|
+
const bKeys = Object.keys(b);
|
|
342
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
343
|
+
return aKeys.every((key) => Object.prototype.hasOwnProperty.call(b, key) && jsonDeepEqual(a[key], b[key]));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function mcpCmdFromInner(inner) {
|
|
347
|
+
const idx = inner.args.indexOf("mcp");
|
|
348
|
+
if (idx >= 0 && inner.args[idx + 1]) return inner.args[idx + 1];
|
|
349
|
+
return inner.args.length > 0 ? inner.args[inner.args.length - 1] : null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Framework fills gaps. User env, enabled/disabled, and version pins win. Unpinned `0xray` is repinned. Launch is `node mcp-launch.cjs --keep names -- cmd`. */
|
|
353
|
+
function mergeUserWinsServer(framework, user, targetDir) {
|
|
354
|
+
if (!user || typeof user !== "object") return framework;
|
|
355
|
+
if (!framework || typeof framework !== "object") return user;
|
|
356
|
+
const next = { ...user };
|
|
357
|
+
const userInner = innerFromTokens(launchTokens(user));
|
|
358
|
+
const frameworkInner = innerFromTokens(launchTokens(framework));
|
|
359
|
+
let inner = userInner || frameworkInner;
|
|
360
|
+
if (userInner && !hasVersionPin(user) && isUnpinnedXrayLaunch(user) && frameworkInner) {
|
|
361
|
+
inner = frameworkInner;
|
|
362
|
+
}
|
|
363
|
+
if (inner && isUnpinnedXrayLaunch({ command: inner.command, args: inner.args })) {
|
|
364
|
+
const mcpCmd = mcpCmdFromInner(inner);
|
|
365
|
+
if (mcpCmd) inner = pinnedMcpLaunch(targetDir, mcpCmd);
|
|
366
|
+
}
|
|
367
|
+
if (!inner) return framework;
|
|
368
|
+
const mergedEnv = mergeScopedEnv([
|
|
369
|
+
baseScopedEnv(targetDir),
|
|
370
|
+
framework.env,
|
|
371
|
+
framework.environment,
|
|
372
|
+
user.env,
|
|
373
|
+
user.environment,
|
|
374
|
+
]);
|
|
375
|
+
const wrapped = wrapScopedLaunch(inner, mergedEnv);
|
|
376
|
+
if (usesCommandArray(user) || usesCommandArray(framework)) {
|
|
377
|
+
next.command = wrapped.commandList;
|
|
378
|
+
delete next.args;
|
|
379
|
+
} else {
|
|
380
|
+
next.command = wrapped.command;
|
|
381
|
+
next.args = wrapped.args;
|
|
382
|
+
}
|
|
383
|
+
const wantEnvironment =
|
|
384
|
+
Object.prototype.hasOwnProperty.call(user, "environment") ||
|
|
385
|
+
Object.prototype.hasOwnProperty.call(framework, "environment");
|
|
386
|
+
const wantEnv =
|
|
387
|
+
Object.prototype.hasOwnProperty.call(user, "env") || Object.prototype.hasOwnProperty.call(framework, "env");
|
|
388
|
+
if (wantEnvironment) next.environment = mergedEnv;
|
|
389
|
+
if (wantEnv) next.env = mergedEnv;
|
|
390
|
+
else if (!wantEnvironment) next.env = mergedEnv;
|
|
391
|
+
if (!Object.prototype.hasOwnProperty.call(user, "enabled") && Object.prototype.hasOwnProperty.call(framework, "enabled")) {
|
|
392
|
+
next.enabled = framework.enabled;
|
|
393
|
+
}
|
|
394
|
+
if (!Object.prototype.hasOwnProperty.call(user, "disabled") && Object.prototype.hasOwnProperty.call(framework, "disabled")) {
|
|
395
|
+
next.disabled = framework.disabled;
|
|
396
|
+
}
|
|
397
|
+
return next;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function mergeMcpMap(frameworkServers, userServers, targetDir) {
|
|
401
|
+
const framework = frameworkServers && typeof frameworkServers === "object" ? frameworkServers : {};
|
|
402
|
+
const user = userServers && typeof userServers === "object" ? userServers : {};
|
|
403
|
+
const merged = {};
|
|
404
|
+
const seen = new Set();
|
|
405
|
+
for (const name of Object.keys(user)) {
|
|
406
|
+
seen.add(name);
|
|
407
|
+
merged[name] = framework[name] ? mergeUserWinsServer(framework[name], user[name], targetDir) : user[name];
|
|
408
|
+
}
|
|
409
|
+
for (const name of Object.keys(framework)) {
|
|
410
|
+
if (seen.has(name)) continue;
|
|
411
|
+
merged[name] = framework[name];
|
|
412
|
+
}
|
|
413
|
+
return merged;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function mergeNamedRecords(src, dest) {
|
|
417
|
+
const source = src && typeof src === "object" ? src : {};
|
|
418
|
+
const user = dest && typeof dest === "object" ? dest : {};
|
|
419
|
+
const merged = {};
|
|
420
|
+
const seen = new Set();
|
|
421
|
+
for (const name of Object.keys(user)) {
|
|
422
|
+
seen.add(name);
|
|
423
|
+
const fromSrc = source[name];
|
|
424
|
+
const fromUser = user[name];
|
|
425
|
+
if (
|
|
426
|
+
fromSrc &&
|
|
427
|
+
fromUser &&
|
|
428
|
+
typeof fromSrc === "object" &&
|
|
429
|
+
typeof fromUser === "object" &&
|
|
430
|
+
!Array.isArray(fromSrc) &&
|
|
431
|
+
!Array.isArray(fromUser)
|
|
432
|
+
) {
|
|
433
|
+
merged[name] = { ...fromSrc, ...fromUser };
|
|
434
|
+
} else {
|
|
435
|
+
merged[name] = fromUser;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
for (const name of Object.keys(source)) {
|
|
439
|
+
if (seen.has(name)) continue;
|
|
440
|
+
merged[name] = source[name];
|
|
441
|
+
}
|
|
442
|
+
return merged;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function buildStdioMcpServer(mcpCmd, env, targetDir) {
|
|
446
|
+
const launch = scopedMcpLaunch(targetDir, mcpCmd, env);
|
|
447
|
+
return {
|
|
448
|
+
command: launch.command,
|
|
449
|
+
args: launch.args,
|
|
450
|
+
env: launch.env,
|
|
173
451
|
};
|
|
174
452
|
}
|
|
175
453
|
|
|
@@ -194,12 +472,12 @@ function buildOpenClawMcpServers(targetDir) {
|
|
|
194
472
|
function buildOpencodeMcpEntries(targetDir) {
|
|
195
473
|
const entries = {};
|
|
196
474
|
for (const s of XRAY_MCP_SERVERS) {
|
|
197
|
-
const
|
|
475
|
+
const launch = scopedMcpLaunch(targetDir, s.mcpCmd, s.env);
|
|
198
476
|
entries[s.name] = {
|
|
199
477
|
type: "local",
|
|
200
|
-
command:
|
|
478
|
+
command: launch.commandList,
|
|
201
479
|
enabled: true,
|
|
202
|
-
|
|
480
|
+
environment: launch.env,
|
|
203
481
|
};
|
|
204
482
|
}
|
|
205
483
|
const extras = detectConsumerExtraMcpServers(targetDir);
|
|
@@ -209,23 +487,25 @@ function buildOpencodeMcpEntries(targetDir) {
|
|
|
209
487
|
function buildPluginMcpJson(targetDir) {
|
|
210
488
|
const mcpServers = {};
|
|
211
489
|
for (const s of XRAY_MCP_SERVERS) {
|
|
490
|
+
const launch = scopedMcpLaunch(targetDir, s.mcpCmd, s.env);
|
|
212
491
|
mcpServers[s.name] = {
|
|
213
|
-
command:
|
|
214
|
-
args:
|
|
215
|
-
env:
|
|
492
|
+
command: launch.command,
|
|
493
|
+
args: launch.args,
|
|
494
|
+
env: launch.env,
|
|
216
495
|
};
|
|
217
496
|
}
|
|
218
497
|
return { mcpServers };
|
|
219
498
|
}
|
|
220
499
|
|
|
221
|
-
/**
|
|
222
|
-
function buildPortableProjectMcpJson() {
|
|
500
|
+
/** Project .mcp.json for Grok/Cursor. Launcher allowlist drops inherited secrets; XRAY_ROOT is set when targetDir is known. */
|
|
501
|
+
function buildPortableProjectMcpJson(targetDir) {
|
|
223
502
|
const mcpServers = {};
|
|
224
503
|
for (const s of XRAY_MCP_SERVERS) {
|
|
504
|
+
const launch = scopedMcpLaunch(targetDir, s.mcpCmd, s.env);
|
|
225
505
|
mcpServers[s.name] = {
|
|
226
|
-
command:
|
|
227
|
-
args:
|
|
228
|
-
|
|
506
|
+
command: launch.command,
|
|
507
|
+
args: launch.args,
|
|
508
|
+
env: launch.env,
|
|
229
509
|
};
|
|
230
510
|
}
|
|
231
511
|
return { mcpServers };
|
|
@@ -312,16 +592,21 @@ function mergeOpencodeMcpRegistry(targetDir) {
|
|
|
312
592
|
config = { $schema: "https://opencode.ai/config.json", mcp: {} };
|
|
313
593
|
}
|
|
314
594
|
}
|
|
315
|
-
|
|
316
|
-
|
|
595
|
+
const existed = fs.existsSync(opencodePath);
|
|
596
|
+
const nextMcp = mergeMcpMap(entries, config.mcp || {}, targetDir);
|
|
597
|
+
const next = { ...config, mcp: nextMcp };
|
|
598
|
+
if (existed && jsonDeepEqual(next, config)) return Object.keys(entries).length;
|
|
599
|
+
fs.writeFileSync(opencodePath, `${JSON.stringify(next, null, 2)}\n`);
|
|
317
600
|
return Object.keys(entries).length;
|
|
318
601
|
}
|
|
319
602
|
|
|
320
603
|
function deployPortableProjectMcpJson(targetDir) {
|
|
321
604
|
const destPath = path.join(targetDir, ".mcp.json");
|
|
322
|
-
const portable = buildPortableProjectMcpJson();
|
|
605
|
+
const portable = buildPortableProjectMcpJson(targetDir);
|
|
323
606
|
let existing = { mcpServers: {} };
|
|
607
|
+
let hadFile = false;
|
|
324
608
|
if (fs.existsSync(destPath)) {
|
|
609
|
+
hadFile = true;
|
|
325
610
|
try {
|
|
326
611
|
existing = JSON.parse(fs.readFileSync(destPath, "utf8"));
|
|
327
612
|
} catch {
|
|
@@ -329,15 +614,14 @@ function deployPortableProjectMcpJson(targetDir) {
|
|
|
329
614
|
}
|
|
330
615
|
}
|
|
331
616
|
const extras = detectConsumerExtraMcpServers(targetDir);
|
|
617
|
+
const framework = { ...portable.mcpServers, ...(extras.openclaw || {}) };
|
|
332
618
|
const merged = {
|
|
333
619
|
...existing,
|
|
334
|
-
mcpServers: {
|
|
335
|
-
...(existing.mcpServers || {}),
|
|
336
|
-
...portable.mcpServers,
|
|
337
|
-
...(extras.openclaw || {}),
|
|
338
|
-
},
|
|
620
|
+
mcpServers: mergeMcpMap(framework, existing.mcpServers || {}, targetDir),
|
|
339
621
|
};
|
|
622
|
+
if (hadFile && jsonDeepEqual(merged, existing)) return false;
|
|
340
623
|
fs.writeFileSync(destPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
624
|
+
return true;
|
|
341
625
|
}
|
|
342
626
|
|
|
343
627
|
function enableHermesPluginBestEffort() {
|
|
@@ -573,6 +857,14 @@ module.exports = {
|
|
|
573
857
|
buildStdioMcpServer,
|
|
574
858
|
buildPluginMcpJson,
|
|
575
859
|
buildPortableProjectMcpJson,
|
|
860
|
+
pinnedMcpLaunch,
|
|
861
|
+
scopedMcpLaunch,
|
|
862
|
+
baseScopedEnv,
|
|
863
|
+
wrapScopedLaunch,
|
|
864
|
+
mergeMcpMap,
|
|
865
|
+
mergeNamedRecords,
|
|
866
|
+
mergeUserWinsServer,
|
|
867
|
+
jsonDeepEqual,
|
|
576
868
|
copyHermesFindProjectRootHelper,
|
|
577
869
|
writeHermesPluginArtifacts,
|
|
578
870
|
syncHermesMcpRegistry,
|
|
@@ -21,16 +21,6 @@ const require = createRequire(import.meta.url);
|
|
|
21
21
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
22
|
const rootDir = path.resolve(__dirname, "../..");
|
|
23
23
|
|
|
24
|
-
const XRAY_MCP_NAMES = [
|
|
25
|
-
"xray-governance",
|
|
26
|
-
"xray-skills",
|
|
27
|
-
"xray-orchestrator",
|
|
28
|
-
"xray-enforcer",
|
|
29
|
-
"xray-researcher",
|
|
30
|
-
"xray-code-review",
|
|
31
|
-
"xray-architect-tools",
|
|
32
|
-
];
|
|
33
|
-
|
|
34
24
|
function run(cmd, cwd, { inherit = false } = {}) {
|
|
35
25
|
const result = execSync(cmd, {
|
|
36
26
|
cwd,
|
|
@@ -311,19 +301,12 @@ function main() {
|
|
|
311
301
|
}
|
|
312
302
|
|
|
313
303
|
const millSkillHanger = path.join(tmpRoot, ".opencode", "skills", "mill", "SKILL.md");
|
|
314
|
-
if (
|
|
315
|
-
throw new Error("
|
|
304
|
+
if (fs.existsSync(millSkillHanger)) {
|
|
305
|
+
throw new Error("postinstall must not fasten mill skills into the project");
|
|
316
306
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
? fs.readdirSync(wornSkills).filter((n) =>
|
|
320
|
-
fs.existsSync(path.join(wornSkills, n, "SKILL.md")),
|
|
321
|
-
)
|
|
322
|
-
: [];
|
|
323
|
-
if (wornNames.length > 3) {
|
|
324
|
-
throw new Error(`costume dump: ${wornNames.join(",")}`);
|
|
307
|
+
if (fs.existsSync(path.join(tmpRoot, ".mcp.json"))) {
|
|
308
|
+
throw new Error("postinstall must not write .mcp.json");
|
|
325
309
|
}
|
|
326
|
-
|
|
327
310
|
const installedPkg = JSON.parse(fs.readFileSync(path.join(nmRoot, "package.json"), "utf-8"));
|
|
328
311
|
if (installedPkg.version !== version) {
|
|
329
312
|
throw new Error(`Installed version ${installedPkg.version} != packed ${version}`);
|
|
@@ -342,23 +325,11 @@ function main() {
|
|
|
342
325
|
}
|
|
343
326
|
console.log(" ✅ consumer target resolution");
|
|
344
327
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const servers = Object.keys(mcp.mcpServers || {});
|
|
351
|
-
for (const name of XRAY_MCP_NAMES) {
|
|
352
|
-
if (!servers.includes(name)) {
|
|
353
|
-
throw new Error(`Missing MCP server in .mcp.json: ${name}`);
|
|
354
|
-
}
|
|
355
|
-
const entry = mcp.mcpServers[name];
|
|
356
|
-
if (entry.command !== "npx") {
|
|
357
|
-
throw new Error(`${name} should use npx, got ${entry.command}`);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
console.log(` ✅ .mcp.json has ${XRAY_MCP_NAMES.length} npx MCP servers`);
|
|
361
|
-
|
|
328
|
+
fs.mkdirSync(path.join(tmpRoot, ".xray"), { recursive: true });
|
|
329
|
+
fs.copyFileSync(
|
|
330
|
+
path.join(nmRoot, "xray", "features.json"),
|
|
331
|
+
path.join(tmpRoot, ".xray", "features.json"),
|
|
332
|
+
);
|
|
362
333
|
assertFreshInstallDefaults(tmpRoot, version);
|
|
363
334
|
assertOrganRequire(tmpRoot);
|
|
364
335
|
|