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,219 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { Parser, Language, Query } = require("@vscode/tree-sitter-wasm");
|
|
4
|
+
|
|
5
|
+
const WASM_DIR = path.join(
|
|
6
|
+
path.dirname(require.resolve("@vscode/tree-sitter-wasm/package.json")),
|
|
7
|
+
"wasm"
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const EXT_LANG = {
|
|
11
|
+
".js": "javascript",
|
|
12
|
+
".jsx": "javascript",
|
|
13
|
+
".mjs": "javascript",
|
|
14
|
+
".cjs": "javascript",
|
|
15
|
+
".ts": "typescript",
|
|
16
|
+
".tsx": "tsx",
|
|
17
|
+
".py": "python",
|
|
18
|
+
".go": "go",
|
|
19
|
+
".rs": "rust",
|
|
20
|
+
".java": "java",
|
|
21
|
+
".cs": "c-sharp",
|
|
22
|
+
".cpp": "cpp",
|
|
23
|
+
".cc": "cpp",
|
|
24
|
+
".cxx": "cpp",
|
|
25
|
+
".hpp": "cpp",
|
|
26
|
+
".hh": "cpp",
|
|
27
|
+
".h": "cpp",
|
|
28
|
+
".php": "php",
|
|
29
|
+
".rb": "ruby",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const QUERIES = {
|
|
33
|
+
javascript: `
|
|
34
|
+
(import_statement source: (string) @path)
|
|
35
|
+
(export_statement source: (string) @path)
|
|
36
|
+
(call_expression
|
|
37
|
+
function: (identifier) @fn
|
|
38
|
+
arguments: (arguments . (string) @path)
|
|
39
|
+
(#eq? @fn "require"))
|
|
40
|
+
(call_expression
|
|
41
|
+
function: (import)
|
|
42
|
+
arguments: (arguments . (string) @path))
|
|
43
|
+
(call_expression
|
|
44
|
+
function: (import)
|
|
45
|
+
arguments: (arguments . (_) @dynamic))
|
|
46
|
+
`,
|
|
47
|
+
typescript: `
|
|
48
|
+
(import_statement source: (string) @path)
|
|
49
|
+
(export_statement source: (string) @path)
|
|
50
|
+
(call_expression
|
|
51
|
+
function: (identifier) @fn
|
|
52
|
+
arguments: (arguments . (string) @path)
|
|
53
|
+
(#eq? @fn "require"))
|
|
54
|
+
(call_expression
|
|
55
|
+
function: (import)
|
|
56
|
+
arguments: (arguments . (string) @path))
|
|
57
|
+
(call_expression
|
|
58
|
+
function: (import)
|
|
59
|
+
arguments: (arguments . (_) @dynamic))
|
|
60
|
+
`,
|
|
61
|
+
tsx: `
|
|
62
|
+
(import_statement source: (string) @path)
|
|
63
|
+
(export_statement source: (string) @path)
|
|
64
|
+
(call_expression
|
|
65
|
+
function: (identifier) @fn
|
|
66
|
+
arguments: (arguments . (string) @path)
|
|
67
|
+
(#eq? @fn "require"))
|
|
68
|
+
(call_expression
|
|
69
|
+
function: (import)
|
|
70
|
+
arguments: (arguments . (string) @path))
|
|
71
|
+
(call_expression
|
|
72
|
+
function: (import)
|
|
73
|
+
arguments: (arguments . (_) @dynamic))
|
|
74
|
+
`,
|
|
75
|
+
python: `
|
|
76
|
+
(import_from_statement module_name: (dotted_name) @name)
|
|
77
|
+
(import_from_statement module_name: (relative_import) @name)
|
|
78
|
+
(import_statement name: (dotted_name) @name)
|
|
79
|
+
(import_statement name: (aliased_import name: (dotted_name) @name))
|
|
80
|
+
(call
|
|
81
|
+
function: (attribute attribute: (identifier) @fn)
|
|
82
|
+
(#match? @fn "^(import_module|__import__)$")
|
|
83
|
+
arguments: (argument_list . (_) @dynamic))
|
|
84
|
+
`,
|
|
85
|
+
go: `
|
|
86
|
+
(import_spec path: (interpreted_string_literal) @path)
|
|
87
|
+
(import_spec path: (raw_string_literal) @path)
|
|
88
|
+
`,
|
|
89
|
+
rust: `
|
|
90
|
+
(mod_item name: (identifier) @mod)
|
|
91
|
+
(use_declaration argument: (scoped_identifier) @use)
|
|
92
|
+
(use_declaration argument: (identifier) @use)
|
|
93
|
+
(use_declaration argument: (use_wildcard (scoped_identifier) @use))
|
|
94
|
+
(use_declaration argument: (use_as_clause path: (_) @use))
|
|
95
|
+
(macro_invocation
|
|
96
|
+
macro: (identifier) @macro
|
|
97
|
+
(#eq? @macro "include")
|
|
98
|
+
(token_tree (string_literal) @path))
|
|
99
|
+
`,
|
|
100
|
+
java: `
|
|
101
|
+
(import_declaration (scoped_identifier) @name)
|
|
102
|
+
(import_declaration (identifier) @name)
|
|
103
|
+
`,
|
|
104
|
+
"c-sharp": `
|
|
105
|
+
(using_directive (qualified_name) @name)
|
|
106
|
+
(using_directive (identifier) @name)
|
|
107
|
+
`,
|
|
108
|
+
cpp: `
|
|
109
|
+
(preproc_include path: (string_literal) @path)
|
|
110
|
+
(preproc_include path: (system_lib_string) @system)
|
|
111
|
+
`,
|
|
112
|
+
php: `
|
|
113
|
+
(include_expression (string) @path)
|
|
114
|
+
(require_expression (string) @path)
|
|
115
|
+
(include_once_expression (string) @path)
|
|
116
|
+
(require_once_expression (string) @path)
|
|
117
|
+
`,
|
|
118
|
+
ruby: `
|
|
119
|
+
(call
|
|
120
|
+
method: (identifier) @fn
|
|
121
|
+
(#match? @fn "^(require|require_relative|load)$")
|
|
122
|
+
arguments: (argument_list (string) @path))
|
|
123
|
+
`,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
let ready = null;
|
|
127
|
+
const languages = new Map();
|
|
128
|
+
const queries = new Map();
|
|
129
|
+
|
|
130
|
+
function wasmFile(lang) {
|
|
131
|
+
return path.join(WASM_DIR, `tree-sitter-${lang}.wasm`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function initTreeSitter() {
|
|
135
|
+
if (ready) return ready;
|
|
136
|
+
ready = (async () => {
|
|
137
|
+
await Parser.init({
|
|
138
|
+
locateFile: (file) => path.join(WASM_DIR, path.basename(file)),
|
|
139
|
+
});
|
|
140
|
+
const langs = [...new Set(Object.values(EXT_LANG))];
|
|
141
|
+
for (const lang of langs) {
|
|
142
|
+
const file = wasmFile(lang);
|
|
143
|
+
if (!fs.existsSync(file)) continue;
|
|
144
|
+
try {
|
|
145
|
+
const language = await Language.load(file);
|
|
146
|
+
languages.set(lang, language);
|
|
147
|
+
const src = QUERIES[lang];
|
|
148
|
+
if (src) {
|
|
149
|
+
try {
|
|
150
|
+
queries.set(lang, new Query(language, src));
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.warn(`[relay] tree-sitter query failed for ${lang}:`, err.message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.warn(`[relay] tree-sitter language ${lang} failed:`, err.message);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
})();
|
|
161
|
+
return ready;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function unquote(text) {
|
|
165
|
+
const t = String(text || "").trim();
|
|
166
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'")) || (t.startsWith("`") && t.endsWith("`"))) {
|
|
167
|
+
return t.slice(1, -1);
|
|
168
|
+
}
|
|
169
|
+
if (t.startsWith("<") && t.endsWith(">")) return t.slice(1, -1);
|
|
170
|
+
return t;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function extractWithQuery(lang, source) {
|
|
174
|
+
const language = languages.get(lang);
|
|
175
|
+
const query = queries.get(lang);
|
|
176
|
+
if (!language) return { staticSpecs: [], dynamic: [], engine: "none" };
|
|
177
|
+
const parser = new Parser();
|
|
178
|
+
parser.setLanguage(language);
|
|
179
|
+
const tree = parser.parse(source);
|
|
180
|
+
if (!tree) return { staticSpecs: [], dynamic: [], engine: "tree-sitter" };
|
|
181
|
+
const staticSpecs = [];
|
|
182
|
+
const dynamic = [];
|
|
183
|
+
if (query) {
|
|
184
|
+
for (const cap of query.captures(tree.rootNode)) {
|
|
185
|
+
const name = cap.name;
|
|
186
|
+
const text = unquote(cap.node.text);
|
|
187
|
+
if (!text) continue;
|
|
188
|
+
if (name === "dynamic") {
|
|
189
|
+
if (/^['"]/.test(cap.node.text) || text.startsWith(".")) staticSpecs.push(text);
|
|
190
|
+
else dynamic.push(text);
|
|
191
|
+
} else if (name === "system") dynamic.push(text);
|
|
192
|
+
else if (name === "fn" || name === "macro") continue;
|
|
193
|
+
else staticSpecs.push(text);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
tree.delete();
|
|
197
|
+
parser.delete();
|
|
198
|
+
return {
|
|
199
|
+
staticSpecs: [...new Set(staticSpecs)],
|
|
200
|
+
dynamic: [...new Set(dynamic)],
|
|
201
|
+
engine: "tree-sitter",
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function languageForExt(ext) {
|
|
206
|
+
return EXT_LANG[ext] || null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function supportedLanguages() {
|
|
210
|
+
return [...languages.keys()];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
initTreeSitter,
|
|
215
|
+
extractWithQuery,
|
|
216
|
+
languageForExt,
|
|
217
|
+
supportedLanguages,
|
|
218
|
+
EXT_LANG,
|
|
219
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { EventEmitter } = require("events");
|
|
4
|
+
|
|
5
|
+
const SKIP = new Set(["node_modules", ".git", ".next", ".relay", "out"]);
|
|
6
|
+
|
|
7
|
+
class WorkspaceWatcher extends EventEmitter {
|
|
8
|
+
constructor() {
|
|
9
|
+
super();
|
|
10
|
+
this.watchers = new Map();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
watch(workspacePath, workspaceId) {
|
|
14
|
+
if (!workspacePath || this.watchers.has(workspacePath) || !fs.existsSync(workspacePath)) return;
|
|
15
|
+
try {
|
|
16
|
+
const watcher = fs.watch(workspacePath, { recursive: true }, (event, filename) => {
|
|
17
|
+
if (!filename) return;
|
|
18
|
+
const rel = String(filename).replace(/\\/g, "/");
|
|
19
|
+
if ([...SKIP].some((s) => rel.split("/").includes(s))) return;
|
|
20
|
+
this.emit("change", { workspacePath, workspaceId, file: rel, event });
|
|
21
|
+
});
|
|
22
|
+
watcher.on("error", () => this.unwatch(workspacePath));
|
|
23
|
+
this.watchers.set(workspacePath, watcher);
|
|
24
|
+
} catch {
|
|
25
|
+
/* fs.watch recursive unsupported — skip, Stop hook still fires */
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
unwatch(workspacePath) {
|
|
30
|
+
const w = this.watchers.get(workspacePath);
|
|
31
|
+
if (w) {
|
|
32
|
+
try {
|
|
33
|
+
w.close();
|
|
34
|
+
} catch {
|
|
35
|
+
/* ignore */
|
|
36
|
+
}
|
|
37
|
+
this.watchers.delete(workspacePath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { WorkspaceWatcher };
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Querying the room
|
|
2
|
+
|
|
3
|
+
Relay collects every member's agent transcripts, edits, locks and presence into
|
|
4
|
+
one place. This is how an agent — or a shell script, or you — asks that store a
|
|
5
|
+
question.
|
|
6
|
+
|
|
7
|
+
Relay does not answer questions. It scopes and returns facts; whoever asked does
|
|
8
|
+
the interpreting. That is deliberate: the caller is already a language model, so
|
|
9
|
+
a second one in the middle would only add cost, latency and a way to be wrong.
|
|
10
|
+
|
|
11
|
+
## The one thing to know
|
|
12
|
+
|
|
13
|
+
**Reads are room-wide. Writes are local.**
|
|
14
|
+
|
|
15
|
+
Anything that reads — chat history, edits, locks, conflicts — answers for every
|
|
16
|
+
member of the room. Anything that writes — claiming a lock, reporting a change —
|
|
17
|
+
only ever runs against the relay on your own machine, over the transport that
|
|
18
|
+
still works when the tunnel does not.
|
|
19
|
+
|
|
20
|
+
## Two transports, one tool surface
|
|
21
|
+
|
|
22
|
+
Both are served by [`tools.js`](./tools.js). They expose the same seventeen tools.
|
|
23
|
+
|
|
24
|
+
### stdio — what your agents already use
|
|
25
|
+
|
|
26
|
+
`backend/mcp/server.js`, started by the agent itself. Nothing to configure; if
|
|
27
|
+
relay is installed, this already works, and after this change its reads span the
|
|
28
|
+
room instead of just this machine.
|
|
29
|
+
|
|
30
|
+
### HTTP — through the room tunnel
|
|
31
|
+
|
|
32
|
+
`POST /mcp` on the host. For an agent on another machine, or one on a box with
|
|
33
|
+
no relay installed at all. Print the config with:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
relay mcp-url
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"relay-room": {
|
|
43
|
+
"type": "http",
|
|
44
|
+
"url": "https://<tunnel>.ngrok-free.app/mcp",
|
|
45
|
+
"headers": {
|
|
46
|
+
"ngrok-skip-browser-warning": "relay",
|
|
47
|
+
"x-relay-room-token": "<your member token>"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Stateless — no session id is issued, so there is nothing to expire. `GET /mcp`
|
|
55
|
+
and `DELETE /mcp` answer `405`; a client that asks for the optional event stream
|
|
56
|
+
just stops asking.
|
|
57
|
+
|
|
58
|
+
**Anyone arriving from off-machine gets the eleven read tools only.** Claiming a
|
|
59
|
+
lock as the host would file it under the host's workspace, the host's user, and
|
|
60
|
+
an agent id the host cannot verify — and would put a second writer on a lock
|
|
61
|
+
table that has exactly one on purpose. `tools/list` hides the other six, so a
|
|
62
|
+
remote agent never sees a tool it cannot call.
|
|
63
|
+
|
|
64
|
+
## What you can ask
|
|
65
|
+
|
|
66
|
+
| Tool | Answers |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `relay_session_brief` | memory-graph brief before editing files: who's on them now and why, blocks, decisions, prompts behind changes |
|
|
69
|
+
| `relay_graph_query` | named Cypher queries over the FalkorDB memory graph (`file_story`, `who_to_ask`, `contention_trace`, `search_memory`, `hotspots`, …) |
|
|
70
|
+
| `relay_get_chat_history` | what everyone told their agents to do, and what the agents said back |
|
|
71
|
+
| `relay_room_brief` | teammate activity brief — **`/relay ask`** in Cursor, Claude, Codex, Copilot, Antigravity |
|
|
72
|
+
| `relay_get_recent_changes` | which files changed, by whom, with diffs |
|
|
73
|
+
| `relay_status` | who holds which lock right now |
|
|
74
|
+
| `relay_get_conflicts` | who edited the same file as someone else in the last 5 minutes |
|
|
75
|
+
| `relay_get_collision_stats` | lifetime counters for collisions Relay prevented |
|
|
76
|
+
| `relay_get_project_context` | Central's view: changes, decisions, tasks, agents |
|
|
77
|
+
| `relay_get_decisions` / `relay_get_active_tasks` | the decision and task logs |
|
|
78
|
+
|
|
79
|
+
Writers (local only): `relay_claim_file`, `relay_release_file`,
|
|
80
|
+
`relay_report_change`, `relay_report_decision`, `relay_update_task`,
|
|
81
|
+
`relay_sync`.
|
|
82
|
+
|
|
83
|
+
### Scoping a question
|
|
84
|
+
|
|
85
|
+
`relay_get_chat_history` takes `agent`, `ownerLogin` and `limit`. Those three
|
|
86
|
+
are the whole query language — "who, which agent, how much".
|
|
87
|
+
|
|
88
|
+
```jsonc
|
|
89
|
+
// What is ana's Antigravity working on?
|
|
90
|
+
{ "ownerLogin": "ana", "agent": "Antigravity", "limit": 20 }
|
|
91
|
+
|
|
92
|
+
// Everything Claude Code has done in this room, whoever is driving it
|
|
93
|
+
{ "agent": "Claude Code" }
|
|
94
|
+
|
|
95
|
+
// The last 5 events from anyone
|
|
96
|
+
{ "limit": 5 }
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`limit` defaults to 50 and is capped at 200 — memory.json is much larger than
|
|
100
|
+
any context window, and a remote read has to cross a tunnel to get here.
|
|
101
|
+
|
|
102
|
+
### What comes back
|
|
103
|
+
|
|
104
|
+
```jsonc
|
|
105
|
+
{
|
|
106
|
+
"history": [ /* oldest first, so the last entry is the newest */
|
|
107
|
+
{
|
|
108
|
+
"ts": 1786996380114,
|
|
109
|
+
"tsIso": "2026-08-17T19:53:00.114Z",
|
|
110
|
+
"agent": "Claude Code",
|
|
111
|
+
"ownerLogin": "prakrititz",
|
|
112
|
+
"sessionId": "03e3a558-…",
|
|
113
|
+
"role": "tool",
|
|
114
|
+
"kind": "code_edit",
|
|
115
|
+
"text": "Edit lockgraph.module.css",
|
|
116
|
+
"file": "lockgraph.module.css",
|
|
117
|
+
"diff": "--- a/lockgraph.module.css\n+++ b/…"
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
"chats": [ { "id": "chat_claude-code_03e3…", "agent": "Claude Code",
|
|
121
|
+
"ownerLogin": "prakrititz", "mine": true,
|
|
122
|
+
"updatedAt": 1786996380114, "messages": [ /* … */ ] } ],
|
|
123
|
+
"timeline": [ /* … */ ],
|
|
124
|
+
"room": { "source": "host", "peers": ["ana"], "peer_snapshot_age_ms": 0 }
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`relay_get_recent_changes` returns `edits`, each with `file`, `agent`,
|
|
129
|
+
`ownerLogin`, `ts` and `diff`. `relay_get_conflicts` returns `conflicts`. Both
|
|
130
|
+
carry the same `room` block.
|
|
131
|
+
|
|
132
|
+
## Reading the `room` block
|
|
133
|
+
|
|
134
|
+
Every read says where its answer came from and how old it is, because the
|
|
135
|
+
freshness is genuinely uneven: locks move room-wide in ~150ms, but transcripts
|
|
136
|
+
only travel on the 8s dashboard poll, and only when they changed. "What file are
|
|
137
|
+
they in" is live; "what did they ask it to do" can be a few seconds behind. That
|
|
138
|
+
is a judgment call about whether it's fresh enough for what you're about to do,
|
|
139
|
+
so the caller gets to make it.
|
|
140
|
+
|
|
141
|
+
| `source` | Means |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `host` | asked the host just now; `peer_snapshot_age_ms` is under a second |
|
|
144
|
+
| `local_snapshot` | the host did not answer — this is the last sync, `peer_snapshot_age_ms` says how old |
|
|
145
|
+
| `solo` | not in a room; there are no peers to see |
|
|
146
|
+
|
|
147
|
+
**A guest whose host is offline still gets an answer.** roomSync parks every
|
|
148
|
+
peer's snapshot in your own `memory.json`, so the fallback is stale rather than
|
|
149
|
+
empty. The tunnel being down costs one short timeout and never an error.
|
|
150
|
+
|
|
151
|
+
Four reads in a row make one trip through the tunnel — the host response is
|
|
152
|
+
cached for a second, which is short enough that nothing is meaningfully staler
|
|
153
|
+
than a single request would have been.
|
|
154
|
+
|
|
155
|
+
## Worked example
|
|
156
|
+
|
|
157
|
+
An agent about to edit a locked file, asking who is in its way:
|
|
158
|
+
|
|
159
|
+
```jsonc
|
|
160
|
+
// relay_status — keyed "<workspaceId>::<filePath>"
|
|
161
|
+
{ "locks": {
|
|
162
|
+
"C:\\repos\\relay.it::src/auth.ts": {
|
|
163
|
+
"filePath": "src/auth.ts", "holder": "ana", "agentId": "antigravity:ana",
|
|
164
|
+
"mode": "write", "claimedAt": 1786996352000, "ttlMs": 40000,
|
|
165
|
+
"expiresAt": 1786996392000, "readers": [], "escalated": false
|
|
166
|
+
} },
|
|
167
|
+
"uptime": 812.4 }
|
|
168
|
+
|
|
169
|
+
// relay_get_chat_history { "ownerLogin": "ana", "agent": "Antigravity", "limit": 5 }
|
|
170
|
+
// → "make the session cookie httpOnly"
|
|
171
|
+
|
|
172
|
+
// relay_get_recent_changes → ana has touched auth.ts, session.ts, auth.test.ts
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Three calls, no model in the middle, and the agent can decide for itself: work
|
|
176
|
+
on the other module, or wait deliberately. The lock stopped being a wall and
|
|
177
|
+
started being an explanation.
|
|
178
|
+
|
|
179
|
+
## Troubleshooting
|
|
180
|
+
|
|
181
|
+
**`"source": "solo"` but you are in a room.** `loadRoom()` found no room for
|
|
182
|
+
this workspace. Check `.relay/room.json` in the repo, or `~/.relay/room.json`.
|
|
183
|
+
|
|
184
|
+
**`401 not_a_member` on the HTTP endpoint.** The host made the room
|
|
185
|
+
invite-only and your member token is missing or revoked. Re-join with the invite
|
|
186
|
+
link, then `relay mcp-url` again.
|
|
187
|
+
|
|
188
|
+
**A remote call returns "not available to remote callers".** Working as
|
|
189
|
+
intended — that tool writes. Call it against your own relay over stdio.
|
|
190
|
+
|
|
191
|
+
**Peers visible in Mission Control but not from MCP.** Both read the same merge
|
|
192
|
+
now, so this should not happen; if it does, `relay doctor` on the machine that
|
|
193
|
+
sees nothing is the place to start.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* MCP stdio — newline JSON-RPC. Chat history + locks. No markdown compile.
|
|
4
|
+
*
|
|
5
|
+
* The tools themselves live in ./tools.js so the room host can serve the same
|
|
6
|
+
* surface over HTTP; this file is only the stdio framing.
|
|
7
|
+
*/
|
|
8
|
+
const { handleRpc, defaultCtx } = require("./tools");
|
|
9
|
+
|
|
10
|
+
function sendResult(id, result) {
|
|
11
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sendErr(id, message) {
|
|
15
|
+
process.stdout.write(JSON.stringify({ jsonrpc: "2.0", id, error: { code: -32000, message } }) + "\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let buf = "";
|
|
19
|
+
process.stdin.setEncoding("utf8");
|
|
20
|
+
process.stdin.on("data", async (chunk) => {
|
|
21
|
+
buf += chunk;
|
|
22
|
+
const lines = buf.split("\n");
|
|
23
|
+
buf = lines.pop() || "";
|
|
24
|
+
for (const line of lines) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (!trimmed) continue;
|
|
27
|
+
let msg;
|
|
28
|
+
try {
|
|
29
|
+
msg = JSON.parse(trimmed);
|
|
30
|
+
} catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
// Rebuilt per message so a project registered after this process started
|
|
35
|
+
// still resolves, exactly as the inline lookup used to.
|
|
36
|
+
const reply = await handleRpc(msg, defaultCtx());
|
|
37
|
+
if (reply) sendResult(reply.id, reply.result);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
sendErr(msg?.id, err.message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
});
|