relaybrain 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +434 -3
- 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/demo.js +221 -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 +125 -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 +150 -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 +638 -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 +437 -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 +3894 -0
- package/cli/graph.js +286 -0
- package/cli/relay.js +475 -0
- package/docker-compose.falkor.yml +17 -0
- package/docs/MEMORY-GRAPH.md +314 -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/BuhfCxd0WQE65yIzZJCOM/_buildManifest.js +11 -0
- package/mission-control/out/_next/static/BuhfCxd0WQE65yIzZJCOM/_clientMiddlewareManifest.js +1 -0
- package/mission-control/out/_next/static/BuhfCxd0WQE65yIzZJCOM/_ssgManifest.js +1 -0
- package/mission-control/out/_next/static/chunks/0-3l04fnym95-.js +22 -0
- package/mission-control/out/_next/static/chunks/064ntycm-fx5v.js +1 -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/1-dy52ls2ntb_.css +4 -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/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/31qgr-p-oulpb.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/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 +76 -6
- package/scripts/falkor-rebuild.js +38 -0
- package/scripts/falkor-seed-demo.js +29 -0
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const { EDIT, READ } = require("./editMatchers");
|
|
5
|
+
const { installProjectRelay, installGlobalRelay } = require("./installAgentRelay");
|
|
6
|
+
|
|
7
|
+
function repoRoot() {
|
|
8
|
+
return path.join(__dirname, "..", "..");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function nodeCmd(scriptRel) {
|
|
12
|
+
const abs = path.join(repoRoot(), scriptRel).replace(/\\/g, "/");
|
|
13
|
+
return `node "${abs}"`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeJson(file, value) {
|
|
17
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
18
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function installProjectHooks(workspacePath) {
|
|
22
|
+
if (!workspacePath) return;
|
|
23
|
+
const claude = nodeCmd("hooks/relay-claude-pre-tool.js");
|
|
24
|
+
const claudePost = nodeCmd("hooks/relay-claude-post-tool.js");
|
|
25
|
+
const claudeStop = nodeCmd("hooks/relay-claude-stop.js");
|
|
26
|
+
const cursor = nodeCmd("hooks/relay-cursor-pre-tool.js");
|
|
27
|
+
const cursorPost = nodeCmd("hooks/relay-cursor-post-tool.js");
|
|
28
|
+
const cursorStop = nodeCmd("hooks/relay-cursor-stop.js");
|
|
29
|
+
const codex = nodeCmd("hooks/relay-codex-pre-tool.js");
|
|
30
|
+
const codexPost = nodeCmd("hooks/relay-codex-post-tool.js");
|
|
31
|
+
const codexStop = nodeCmd("hooks/relay-codex-stop.js");
|
|
32
|
+
const copilot = nodeCmd("hooks/relay-copilot-pre-tool.js");
|
|
33
|
+
const copilotPost = nodeCmd("hooks/relay-copilot-post-tool.js");
|
|
34
|
+
const copilotStop = nodeCmd("hooks/relay-copilot-stop.js");
|
|
35
|
+
const anti = nodeCmd("hooks/relay-antigravity-pre-tool.js");
|
|
36
|
+
const antiPost = nodeCmd("hooks/relay-antigravity-post-tool.js");
|
|
37
|
+
const antiStop = nodeCmd("hooks/relay-antigravity-stop.js");
|
|
38
|
+
|
|
39
|
+
writeJson(path.join(workspacePath, ".claude", "settings.json"), {
|
|
40
|
+
hooks: {
|
|
41
|
+
PreToolUse: [
|
|
42
|
+
{
|
|
43
|
+
matcher: EDIT.claude,
|
|
44
|
+
hooks: [{ type: "command", command: claude }],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
matcher: READ.claude,
|
|
48
|
+
hooks: [{ type: "command", command: nodeCmd("hooks/relay-claude-pre-read.js") }],
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
PostToolUse: [
|
|
52
|
+
{
|
|
53
|
+
matcher: EDIT.claude,
|
|
54
|
+
hooks: [{ type: "command", command: claudePost }],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
Stop: [{ matcher: "*", hooks: [{ type: "command", command: claudeStop }] }],
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
writeJson(path.join(workspacePath, ".cursor", "hooks.json"), {
|
|
62
|
+
version: 1,
|
|
63
|
+
hooks: {
|
|
64
|
+
preToolUse: [
|
|
65
|
+
{
|
|
66
|
+
command: cursor,
|
|
67
|
+
matcher: EDIT.cursor,
|
|
68
|
+
timeout: 10,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
command: nodeCmd("hooks/relay-cursor-pre-read.js"),
|
|
72
|
+
matcher: READ.cursor,
|
|
73
|
+
timeout: 10,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
postToolUse: [
|
|
77
|
+
{
|
|
78
|
+
command: cursorPost,
|
|
79
|
+
matcher: EDIT.cursor,
|
|
80
|
+
timeout: 10,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
// Documented Cursor read hook. Payload: { file_path, content }. Matcher
|
|
84
|
+
// filters by tool type (Read, TabRead, …). Fail-open by default: reporting
|
|
85
|
+
// a read must never be able to stop the agent from reading.
|
|
86
|
+
beforeReadFile: [
|
|
87
|
+
{
|
|
88
|
+
command: nodeCmd("hooks/relay-cursor-pre-read.js"),
|
|
89
|
+
matcher: "Read",
|
|
90
|
+
timeout: 10,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
stop: [{ command: cursorStop, loop_limit: 1 }],
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// No read hook for Codex: it exposes no read or search tool, so file reads
|
|
98
|
+
// arrive as shell commands under the Bash matcher. Matching Bash to sniff
|
|
99
|
+
// reads out of a command line would report greps and cats as "reads" and miss
|
|
100
|
+
// everything else, so Codex contributes edits only.
|
|
101
|
+
writeJson(path.join(workspacePath, ".codex", "hooks.json"), {
|
|
102
|
+
hooks: {
|
|
103
|
+
PreToolUse: [
|
|
104
|
+
{
|
|
105
|
+
matcher: EDIT.codex,
|
|
106
|
+
hooks: [{ type: "command", command: codex, timeout: 10 }],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
PostToolUse: [
|
|
110
|
+
{
|
|
111
|
+
matcher: EDIT.codex,
|
|
112
|
+
hooks: [{ type: "command", command: codexPost, timeout: 10 }],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
Stop: [{ hooks: [{ type: "command", command: codexStop, timeout: 120 }] }],
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
writeJson(path.join(workspacePath, ".github", "hooks", "relay-os.json"), {
|
|
120
|
+
version: 1,
|
|
121
|
+
hooks: {
|
|
122
|
+
preToolUse: [
|
|
123
|
+
{
|
|
124
|
+
type: "command",
|
|
125
|
+
bash: copilot,
|
|
126
|
+
powershell: copilot,
|
|
127
|
+
matcher: EDIT.copilot,
|
|
128
|
+
cwd: ".",
|
|
129
|
+
timeoutSec: 10,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: "command",
|
|
133
|
+
bash: nodeCmd("hooks/relay-copilot-pre-read.js"),
|
|
134
|
+
powershell: nodeCmd("hooks/relay-copilot-pre-read.js"),
|
|
135
|
+
matcher: READ.copilot,
|
|
136
|
+
cwd: ".",
|
|
137
|
+
timeoutSec: 10,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
postToolUse: [
|
|
141
|
+
{
|
|
142
|
+
type: "command",
|
|
143
|
+
bash: copilotPost,
|
|
144
|
+
powershell: copilotPost,
|
|
145
|
+
matcher: EDIT.copilot,
|
|
146
|
+
cwd: ".",
|
|
147
|
+
timeoutSec: 10,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
agentStop: [
|
|
151
|
+
{
|
|
152
|
+
type: "command",
|
|
153
|
+
bash: copilotStop,
|
|
154
|
+
powershell: copilotStop,
|
|
155
|
+
cwd: ".",
|
|
156
|
+
timeoutSec: 120,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
writeJson(path.join(workspacePath, ".agents", "hooks.json"), antigravityHooks(anti, antiPost, antiStop));
|
|
163
|
+
try {
|
|
164
|
+
installProjectRelay(workspacePath);
|
|
165
|
+
} catch {
|
|
166
|
+
/* instructions + MCP are best-effort */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Antigravity's config is keyed by hook NAME at the top level, and each name
|
|
172
|
+
* maps to its events — `{"relay": {"PreToolUse": [...]}}`. We were writing
|
|
173
|
+
* `{"hooks": {...}}`, which only worked by accident, as a hook that happened to
|
|
174
|
+
* be named "hooks".
|
|
175
|
+
*
|
|
176
|
+
* The event shapes also differ from each other: PreToolUse and PostToolUse take
|
|
177
|
+
* a matcher group wrapping a `hooks` array, while Stop takes the handler
|
|
178
|
+
* directly.
|
|
179
|
+
*/
|
|
180
|
+
function antigravityHooks(pre, post, stop) {
|
|
181
|
+
return {
|
|
182
|
+
relay: {
|
|
183
|
+
PreToolUse: [
|
|
184
|
+
{
|
|
185
|
+
matcher: EDIT.antigravity,
|
|
186
|
+
hooks: [{ type: "command", command: pre, timeout: 10 }],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
matcher: READ.antigravity,
|
|
190
|
+
hooks: [{ type: "command", command: nodeCmd("hooks/relay-antigravity-pre-read.js"), timeout: 10 }],
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
PostToolUse: [
|
|
194
|
+
{
|
|
195
|
+
matcher: EDIT.antigravity,
|
|
196
|
+
hooks: [{ type: "command", command: post, timeout: 10 }],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
Stop: [{ type: "command", command: stop, timeout: 120 }],
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function mergeClaudeSettings(file, preCmd, postCmd, stopCmd) {
|
|
205
|
+
let existing = {};
|
|
206
|
+
try {
|
|
207
|
+
existing = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
208
|
+
} catch {
|
|
209
|
+
existing = {};
|
|
210
|
+
}
|
|
211
|
+
existing.hooks = existing.hooks || {};
|
|
212
|
+
existing.hooks.PreToolUse = [
|
|
213
|
+
{
|
|
214
|
+
matcher: EDIT.claude,
|
|
215
|
+
hooks: [{ type: "command", command: preCmd }],
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
matcher: READ.claude,
|
|
219
|
+
hooks: [{ type: "command", command: nodeCmd("hooks/relay-claude-pre-read.js") }],
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
existing.hooks.PostToolUse = [
|
|
223
|
+
{
|
|
224
|
+
matcher: EDIT.claude,
|
|
225
|
+
hooks: [{ type: "command", command: postCmd }],
|
|
226
|
+
},
|
|
227
|
+
];
|
|
228
|
+
existing.hooks.Stop = [{ matcher: "*", hooks: [{ type: "command", command: stopCmd }] }];
|
|
229
|
+
writeJson(file, existing);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function installGlobalHooks() {
|
|
233
|
+
const home = os.homedir();
|
|
234
|
+
mergeClaudeSettings(
|
|
235
|
+
path.join(home, ".claude", "settings.json"),
|
|
236
|
+
nodeCmd("hooks/relay-claude-pre-tool.js"),
|
|
237
|
+
nodeCmd("hooks/relay-claude-post-tool.js"),
|
|
238
|
+
nodeCmd("hooks/relay-claude-stop.js")
|
|
239
|
+
);
|
|
240
|
+
writeJson(path.join(home, ".cursor", "hooks.json"), {
|
|
241
|
+
version: 1,
|
|
242
|
+
hooks: {
|
|
243
|
+
preToolUse: [
|
|
244
|
+
{
|
|
245
|
+
command: nodeCmd("hooks/relay-cursor-pre-tool.js"),
|
|
246
|
+
matcher: EDIT.cursor,
|
|
247
|
+
timeout: 10,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
command: nodeCmd("hooks/relay-cursor-pre-read.js"),
|
|
251
|
+
matcher: READ.cursor,
|
|
252
|
+
timeout: 10,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
postToolUse: [
|
|
256
|
+
{
|
|
257
|
+
command: nodeCmd("hooks/relay-cursor-post-tool.js"),
|
|
258
|
+
matcher: EDIT.cursor,
|
|
259
|
+
timeout: 10,
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
beforeReadFile: [
|
|
263
|
+
{
|
|
264
|
+
command: nodeCmd("hooks/relay-cursor-pre-read.js"),
|
|
265
|
+
matcher: "Read",
|
|
266
|
+
timeout: 10,
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
stop: [{ command: nodeCmd("hooks/relay-cursor-stop.js"), loop_limit: 1 }],
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
writeJson(path.join(home, ".codex", "hooks.json"), {
|
|
273
|
+
hooks: {
|
|
274
|
+
PreToolUse: [
|
|
275
|
+
{
|
|
276
|
+
matcher: EDIT.codex,
|
|
277
|
+
hooks: [{ type: "command", command: nodeCmd("hooks/relay-codex-pre-tool.js"), timeout: 10 }],
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
PostToolUse: [
|
|
281
|
+
{
|
|
282
|
+
matcher: EDIT.codex,
|
|
283
|
+
hooks: [{ type: "command", command: nodeCmd("hooks/relay-codex-post-tool.js"), timeout: 10 }],
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
Stop: [
|
|
287
|
+
{
|
|
288
|
+
hooks: [{ type: "command", command: nodeCmd("hooks/relay-codex-stop.js"), timeout: 120 }],
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
const antigravity = antigravityHooks(
|
|
294
|
+
nodeCmd("hooks/relay-antigravity-pre-tool.js"),
|
|
295
|
+
nodeCmd("hooks/relay-antigravity-post-tool.js"),
|
|
296
|
+
nodeCmd("hooks/relay-antigravity-stop.js")
|
|
297
|
+
);
|
|
298
|
+
// Antigravity reads global hooks from ~/.gemini/config, either directly or
|
|
299
|
+
// from a plugin bundle under it.
|
|
300
|
+
writeJson(path.join(home, ".gemini", "config", "plugins", "relay", "hooks.json"), antigravity);
|
|
301
|
+
writeJson(path.join(home, ".gemini", "config", "hooks.json"), antigravity);
|
|
302
|
+
try {
|
|
303
|
+
installGlobalRelay();
|
|
304
|
+
} catch {
|
|
305
|
+
/* global MCP is best-effort */
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = { installProjectHooks, installGlobalHooks };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const clocks = new Map();
|
|
2
|
+
|
|
3
|
+
function tick(workspaceId, incoming = 0) {
|
|
4
|
+
const key = workspaceId || "global";
|
|
5
|
+
const next = Math.max((clocks.get(key) || 0) + 1, Number(incoming) + 1);
|
|
6
|
+
clocks.set(key, next);
|
|
7
|
+
return next;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function peek(workspaceId) {
|
|
11
|
+
return clocks.get(workspaceId || "global") || 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = { tick, peek };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Backs the "Add workspace" window: a local directory browser and the signed-in
|
|
2
|
+
// GitHub account's repositories. These used to be Next.js server routes; living
|
|
3
|
+
// here lets Mission Control ship as a static build that `relay serve` hosts.
|
|
4
|
+
// Both are host-only — a room member must never browse the host's disk.
|
|
5
|
+
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const { spawn } = require("child_process");
|
|
9
|
+
|
|
10
|
+
function listDrives() {
|
|
11
|
+
const drives = [];
|
|
12
|
+
for (let c = 65; c <= 90; c++) {
|
|
13
|
+
const root = `${String.fromCharCode(c)}:\\`;
|
|
14
|
+
try {
|
|
15
|
+
if (fs.existsSync(root)) drives.push({ name: root, path: root, isDir: true });
|
|
16
|
+
} catch {
|
|
17
|
+
/* drive not ready */
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return drives;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function listDir(dir) {
|
|
24
|
+
if (!dir) {
|
|
25
|
+
const entries = process.platform === "win32" ? listDrives() : [{ name: "/", path: "/", isDir: true }];
|
|
26
|
+
return { status: 200, body: { path: null, parent: null, entries } };
|
|
27
|
+
}
|
|
28
|
+
const resolved = path.resolve(dir);
|
|
29
|
+
try {
|
|
30
|
+
const dirents = await fs.promises.readdir(resolved, { withFileTypes: true });
|
|
31
|
+
const entries = dirents
|
|
32
|
+
.map((d) => ({ name: d.name, path: path.join(resolved, d.name), isDir: d.isDirectory() }))
|
|
33
|
+
.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1));
|
|
34
|
+
const parent = path.dirname(resolved);
|
|
35
|
+
return { status: 200, body: { path: resolved, parent: parent === resolved ? null : parent, entries } };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return { status: 400, body: { error: err.message } };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function runGh(args) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
const child = spawn("gh", args, { stdio: ["ignore", "pipe", "pipe"], windowsHide: true, shell: false });
|
|
44
|
+
let out = "";
|
|
45
|
+
let err = "";
|
|
46
|
+
const timer = setTimeout(() => {
|
|
47
|
+
child.kill();
|
|
48
|
+
reject(new Error("gh timed out"));
|
|
49
|
+
}, 25_000);
|
|
50
|
+
child.stdout.on("data", (c) => (out += c));
|
|
51
|
+
child.stderr.on("data", (c) => (err += c));
|
|
52
|
+
child.on("error", () => {
|
|
53
|
+
clearTimeout(timer);
|
|
54
|
+
reject(new Error("gh_not_installed"));
|
|
55
|
+
});
|
|
56
|
+
child.on("exit", (code) => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
if (code === 0) resolve(out.trim());
|
|
59
|
+
else reject(new Error(err.trim() || `gh exited ${code}`));
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function classify(message) {
|
|
65
|
+
if (message === "gh_not_installed") return "gh_not_installed";
|
|
66
|
+
if (/not logged|authentication|gh auth login|HTTP 401/i.test(message)) return "gh_not_authenticated";
|
|
67
|
+
return message;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const CACHE_MS = 60_000;
|
|
71
|
+
let cache = null;
|
|
72
|
+
|
|
73
|
+
async function listGithubRepos({ refresh = false } = {}) {
|
|
74
|
+
if (!refresh && cache && Date.now() - cache.at < CACHE_MS) return cache.body;
|
|
75
|
+
try {
|
|
76
|
+
const raw = await runGh([
|
|
77
|
+
"api",
|
|
78
|
+
"user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member",
|
|
79
|
+
"--paginate",
|
|
80
|
+
]);
|
|
81
|
+
// --paginate concatenates JSON arrays; normalise "][" joins into one array.
|
|
82
|
+
const list = JSON.parse(raw.replace(/\]\s*\[/g, ","));
|
|
83
|
+
const repos = list.map((r) => ({
|
|
84
|
+
id: `gh_${r.id}`,
|
|
85
|
+
name: r.name,
|
|
86
|
+
fullName: r.full_name,
|
|
87
|
+
owner: r.owner?.login ?? null,
|
|
88
|
+
ownerAvatarUrl: r.owner?.avatar_url ?? null,
|
|
89
|
+
description: r.description,
|
|
90
|
+
private: Boolean(r.private),
|
|
91
|
+
fork: Boolean(r.fork),
|
|
92
|
+
language: r.language,
|
|
93
|
+
defaultBranch: r.default_branch,
|
|
94
|
+
cloneUrl: r.clone_url,
|
|
95
|
+
sshUrl: r.ssh_url,
|
|
96
|
+
htmlUrl: r.html_url,
|
|
97
|
+
updatedAt: r.pushed_at || r.updated_at,
|
|
98
|
+
stars: r.stargazers_count || 0,
|
|
99
|
+
}));
|
|
100
|
+
const body = { repos, error: null };
|
|
101
|
+
cache = { at: Date.now(), body };
|
|
102
|
+
return body;
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Never 500 here — the window renders a helpful empty state off `error`.
|
|
105
|
+
return { repos: [], error: classify(err.message) };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { listDir, listGithubRepos };
|