0xray 3.4.4 → 3.4.5
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/AGENTS-consumer.md +1 -1
- package/AGENTS.md +1 -1
- package/README.md +1 -1
- package/SKILLS.md +1 -1
- package/dist/AGENTS.md +1 -1
- package/dist/CHANGELOG.md +7 -0
- package/dist/README.md +1 -1
- package/dist/SKILLS.md +1 -1
- package/dist/cli/commands/hermes-install.js +29 -22
- package/dist/cli/commands/openclaw-install.js +19 -13
- package/dist/cli/commands/opencode-install.js +34 -23
- package/package.json +1 -1
- package/scripts/node/bridge-mcp-wiring.cjs +310 -0
- package/scripts/node/install-bridges.cjs +36 -32
- package/scripts/node/sync-hermes-mcp-servers.py +31 -0
package/AGENTS-consumer.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Quick reference for the 0xRay AI orchestration framework (shipped to your project on `npm install 0xray`).
|
|
4
4
|
|
|
5
|
-
**v3.4.
|
|
5
|
+
**v3.4.5** — 7 MCP servers · 45 skills · 68 codex terms · 4 platform bridges
|
|
6
6
|
|
|
7
7
|
## Postinstall (automatic)
|
|
8
8
|
|
package/AGENTS.md
CHANGED
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# xray — MCP-Centric AI Governance OS
|
|
2
2
|
|
|
3
|
-
**v3.4.
|
|
3
|
+
**v3.4.5** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
|
|
4
4
|
|
|
5
5
|
xray is the pure v2 three-subsystem AI orchestration framework — **MCP-centric**, governed by Dynamo, and autonomous via thinDispatch. Consumer `npm install 0xray` auto-wires all four platform bridges and seven MCP servers with zero manual config.
|
|
6
6
|
|
package/SKILLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0xray Skills
|
|
2
2
|
|
|
3
|
-
**v3.4.
|
|
3
|
+
**v3.4.5** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
|
|
4
4
|
|
|
5
5
|
xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
|
|
6
6
|
|
package/dist/AGENTS.md
CHANGED
package/dist/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Conventional Commits](https://www.conventionalcommits.org/).
|
|
6
6
|
|
|
7
|
+
## [3.4.5] - 2026-06-18
|
|
8
|
+
|
|
9
|
+
### ✨ Features
|
|
10
|
+
- **Native bridge MCP wiring** — `bridge-mcp-wiring.cjs` SSOT wires 7 xray MCP servers (+ repertoire auto-detect) into Hermes `mcp_servers`, OpenCode `opencode.json`, and OpenClaw `mcp.servers` via `npx 0xray {hermes,opencode,openclaw} install` and postinstall
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
7
14
|
## [3.4.4] - 2026-06-18
|
|
8
15
|
|
|
9
16
|
### 🐛 Bug Fixes
|
package/dist/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# xray — MCP-Centric AI Governance OS
|
|
2
2
|
|
|
3
|
-
**v3.4.
|
|
3
|
+
**v3.4.5** — 42 agents · 45 skills · 7 MCP servers · 68 codex terms · 3,226 tests
|
|
4
4
|
|
|
5
5
|
xray is the pure v2 three-subsystem AI orchestration framework — **MCP-centric**, governed by Dynamo, and autonomous via thinDispatch. Consumer `npm install 0xray` auto-wires all four platform bridges and seven MCP servers with zero manual config.
|
|
6
6
|
|
package/dist/SKILLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 0xray Skills
|
|
2
2
|
|
|
3
|
-
**v3.4.
|
|
3
|
+
**v3.4.5** — **45 skills** · invoked via `xray-skills` MCP · synced on postinstall
|
|
4
4
|
|
|
5
5
|
xray ships 45 knowledge skills (`src/skills/<name>/SKILL.md`) across the full software lifecycle. Lead-dev operating model lives in **`orchestrator`** skill + `multi_agent_orchestration.lead_dev_mode` in features.json. Skills load on demand via the **xray-skills** MCP server (`npx -y 0xray mcp skills`), agent `@mentions`, or platform skill directories after postinstall.
|
|
6
6
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { createRequire } from 'module';
|
|
4
5
|
import { frameworkLogger } from '../../core/framework-logger.js';
|
|
5
6
|
import { syncBuiltinSkills } from './skill-install.js';
|
|
6
7
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const wiring = require(path.join(__dirname, '..', '..', '..', 'scripts', 'node', 'bridge-mcp-wiring.cjs'));
|
|
7
10
|
export function registerHermesCommands(hermesCmd) {
|
|
8
11
|
hermesCmd
|
|
9
12
|
.command('install')
|
|
@@ -31,40 +34,44 @@ async function installForHermes(options = {}) {
|
|
|
31
34
|
console.log(`[Hermes] Dry run: Would copy plugin from ${sourceDir} → ${targetPluginDir}`);
|
|
32
35
|
return;
|
|
33
36
|
}
|
|
37
|
+
const targetDir = process.cwd();
|
|
34
38
|
try {
|
|
35
|
-
|
|
39
|
+
const pluginExists = fs.existsSync(targetPluginDir);
|
|
40
|
+
if (pluginExists && !options.force) {
|
|
36
41
|
console.log('[Hermes] xray-hermes plugin is already installed.');
|
|
37
42
|
console.log('Use --force to reinstall.');
|
|
38
|
-
return;
|
|
39
43
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
fs.
|
|
44
|
+
else {
|
|
45
|
+
fs.mkdirSync(targetPluginDir, { recursive: true });
|
|
46
|
+
for (const entry of fs.readdirSync(sourceDir)) {
|
|
47
|
+
const src = path.join(sourceDir, entry);
|
|
48
|
+
const dst = path.join(targetPluginDir, entry);
|
|
49
|
+
if (fs.statSync(src).isDirectory()) {
|
|
50
|
+
fs.cpSync(src, dst, { recursive: true, force: true });
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
fs.copyFileSync(src, dst);
|
|
54
|
+
}
|
|
46
55
|
}
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
frameworkLogger.log('hermes-integration', 'plugin-copied', 'info', { destination: targetPluginDir });
|
|
57
|
+
console.log(`\x1b[32m✓ Copied Hermes plugin to ${targetPluginDir}\x1b[0m`);
|
|
58
|
+
const hermesSkillsDir = path.join(targetPluginDir, 'skills');
|
|
59
|
+
const skillsCopied = syncBuiltinSkills(hermesSkillsDir);
|
|
60
|
+
if (skillsCopied > 0) {
|
|
61
|
+
console.log(`\x1b[32m✓ Synced ${skillsCopied} builtin skills to Hermes plugin\x1b[0m`);
|
|
49
62
|
}
|
|
63
|
+
frameworkLogger.log('hermes-integration', 'skills-synced', 'info', { count: skillsCopied });
|
|
50
64
|
}
|
|
51
|
-
|
|
52
|
-
console.log(`\x1b[32m✓
|
|
53
|
-
|
|
54
|
-
const hermesSkillsDir = path.join(targetPluginDir, 'skills');
|
|
55
|
-
const skillsCopied = syncBuiltinSkills(hermesSkillsDir);
|
|
56
|
-
if (skillsCopied > 0) {
|
|
57
|
-
console.log(`\x1b[32m✓ Synced ${skillsCopied} builtin skills to Hermes plugin\x1b[0m`);
|
|
58
|
-
}
|
|
59
|
-
frameworkLogger.log('hermes-integration', 'skills-synced', 'info', { count: skillsCopied });
|
|
60
|
-
// Write after-install instructions
|
|
65
|
+
const wired = wiring.wireHermesBridge(targetDir);
|
|
66
|
+
console.log(`\x1b[32m✓ Wired Hermes mcp_servers (${wired.count} servers)\x1b[0m`);
|
|
67
|
+
console.log(`\x1b[32m✓ Consumer root → ${targetDir}\x1b[0m`);
|
|
61
68
|
const afterInstallPath = path.join(targetPluginDir, 'after-install.md');
|
|
62
69
|
if (fs.existsSync(afterInstallPath)) {
|
|
63
70
|
console.log(`\nSee ${afterInstallPath} for post-install steps.`);
|
|
64
71
|
}
|
|
65
72
|
console.log('\n✅ 0xRay is now installed as a Hermes Agent plugin!');
|
|
66
|
-
console.log('Restart Hermes to load the plugin and
|
|
67
|
-
console.log(' Run:
|
|
73
|
+
console.log('Restart Hermes to load the plugin, MCP servers, and tools.');
|
|
74
|
+
console.log(' Run: hermes mcp list (to verify)');
|
|
68
75
|
}
|
|
69
76
|
catch (err) {
|
|
70
77
|
frameworkLogger.log('hermes-integration', 'install-error', 'error', { error: err.message });
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { createRequire } from 'module';
|
|
4
5
|
import { frameworkLogger } from '../../core/framework-logger.js';
|
|
5
6
|
import { syncBuiltinSkills } from './skill-install.js';
|
|
6
7
|
import { OpenClawConfigLoader } from '../../integrations/openclaw/config.js';
|
|
8
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const wiring = require(path.join(__dirname, '..', '..', '..', 'scripts', 'node', 'bridge-mcp-wiring.cjs'));
|
|
7
11
|
export function registerOpenClawCommands(openclawCmd) {
|
|
8
12
|
openclawCmd
|
|
9
13
|
.command('install')
|
|
@@ -21,29 +25,31 @@ async function installForOpenClaw(options = {}) {
|
|
|
21
25
|
console.log(`[OpenClaw] Dry run: Would create config at ${configPath}`);
|
|
22
26
|
return;
|
|
23
27
|
}
|
|
28
|
+
const targetDir = process.cwd();
|
|
24
29
|
try {
|
|
25
|
-
|
|
30
|
+
const configExists = fs.existsSync(configPath);
|
|
31
|
+
if (configExists && !options.force) {
|
|
26
32
|
console.log(`[OpenClaw] Config already exists at ${configPath}.`);
|
|
27
|
-
console.log('Use --force to overwrite.');
|
|
28
|
-
|
|
33
|
+
console.log('Use --force to overwrite sample config.');
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const loader = new OpenClawConfigLoader(configPath);
|
|
37
|
+
loader.createSampleConfig();
|
|
38
|
+
const relative = path.relative(process.cwd(), configPath);
|
|
39
|
+
console.log(`\x1b[32m✓ Created OpenClaw config at ${relative}\x1b[0m`);
|
|
29
40
|
}
|
|
30
|
-
const loader = new OpenClawConfigLoader(configPath);
|
|
31
|
-
loader.createSampleConfig();
|
|
32
|
-
const relative = path.relative(process.cwd(), configPath);
|
|
33
|
-
console.log(`\x1b[32m✓ Created OpenClaw config at ${relative}\x1b[0m`);
|
|
34
|
-
// Sync builtin skills to ~/.openclaw/skills/
|
|
35
41
|
const openclawSkillsDir = path.join(homedir(), '.openclaw', 'skills');
|
|
36
42
|
const skillsCopied = syncBuiltinSkills(openclawSkillsDir);
|
|
37
43
|
if (skillsCopied > 0) {
|
|
38
44
|
console.log(`\x1b[32m✓ Synced ${skillsCopied} builtin skills to ~/.openclaw/skills/\x1b[0m`);
|
|
39
45
|
}
|
|
40
46
|
frameworkLogger.log('openclaw-integration', 'skills-synced', 'info', { count: skillsCopied });
|
|
47
|
+
const wired = wiring.wireOpenClawBridge(targetDir);
|
|
48
|
+
console.log(`\x1b[32m✓ Wired OpenClaw MCP servers (${wired.count} via ${wired.method}) → ${wired.path}\x1b[0m`);
|
|
49
|
+
console.log(`\x1b[32m✓ Consumer root → ${targetDir}\x1b[0m`);
|
|
41
50
|
console.log('\n✅ 0xRay OpenClaw integration configured!');
|
|
42
|
-
console.log('
|
|
43
|
-
console.log('');
|
|
44
|
-
console.log('To initialize the integration in code:');
|
|
45
|
-
console.log(' import { initializeOpenClawIntegration } from "0xray";');
|
|
46
|
-
console.log(' const integration = await initializeOpenClawIntegration();');
|
|
51
|
+
console.log('Run `openclaw mcp list` to verify MCP servers.');
|
|
52
|
+
console.log('Edit `.xray/config/openclaw.json` for gateway URL, auth token, and device ID.');
|
|
47
53
|
}
|
|
48
54
|
catch (err) {
|
|
49
55
|
frameworkLogger.log('openclaw-integration', 'install-error', 'error', { error: err.message });
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { createRequire } from 'module';
|
|
3
4
|
import { frameworkLogger } from '../../core/framework-logger.js';
|
|
4
5
|
import { syncBuiltinSkills } from './skill-install.js';
|
|
5
6
|
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const wiring = require(path.join(__dirname, '..', '..', '..', 'scripts', 'node', 'bridge-mcp-wiring.cjs'));
|
|
6
9
|
export function registerOpencodeCommands(opencodeCmd) {
|
|
7
10
|
opencodeCmd
|
|
8
11
|
.command('install')
|
|
@@ -95,36 +98,44 @@ async function installForOpencode(options = {}) {
|
|
|
95
98
|
fs.writeFileSync(consumerPkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
96
99
|
console.log(`\x1b[32m✓ Created package.json with 0xray dependency\x1b[0m`);
|
|
97
100
|
}
|
|
98
|
-
|
|
101
|
+
const opencodeDestExists = fs.existsSync(opencodeDest);
|
|
102
|
+
if (opencodeDestExists && !options.force) {
|
|
99
103
|
console.log('[Opencode] .opencode/ directory already exists.');
|
|
100
104
|
console.log('Use --force to reinstall.');
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
copyDir(opencodeSource, opencodeDest);
|
|
104
|
-
frameworkLogger.log('opencode-integration', 'opencode-copied', 'info', { destination: opencodeDest });
|
|
105
|
-
// Copy plugin from dist/ to .opencode/plugin/
|
|
106
|
-
const pluginSource = path.join(__dirname, '..', '..', '..', 'dist', 'plugin', 'xray-codex-injection.js');
|
|
107
|
-
const pluginDest = path.join(opencodeDest, 'plugin', 'xray-codex-injection.js');
|
|
108
|
-
if (fs.existsSync(pluginSource)) {
|
|
109
|
-
const pluginDestDir = path.dirname(pluginDest);
|
|
110
|
-
if (!fs.existsSync(pluginDestDir))
|
|
111
|
-
fs.mkdirSync(pluginDestDir, { recursive: true });
|
|
112
|
-
fs.copyFileSync(pluginSource, pluginDest);
|
|
113
|
-
console.log(`\x1b[32m✓ Copied plugin to .opencode/plugin/\x1b[0m`);
|
|
114
105
|
}
|
|
115
106
|
else {
|
|
116
|
-
|
|
107
|
+
copyDir(opencodeSource, opencodeDest);
|
|
108
|
+
frameworkLogger.log('opencode-integration', 'opencode-copied', 'info', { destination: opencodeDest });
|
|
109
|
+
const pluginSource = path.join(__dirname, '..', '..', '..', 'dist', 'plugin', 'xray-codex-injection.js');
|
|
110
|
+
const pluginDest = path.join(opencodeDest, 'plugin', 'xray-codex-injection.js');
|
|
111
|
+
if (fs.existsSync(pluginSource)) {
|
|
112
|
+
const pluginDestDir = path.dirname(pluginDest);
|
|
113
|
+
if (!fs.existsSync(pluginDestDir))
|
|
114
|
+
fs.mkdirSync(pluginDestDir, { recursive: true });
|
|
115
|
+
fs.copyFileSync(pluginSource, pluginDest);
|
|
116
|
+
console.log(`\x1b[32m✓ Copied plugin to .opencode/plugin/\x1b[0m`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.log('ℹ️ Plugin source not found (build may be needed)');
|
|
120
|
+
}
|
|
121
|
+
const opencodeSkillsDir = path.join(opencodeDest, 'skills');
|
|
122
|
+
const skillsCopied = syncBuiltinSkills(opencodeSkillsDir);
|
|
123
|
+
if (skillsCopied > 0) {
|
|
124
|
+
console.log(`\x1b[32m✓ Synced ${skillsCopied} builtin skills to .opencode/skills/\x1b[0m`);
|
|
125
|
+
}
|
|
126
|
+
frameworkLogger.log('opencode-integration', 'skills-synced', 'info', { count: skillsCopied });
|
|
127
|
+
console.log(`\x1b[32m✓ Copied OpenCode config to .opencode/\x1b[0m`);
|
|
117
128
|
}
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
console.log(`\x1b[32m✓
|
|
129
|
+
const rootOpencode = path.join(__dirname, '..', '..', '..', 'opencode.json');
|
|
130
|
+
const userOpencode = path.join(process.cwd(), 'opencode.json');
|
|
131
|
+
if (fs.existsSync(rootOpencode) && !fs.existsSync(userOpencode)) {
|
|
132
|
+
fs.copyFileSync(rootOpencode, userOpencode);
|
|
133
|
+
console.log(`\x1b[32m✓ Seeded opencode.json from 0xRay template\x1b[0m`);
|
|
123
134
|
}
|
|
124
|
-
|
|
125
|
-
console.log(`\x1b[32m✓
|
|
135
|
+
const mcpCount = wiring.wireOpencodeBridge(process.cwd());
|
|
136
|
+
console.log(`\x1b[32m✓ Wired opencode.json mcp (${mcpCount} servers)\x1b[0m`);
|
|
126
137
|
console.log('\n✅ 0xRay OpenCode plugin installed!');
|
|
127
|
-
console.log('Restart OpenCode or run `opencode` to
|
|
138
|
+
console.log('Restart OpenCode or run `opencode mcp list` to verify MCP servers.');
|
|
128
139
|
}
|
|
129
140
|
catch (err) {
|
|
130
141
|
frameworkLogger.log('opencode-integration', 'install-error', 'error', { error: err.message });
|
package/package.json
CHANGED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Shared MCP registry wiring for Hermes + OpenCode + OpenClaw bridges.
|
|
4
|
+
* Used by install-bridges.cjs and platform install commands.
|
|
5
|
+
*/
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const os = require("os");
|
|
9
|
+
const { execSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
/** Canonical 7-server MCP surface */
|
|
12
|
+
const XRAY_MCP_SERVERS = [
|
|
13
|
+
{ name: "xray-governance", mcpCmd: "governance", env: { XRAY_FORCE_MCP_GOVERNANCE: "true" } },
|
|
14
|
+
{ name: "xray-skills", mcpCmd: "skills", env: {} },
|
|
15
|
+
{ name: "xray-orchestrator", mcpCmd: "orchestrator", env: {} },
|
|
16
|
+
{ name: "xray-enforcer", mcpCmd: "enforcer", env: {} },
|
|
17
|
+
{ name: "xray-researcher", mcpCmd: "researcher", env: {} },
|
|
18
|
+
{ name: "xray-code-review", mcpCmd: "code-review", env: {} },
|
|
19
|
+
{ name: "xray-architect-tools", mcpCmd: "architect-tools", env: {} },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const HERMES_CONFIG_PATH = path.join(os.homedir(), ".hermes", "config.yaml");
|
|
23
|
+
const HERMES_PLUGIN_DIR = path.join(os.homedir(), ".hermes", "plugins", "xray-hermes");
|
|
24
|
+
const OPENCLAW_CONFIG_PATH = path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
25
|
+
const OPENCLAW_STATE_DIR = path.join(os.homedir(), ".openclaw");
|
|
26
|
+
|
|
27
|
+
function detectConsumerExtraMcpServers(targetDir) {
|
|
28
|
+
const extras = { hermes: {}, opencode: {}, openclaw: {} };
|
|
29
|
+
try {
|
|
30
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
31
|
+
if (!fs.existsSync(pkgPath)) return extras;
|
|
32
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
33
|
+
const localRepertoireMcp = path.join(targetDir, "dist", "mcp", "server.js");
|
|
34
|
+
if (pkg.name === "@0xray/repertoire" && fs.existsSync(localRepertoireMcp)) {
|
|
35
|
+
extras.hermes.repertoire = {
|
|
36
|
+
command: "node",
|
|
37
|
+
args: [localRepertoireMcp],
|
|
38
|
+
env: { XRAY_ROOT: targetDir },
|
|
39
|
+
};
|
|
40
|
+
extras.opencode.repertoire = {
|
|
41
|
+
type: "local",
|
|
42
|
+
command: ["node", "dist/mcp/server.js"],
|
|
43
|
+
enabled: true,
|
|
44
|
+
cwd: ".",
|
|
45
|
+
};
|
|
46
|
+
extras.openclaw.repertoire = {
|
|
47
|
+
command: "node",
|
|
48
|
+
args: [localRepertoireMcp],
|
|
49
|
+
env: { XRAY_ROOT: targetDir },
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// best-effort
|
|
54
|
+
}
|
|
55
|
+
return extras;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildStdioMcpServer(mcpCmd, env, targetDir) {
|
|
59
|
+
return {
|
|
60
|
+
command: "npx",
|
|
61
|
+
args: ["-y", "0xray", "mcp", mcpCmd],
|
|
62
|
+
env: { ...env, XRAY_ROOT: targetDir },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function buildHermesMcpServers(targetDir) {
|
|
67
|
+
const servers = {};
|
|
68
|
+
for (const s of XRAY_MCP_SERVERS) {
|
|
69
|
+
servers[s.name] = buildStdioMcpServer(s.mcpCmd, s.env, targetDir);
|
|
70
|
+
}
|
|
71
|
+
const extras = detectConsumerExtraMcpServers(targetDir);
|
|
72
|
+
return { ...servers, ...extras.hermes };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildOpenClawMcpServers(targetDir) {
|
|
76
|
+
const servers = {};
|
|
77
|
+
for (const s of XRAY_MCP_SERVERS) {
|
|
78
|
+
servers[s.name] = buildStdioMcpServer(s.mcpCmd, s.env, targetDir);
|
|
79
|
+
}
|
|
80
|
+
const extras = detectConsumerExtraMcpServers(targetDir);
|
|
81
|
+
return { ...servers, ...extras.openclaw };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildOpencodeMcpEntries(targetDir) {
|
|
85
|
+
const entries = {};
|
|
86
|
+
for (const s of XRAY_MCP_SERVERS) {
|
|
87
|
+
const environment = { ...s.env };
|
|
88
|
+
entries[s.name] = {
|
|
89
|
+
type: "local",
|
|
90
|
+
command: ["npx", "-y", "0xray", "mcp", s.mcpCmd],
|
|
91
|
+
enabled: true,
|
|
92
|
+
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const extras = detectConsumerExtraMcpServers(targetDir);
|
|
96
|
+
return { ...entries, ...extras.opencode };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildPluginMcpJson(targetDir) {
|
|
100
|
+
const mcpServers = {};
|
|
101
|
+
for (const s of XRAY_MCP_SERVERS) {
|
|
102
|
+
mcpServers[s.name] = {
|
|
103
|
+
command: "npx",
|
|
104
|
+
args: ["-y", "0xray", "mcp", s.mcpCmd],
|
|
105
|
+
env: { ...s.env, XRAY_ROOT: targetDir },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { mcpServers };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Portable .mcp.json for Grok/Cursor — no absolute XRAY_ROOT */
|
|
112
|
+
function buildPortableProjectMcpJson() {
|
|
113
|
+
const mcpServers = {};
|
|
114
|
+
for (const s of XRAY_MCP_SERVERS) {
|
|
115
|
+
mcpServers[s.name] = {
|
|
116
|
+
command: "npx",
|
|
117
|
+
args: ["-y", "0xray", "mcp", s.mcpCmd],
|
|
118
|
+
...(Object.keys(s.env).length > 0 ? { env: { ...s.env } } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { mcpServers };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function writeHermesPluginArtifacts(targetDir) {
|
|
125
|
+
if (!fs.existsSync(HERMES_PLUGIN_DIR)) return false;
|
|
126
|
+
fs.writeFileSync(path.join(HERMES_PLUGIN_DIR, "xray-consumer-root.txt"), `${targetDir}\n`);
|
|
127
|
+
fs.writeFileSync(
|
|
128
|
+
path.join(HERMES_PLUGIN_DIR, ".mcp.json"),
|
|
129
|
+
`${JSON.stringify(buildPluginMcpJson(targetDir), null, 2)}\n`
|
|
130
|
+
);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function syncHermesMcpRegistry(targetDir) {
|
|
135
|
+
const servers = buildHermesMcpServers(targetDir);
|
|
136
|
+
const syncScript = path.join(__dirname, "sync-hermes-mcp-servers.py");
|
|
137
|
+
if (!fs.existsSync(syncScript)) {
|
|
138
|
+
throw new Error("sync-hermes-mcp-servers.py missing");
|
|
139
|
+
}
|
|
140
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "xray-hermes-mcp-"));
|
|
141
|
+
const serversPath = path.join(tmpDir, "servers.json");
|
|
142
|
+
try {
|
|
143
|
+
fs.writeFileSync(serversPath, JSON.stringify(servers));
|
|
144
|
+
if (!fs.existsSync(path.dirname(HERMES_CONFIG_PATH))) {
|
|
145
|
+
fs.mkdirSync(path.dirname(HERMES_CONFIG_PATH), { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
if (!fs.existsSync(HERMES_CONFIG_PATH)) {
|
|
148
|
+
fs.writeFileSync(HERMES_CONFIG_PATH, "mcp_servers: {}\n");
|
|
149
|
+
}
|
|
150
|
+
const out = execSync(`python3 "${syncScript}" "${HERMES_CONFIG_PATH}" "${serversPath}"`, {
|
|
151
|
+
encoding: "utf8",
|
|
152
|
+
});
|
|
153
|
+
return JSON.parse(out.trim());
|
|
154
|
+
} finally {
|
|
155
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function mergeOpencodeMcpRegistry(targetDir) {
|
|
160
|
+
const opencodePath = path.join(targetDir, "opencode.json");
|
|
161
|
+
const entries = buildOpencodeMcpEntries(targetDir);
|
|
162
|
+
let config = { $schema: "https://opencode.ai/config.json", mcp: {} };
|
|
163
|
+
if (fs.existsSync(opencodePath)) {
|
|
164
|
+
try {
|
|
165
|
+
config = JSON.parse(fs.readFileSync(opencodePath, "utf8"));
|
|
166
|
+
} catch {
|
|
167
|
+
config = { $schema: "https://opencode.ai/config.json", mcp: {} };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
config.mcp = { ...(config.mcp || {}), ...entries };
|
|
171
|
+
fs.writeFileSync(opencodePath, `${JSON.stringify(config, null, 2)}\n`);
|
|
172
|
+
return Object.keys(entries).length;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function deployPortableProjectMcpJson(targetDir) {
|
|
176
|
+
const destPath = path.join(targetDir, ".mcp.json");
|
|
177
|
+
const portable = buildPortableProjectMcpJson();
|
|
178
|
+
let existing = { mcpServers: {} };
|
|
179
|
+
if (fs.existsSync(destPath)) {
|
|
180
|
+
try {
|
|
181
|
+
existing = JSON.parse(fs.readFileSync(destPath, "utf8"));
|
|
182
|
+
} catch {
|
|
183
|
+
existing = { mcpServers: {} };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const merged = {
|
|
187
|
+
...existing,
|
|
188
|
+
mcpServers: {
|
|
189
|
+
...(existing.mcpServers || {}),
|
|
190
|
+
...portable.mcpServers,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
fs.writeFileSync(destPath, `${JSON.stringify(merged, null, 2)}\n`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function enableHermesPluginBestEffort() {
|
|
197
|
+
try {
|
|
198
|
+
execSync("hermes plugins enable 0xray-hermes", { stdio: "pipe", encoding: "utf8" });
|
|
199
|
+
return true;
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function wireHermesBridge(targetDir) {
|
|
206
|
+
writeHermesPluginArtifacts(targetDir);
|
|
207
|
+
const result = syncHermesMcpRegistry(targetDir);
|
|
208
|
+
enableHermesPluginBestEffort();
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function wireOpencodeBridge(targetDir) {
|
|
213
|
+
return mergeOpencodeMcpRegistry(targetDir);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveOpenClawConfigPath() {
|
|
217
|
+
if (process.env.OPENCLAW_CONFIG_PATH && fs.existsSync(process.env.OPENCLAW_CONFIG_PATH)) {
|
|
218
|
+
return process.env.OPENCLAW_CONFIG_PATH;
|
|
219
|
+
}
|
|
220
|
+
return OPENCLAW_CONFIG_PATH;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function syncOpenClawMcpRegistryFile(targetDir, configPath) {
|
|
224
|
+
const servers = buildOpenClawMcpServers(targetDir);
|
|
225
|
+
if (!fs.existsSync(path.dirname(configPath))) {
|
|
226
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
let config = {};
|
|
229
|
+
if (fs.existsSync(configPath)) {
|
|
230
|
+
try {
|
|
231
|
+
config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
232
|
+
} catch {
|
|
233
|
+
config = {};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
config.mcp = config.mcp || {};
|
|
237
|
+
config.mcp.servers = { ...(config.mcp.servers || {}), ...servers };
|
|
238
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
239
|
+
return { count: Object.keys(config.mcp.servers).length, path: configPath, method: "file" };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function syncOpenClawMcpRegistryCli(targetDir) {
|
|
243
|
+
const servers = buildOpenClawMcpServers(targetDir);
|
|
244
|
+
let wired = 0;
|
|
245
|
+
for (const [name, serverConfig] of Object.entries(servers)) {
|
|
246
|
+
const payload = JSON.stringify(serverConfig).replace(/'/g, "'\\''");
|
|
247
|
+
execSync(`openclaw mcp set ${name} '${payload}'`, { stdio: "pipe", encoding: "utf8" });
|
|
248
|
+
wired++;
|
|
249
|
+
}
|
|
250
|
+
return { count: wired, path: resolveOpenClawConfigPath(), method: "cli" };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function syncOpenClawMcpRegistry(targetDir) {
|
|
254
|
+
try {
|
|
255
|
+
execSync("which openclaw", { stdio: "ignore" });
|
|
256
|
+
return syncOpenClawMcpRegistryCli(targetDir);
|
|
257
|
+
} catch {
|
|
258
|
+
return syncOpenClawMcpRegistryFile(targetDir, resolveOpenClawConfigPath());
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function writeOpenClawConsumerArtifacts(targetDir) {
|
|
263
|
+
if (!fs.existsSync(OPENCLAW_STATE_DIR)) {
|
|
264
|
+
fs.mkdirSync(OPENCLAW_STATE_DIR, { recursive: true });
|
|
265
|
+
}
|
|
266
|
+
fs.writeFileSync(path.join(OPENCLAW_STATE_DIR, "xray-consumer-root.txt"), `${targetDir}\n`);
|
|
267
|
+
|
|
268
|
+
const consumerConfigPath = path.join(targetDir, ".xray", "config", "openclaw.json");
|
|
269
|
+
if (fs.existsSync(consumerConfigPath)) {
|
|
270
|
+
try {
|
|
271
|
+
const consumerConfig = JSON.parse(fs.readFileSync(consumerConfigPath, "utf8"));
|
|
272
|
+
consumerConfig.xrayRoot = targetDir;
|
|
273
|
+
fs.writeFileSync(consumerConfigPath, `${JSON.stringify(consumerConfig, null, 2)}\n`);
|
|
274
|
+
} catch {
|
|
275
|
+
// best-effort
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function wireOpenClawBridge(targetDir) {
|
|
282
|
+
writeOpenClawConsumerArtifacts(targetDir);
|
|
283
|
+
return syncOpenClawMcpRegistry(targetDir);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = {
|
|
287
|
+
XRAY_MCP_SERVERS,
|
|
288
|
+
HERMES_CONFIG_PATH,
|
|
289
|
+
HERMES_PLUGIN_DIR,
|
|
290
|
+
OPENCLAW_CONFIG_PATH,
|
|
291
|
+
OPENCLAW_STATE_DIR,
|
|
292
|
+
buildHermesMcpServers,
|
|
293
|
+
buildOpenClawMcpServers,
|
|
294
|
+
buildOpencodeMcpEntries,
|
|
295
|
+
buildStdioMcpServer,
|
|
296
|
+
buildPluginMcpJson,
|
|
297
|
+
buildPortableProjectMcpJson,
|
|
298
|
+
writeHermesPluginArtifacts,
|
|
299
|
+
syncHermesMcpRegistry,
|
|
300
|
+
mergeOpencodeMcpRegistry,
|
|
301
|
+
deployPortableProjectMcpJson,
|
|
302
|
+
enableHermesPluginBestEffort,
|
|
303
|
+
wireHermesBridge,
|
|
304
|
+
wireOpencodeBridge,
|
|
305
|
+
wireOpenClawBridge,
|
|
306
|
+
syncOpenClawMcpRegistry,
|
|
307
|
+
writeOpenClawConsumerArtifacts,
|
|
308
|
+
resolveOpenClawConfigPath,
|
|
309
|
+
detectConsumerExtraMcpServers,
|
|
310
|
+
};
|
|
@@ -8,6 +8,12 @@ const fs = require("fs");
|
|
|
8
8
|
const path = require("path");
|
|
9
9
|
const os = require("os");
|
|
10
10
|
const { execSync } = require("child_process");
|
|
11
|
+
const {
|
|
12
|
+
wireHermesBridge,
|
|
13
|
+
wireOpencodeBridge,
|
|
14
|
+
wireOpenClawBridge,
|
|
15
|
+
deployPortableProjectMcpJson,
|
|
16
|
+
} = require("./bridge-mcp-wiring.cjs");
|
|
11
17
|
|
|
12
18
|
const SKIP_DIRS = new Set(["node_modules", "logs"]);
|
|
13
19
|
const MERGE_FILES = new Set(["enforcer-config.json"]);
|
|
@@ -126,44 +132,19 @@ function copyPluginDir(src, dest) {
|
|
|
126
132
|
return true;
|
|
127
133
|
}
|
|
128
134
|
|
|
129
|
-
function buildConsumerMcpConfig(targetDir) {
|
|
130
|
-
const mcpServers = {};
|
|
131
|
-
for (const s of XRAY_MCP_SERVERS) {
|
|
132
|
-
mcpServers[s.name] = {
|
|
133
|
-
command: "npx",
|
|
134
|
-
args: ["-y", "0xray", "mcp", s.mcpCmd],
|
|
135
|
-
env: { ...s.env, XRAY_ROOT: targetDir },
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
return { mcpServers };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function deployProjectMcpJson(targetDir, log) {
|
|
142
|
-
const destPath = path.join(targetDir, ".mcp.json");
|
|
143
|
-
const consumerConfig = buildConsumerMcpConfig(targetDir);
|
|
144
|
-
let existing = { mcpServers: {} };
|
|
145
|
-
if (fs.existsSync(destPath)) {
|
|
146
|
-
try {
|
|
147
|
-
existing = JSON.parse(fs.readFileSync(destPath, "utf8"));
|
|
148
|
-
} catch {
|
|
149
|
-
existing = { mcpServers: {} };
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const merged = {
|
|
153
|
-
...existing,
|
|
154
|
-
mcpServers: { ...(existing.mcpServers || {}), ...consumerConfig.mcpServers },
|
|
155
|
-
};
|
|
156
|
-
fs.writeFileSync(destPath, JSON.stringify(merged, null, 2) + "\n");
|
|
157
|
-
log("mcp-config", "project .mcp.json deployed (7 xray servers)", "info");
|
|
158
|
-
}
|
|
159
|
-
|
|
160
135
|
function writePluginMcpJson(pluginDir, targetDir, log, label) {
|
|
161
136
|
if (!fs.existsSync(pluginDir)) return;
|
|
137
|
+
const { buildPluginMcpJson } = require("./bridge-mcp-wiring.cjs");
|
|
162
138
|
const dest = path.join(pluginDir, ".mcp.json");
|
|
163
|
-
fs.writeFileSync(dest, JSON.stringify(
|
|
139
|
+
fs.writeFileSync(dest, JSON.stringify(buildPluginMcpJson(targetDir), null, 2) + "\n");
|
|
164
140
|
log(label, "plugin .mcp.json patched for consumer", "info");
|
|
165
141
|
}
|
|
166
142
|
|
|
143
|
+
function deployProjectMcpJson(targetDir, log) {
|
|
144
|
+
deployPortableProjectMcpJson(targetDir);
|
|
145
|
+
log("mcp-config", "project .mcp.json deployed (7 xray servers, portable)", "info");
|
|
146
|
+
}
|
|
147
|
+
|
|
167
148
|
function mergeOpencodeJson(targetDir, packageRoot, log) {
|
|
168
149
|
const rootOpencode = path.join(packageRoot, "opencode.json");
|
|
169
150
|
const userOpencode = path.join(targetDir, "opencode.json");
|
|
@@ -242,6 +223,13 @@ function installOpencodeBridge(targetDir, packageRoot, log) {
|
|
|
242
223
|
|
|
243
224
|
const copied = syncBuiltinSkills(path.join(opencodeDest, "skills"), packageRoot);
|
|
244
225
|
if (copied > 0) log("opencode-bridge", `skills synced (${copied})`, "info", { path: ".opencode/skills/" });
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const count = wireOpencodeBridge(targetDir);
|
|
229
|
+
log("opencode-bridge", `opencode.json mcp wired (${count} servers)`, "info");
|
|
230
|
+
} catch (e) {
|
|
231
|
+
log("opencode-bridge", "opencode.json mcp wire failed", "warn", { error: e.message });
|
|
232
|
+
}
|
|
245
233
|
}
|
|
246
234
|
|
|
247
235
|
function findGrokPluginSource(packageRoot) {
|
|
@@ -358,6 +346,13 @@ function installHermesBridge(targetDir, packageRoot, log) {
|
|
|
358
346
|
const rootMarker = path.join(targetPluginDir, "xray-consumer-root.txt");
|
|
359
347
|
fs.writeFileSync(rootMarker, targetDir + "\n");
|
|
360
348
|
log("hermes-bridge", "consumer root marker written", "info");
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const result = wireHermesBridge(targetDir);
|
|
352
|
+
log("hermes-bridge", `mcp_servers wired (${result.count} servers)`, "info");
|
|
353
|
+
} catch (e) {
|
|
354
|
+
log("hermes-bridge", "mcp_servers wire failed", "warn", { error: e.message });
|
|
355
|
+
}
|
|
361
356
|
}
|
|
362
357
|
|
|
363
358
|
function installOpenclawBridge(targetDir, packageRoot, log) {
|
|
@@ -395,6 +390,15 @@ function installOpenclawBridge(targetDir, packageRoot, log) {
|
|
|
395
390
|
|
|
396
391
|
const copied = syncBuiltinSkills(path.join(os.homedir(), ".openclaw", "skills"), packageRoot);
|
|
397
392
|
if (copied > 0) log("openclaw-bridge", `skills synced (${copied})`, "info", { path: "~/.openclaw/skills/" });
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const result = wireOpenClawBridge(targetDir);
|
|
396
|
+
log("openclaw-bridge", `openclaw.json mcp wired (${result.count} servers)`, "info", {
|
|
397
|
+
method: result.method,
|
|
398
|
+
});
|
|
399
|
+
} catch (e) {
|
|
400
|
+
log("openclaw-bridge", "openclaw.json mcp wire failed", "warn", { error: e.message });
|
|
401
|
+
}
|
|
398
402
|
}
|
|
399
403
|
|
|
400
404
|
const XRAY_CONFIG_FILES = ["codex.json", "features.json", "features.schema.json", "config.json"];
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Merge MCP server entries into ~/.hermes/config.yaml mcp_servers."""
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> int:
|
|
11
|
+
if len(sys.argv) != 3:
|
|
12
|
+
print("usage: sync-hermes-mcp-servers.py <config.yaml> <servers.json>", file=sys.stderr)
|
|
13
|
+
return 1
|
|
14
|
+
|
|
15
|
+
config_path = Path(sys.argv[1])
|
|
16
|
+
servers = json.loads(Path(sys.argv[2]).read_text())
|
|
17
|
+
|
|
18
|
+
config = {}
|
|
19
|
+
if config_path.exists():
|
|
20
|
+
config = yaml.safe_load(config_path.read_text()) or {}
|
|
21
|
+
|
|
22
|
+
existing = config.get("mcp_servers") or {}
|
|
23
|
+
existing.update(servers)
|
|
24
|
+
config["mcp_servers"] = existing
|
|
25
|
+
config_path.write_text(yaml.safe_dump(config, default_flow_style=False, sort_keys=False))
|
|
26
|
+
print(json.dumps({"count": len(existing)}))
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
raise SystemExit(main())
|