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,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 };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const { loadMemory, saveMemory, id } = require("./store");
|
|
3
|
+
|
|
4
|
+
function appendStopTurn(projectId, { agent, ownerLogin, sessionId, messages, edits }) {
|
|
5
|
+
const memory = loadMemory(projectId);
|
|
6
|
+
const ts = Date.now();
|
|
7
|
+
const mineLogin = ownerLogin;
|
|
8
|
+
const chatId = `chat_${sessionId || id("sess")}`;
|
|
9
|
+
let thread = memory.chats.find((c) => c.id === chatId);
|
|
10
|
+
if (!thread) {
|
|
11
|
+
thread = { id: chatId, agent, ownerLogin, mine: true, messages: [] };
|
|
12
|
+
memory.chats.unshift(thread);
|
|
13
|
+
}
|
|
14
|
+
for (const m of messages || []) thread.messages.push(m);
|
|
15
|
+
|
|
16
|
+
const events = (messages || []).map((m) => ({
|
|
17
|
+
type: m.role === "user" ? "user" : m.role === "tool" ? "edit" : "assistant",
|
|
18
|
+
text: m.text,
|
|
19
|
+
file: m.file,
|
|
20
|
+
}));
|
|
21
|
+
memory.timeline.unshift({
|
|
22
|
+
id: id("seg"),
|
|
23
|
+
agent,
|
|
24
|
+
ownerLogin,
|
|
25
|
+
mine: true,
|
|
26
|
+
ts,
|
|
27
|
+
title: `Stop sync · ${agent}`,
|
|
28
|
+
events,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
for (const edit of edits || []) {
|
|
32
|
+
memory.edits.unshift({
|
|
33
|
+
id: id("e"),
|
|
34
|
+
agent,
|
|
35
|
+
ownerLogin,
|
|
36
|
+
mine: true,
|
|
37
|
+
file: edit.file,
|
|
38
|
+
ts,
|
|
39
|
+
diff: edit.diff || "",
|
|
40
|
+
});
|
|
41
|
+
memory.activity.unshift({
|
|
42
|
+
id: id("a"),
|
|
43
|
+
kind: "edit",
|
|
44
|
+
agent,
|
|
45
|
+
ownerLogin,
|
|
46
|
+
mine: true,
|
|
47
|
+
text: `Edited ${edit.file}`,
|
|
48
|
+
ts,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
memory.stats = memory.stats || {};
|
|
53
|
+
memory.stats.events = (memory.stats.events || 0) + 1;
|
|
54
|
+
memory.stats.patches = memory.edits.length;
|
|
55
|
+
saveMemory(projectId, memory);
|
|
56
|
+
return memory;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function clientEventId(ts, filepath, index) {
|
|
60
|
+
return crypto.createHash("sha256").update(`${ts}|${filepath}|${index}`).digest("hex");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { appendStopTurn, clientEventId };
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { dataDir } = require("./paths");
|
|
4
|
+
const { readJson, writeJson } = require("./store");
|
|
5
|
+
|
|
6
|
+
const MAX = 80;
|
|
7
|
+
|
|
8
|
+
function noticesPath() {
|
|
9
|
+
return path.join(dataDir(), "notices.json");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function load() {
|
|
13
|
+
const rows = readJson(noticesPath(), []);
|
|
14
|
+
return Array.isArray(rows) ? rows : [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function save(rows) {
|
|
18
|
+
writeJson(noticesPath(), rows.slice(0, MAX));
|
|
19
|
+
return rows;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function prune(rows, now = Date.now()) {
|
|
23
|
+
return rows.filter((n) => {
|
|
24
|
+
if (n.dismissedAt && now - n.dismissedAt > 7 * 24 * 60 * 60 * 1000) return false;
|
|
25
|
+
if (n.readAt && !n.dismissedAt && now - n.readAt > 14 * 24 * 60 * 60 * 1000) return false;
|
|
26
|
+
return true;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function view(row) {
|
|
31
|
+
return {
|
|
32
|
+
id: row.id,
|
|
33
|
+
type: row.type,
|
|
34
|
+
key: row.key,
|
|
35
|
+
title: row.title,
|
|
36
|
+
body: row.body || "",
|
|
37
|
+
action: row.action || null,
|
|
38
|
+
payload: row.payload || null,
|
|
39
|
+
workspaceId: row.workspaceId || null,
|
|
40
|
+
ts: row.ts,
|
|
41
|
+
readAt: row.readAt || null,
|
|
42
|
+
dismissedAt: row.dismissedAt || null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function listNotices({ includeDismissed } = {}) {
|
|
47
|
+
return prune(load())
|
|
48
|
+
.filter((n) => includeDismissed || !n.dismissedAt)
|
|
49
|
+
.sort((a, b) => (b.ts || 0) - (a.ts || 0))
|
|
50
|
+
.map(view);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function unreadCount() {
|
|
54
|
+
return listNotices().filter((n) => !n.readAt).length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Inserts a notice unless one with the same key is already sitting unread.
|
|
59
|
+
* `revive` reopens a read/dismissed row (host came back, teammate rejoined).
|
|
60
|
+
*/
|
|
61
|
+
function pushNotice(input, { revive } = {}) {
|
|
62
|
+
if (!input?.type || !input?.key || !input?.title) return null;
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
const rows = prune(load(), now);
|
|
65
|
+
const existing = rows.find((n) => n.key === input.key);
|
|
66
|
+
if (existing && !revive) return null;
|
|
67
|
+
if (existing && revive) {
|
|
68
|
+
existing.title = input.title;
|
|
69
|
+
existing.body = input.body || existing.body || "";
|
|
70
|
+
existing.action = input.action || existing.action || null;
|
|
71
|
+
existing.payload = input.payload || existing.payload || null;
|
|
72
|
+
existing.workspaceId = input.workspaceId || existing.workspaceId || null;
|
|
73
|
+
existing.ts = now;
|
|
74
|
+
existing.readAt = null;
|
|
75
|
+
existing.dismissedAt = null;
|
|
76
|
+
save(rows);
|
|
77
|
+
return view(existing);
|
|
78
|
+
}
|
|
79
|
+
const row = {
|
|
80
|
+
id: `ntc_${crypto.randomBytes(6).toString("hex")}`,
|
|
81
|
+
type: input.type,
|
|
82
|
+
key: input.key,
|
|
83
|
+
title: input.title,
|
|
84
|
+
body: input.body || "",
|
|
85
|
+
action: input.action || null,
|
|
86
|
+
payload: input.payload || null,
|
|
87
|
+
workspaceId: input.workspaceId || null,
|
|
88
|
+
ts: now,
|
|
89
|
+
readAt: null,
|
|
90
|
+
dismissedAt: null,
|
|
91
|
+
};
|
|
92
|
+
save([row, ...rows.filter((n) => n.key !== input.key)]);
|
|
93
|
+
return view(row);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function markRead(id) {
|
|
97
|
+
const rows = load();
|
|
98
|
+
const row = rows.find((n) => n.id === id);
|
|
99
|
+
if (!row || row.readAt) return row ? view(row) : null;
|
|
100
|
+
row.readAt = Date.now();
|
|
101
|
+
save(rows);
|
|
102
|
+
return view(row);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function markAllRead() {
|
|
106
|
+
const now = Date.now();
|
|
107
|
+
const rows = load().map((n) => (n.readAt || n.dismissedAt ? n : { ...n, readAt: now }));
|
|
108
|
+
save(rows);
|
|
109
|
+
return listNotices();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function dismissNotice(id) {
|
|
113
|
+
const rows = load();
|
|
114
|
+
const row = rows.find((n) => n.id === id);
|
|
115
|
+
if (!row) return null;
|
|
116
|
+
row.dismissedAt = Date.now();
|
|
117
|
+
row.readAt = row.readAt || row.dismissedAt;
|
|
118
|
+
save(rows);
|
|
119
|
+
return view(row);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function dismissByKey(key) {
|
|
123
|
+
if (!key) return 0;
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
let count = 0;
|
|
126
|
+
const rows = load().map((n) => {
|
|
127
|
+
if (n.key !== key || n.dismissedAt) return n;
|
|
128
|
+
count += 1;
|
|
129
|
+
return { ...n, dismissedAt: now, readAt: n.readAt || now };
|
|
130
|
+
});
|
|
131
|
+
if (count) save(rows);
|
|
132
|
+
return count;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = {
|
|
136
|
+
listNotices,
|
|
137
|
+
unreadCount,
|
|
138
|
+
pushNotice,
|
|
139
|
+
markRead,
|
|
140
|
+
markAllRead,
|
|
141
|
+
dismissNotice,
|
|
142
|
+
dismissByKey,
|
|
143
|
+
};
|