0xray 4.0.26 → 4.0.28

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 (35) hide show
  1. package/.grok-plugin/plugin.json +1 -1
  2. package/.opencode/skills/orchestrator/SKILL.md +5 -5
  3. package/.opencode/skills/storyteller/SKILL.md +1 -1
  4. package/.xray/features.json +1 -1
  5. package/AGENTS.md +1 -1
  6. package/README.md +2 -0
  7. package/SKILLS.md +1 -1
  8. package/dist/AGENTS.md +1 -1
  9. package/dist/CHANGELOG.md +34 -0
  10. package/dist/README.md +2 -0
  11. package/dist/SKILLS.md +1 -1
  12. package/dist/integrations/cursor/README.md +3 -1
  13. package/dist/integrations/cursor/hooks/cursor-hook-utils.js +77 -7
  14. package/dist/integrations/cursor/hooks/pre-compact.js +22 -16
  15. package/dist/integrations/cursor/hooks/xray-cloud-hook.sh +1 -1
  16. package/dist/integrations/hooks/plates.cjs +7 -0
  17. package/dist/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  18. package/dist/memory-routing/plates.d.ts +1 -1
  19. package/dist/memory-routing/plates.js +1 -0
  20. package/dist/skills/orchestrator/SKILL.md +5 -5
  21. package/dist/skills/storyteller/SKILL.md +1 -1
  22. package/docs-site/docs/plates/house.md +50 -0
  23. package/docs-site/docs/plates/index.md +1 -1
  24. package/llms.txt +6 -1
  25. package/package.json +3 -2
  26. package/scripts/foundry/inspect.mjs +47 -2
  27. package/scripts/node/bridge-mcp-wiring.cjs +320 -29
  28. package/scripts/node/consumer-install-smoke.mjs +37 -3
  29. package/scripts/node/install-bridges.cjs +34 -19
  30. package/scripts/node/mcp-launch.cjs +70 -0
  31. package/scripts/node/postinstall.cjs +42 -13
  32. package/src/integrations/openclaw/plugin/xray-pre-tool/package.json +1 -1
  33. package/src/skills/orchestrator/SKILL.md +5 -5
  34. package/src/skills/storyteller/SKILL.md +1 -1
  35. package/xray/features.json +1 -1
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Mill inspect organ. Runs the six plant checks plus observational harness.
3
+ * Mill inspect organ. Runs the plant checks, a machinePlugin check, and observational harness.
4
4
  * Not an 8th MCP. Not PPE. Does not fasten hooks or Repertoire.
5
5
  *
6
6
  * npx @0xray/foundry inspect [--skip-live] [--go] [--go-out PATH]
@@ -515,7 +515,7 @@ export function packagesToProbe(root) {
515
515
  });
516
516
  }
517
517
 
518
- function checkIsolatedHome(root, env = process.env, machine = mint.machineHome()) {
518
+ export function checkIsolatedHome(root, env = process.env, machine = mint.machineHome()) {
519
519
  const isolated = isIsolatedHome(env, machine);
520
520
  const machinePlugin = machineGrokPluginDir(machine);
521
521
  const dests = mint.resolveGrokPluginDests(root, env, machine);
@@ -542,6 +542,50 @@ function checkIsolatedHome(root, env = process.env, machine = mint.machineHome()
542
542
  };
543
543
  }
544
544
 
545
+ function isXrayPluginName(name) {
546
+ return name === "0xray" || name.startsWith("0xray");
547
+ }
548
+
549
+ /** 0xray plugin dirs under the machine HOME's grok plugin folder. */
550
+ export function machineXrayPluginPaths(machine = mint.machineHome()) {
551
+ const pluginsDir = path.join(machine, ".grok", "plugins");
552
+ if (!fs.existsSync(pluginsDir)) return [];
553
+ let names;
554
+ try {
555
+ names = fs.readdirSync(pluginsDir);
556
+ } catch {
557
+ return [];
558
+ }
559
+ return names
560
+ .filter((name) => isXrayPluginName(name))
561
+ .sort()
562
+ .map((name) => path.join(pluginsDir, name));
563
+ }
564
+
565
+ /**
566
+ * `isolated` stays HOME != machine home. This check is whether a machine-level
567
+ * 0xray Grok plugin exists. Overall inspect ok is false when this check fails.
568
+ */
569
+ export function checkMachinePlugin(machine = mint.machineHome()) {
570
+ const machinePlugin = machineGrokPluginDir(machine);
571
+ const hits = machineXrayPluginPaths(machine);
572
+ if (hits.length > 0) {
573
+ return {
574
+ id: "machinePlugin",
575
+ ok: false,
576
+ machinePlugin,
577
+ hits,
578
+ detail: `machine-level ${hits.join(", ")} exists — delete it`,
579
+ };
580
+ }
581
+ return {
582
+ id: "machinePlugin",
583
+ ok: true,
584
+ machinePlugin,
585
+ hits: [],
586
+ };
587
+ }
588
+
545
589
  export async function inspectSuit(root, opts = {}) {
546
590
  const millRoot = opts.millRoot || millPackageDir();
547
591
  const fetchFn = opts.fetch || fetch;
@@ -584,6 +628,7 @@ export async function inspectSuit(root, opts = {}) {
584
628
  }
585
629
 
586
630
  checks.push(checkIsolatedHome(root, env, machineHome));
631
+ checks.push(checkMachinePlugin(machineHome));
587
632
  checks.push(checkHarness(root, requireHarness));
588
633
 
589
634
  const failed = checks.filter((c) => c.ok === false);
@@ -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: "node",
149
- args: [repertoireMcp],
150
- env: { XRAY_ROOT: targetDir },
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: ["node", repertoireMcp],
158
+ command: launch.commandList,
155
159
  enabled: true,
160
+ environment: launch.env,
156
161
  };
157
162
  extras.openclaw.repertoire = {
158
- command: "node",
159
- args: [repertoireMcp],
160
- env: { XRAY_ROOT: targetDir },
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 buildStdioMcpServer(mcpCmd, env, targetDir) {
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", "0xray", "mcp", mcpCmd],
172
- env: { ...env, XRAY_ROOT: targetDir },
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 environment = { ...s.env };
475
+ const launch = scopedMcpLaunch(targetDir, s.mcpCmd, s.env);
198
476
  entries[s.name] = {
199
477
  type: "local",
200
- command: ["npx", "-y", "0xray", "mcp", s.mcpCmd],
478
+ command: launch.commandList,
201
479
  enabled: true,
202
- ...(Object.keys(environment).length > 0 ? { environment } : {}),
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: "npx",
214
- args: ["-y", "0xray", "mcp", s.mcpCmd],
215
- env: { ...s.env, XRAY_ROOT: targetDir },
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
- /** Portable .mcp.json for Grok/Cursor — no absolute XRAY_ROOT */
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: "npx",
227
- args: ["-y", "0xray", "mcp", s.mcpCmd],
228
- ...(Object.keys(s.env).length > 0 ? { env: { ...s.env } } : {}),
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
- config.mcp = { ...(config.mcp || {}), ...entries };
316
- fs.writeFileSync(opencodePath, `${JSON.stringify(config, null, 2)}\n`);
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,14 +614,12 @@ 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;
340
623
  fs.writeFileSync(destPath, `${JSON.stringify(merged, null, 2)}\n`);
341
624
  }
342
625
 
@@ -573,6 +856,14 @@ module.exports = {
573
856
  buildStdioMcpServer,
574
857
  buildPluginMcpJson,
575
858
  buildPortableProjectMcpJson,
859
+ pinnedMcpLaunch,
860
+ scopedMcpLaunch,
861
+ baseScopedEnv,
862
+ wrapScopedLaunch,
863
+ mergeMcpMap,
864
+ mergeNamedRecords,
865
+ mergeUserWinsServer,
866
+ jsonDeepEqual,
576
867
  copyHermesFindProjectRootHelper,
577
868
  writeHermesPluginArtifacts,
578
869
  syncHermesMcpRegistry,
@@ -353,11 +353,45 @@ function main() {
353
353
  throw new Error(`Missing MCP server in .mcp.json: ${name}`);
354
354
  }
355
355
  const entry = mcp.mcpServers[name];
356
- if (entry.command !== "npx") {
357
- throw new Error(`${name} should use npx, got ${entry.command}`);
356
+ const cli = path.join(tmpRoot, "node_modules", "0xray", "dist", "cli", "index.js");
357
+ const args = Array.isArray(entry.args) ? entry.args.map(String) : [];
358
+ const launcher = path.join(nmRoot, "scripts", "node", "mcp-launch.cjs");
359
+ if (entry.command !== "node" || args[0] !== launcher || args[1] !== "--keep") {
360
+ throw new Error(`${name} must launch via node mcp-launch.cjs --keep, got ${entry.command} ${args.join(" ")}`);
361
+ }
362
+ const dash = args.indexOf("--");
363
+ if (dash < 0) throw new Error(`${name} launcher args missing --`);
364
+ const kept = String(args[2] || "").split(",").filter(Boolean);
365
+ const inner = args.slice(dash + 1);
366
+ const pinnedNode = inner[0] === "node" && inner[1] === cli && inner[2] === "mcp";
367
+ const pinnedNpx =
368
+ inner[0] === "npx" && inner[1] === "-y" && /^0xray@\d/.test(inner[2] || "") && inner[3] === "mcp";
369
+ if (!pinnedNode && !pinnedNpx) {
370
+ throw new Error(`${name} must pin node CLI or npx 0xray@version after --, got ${inner.join(" ")}`);
371
+ }
372
+ if (inner.includes("0xray")) {
373
+ throw new Error(`${name} launched unpinned 0xray`);
374
+ }
375
+ for (const required of ["PATH", "HOME", "XRAY_ROOT"]) {
376
+ if (!kept.includes(required)) {
377
+ throw new Error(`${name} keep list missing ${required}`);
378
+ }
379
+ }
380
+ if (entry.env && (entry.env.PATH || entry.env.HOME)) {
381
+ throw new Error(`${name} baked PATH or HOME into the env block`);
382
+ }
383
+ for (const arg of args) {
384
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(arg)) {
385
+ throw new Error(`${name} put an env assignment in argv: ${arg.split("=")[0]}`);
386
+ }
387
+ }
388
+ for (const banned of ["NPM_TOKEN", "CURSOR_AUTH_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "RAILWAY_TOKEN"]) {
389
+ if (kept.includes(banned) || (entry.env && Object.prototype.hasOwnProperty.call(entry.env, banned))) {
390
+ throw new Error(`${name} inherited ${banned}`);
391
+ }
358
392
  }
359
393
  }
360
- console.log(` ✅ .mcp.json has ${XRAY_MCP_NAMES.length} npx MCP servers`);
394
+ console.log(` ✅ .mcp.json has ${XRAY_MCP_NAMES.length} version-pinned MCP servers`);
361
395
 
362
396
  assertFreshInstallDefaults(tmpRoot, version);
363
397
  assertOrganRequire(tmpRoot);
@@ -22,6 +22,10 @@ const {
22
22
  wireOpencodeBridge,
23
23
  wireOpenClawBridge,
24
24
  deployPortableProjectMcpJson,
25
+ scopedMcpLaunch,
26
+ mergeMcpMap,
27
+ mergeNamedRecords,
28
+ jsonDeepEqual,
25
29
  copyHermesFindProjectRootHelper,
26
30
  copyHermesHookRuntimes,
27
31
  installOpenClawHostWear,
@@ -199,10 +203,21 @@ function mergeOpencodeJson(targetDir, packageRoot, log) {
199
203
  if (fs.existsSync(userOpencode)) {
200
204
  const destData = JSON.parse(fs.readFileSync(userOpencode, "utf8"));
201
205
  const merged = { ...destData };
202
- if (srcData.agent) merged.agent = srcData.agent;
203
- if (srcData.mcp) merged.mcp = { ...destData.mcp, ...srcData.mcp };
204
- if (srcData.compaction) merged.compaction = srcData.compaction;
205
- fs.writeFileSync(userOpencode, JSON.stringify(merged, null, 2) + "\n");
206
+ if (srcData.agent || destData.agent) {
207
+ merged.agent = mergeNamedRecords(srcData.agent, destData.agent);
208
+ }
209
+ if (srcData.mcp || destData.mcp) {
210
+ merged.mcp = mergeMcpMap(srcData.mcp, destData.mcp, targetDir);
211
+ }
212
+ if (srcData.compaction || destData.compaction) {
213
+ merged.compaction =
214
+ srcData.compaction && destData.compaction
215
+ ? { ...srcData.compaction, ...destData.compaction }
216
+ : destData.compaction || srcData.compaction;
217
+ }
218
+ if (!jsonDeepEqual(merged, destData)) {
219
+ fs.writeFileSync(userOpencode, JSON.stringify(merged, null, 2) + "\n");
220
+ }
206
221
  } else {
207
222
  fs.copyFileSync(rootOpencode, userOpencode);
208
223
  }
@@ -308,13 +323,13 @@ function copyOpencodePlugin(packageRoot, opencodeDest, log) {
308
323
  if (!fs.existsSync(pluginSource)) return;
309
324
  const pluginDestDir = path.dirname(pluginDest);
310
325
  if (!fs.existsSync(pluginDestDir)) fs.mkdirSync(pluginDestDir, { recursive: true });
311
- const shouldCopy =
312
- !fs.existsSync(pluginDest) ||
313
- fs.statSync(pluginSource).mtime > fs.statSync(pluginDest).mtime;
314
- if (shouldCopy) {
315
- fs.copyFileSync(pluginSource, pluginDest);
316
- log("opencode-bridge", "plugin updated", "info");
326
+ const scopePkg = path.join(opencodeDest, "package.json");
327
+ if (!fs.existsSync(scopePkg)) {
328
+ fs.writeFileSync(scopePkg, `${JSON.stringify({ type: "module" })}\n`);
317
329
  }
330
+ const shim = `export { default } from ${JSON.stringify(pluginSource)};\n`;
331
+ fs.writeFileSync(pluginDest, shim);
332
+ log("opencode-bridge", "plugin shim written", "info");
318
333
  }
319
334
 
320
335
  function installOpencodeBridge(targetDir, packageRoot, log) {
@@ -368,15 +383,13 @@ function registerGrokMcpServers(targetDir, log, pluginDirs) {
368
383
 
369
384
  for (const s of XRAY_MCP_SERVERS) {
370
385
  try {
371
- const envEntries = { ...s.env, XRAY_ROOT: targetDir };
372
- const envFlags = Object.entries(envEntries)
373
- .map(([k, v]) => `--env "${k}=${v}"`)
374
- .join(" ");
375
- execSync(
376
- `grok mcp add ${s.name} --command npx --args "-y" "0xray" "mcp" "${s.mcpCmd}" ${envFlags}`,
377
- { stdio: "pipe" }
378
- );
379
- log("grok-bridge", `registered ${s.name} (npx)`, "info");
386
+ const launch = scopedMcpLaunch(targetDir, s.mcpCmd, s.env);
387
+ const argv = ["mcp", "add", s.name, "--command", launch.command, "--args", launch.args[0], ...launch.args.slice(1)];
388
+ for (const [key, value] of Object.entries(launch.env)) {
389
+ argv.push("--env", `${key}=${value}`);
390
+ }
391
+ execFileSync("grok", argv, { stdio: "pipe" });
392
+ log("grok-bridge", `registered ${s.name} (mcp-launch)`, "info");
380
393
  } catch {
381
394
  // already registered or grok config conflict — non-blocking
382
395
  }
@@ -1068,6 +1081,8 @@ module.exports = {
1068
1081
  installFrameworkDogfoodWear,
1069
1082
  wearVendoredRepertoire,
1070
1083
  installCursorBridge,
1084
+ mergeOpencodeJson,
1085
+ copyOpencodePlugin,
1071
1086
  parseExecDaemonCmdline,
1072
1087
  reloadCursorHostHooks,
1073
1088
  resolveCursorHooksTemplate,