0xray 3.3.0 → 3.4.1
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/.opencode/codex.codex +1 -1
- package/.opencode/enforcer-config.json +1 -4
- package/dist/CHANGELOG.md +22 -0
- package/dist/analytics/routing-refiner.js +0 -2
- package/dist/core/boot-orchestrator.js +9 -1
- package/dist/core/features-config.js +0 -1
- package/dist/integrations/grok/grok-cli.js +53 -25
- package/dist/integrations/hermes-agent/bridge.mjs +11 -1
- package/dist/mcps/index.js +1 -0
- package/dist/mcps/knowledge-skills/skill-invocation.server.js +3 -2
- package/dist/mcps/knowledge-skills/storyteller.server.js +167 -0
- package/dist/mcps/orchestrator/execution/execution-planner.js +14 -2
- package/dist/mcps/registry.json +0 -1
- package/dist/mcps/researcher-confidence.js +92 -0
- package/dist/mcps/researcher.server.js +19 -3
- package/dist/memory-routing/null-provider.js +12 -0
- package/dist/orchestrator/universal-registry-bridge.js +1 -1
- package/dist/skills/registry.json +0 -1
- package/package.json +1 -1
- package/scripts/node/enforce-agents-md.mjs +0 -26
- package/scripts/node/enforce-version-compliance.sh +0 -1
- package/scripts/node/enforce-version-compliance.ts +0 -10
- package/scripts/node/install-bridges.cjs +466 -0
- package/scripts/node/postinstall.cjs +24 -83
- package/scripts/node/pre-publish-guard.js +20 -16
- package/scripts/node/setup.cjs +11 -10
- package/scripts/node/universal-version-manager.js +17 -3
- package/src/mcps/index.ts +1 -0
- package/src/mcps/knowledge-skills/skill-invocation.server.ts +3 -2
- package/src/mcps/knowledge-skills/storyteller.server.ts +206 -0
- package/src/mcps/orchestrator/execution/execution-planner.ts +14 -2
- package/src/mcps/orchestrator/types.ts +4 -0
- package/src/mcps/registry.json +0 -1
- package/src/mcps/researcher-confidence.ts +144 -0
- package/src/mcps/researcher.server.ts +28 -3
- package/src/opencode/codex.codex +1 -1
- package/src/opencode/enforcer-config.json +1 -4
- package/src/skills/registry.json +0 -1
package/package.json
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* AGENTS.md Enforcement Script
|
|
4
|
-
*
|
|
5
|
-
* Validates that AGENTS.md exists, is up-to-date, and follows the required format.
|
|
6
|
-
* This script should be run in CI/CD, pre-commit hooks, and local validation.
|
|
7
|
-
*
|
|
8
|
-
* @version 1.0.0
|
|
9
|
-
* @framework xray v2
|
|
10
3
|
*/
|
|
11
4
|
|
|
12
5
|
import fs from 'fs';
|
|
@@ -62,7 +55,6 @@ class AgentsMdEnforcer {
|
|
|
62
55
|
}
|
|
63
56
|
|
|
64
57
|
/**
|
|
65
|
-
* Run all validation checks
|
|
66
58
|
*/
|
|
67
59
|
async validate() {
|
|
68
60
|
console.log('🔍 AGENTS.md Enforcement Check');
|
|
@@ -148,14 +140,12 @@ class AgentsMdEnforcer {
|
|
|
148
140
|
}
|
|
149
141
|
|
|
150
142
|
/**
|
|
151
|
-
* Check if AGENTS.md file exists
|
|
152
143
|
*/
|
|
153
144
|
checkFileExists() {
|
|
154
145
|
return fs.existsSync(this.agentsPath);
|
|
155
146
|
}
|
|
156
147
|
|
|
157
148
|
/**
|
|
158
|
-
* Check for required sections
|
|
159
149
|
*/
|
|
160
150
|
checkRequiredSections(content) {
|
|
161
151
|
const missing = [];
|
|
@@ -188,7 +178,6 @@ class AgentsMdEnforcer {
|
|
|
188
178
|
}
|
|
189
179
|
|
|
190
180
|
/**
|
|
191
|
-
* Check for required agent definitions
|
|
192
181
|
*/
|
|
193
182
|
checkRequiredAgents(content) {
|
|
194
183
|
// Be flexible - check for agent names with or without @ prefix
|
|
@@ -199,7 +188,6 @@ class AgentsMdEnforcer {
|
|
|
199
188
|
}
|
|
200
189
|
|
|
201
190
|
/**
|
|
202
|
-
* Extract version from header
|
|
203
191
|
*/
|
|
204
192
|
extractVersion(content) {
|
|
205
193
|
const match = content.match(/\*\*Version\*\*:\s*(\d+\.\d+\.\d+)/);
|
|
@@ -207,7 +195,6 @@ class AgentsMdEnforcer {
|
|
|
207
195
|
}
|
|
208
196
|
|
|
209
197
|
/**
|
|
210
|
-
* Extract date from header
|
|
211
198
|
*/
|
|
212
199
|
extractDate(content) {
|
|
213
200
|
const match = content.match(/\*\*Updated\*\*:\s*(\d{4}-\d{2}-\d{2})/);
|
|
@@ -215,7 +202,6 @@ class AgentsMdEnforcer {
|
|
|
215
202
|
}
|
|
216
203
|
|
|
217
204
|
/**
|
|
218
|
-
* Calculate how many days old the file is
|
|
219
205
|
*/
|
|
220
206
|
calculateDaysOld(dateString) {
|
|
221
207
|
const date = new Date(dateString);
|
|
@@ -225,18 +211,12 @@ class AgentsMdEnforcer {
|
|
|
225
211
|
}
|
|
226
212
|
|
|
227
213
|
/**
|
|
228
|
-
* Calculate content hash
|
|
229
214
|
*/
|
|
230
215
|
calculateHash(content) {
|
|
231
216
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
232
217
|
}
|
|
233
218
|
|
|
234
219
|
/**
|
|
235
|
-
* Count codex terms (numbered rules or sections with multiple items)
|
|
236
|
-
* Looks for patterns like:
|
|
237
|
-
* - #### 1. Something
|
|
238
|
-
* - 1. **Something**
|
|
239
|
-
* - ## CRITICAL RULES (count as one term block)
|
|
240
220
|
*/
|
|
241
221
|
countCodexTerms(content) {
|
|
242
222
|
// Count #### numbered items
|
|
@@ -257,7 +237,6 @@ class AgentsMdEnforcer {
|
|
|
257
237
|
}
|
|
258
238
|
|
|
259
239
|
/**
|
|
260
|
-
* Log individual check result
|
|
261
240
|
*/
|
|
262
241
|
logResult(check, status, message) {
|
|
263
242
|
const icon = status === 'PASS' ? '✅' : status === 'FAIL' ? '❌' : status === 'WARN' ? '⚠️' : 'ℹ️';
|
|
@@ -266,7 +245,6 @@ class AgentsMdEnforcer {
|
|
|
266
245
|
}
|
|
267
246
|
|
|
268
247
|
/**
|
|
269
|
-
* Generate and display report
|
|
270
248
|
*/
|
|
271
249
|
report(exitCode) {
|
|
272
250
|
console.log('\n================================');
|
|
@@ -309,7 +287,6 @@ class AgentsMdEnforcer {
|
|
|
309
287
|
}
|
|
310
288
|
|
|
311
289
|
/**
|
|
312
|
-
* Auto-generate AGENTS.md from current agent configuration
|
|
313
290
|
*/
|
|
314
291
|
async generate() {
|
|
315
292
|
console.log('📝 Generating AGENTS.md...\n');
|
|
@@ -325,7 +302,6 @@ class AgentsMdEnforcer {
|
|
|
325
302
|
}
|
|
326
303
|
|
|
327
304
|
/**
|
|
328
|
-
* Load template file
|
|
329
305
|
*/
|
|
330
306
|
async loadTemplate() {
|
|
331
307
|
const templatePath = path.join(__dirname, '..', '..', '..', 'docs', 'AGENTS_TEMPLATE.md');
|
|
@@ -342,7 +318,6 @@ class AgentsMdEnforcer {
|
|
|
342
318
|
}
|
|
343
319
|
|
|
344
320
|
/**
|
|
345
|
-
* Discover agents from configuration
|
|
346
321
|
*/
|
|
347
322
|
async discoverAgents() {
|
|
348
323
|
const agents = [];
|
|
@@ -366,7 +341,6 @@ class AgentsMdEnforcer {
|
|
|
366
341
|
}
|
|
367
342
|
|
|
368
343
|
/**
|
|
369
|
-
* Populate template with agent data
|
|
370
344
|
*/
|
|
371
345
|
populateTemplate(template, agents) {
|
|
372
346
|
const now = new Date().toISOString().split('T')[0];
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Version Compliance Enforcement Script
|
|
4
|
-
*
|
|
5
|
-
* Thin CLI wrapper around VersionComplianceProcessor.
|
|
6
|
-
* Called by CI/CD, pre-commit hooks, and manual runs.
|
|
7
|
-
*
|
|
8
|
-
* This script should NOT contain enforcement logic -
|
|
9
|
-
* all logic lives in src/processors/version-compliance-processor.ts
|
|
10
|
-
*
|
|
11
|
-
* @version 1.0.0
|
|
12
|
-
* @deprecated Use VersionComplianceProcessor directly in new code
|
|
13
3
|
*/
|
|
14
4
|
|
|
15
5
|
import path from 'path';
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* install-bridges.cjs — Unified 4-platform bridge installer for consumer postinstall.
|
|
4
|
+
* Mirrors npx 0xray {opencode,grok,hermes,openclaw} install in one synchronous pass.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const os = require("os");
|
|
10
|
+
const { execSync } = require("child_process");
|
|
11
|
+
|
|
12
|
+
const SKIP_DIRS = new Set(["node_modules", "logs"]);
|
|
13
|
+
const MERGE_FILES = new Set(["enforcer-config.json"]);
|
|
14
|
+
const KEEP_IF_EXISTS = new Set([".yml", ".yaml", ".md"]);
|
|
15
|
+
|
|
16
|
+
/** Canonical 7-server MCP surface — matches package .mcp.json and Grok plugin */
|
|
17
|
+
const XRAY_MCP_SERVERS = [
|
|
18
|
+
{ name: "xray-governance", mcpCmd: "governance", env: { XRAY_FORCE_MCP_GOVERNANCE: "true" } },
|
|
19
|
+
{ name: "xray-skills", mcpCmd: "skills", env: {} },
|
|
20
|
+
{ name: "xray-orchestrator", mcpCmd: "orchestrator", env: {} },
|
|
21
|
+
{ name: "xray-enforcer", mcpCmd: "enforcer", env: {} },
|
|
22
|
+
{ name: "xray-researcher", mcpCmd: "researcher", env: {} },
|
|
23
|
+
{ name: "xray-code-review", mcpCmd: "code-review", env: {} },
|
|
24
|
+
{ name: "xray-architect-tools", mcpCmd: "architect-tools", env: {} },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function resolveConsumerTargetDir(packageRoot, fallbackDir) {
|
|
28
|
+
const resolved = path.resolve(packageRoot);
|
|
29
|
+
const inNodeModules =
|
|
30
|
+
resolved.includes(`${path.sep}node_modules${path.sep}`) ||
|
|
31
|
+
resolved.endsWith(`${path.sep}node_modules`);
|
|
32
|
+
|
|
33
|
+
if (!inNodeModules) {
|
|
34
|
+
return fallbackDir || process.env.PWD || process.cwd();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let current = resolved;
|
|
38
|
+
while (path.basename(current) !== "node_modules") {
|
|
39
|
+
const parent = path.dirname(current);
|
|
40
|
+
if (parent === current) return fallbackDir || process.cwd();
|
|
41
|
+
current = parent;
|
|
42
|
+
}
|
|
43
|
+
return path.dirname(current);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isConsumerInstall(packageRoot, targetDir) {
|
|
47
|
+
return path.resolve(packageRoot) !== path.resolve(targetDir);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function deepMerge(src, dest) {
|
|
51
|
+
if (typeof src !== "object" || src === null) return dest !== undefined ? dest : src;
|
|
52
|
+
if (Array.isArray(src)) return Array.isArray(dest) ? dest : src;
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const key of Object.keys(src)) {
|
|
55
|
+
result[key] = dest && typeof dest[key] !== "undefined" ? deepMerge(src[key], dest[key]) : src[key];
|
|
56
|
+
}
|
|
57
|
+
if (dest && typeof dest === "object") {
|
|
58
|
+
for (const key of Object.keys(dest)) {
|
|
59
|
+
if (!(key in src)) result[key] = dest[key];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function syncBuiltinSkills(targetSkillsDir, packageRoot) {
|
|
66
|
+
const candidateDirs = [
|
|
67
|
+
path.join(packageRoot, "dist", "skills"),
|
|
68
|
+
path.join(packageRoot, "src", "skills"),
|
|
69
|
+
];
|
|
70
|
+
const sourceDir = candidateDirs.find((p) => fs.existsSync(p));
|
|
71
|
+
if (!sourceDir) return 0;
|
|
72
|
+
|
|
73
|
+
let copied = 0;
|
|
74
|
+
try {
|
|
75
|
+
if (!fs.existsSync(targetSkillsDir)) fs.mkdirSync(targetSkillsDir, { recursive: true });
|
|
76
|
+
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
|
|
77
|
+
if (!entry.isDirectory()) continue;
|
|
78
|
+
const skillMd = path.join(sourceDir, entry.name, "SKILL.md");
|
|
79
|
+
if (!fs.existsSync(skillMd)) continue;
|
|
80
|
+
const destMd = path.join(targetSkillsDir, entry.name, "SKILL.md");
|
|
81
|
+
const destDir = path.dirname(destMd);
|
|
82
|
+
if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
|
|
83
|
+
if (fs.existsSync(destMd) && fs.statSync(skillMd).mtime <= fs.statSync(destMd).mtime) continue;
|
|
84
|
+
fs.copyFileSync(skillMd, destMd);
|
|
85
|
+
copied++;
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// best-effort
|
|
89
|
+
}
|
|
90
|
+
return copied;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function copyTree(src, dest, relPath = "") {
|
|
94
|
+
if (!fs.existsSync(src)) return;
|
|
95
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
96
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
97
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
98
|
+
const srcPath = path.join(src, entry.name);
|
|
99
|
+
const destPath = path.join(dest, entry.name);
|
|
100
|
+
const rel = path.join(relPath, entry.name);
|
|
101
|
+
if (entry.isDirectory()) {
|
|
102
|
+
copyTree(srcPath, destPath, rel);
|
|
103
|
+
} else if (MERGE_FILES.has(rel)) {
|
|
104
|
+
try {
|
|
105
|
+
const srcData = JSON.parse(fs.readFileSync(srcPath, "utf8"));
|
|
106
|
+
if (fs.existsSync(destPath)) {
|
|
107
|
+
const destData = JSON.parse(fs.readFileSync(destPath, "utf8"));
|
|
108
|
+
fs.writeFileSync(destPath, JSON.stringify(deepMerge(srcData, destData), null, 2));
|
|
109
|
+
} else {
|
|
110
|
+
fs.copyFileSync(srcPath, destPath);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
fs.copyFileSync(srcPath, destPath);
|
|
114
|
+
}
|
|
115
|
+
} else if (KEEP_IF_EXISTS.has(path.extname(srcPath)) && fs.existsSync(destPath)) {
|
|
116
|
+
continue;
|
|
117
|
+
} else {
|
|
118
|
+
fs.copyFileSync(srcPath, destPath);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function copyPluginDir(src, dest) {
|
|
124
|
+
if (!fs.existsSync(src)) return false;
|
|
125
|
+
fs.cpSync(src, dest, { recursive: true, force: true });
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
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
|
+
function writePluginMcpJson(pluginDir, targetDir, log, label) {
|
|
161
|
+
if (!fs.existsSync(pluginDir)) return;
|
|
162
|
+
const dest = path.join(pluginDir, ".mcp.json");
|
|
163
|
+
fs.writeFileSync(dest, JSON.stringify(buildConsumerMcpConfig(targetDir), null, 2) + "\n");
|
|
164
|
+
log(label, "plugin .mcp.json patched for consumer", "info");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function mergeOpencodeJson(targetDir, packageRoot, log) {
|
|
168
|
+
const rootOpencode = path.join(packageRoot, "opencode.json");
|
|
169
|
+
const userOpencode = path.join(targetDir, "opencode.json");
|
|
170
|
+
if (!fs.existsSync(rootOpencode)) return;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const srcData = JSON.parse(fs.readFileSync(rootOpencode, "utf8"));
|
|
174
|
+
if (fs.existsSync(userOpencode)) {
|
|
175
|
+
const destData = JSON.parse(fs.readFileSync(userOpencode, "utf8"));
|
|
176
|
+
const merged = { ...destData };
|
|
177
|
+
if (srcData.agent) merged.agent = srcData.agent;
|
|
178
|
+
if (srcData.mcp) merged.mcp = { ...destData.mcp, ...srcData.mcp };
|
|
179
|
+
if (srcData.compaction) merged.compaction = srcData.compaction;
|
|
180
|
+
fs.writeFileSync(userOpencode, JSON.stringify(merged, null, 2) + "\n");
|
|
181
|
+
} else {
|
|
182
|
+
fs.copyFileSync(rootOpencode, userOpencode);
|
|
183
|
+
}
|
|
184
|
+
log("opencode-bridge", "opencode.json merged", "info");
|
|
185
|
+
} catch (e) {
|
|
186
|
+
log("opencode-bridge", "opencode.json merge failed", "warn", { error: e.message });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function patchGrokHooks(pluginDir, packageRoot, targetDir, log, label) {
|
|
191
|
+
const hooksPath = path.join(pluginDir, "hooks", "hooks.json");
|
|
192
|
+
const hookScript = path.join(packageRoot, "dist", "integrations", "grok", "hooks", "pre-tool-use.js");
|
|
193
|
+
if (!fs.existsSync(hooksPath)) return;
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const hooks = JSON.parse(fs.readFileSync(hooksPath, "utf8"));
|
|
197
|
+
const preTool = hooks.hooks?.PreToolUse?.[0]?.hooks?.[0];
|
|
198
|
+
if (preTool && fs.existsSync(hookScript)) {
|
|
199
|
+
preTool.command = "node";
|
|
200
|
+
preTool.args = [hookScript];
|
|
201
|
+
preTool.env = {
|
|
202
|
+
...(preTool.env || {}),
|
|
203
|
+
XRAY_ROOT: targetDir,
|
|
204
|
+
XRAY_AI_PATH: packageRoot,
|
|
205
|
+
};
|
|
206
|
+
fs.writeFileSync(hooksPath, JSON.stringify(hooks, null, 2) + "\n");
|
|
207
|
+
log(label, "hooks.json patched → enforcement gate", "info");
|
|
208
|
+
}
|
|
209
|
+
} catch (e) {
|
|
210
|
+
log(label, "hooks.json patch failed", "warn", { error: e.message });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function installOpencodeBridge(targetDir, packageRoot, log) {
|
|
215
|
+
const opencodeSource = path.join(packageRoot, ".opencode");
|
|
216
|
+
const opencodeDest = path.join(targetDir, ".opencode");
|
|
217
|
+
if (!fs.existsSync(opencodeSource)) {
|
|
218
|
+
log("opencode-bridge", "skipped", "warn", { reason: ".opencode source missing" });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!fs.existsSync(opencodeDest)) {
|
|
223
|
+
copyTree(opencodeSource, opencodeDest);
|
|
224
|
+
log("opencode-bridge", "copied .opencode tree", "info");
|
|
225
|
+
} else {
|
|
226
|
+
log("opencode-bridge", ".opencode exists — merging skills/plugin only", "info");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const pluginSource = path.join(packageRoot, "dist", "plugin", "xray-codex-injection.js");
|
|
230
|
+
const pluginDest = path.join(opencodeDest, "plugin", "xray-codex-injection.js");
|
|
231
|
+
if (fs.existsSync(pluginSource)) {
|
|
232
|
+
const pluginDestDir = path.dirname(pluginDest);
|
|
233
|
+
if (!fs.existsSync(pluginDestDir)) fs.mkdirSync(pluginDestDir, { recursive: true });
|
|
234
|
+
const shouldCopy =
|
|
235
|
+
!fs.existsSync(pluginDest) ||
|
|
236
|
+
fs.statSync(pluginSource).mtime > fs.statSync(pluginDest).mtime;
|
|
237
|
+
if (shouldCopy) {
|
|
238
|
+
fs.copyFileSync(pluginSource, pluginDest);
|
|
239
|
+
log("opencode-bridge", "plugin updated", "info");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const copied = syncBuiltinSkills(path.join(opencodeDest, "skills"), packageRoot);
|
|
244
|
+
if (copied > 0) log("opencode-bridge", `skills synced (${copied})`, "info", { path: ".opencode/skills/" });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function findGrokPluginSource(packageRoot) {
|
|
248
|
+
const candidates = [
|
|
249
|
+
path.join(packageRoot, "src", "integrations", "grok", "plugin", "0xray"),
|
|
250
|
+
path.join(packageRoot, ".grok", "plugins", "0xray"),
|
|
251
|
+
];
|
|
252
|
+
return candidates.find((p) => fs.existsSync(p));
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function registerGrokMcpServers(targetDir, log) {
|
|
256
|
+
try {
|
|
257
|
+
execSync("which grok", { stdio: "ignore" });
|
|
258
|
+
} catch {
|
|
259
|
+
log("grok-bridge", "grok CLI not on PATH — plugin .mcp.json still configured", "info");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const s of XRAY_MCP_SERVERS) {
|
|
264
|
+
try {
|
|
265
|
+
const envEntries = { ...s.env, XRAY_ROOT: targetDir };
|
|
266
|
+
const envFlags = Object.entries(envEntries)
|
|
267
|
+
.map(([k, v]) => `--env "${k}=${v}"`)
|
|
268
|
+
.join(" ");
|
|
269
|
+
execSync(
|
|
270
|
+
`grok mcp add ${s.name} --command npx --args "-y" "0xray" "mcp" "${s.mcpCmd}" ${envFlags}`,
|
|
271
|
+
{ stdio: "pipe" }
|
|
272
|
+
);
|
|
273
|
+
log("grok-bridge", `registered ${s.name} (npx)`, "info");
|
|
274
|
+
} catch {
|
|
275
|
+
// already registered or grok config conflict — non-blocking
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
for (const pluginDir of [
|
|
280
|
+
path.join(targetDir, ".grok", "plugins", "0xray"),
|
|
281
|
+
path.join(os.homedir(), ".grok", "plugins", "0xray"),
|
|
282
|
+
]) {
|
|
283
|
+
if (!fs.existsSync(pluginDir)) continue;
|
|
284
|
+
try {
|
|
285
|
+
execSync(`grok plugins trust "${pluginDir}"`, { stdio: "ignore" });
|
|
286
|
+
log("grok-bridge", "plugin trusted", "info", { path: pluginDir });
|
|
287
|
+
break;
|
|
288
|
+
} catch {
|
|
289
|
+
// best-effort
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function installGrokBridge(targetDir, packageRoot, log) {
|
|
295
|
+
const sourceDir = findGrokPluginSource(packageRoot);
|
|
296
|
+
if (!sourceDir) {
|
|
297
|
+
log("grok-bridge", "skipped", "warn", { reason: "grok plugin source missing" });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const home = os.homedir();
|
|
302
|
+
const targets = [
|
|
303
|
+
path.join(targetDir, ".grok", "plugins", "0xray"),
|
|
304
|
+
path.join(home, ".grok", "plugins", "0xray"),
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
for (const dest of targets) {
|
|
308
|
+
if (copyPluginDir(sourceDir, dest)) {
|
|
309
|
+
const rel = dest.startsWith(home) ? dest.replace(home, "~") : path.relative(targetDir, dest);
|
|
310
|
+
log("grok-bridge", "plugin copied", "info", { path: rel || dest });
|
|
311
|
+
writePluginMcpJson(dest, targetDir, log, "grok-bridge");
|
|
312
|
+
patchGrokHooks(dest, packageRoot, targetDir, log, "grok-bridge");
|
|
313
|
+
const copied = syncBuiltinSkills(path.join(dest, "skills"), packageRoot);
|
|
314
|
+
if (copied > 0) log("grok-bridge", `plugin skills synced (${copied})`, "info", { path: rel });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Grok Build / Cursor also reads ~/.grok/skills/ for agent_skills
|
|
319
|
+
const grokGlobalSkills = path.join(home, ".grok", "skills");
|
|
320
|
+
const globalCopied = syncBuiltinSkills(grokGlobalSkills, packageRoot);
|
|
321
|
+
if (globalCopied > 0) {
|
|
322
|
+
log("grok-bridge", `global skills synced (${globalCopied})`, "info", { path: "~/.grok/skills/" });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
registerGrokMcpServers(targetDir, log);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function installHermesBridge(targetDir, packageRoot, log) {
|
|
329
|
+
const sources = [
|
|
330
|
+
path.join(packageRoot, "dist", "integrations", "hermes-agent"),
|
|
331
|
+
path.join(packageRoot, "src", "integrations", "hermes-agent"),
|
|
332
|
+
];
|
|
333
|
+
const sourceDir = sources.find((p) => fs.existsSync(p));
|
|
334
|
+
if (!sourceDir) {
|
|
335
|
+
log("hermes-bridge", "skipped", "warn", { reason: "hermes plugin source missing" });
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const targetPluginDir = path.join(os.homedir(), ".hermes", "plugins", "xray-hermes");
|
|
340
|
+
fs.mkdirSync(targetPluginDir, { recursive: true });
|
|
341
|
+
for (const entry of fs.readdirSync(sourceDir)) {
|
|
342
|
+
const src = path.join(sourceDir, entry);
|
|
343
|
+
const dst = path.join(targetPluginDir, entry);
|
|
344
|
+
if (fs.statSync(src).isDirectory()) {
|
|
345
|
+
fs.cpSync(src, dst, { recursive: true, force: true });
|
|
346
|
+
} else {
|
|
347
|
+
fs.copyFileSync(src, dst);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
log("hermes-bridge", "plugin copied", "info", { path: "~/.hermes/plugins/xray-hermes" });
|
|
351
|
+
|
|
352
|
+
writePluginMcpJson(targetPluginDir, targetDir, log, "hermes-bridge");
|
|
353
|
+
|
|
354
|
+
const copied = syncBuiltinSkills(path.join(targetPluginDir, "skills"), packageRoot);
|
|
355
|
+
if (copied > 0) log("hermes-bridge", `skills synced (${copied})`, "info");
|
|
356
|
+
|
|
357
|
+
// Marker so bridge.mjs resolves the consumer project on hook invocation
|
|
358
|
+
const rootMarker = path.join(targetPluginDir, "xray-consumer-root.txt");
|
|
359
|
+
fs.writeFileSync(rootMarker, targetDir + "\n");
|
|
360
|
+
log("hermes-bridge", "consumer root marker written", "info");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function installOpenclawBridge(targetDir, packageRoot, log) {
|
|
364
|
+
const configPath = path.join(targetDir, ".xray", "config", "openclaw.json");
|
|
365
|
+
if (!fs.existsSync(configPath)) {
|
|
366
|
+
const configDir = path.dirname(configPath);
|
|
367
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
368
|
+
const sampleConfig = {
|
|
369
|
+
gatewayUrl: "ws://127.0.0.1:18789",
|
|
370
|
+
authToken: process.env.OPENCLAW_AUTH_TOKEN || "",
|
|
371
|
+
deviceId: process.env.OPENCLAW_DEVICE_ID || "your-device-id",
|
|
372
|
+
autoReconnect: true,
|
|
373
|
+
maxReconnectAttempts: 5,
|
|
374
|
+
reconnectDelay: 1000,
|
|
375
|
+
apiServer: {
|
|
376
|
+
enabled: true,
|
|
377
|
+
port: 18431,
|
|
378
|
+
host: "127.0.0.1",
|
|
379
|
+
...(process.env.OPENCLAW_API_KEY ? { apiKey: process.env.OPENCLAW_API_KEY } : {}),
|
|
380
|
+
},
|
|
381
|
+
hooks: {
|
|
382
|
+
enabled: true,
|
|
383
|
+
toolBefore: true,
|
|
384
|
+
toolAfter: true,
|
|
385
|
+
includeArgs: true,
|
|
386
|
+
includeResult: true,
|
|
387
|
+
},
|
|
388
|
+
enabled: true,
|
|
389
|
+
debug: false,
|
|
390
|
+
logLevel: "info",
|
|
391
|
+
};
|
|
392
|
+
fs.writeFileSync(configPath, JSON.stringify(sampleConfig, null, 2) + "\n");
|
|
393
|
+
log("openclaw-bridge", "config created", "info", { path: ".xray/config/openclaw.json" });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const copied = syncBuiltinSkills(path.join(os.homedir(), ".openclaw", "skills"), packageRoot);
|
|
397
|
+
if (copied > 0) log("openclaw-bridge", `skills synced (${copied})`, "info", { path: "~/.openclaw/skills/" });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function deployXrayConfig(targetDir, packageRoot, log) {
|
|
401
|
+
const xraySourceDir = path.join(packageRoot, ".xray");
|
|
402
|
+
const xrayTargetDir = path.join(targetDir, ".xray");
|
|
403
|
+
const configFiles = ["codex.json", "features.json", "config.json"];
|
|
404
|
+
if (!fs.existsSync(xraySourceDir)) return;
|
|
405
|
+
|
|
406
|
+
if (!fs.existsSync(xrayTargetDir)) fs.mkdirSync(xrayTargetDir, { recursive: true });
|
|
407
|
+
let copied = 0;
|
|
408
|
+
for (const file of configFiles) {
|
|
409
|
+
const src = path.join(xraySourceDir, file);
|
|
410
|
+
const dst = path.join(xrayTargetDir, file);
|
|
411
|
+
if (!fs.existsSync(src)) continue;
|
|
412
|
+
const shouldCopy = !fs.existsSync(dst) || fs.statSync(src).mtime > fs.statSync(dst).mtime;
|
|
413
|
+
if (shouldCopy) {
|
|
414
|
+
fs.copyFileSync(src, dst);
|
|
415
|
+
copied++;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
if (copied > 0) log("xray-config", `${copied} files deployed`, "info", { path: ".xray/" });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function installGitHooks(packageRoot, log) {
|
|
422
|
+
const installHooks = path.join(packageRoot, "scripts", "hooks", "install-hooks.cjs");
|
|
423
|
+
if (!fs.existsSync(installHooks)) return;
|
|
424
|
+
try {
|
|
425
|
+
execSync(`node "${installHooks}"`, { stdio: "pipe" });
|
|
426
|
+
log("hooks", "pre-commit hook installed", "info");
|
|
427
|
+
} catch {
|
|
428
|
+
// non-git or hook failure — not blocking
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Install all 4 platform bridges for a consumer project.
|
|
434
|
+
* @param {{ targetDir: string, packageRoot: string, log?: Function }} opts
|
|
435
|
+
*/
|
|
436
|
+
function installAllBridges(opts) {
|
|
437
|
+
const packageRoot = path.resolve(opts.packageRoot);
|
|
438
|
+
const targetDir = path.resolve(opts.targetDir);
|
|
439
|
+
const log =
|
|
440
|
+
opts.log ||
|
|
441
|
+
((_component, _action, _status, _details) => {
|
|
442
|
+
/* noop */
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
if (!isConsumerInstall(packageRoot, targetDir)) return;
|
|
446
|
+
|
|
447
|
+
log("install-bridges", "starting 4-platform install", "info");
|
|
448
|
+
|
|
449
|
+
deployXrayConfig(targetDir, packageRoot, log);
|
|
450
|
+
deployProjectMcpJson(targetDir, log);
|
|
451
|
+
mergeOpencodeJson(targetDir, packageRoot, log);
|
|
452
|
+
installOpencodeBridge(targetDir, packageRoot, log);
|
|
453
|
+
installGrokBridge(targetDir, packageRoot, log);
|
|
454
|
+
installHermesBridge(targetDir, packageRoot, log);
|
|
455
|
+
installOpenclawBridge(targetDir, packageRoot, log);
|
|
456
|
+
installGitHooks(packageRoot, log);
|
|
457
|
+
|
|
458
|
+
log("install-bridges", "4-platform install complete", "success");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
module.exports = {
|
|
462
|
+
installAllBridges,
|
|
463
|
+
resolveConsumerTargetDir,
|
|
464
|
+
syncBuiltinSkills,
|
|
465
|
+
isConsumerInstall,
|
|
466
|
+
};
|