relaybrain 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +290 -2
- package/backend/coordinator/client.js +364 -0
- package/backend/coordinator/lockTable.js +649 -0
- package/backend/coordinator/server.js +179 -0
- package/backend/lib/centralAuth.js +62 -0
- package/backend/lib/centralStore.js +112 -0
- package/backend/lib/centralSync.js +88 -0
- package/backend/lib/cloneRepo.js +108 -0
- package/backend/lib/collisionMetrics.js +231 -0
- package/backend/lib/conflicts.js +35 -0
- package/backend/lib/dashboard.js +53 -0
- package/backend/lib/depGraph.js +393 -0
- package/backend/lib/deps.js +46 -0
- package/backend/lib/dirtyFiles.js +33 -0
- package/backend/lib/editMatchers.js +55 -0
- package/backend/lib/falkor/analytics.js +228 -0
- package/backend/lib/falkor/bus.js +16 -0
- package/backend/lib/falkor/central.js +281 -0
- package/backend/lib/falkor/client.js +165 -0
- package/backend/lib/falkor/config.js +73 -0
- package/backend/lib/falkor/coord.js +299 -0
- package/backend/lib/falkor/cursor.js +58 -0
- package/backend/lib/falkor/fixtures.js +85 -0
- package/backend/lib/falkor/index.js +146 -0
- package/backend/lib/falkor/ingest.js +233 -0
- package/backend/lib/falkor/intents.js +32 -0
- package/backend/lib/falkor/keys.js +102 -0
- package/backend/lib/falkor/migrations.js +56 -0
- package/backend/lib/falkor/project.js +307 -0
- package/backend/lib/falkor/query.js +344 -0
- package/backend/lib/falkor/redact.js +30 -0
- package/backend/lib/falkor/routes.js +81 -0
- package/backend/lib/falkor/schema.js +108 -0
- package/backend/lib/falkor/write.js +281 -0
- package/backend/lib/githubLogin.js +108 -0
- package/backend/lib/githubOAuth.js +42 -0
- package/backend/lib/githubRepo.js +117 -0
- package/backend/lib/harvest.js +85 -0
- package/backend/lib/httpServer.js +27 -0
- package/backend/lib/installAgentRelay.js +235 -0
- package/backend/lib/installHooks.js +309 -0
- package/backend/lib/lamport.js +14 -0
- package/backend/lib/localBrowse.js +109 -0
- package/backend/lib/memorySync.js +63 -0
- package/backend/lib/notices.js +143 -0
- package/backend/lib/ot.js +74 -0
- package/backend/lib/patches.js +192 -0
- package/backend/lib/paths.js +85 -0
- package/backend/lib/peerClash.js +64 -0
- package/backend/lib/presence.js +35 -0
- package/backend/lib/relayOsBlock.js +84 -0
- package/backend/lib/repoSnapshot.js +125 -0
- package/backend/lib/room.js +498 -0
- package/backend/lib/roomAsk.js +148 -0
- package/backend/lib/roomAuth.js +215 -0
- package/backend/lib/roomControl.js +226 -0
- package/backend/lib/roomLocks.js +201 -0
- package/backend/lib/roomSignal.js +265 -0
- package/backend/lib/roomSync.js +194 -0
- package/backend/lib/seed.js +107 -0
- package/backend/lib/sseHub.js +40 -0
- package/backend/lib/store.js +79 -0
- package/backend/lib/supergraph.js +46 -0
- package/backend/lib/transcripts/discover.js +338 -0
- package/backend/lib/transcripts/parse.js +558 -0
- package/backend/lib/transcripts/sessionId.js +36 -0
- package/backend/lib/transcripts/sync.js +291 -0
- package/backend/lib/transcripts/util.js +299 -0
- package/backend/lib/treeSitterImports.js +219 -0
- package/backend/lib/watchWorkspace.js +42 -0
- package/backend/mcp/README.md +193 -0
- package/backend/mcp/server.js +42 -0
- package/backend/mcp/tools.js +484 -0
- package/backend/server.js +3839 -0
- package/cli/graph.js +278 -0
- package/cli/relay.js +419 -0
- package/docker-compose.falkor.yml +17 -0
- package/docs/MEMORY-GRAPH.md +269 -0
- package/hooks/relay-antigravity-post-tool.js +2 -0
- package/hooks/relay-antigravity-pre-read.js +2 -0
- package/hooks/relay-antigravity-pre-tool.js +2 -0
- package/hooks/relay-antigravity-stop.js +2 -0
- package/hooks/relay-claude-post-tool.js +2 -0
- package/hooks/relay-claude-pre-read.js +2 -0
- package/hooks/relay-claude-pre-tool.js +2 -0
- package/hooks/relay-claude-stop.js +2 -0
- package/hooks/relay-codex-post-tool.js +2 -0
- package/hooks/relay-codex-pre-read.js +2 -0
- package/hooks/relay-codex-pre-tool.js +2 -0
- package/hooks/relay-codex-stop.js +2 -0
- package/hooks/relay-copilot-post-tool.js +2 -0
- package/hooks/relay-copilot-pre-read.js +2 -0
- package/hooks/relay-copilot-pre-tool.js +2 -0
- package/hooks/relay-copilot-stop.js +2 -0
- package/hooks/relay-cursor-post-tool.js +2 -0
- package/hooks/relay-cursor-pre-read.js +2 -0
- package/hooks/relay-cursor-pre-tool.js +2 -0
- package/hooks/relay-cursor-stop.js +2 -0
- package/hooks/relay-hook-lib.js +621 -0
- package/mission-control/out/404.html +1 -0
- package/mission-control/out/__next.__PAGE__.txt +26 -0
- package/mission-control/out/__next._full.txt +21 -0
- package/mission-control/out/__next._tree.txt +4 -0
- package/mission-control/out/_next/static/chunks/0-3l04fnym95-.js +22 -0
- package/mission-control/out/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- package/mission-control/out/_next/static/chunks/0ehjiuuxbbhq9.js +1 -0
- package/mission-control/out/_next/static/chunks/1q0cmt0kqufow.js +2 -0
- package/mission-control/out/_next/static/chunks/1ya1u0izct4f6.js +1 -0
- package/mission-control/out/_next/static/chunks/25rvg4bnew99f.css +4 -0
- package/mission-control/out/_next/static/chunks/2n66mlnb34vjq.js +1 -0
- package/mission-control/out/_next/static/chunks/2xar70jzx8f1y.css +1 -0
- package/mission-control/out/_next/static/chunks/31cgqj_-l15yt.js +31 -0
- package/mission-control/out/_next/static/chunks/3_5x4qn1_b4al.js +1 -0
- package/mission-control/out/_next/static/chunks/3o1tiwncjss_k.js +1 -0
- package/mission-control/out/_next/static/chunks/3vq9c4hivvgn_.js +1 -0
- package/mission-control/out/_next/static/chunks/turbopack-3hsllzpnzz12p.js +1 -0
- package/mission-control/out/_next/static/dglM39XRu0cmneClUkGYd/_buildManifest.js +11 -0
- package/mission-control/out/_next/static/dglM39XRu0cmneClUkGYd/_clientMiddlewareManifest.js +1 -0
- package/mission-control/out/_next/static/dglM39XRu0cmneClUkGYd/_ssgManifest.js +1 -0
- package/mission-control/out/_next/static/media/icon.27otmh_fv3buh.svg +4 -0
- package/mission-control/out/_not-found/__next._full.txt +18 -0
- package/mission-control/out/_not-found/__next._not-found/__PAGE__.txt +24 -0
- package/mission-control/out/_not-found/__next._tree.txt +3 -0
- package/mission-control/out/_not-found.html +1 -0
- package/mission-control/out/_not-found.txt +18 -0
- package/mission-control/out/agents/antigravity.png +0 -0
- package/mission-control/out/agents/claude.png +0 -0
- package/mission-control/out/agents/codex.png +0 -0
- package/mission-control/out/agents/copilot.png +0 -0
- package/mission-control/out/agents/cursor.png +0 -0
- package/mission-control/out/icon.svg +4 -0
- package/mission-control/out/index.html +1 -0
- package/mission-control/out/index.txt +21 -0
- package/mission-control/out/logo.png +0 -0
- package/mission-control/out/logo_transparent.png +0 -0
- package/package.json +74 -4
- package/scripts/falkor-rebuild.js +57 -0
- package/scripts/falkor-seed-demo.js +157 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const { spawn } = require("child_process");
|
|
2
|
+
|
|
3
|
+
// In-memory only — a per-repo cache is enough to stop every dashboard poll
|
|
4
|
+
// (RelayContext polls on an interval) from re-hitting GitHub's API/gh CLI.
|
|
5
|
+
const CACHE_MS = 60_000;
|
|
6
|
+
const cache = new Map(); // "owner/repo" -> { at, data }
|
|
7
|
+
|
|
8
|
+
function runGh(args) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const child = spawn("gh", args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true, shell: false });
|
|
11
|
+
let out = "";
|
|
12
|
+
let err = "";
|
|
13
|
+
child.stdout.on("data", (c) => (out += c));
|
|
14
|
+
child.stderr.on("data", (c) => (err += c));
|
|
15
|
+
child.on("error", () => reject(new Error("gh_not_installed")));
|
|
16
|
+
child.on("exit", (code) => {
|
|
17
|
+
if (code === 0) resolve(out.trim());
|
|
18
|
+
else reject(new Error(err.trim() || `gh exited ${code}`));
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Matches both https://github.com/owner/repo(.git) and git@github.com:owner/repo(.git)
|
|
24
|
+
function parseGithubRepo(remoteUrl) {
|
|
25
|
+
if (!remoteUrl) return null;
|
|
26
|
+
const m = String(remoteUrl).match(/github\.com[/:]([^/]+)\/([^/.]+)(?:\.git)?\/?$/i);
|
|
27
|
+
if (!m) return null;
|
|
28
|
+
return { owner: m[1], repo: m[2] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function permissionOf(u) {
|
|
32
|
+
if (u.role_name) return u.role_name;
|
|
33
|
+
if (u.permissions?.admin) return "admin";
|
|
34
|
+
if (u.permissions?.push) return "write";
|
|
35
|
+
if (u.permissions?.pull) return "read";
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function fetchCollaboratorsRaw(owner, repo) {
|
|
40
|
+
try {
|
|
41
|
+
// Requires push access on the repo (via `gh auth login`) — this is the
|
|
42
|
+
// real "who can push here" collaborator list, avatars included.
|
|
43
|
+
const raw = await runGh(["api", `repos/${owner}/${repo}/collaborators`, "--paginate"]);
|
|
44
|
+
const list = JSON.parse(raw);
|
|
45
|
+
return list.map((u) => ({
|
|
46
|
+
id: `gh_${u.id}`,
|
|
47
|
+
login: u.login,
|
|
48
|
+
name: u.login,
|
|
49
|
+
avatarUrl: u.avatar_url,
|
|
50
|
+
permission: permissionOf(u),
|
|
51
|
+
source: "collaborators",
|
|
52
|
+
}));
|
|
53
|
+
} catch {
|
|
54
|
+
// Fallback for when `gh` isn't installed/authed, or the caller lacks push
|
|
55
|
+
// access: public contributor list needs no auth at all. Not the same set
|
|
56
|
+
// as real collaborators (it's activity-based, no pending/read-only
|
|
57
|
+
// invites) but better than an empty panel.
|
|
58
|
+
try {
|
|
59
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/contributors`, {
|
|
60
|
+
headers: { "User-Agent": "relay-it" },
|
|
61
|
+
});
|
|
62
|
+
if (!res.ok) throw new Error(`http_${res.status}`);
|
|
63
|
+
const list = await res.json();
|
|
64
|
+
return list.map((u) => ({
|
|
65
|
+
id: `gh_${u.id}`,
|
|
66
|
+
login: u.login,
|
|
67
|
+
name: u.login,
|
|
68
|
+
avatarUrl: u.avatar_url,
|
|
69
|
+
permission: null,
|
|
70
|
+
source: "contributors-fallback",
|
|
71
|
+
}));
|
|
72
|
+
} catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fetchCollaborators(remoteUrl) {
|
|
79
|
+
const parsed = parseGithubRepo(remoteUrl);
|
|
80
|
+
if (!parsed) return Promise.resolve([]);
|
|
81
|
+
const key = `${parsed.owner}/${parsed.repo}`;
|
|
82
|
+
const hit = cache.get(key);
|
|
83
|
+
if (hit && Date.now() - hit.at < CACHE_MS && hit.data.length) return Promise.resolve(hit.data);
|
|
84
|
+
return fetchCollaboratorsRaw(parsed.owner, parsed.repo).then((data) => {
|
|
85
|
+
const prev = cache.get(key);
|
|
86
|
+
const prevAuth = (prev?.data || []).filter((c) => c.source === "collaborators");
|
|
87
|
+
const nextAuth = (data || []).filter((c) => c.source === "collaborators");
|
|
88
|
+
// Prefer a real collaborator list. A later contributors-fallback (gh
|
|
89
|
+
// flaked, no push access this poll) must not shrink a roster we already had.
|
|
90
|
+
if (nextAuth.length) {
|
|
91
|
+
cache.set(key, { at: Date.now(), data });
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
if (prevAuth.length) {
|
|
95
|
+
cache.set(key, { at: Date.now(), data: prev.data });
|
|
96
|
+
return prev.data;
|
|
97
|
+
}
|
|
98
|
+
if (data.length) {
|
|
99
|
+
cache.set(key, { at: Date.now(), data });
|
|
100
|
+
return data;
|
|
101
|
+
}
|
|
102
|
+
if (prev?.data?.length) {
|
|
103
|
+
cache.set(key, { at: Date.now(), data: prev.data });
|
|
104
|
+
return prev.data;
|
|
105
|
+
}
|
|
106
|
+
cache.set(key, { at: Date.now(), data: [] });
|
|
107
|
+
return [];
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function peekCollaborators(remoteUrl) {
|
|
112
|
+
const parsed = parseGithubRepo(remoteUrl);
|
|
113
|
+
if (!parsed) return [];
|
|
114
|
+
return cache.get(`${parsed.owner}/${parsed.repo}`)?.data || [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { fetchCollaborators, peekCollaborators, parseGithubRepo };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
|
|
5
|
+
function readJsonl(file) {
|
|
6
|
+
if (!file || !fs.existsSync(file)) return [];
|
|
7
|
+
return fs
|
|
8
|
+
.readFileSync(file, "utf8")
|
|
9
|
+
.split("\n")
|
|
10
|
+
.filter(Boolean)
|
|
11
|
+
.map((line) => {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(line);
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeMessages(rows) {
|
|
22
|
+
const messages = [];
|
|
23
|
+
const edits = [];
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
const role = row.role || row.type || row.message?.role;
|
|
26
|
+
const text =
|
|
27
|
+
row.text ||
|
|
28
|
+
row.content ||
|
|
29
|
+
row.message?.content ||
|
|
30
|
+
(Array.isArray(row.message?.content) ? row.message.content.map((c) => c.text || "").join("\n") : "");
|
|
31
|
+
if (role === "user" || role === "assistant" || role === "human") {
|
|
32
|
+
messages.push({ role: role === "human" ? "user" : role, text: String(text || "").slice(0, 4000) });
|
|
33
|
+
}
|
|
34
|
+
const file =
|
|
35
|
+
row.tool_input?.file_path ||
|
|
36
|
+
row.tool_input?.target_file ||
|
|
37
|
+
row.message?.tool_use?.input?.file_path ||
|
|
38
|
+
null;
|
|
39
|
+
if (file && (role === "tool" || row.type === "tool_use" || row.tool_name)) {
|
|
40
|
+
edits.push({ file: String(file).replace(/\\/g, "/"), diff: "" });
|
|
41
|
+
messages.push({ role: "tool", text: `Edit ${file}`, file });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return { messages, edits };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findCursorTranscript(sessionId) {
|
|
48
|
+
if (!sessionId) return null;
|
|
49
|
+
const roots = [
|
|
50
|
+
path.join(os.homedir(), ".cursor", "projects"),
|
|
51
|
+
path.join(os.homedir(), ".cursor-tutor"),
|
|
52
|
+
];
|
|
53
|
+
for (const root of roots) {
|
|
54
|
+
if (!fs.existsSync(root)) continue;
|
|
55
|
+
const stack = [root];
|
|
56
|
+
let walked = 0;
|
|
57
|
+
while (stack.length && walked < 400) {
|
|
58
|
+
walked += 1;
|
|
59
|
+
const dir = stack.pop();
|
|
60
|
+
let entries = [];
|
|
61
|
+
try {
|
|
62
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
63
|
+
} catch {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const e of entries) {
|
|
67
|
+
const full = path.join(dir, e.name);
|
|
68
|
+
if (e.isDirectory() && e.name !== "node_modules") stack.push(full);
|
|
69
|
+
else if (e.name.includes(sessionId) && e.name.endsWith(".jsonl")) return full;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function harvest({ transcript_path, sessionId, conversationId }) {
|
|
77
|
+
const file = transcript_path || findCursorTranscript(sessionId || conversationId);
|
|
78
|
+
if (!file) return { messages: [], edits: [], source: null };
|
|
79
|
+
const rows = readJsonl(file);
|
|
80
|
+
const parsed = normalizeMessages(rows);
|
|
81
|
+
parsed.source = file;
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { harvest, readJsonl, normalizeMessages };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Next's rewrite proxy reuses sockets; Node's 5s keep-alive closes them first → ECONNRESET. */
|
|
2
|
+
function hardenHttpServer(server) {
|
|
3
|
+
server.keepAliveTimeout = 65_000;
|
|
4
|
+
server.headersTimeout = 66_000;
|
|
5
|
+
server.requestTimeout = 0;
|
|
6
|
+
server.timeout = 0;
|
|
7
|
+
server.on("error", (err) => {
|
|
8
|
+
console.error("[relay-api]", err.message);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
});
|
|
11
|
+
server.on("clientError", (err, socket) => {
|
|
12
|
+
if (err?.code === "ECONNRESET" || !socket || socket.destroyed) return;
|
|
13
|
+
try {
|
|
14
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
15
|
+
} catch {
|
|
16
|
+
/* already closed */
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return server;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function apiErrorHandler(err, _req, res, _next) {
|
|
23
|
+
console.error("[relay-api]", err);
|
|
24
|
+
if (!res.headersSent) res.status(500).json({ error: String(err.message || err) });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = { hardenHttpServer, apiErrorHandler };
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch relay-os instructions + Relay MCP into every supported agent surface.
|
|
3
|
+
* Cursor · Claude Code · Codex · Copilot CLI · Antigravity
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("fs");
|
|
6
|
+
const path = require("path");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const { relayOsBlock, agentBootstrap, RELAY_OS_BEGIN, RELAY_OS_END } = require("./relayOsBlock");
|
|
9
|
+
|
|
10
|
+
function repoRoot() {
|
|
11
|
+
return path.join(__dirname, "..", "..");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function mcpServerScript() {
|
|
15
|
+
return path.join(repoRoot(), "backend", "mcp", "server.js");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function relayMcpEnv(workspacePath, agentLabel) {
|
|
19
|
+
const env = {};
|
|
20
|
+
if (workspacePath) env.RELAY_WORKSPACE_PATH = path.resolve(workspacePath);
|
|
21
|
+
// The stdio server can't otherwise tell which agent is calling it.
|
|
22
|
+
if (agentLabel) env.RELAY_AGENT_LABEL = agentLabel;
|
|
23
|
+
return env;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function relayStdioMcp(workspacePath, agentLabel) {
|
|
27
|
+
const script = mcpServerScript();
|
|
28
|
+
return {
|
|
29
|
+
command: process.execPath,
|
|
30
|
+
args: [script],
|
|
31
|
+
env: relayMcpEnv(workspacePath, agentLabel),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readText(file) {
|
|
36
|
+
try {
|
|
37
|
+
return fs.readFileSync(file, "utf8");
|
|
38
|
+
} catch {
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeText(file, text) {
|
|
44
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
45
|
+
fs.writeFileSync(file, text);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function patchRelayOsBlock(filePath, block) {
|
|
49
|
+
const existing = readText(filePath);
|
|
50
|
+
const begin = existing.indexOf(RELAY_OS_BEGIN);
|
|
51
|
+
const end = existing.indexOf(RELAY_OS_END);
|
|
52
|
+
if (begin >= 0 && end > begin) {
|
|
53
|
+
writeText(filePath, `${existing.slice(0, begin)}${block}${existing.slice(end + RELAY_OS_END.length)}`);
|
|
54
|
+
return "updated";
|
|
55
|
+
}
|
|
56
|
+
const trimmed = existing.trimEnd();
|
|
57
|
+
const sep = trimmed ? "\n\n" : "";
|
|
58
|
+
writeText(filePath, `${trimmed}${sep}${block}\n`);
|
|
59
|
+
return trimmed ? "appended" : "created";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mergeJsonMcp(filePath, entry, { copilotType } = {}) {
|
|
63
|
+
let data = {};
|
|
64
|
+
try {
|
|
65
|
+
data = JSON.parse(readText(filePath) || "{}");
|
|
66
|
+
} catch {
|
|
67
|
+
data = {};
|
|
68
|
+
}
|
|
69
|
+
data.mcpServers = data.mcpServers && typeof data.mcpServers === "object" ? data.mcpServers : {};
|
|
70
|
+
const relay = copilotType
|
|
71
|
+
? { type: "local", ...entry, tools: ["*"] }
|
|
72
|
+
: { ...entry };
|
|
73
|
+
data.mcpServers.relay = relay;
|
|
74
|
+
writeText(filePath, `${JSON.stringify(data, null, 2)}\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function mergeClaudeMcp(settingsPath, entry) {
|
|
78
|
+
let data = {};
|
|
79
|
+
try {
|
|
80
|
+
data = JSON.parse(readText(settingsPath) || "{}");
|
|
81
|
+
} catch {
|
|
82
|
+
data = {};
|
|
83
|
+
}
|
|
84
|
+
data.mcpServers = data.mcpServers && typeof data.mcpServers === "object" ? data.mcpServers : {};
|
|
85
|
+
data.mcpServers.relay = entry;
|
|
86
|
+
writeText(settingsPath, `${JSON.stringify(data, null, 2)}\n`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mergeCodexMcp(configPath, workspacePath) {
|
|
90
|
+
const node = process.execPath.replace(/\\/g, "/");
|
|
91
|
+
const script = mcpServerScript().replace(/\\/g, "/");
|
|
92
|
+
const ws = path.resolve(workspacePath).replace(/\\/g, "/");
|
|
93
|
+
const envLines = [`RELAY_WORKSPACE_PATH = "${ws.replace(/"/g, '\\"')}"`, `RELAY_AGENT_LABEL = "Codex"`];
|
|
94
|
+
const block = `[mcp_servers.relay]
|
|
95
|
+
command = "${node.replace(/"/g, '\\"')}"
|
|
96
|
+
args = ["${script.replace(/"/g, '\\"')}"]
|
|
97
|
+
|
|
98
|
+
[mcp_servers.relay.env]
|
|
99
|
+
${envLines.join("\n")}
|
|
100
|
+
`;
|
|
101
|
+
let text = readText(configPath);
|
|
102
|
+
const marker = "[mcp_servers.relay]";
|
|
103
|
+
const idx = text.indexOf(marker);
|
|
104
|
+
if (idx >= 0) {
|
|
105
|
+
// Skip our own [mcp_servers.relay.env] table, or the old one survives and TOML sees it twice.
|
|
106
|
+
const next = text.slice(idx + marker.length).search(/\n\[(?!mcp_servers\.relay(\.env)?\])/);
|
|
107
|
+
text = next >= 0 ? `${text.slice(0, idx)}${block}${text.slice(idx + marker.length + next)}` : `${text.slice(0, idx)}${block}`;
|
|
108
|
+
} else {
|
|
109
|
+
text = `${text.trimEnd()}\n\n${block}`;
|
|
110
|
+
}
|
|
111
|
+
writeText(configPath, text.endsWith("\n") ? text : `${text}\n`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function cursorMdc(block) {
|
|
115
|
+
return `---
|
|
116
|
+
description: Relay coordination — room context, locks, /relay ask
|
|
117
|
+
alwaysApply: true
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
${block}
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function ensureRelayDir(workspacePath) {
|
|
125
|
+
fs.mkdirSync(path.join(workspacePath, ".relay"), { recursive: true });
|
|
126
|
+
writeText(path.join(workspacePath, ".relay", "AGENT_BOOTSTRAP.md"), agentBootstrap(workspacePath));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function installProjectRelay(workspacePath) {
|
|
130
|
+
if (!workspacePath) return { ok: false, error: "workspace_required" };
|
|
131
|
+
const resolved = path.resolve(workspacePath);
|
|
132
|
+
const block = relayOsBlock();
|
|
133
|
+
const mcpFor = (label) => relayStdioMcp(resolved, label);
|
|
134
|
+
const results = { instructions: {}, mcp: {} };
|
|
135
|
+
|
|
136
|
+
const instructionTargets = [
|
|
137
|
+
path.join(resolved, "AGENTS.md"),
|
|
138
|
+
path.join(resolved, "CLAUDE.md"),
|
|
139
|
+
path.join(resolved, ".github", "copilot-instructions.md"),
|
|
140
|
+
path.join(resolved, ".cursorrules"),
|
|
141
|
+
];
|
|
142
|
+
for (const file of instructionTargets) {
|
|
143
|
+
try {
|
|
144
|
+
results.instructions[path.relative(resolved, file)] = patchRelayOsBlock(file, block);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
results.instructions[path.relative(resolved, file)] = String(err.message || err);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
writeText(path.join(resolved, ".cursor", "rules", "relay.mdc"), cursorMdc(block));
|
|
152
|
+
results.instructions[".cursor/rules/relay.mdc"] = "created";
|
|
153
|
+
} catch (err) {
|
|
154
|
+
results.instructions[".cursor/rules/relay.mdc"] = String(err.message || err);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
ensureRelayDir(resolved);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
mergeJsonMcp(path.join(resolved, ".cursor", "mcp.json"), mcpFor("Cursor"));
|
|
161
|
+
results.mcp[".cursor/mcp.json"] = "ok";
|
|
162
|
+
} catch (err) {
|
|
163
|
+
results.mcp[".cursor/mcp.json"] = String(err.message || err);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
mergeClaudeMcp(path.join(resolved, ".claude", "settings.json"), mcpFor("Claude Code"));
|
|
168
|
+
results.mcp[".claude/settings.json"] = "ok";
|
|
169
|
+
} catch (err) {
|
|
170
|
+
results.mcp[".claude/settings.json"] = String(err.message || err);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
mergeCodexMcp(path.join(resolved, ".codex", "config.toml"), resolved);
|
|
175
|
+
results.mcp[".codex/config.toml"] = "ok";
|
|
176
|
+
} catch (err) {
|
|
177
|
+
results.mcp[".codex/config.toml"] = String(err.message || err);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
mergeJsonMcp(path.join(resolved, ".github", "mcp.json"), mcpFor("Copilot"), { copilotType: true });
|
|
182
|
+
results.mcp[".github/mcp.json"] = "ok";
|
|
183
|
+
} catch (err) {
|
|
184
|
+
results.mcp[".github/mcp.json"] = String(err.message || err);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
mergeJsonMcp(path.join(resolved, ".agents", "mcp_config.json"), mcpFor("Antigravity"));
|
|
189
|
+
results.mcp[".agents/mcp_config.json"] = "ok";
|
|
190
|
+
} catch (err) {
|
|
191
|
+
results.mcp[".agents/mcp_config.json"] = String(err.message || err);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return { ok: true, workspace: resolved, results };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function installGlobalRelay() {
|
|
198
|
+
const home = os.homedir();
|
|
199
|
+
const mcpFor = (label) => relayStdioMcp(null, label);
|
|
200
|
+
const results = {};
|
|
201
|
+
try {
|
|
202
|
+
mergeJsonMcp(path.join(home, ".cursor", "mcp.json"), mcpFor("Cursor"));
|
|
203
|
+
results["~/.cursor/mcp.json"] = "ok";
|
|
204
|
+
} catch (err) {
|
|
205
|
+
results["~/.cursor/mcp.json"] = String(err.message || err);
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
mergeClaudeMcp(path.join(home, ".claude", "settings.json"), mcpFor("Claude Code"));
|
|
209
|
+
results["~/.claude/settings.json"] = "ok";
|
|
210
|
+
} catch (err) {
|
|
211
|
+
results["~/.claude/settings.json"] = String(err.message || err);
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
mergeCodexMcp(path.join(home, ".codex", "config.toml"), home);
|
|
215
|
+
results["~/.codex/config.toml"] = "ok";
|
|
216
|
+
} catch (err) {
|
|
217
|
+
results["~/.codex/config.toml"] = String(err.message || err);
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
const copilotPath = path.join(home, ".copilot", "mcp-config.json");
|
|
221
|
+
mergeJsonMcp(copilotPath, mcpFor("Copilot"), { copilotType: true });
|
|
222
|
+
results["~/.copilot/mcp-config.json"] = "ok";
|
|
223
|
+
} catch (err) {
|
|
224
|
+
results["~/.copilot/mcp-config.json"] = String(err.message || err);
|
|
225
|
+
}
|
|
226
|
+
return results;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = {
|
|
230
|
+
installProjectRelay,
|
|
231
|
+
installGlobalRelay,
|
|
232
|
+
relayStdioMcp,
|
|
233
|
+
patchRelayOsBlock,
|
|
234
|
+
relayOsBlock,
|
|
235
|
+
};
|