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
package/cli/graph.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// `relay graph …`: run, enable and inspect the FalkorDB memory graph without env vars.
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { spawn, spawnSync } = require("child_process");
|
|
5
|
+
const { readConfig, saveFileConfig, clearFileConfig, loadFileConfig, configPath, validUrl } = require("../backend/lib/falkor/config");
|
|
6
|
+
|
|
7
|
+
const root = path.join(__dirname, "..");
|
|
8
|
+
const compose = path.join(root, "docker-compose.falkor.yml");
|
|
9
|
+
const DEFAULT_URL = "falkor://127.0.0.1:6379";
|
|
10
|
+
|
|
11
|
+
function api() {
|
|
12
|
+
return `http://127.0.0.1:${Number(process.env.RELAY_PORT || 3001)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function mask(url) {
|
|
16
|
+
return url ? url.replace(/\/\/[^@/]*@/, "//***@") : url;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function getJson(url, init) {
|
|
20
|
+
const r = await fetch(url, { ...init, signal: AbortSignal.timeout(6000) });
|
|
21
|
+
return r.json();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function help() {
|
|
25
|
+
console.log(`relay graph: the FalkorDB memory graph
|
|
26
|
+
|
|
27
|
+
relay graph up Start FalkorDB in Docker (docker-compose.falkor.yml)
|
|
28
|
+
relay graph enable [url] Save the FalkorDB URL (default ${DEFAULT_URL}) and switch a running relay on
|
|
29
|
+
relay graph disable Turn the graph off (takes effect on the next relay serve)
|
|
30
|
+
relay graph status Config, connection and per-project ingest state
|
|
31
|
+
relay graph stats [id|all] Node/edge counts per label, memory usage, and turn time range
|
|
32
|
+
relay graph rebuild [id|all] Rebuild a project's graph from its relay history (default: all)
|
|
33
|
+
relay graph seed [id] Seed a realistic 5-agent coordination graph for live demos
|
|
34
|
+
`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function up() {
|
|
38
|
+
const docker = spawnSync("docker", ["info", "--format", "{{.ServerVersion}}"], { encoding: "utf8", windowsHide: true });
|
|
39
|
+
if (docker.error || docker.status !== 0) {
|
|
40
|
+
console.error("Docker is not running. Start Docker Desktop (or the docker daemon), then run `relay graph up` again.");
|
|
41
|
+
console.error(`Or run FalkorDB yourself and point relay at it: relay graph enable falkor://host:port`);
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const r = spawnSync("docker", ["compose", "-f", compose, "up", "-d"], { stdio: "inherit", windowsHide: true });
|
|
46
|
+
if (r.status !== 0) {
|
|
47
|
+
process.exitCode = r.status || 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(`\nFalkorDB is up on ${DEFAULT_URL} (browser UI: http://localhost:3010).`);
|
|
51
|
+
console.log("Next: relay graph enable");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function probe(url) {
|
|
55
|
+
const { FalkorDB } = require("falkordb");
|
|
56
|
+
const db = await Promise.race([
|
|
57
|
+
FalkorDB.connect({ url, socket: { connectTimeout: 3000, reconnectStrategy: false } }),
|
|
58
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timed out")), 4000)),
|
|
59
|
+
]);
|
|
60
|
+
try {
|
|
61
|
+
db.on?.("error", () => undefined);
|
|
62
|
+
return await db.list();
|
|
63
|
+
} finally {
|
|
64
|
+
await db.close().catch(() => undefined);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function enable(url) {
|
|
69
|
+
const target = (url || "").trim() || DEFAULT_URL;
|
|
70
|
+
if (!validUrl(target)) {
|
|
71
|
+
console.error(`Not a FalkorDB URL: ${target} (expected falkor://[user:pass@]host:port)`);
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const graphs = await probe(target);
|
|
77
|
+
console.log(`Connected to ${mask(target)} (${graphs.length} graph(s)).`);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(`Cannot reach ${mask(target)}: ${err.message || err}`);
|
|
80
|
+
console.error("Start it with `relay graph up`, or pass the right URL. Saving anyway.");
|
|
81
|
+
}
|
|
82
|
+
let running = false;
|
|
83
|
+
try {
|
|
84
|
+
const body = await getJson(`${api()}/api/graph/enable`, {
|
|
85
|
+
method: "POST",
|
|
86
|
+
headers: { "Content-Type": "application/json" },
|
|
87
|
+
body: JSON.stringify({ url: target }),
|
|
88
|
+
});
|
|
89
|
+
running = true;
|
|
90
|
+
if (!body.ok) console.error(`relay serve refused: ${body.reason}`);
|
|
91
|
+
else console.log(body.note || body.warning || "relay serve: memory graph is on.");
|
|
92
|
+
} catch {
|
|
93
|
+
saveFileConfig({ ...(loadFileConfig() || {}), url: target });
|
|
94
|
+
}
|
|
95
|
+
console.log(`Saved to ${configPath()}.`);
|
|
96
|
+
if (!running) console.log("Start relay (`relay serve`) and it will use the graph.");
|
|
97
|
+
const env = readConfig();
|
|
98
|
+
if (env.source === "env") console.log(`Note: RELAY_FALKOR_URL is set in this shell and overrides the saved URL.`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function disable() {
|
|
102
|
+
const cfg = loadFileConfig();
|
|
103
|
+
if (!cfg) {
|
|
104
|
+
console.log("No saved graph config. (If RELAY_FALKOR_URL is set in your shell, unset it.)");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
clearFileConfig();
|
|
108
|
+
console.log(`Removed ${configPath()}. Restart relay serve to stop using the graph.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function status() {
|
|
112
|
+
const cfg = readConfig();
|
|
113
|
+
console.log(`config: ${cfg.url ? `${mask(cfg.url)} (from ${cfg.source})` : "none — run `relay graph enable`"}`);
|
|
114
|
+
console.log(`file: ${configPath()}${fs.existsSync(configPath()) ? "" : " (not present)"}`);
|
|
115
|
+
let health = null;
|
|
116
|
+
try {
|
|
117
|
+
health = await getJson(`${api()}/api/graph/health`);
|
|
118
|
+
} catch {
|
|
119
|
+
console.log("relay: not running on " + api());
|
|
120
|
+
}
|
|
121
|
+
if (health) {
|
|
122
|
+
const b = health.breaker;
|
|
123
|
+
// Breaker states, in plain words: "closed" is the healthy one.
|
|
124
|
+
const conn = !b ? "" : b.state === "closed" ? ", connected" : b.state === "open" ? ", FalkorDB unreachable (retrying)" : ", reconnecting";
|
|
125
|
+
console.log(`relay: graph ${health.running ? "running" : "off"}${conn}${b?.lastError && b.state !== "closed" ? ` (${b.lastError})` : ""}`);
|
|
126
|
+
const projects = Object.entries(health.projects || {});
|
|
127
|
+
if (!projects.length) console.log(" no project ingested yet in this process");
|
|
128
|
+
for (const [id, p] of projects) {
|
|
129
|
+
const when = p.lastIngestAt ? new Date(p.lastIngestAt).toLocaleString() : "never";
|
|
130
|
+
console.log(` ${id}: last ingest ${when}${p.needsRebuild ? ", NEEDS REBUILD (relay graph rebuild)" : ""}${p.lastError ? `, error: ${p.lastError}` : ""}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (cfg.url) {
|
|
134
|
+
try {
|
|
135
|
+
const graphs = await probe(cfg.url);
|
|
136
|
+
console.log(`db: reachable, ${graphs.length} graph(s): ${graphs.slice(0, 8).join(", ")}${graphs.length > 8 ? ", …" : ""}`);
|
|
137
|
+
} catch (err) {
|
|
138
|
+
console.log(`db: unreachable (${err.message || err}). Try \`relay graph up\`.`);
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function rebuild(target) {
|
|
145
|
+
const cfg = readConfig();
|
|
146
|
+
if (!cfg.url) {
|
|
147
|
+
console.error("No FalkorDB configured. Run `relay graph enable` first.");
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
return Promise.resolve();
|
|
150
|
+
}
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
const child = spawn(process.execPath, [path.join(root, "scripts", "falkor-rebuild.js"), target || "all"], {
|
|
153
|
+
stdio: "inherit",
|
|
154
|
+
env: { ...process.env, RELAY_FALKOR_URL: cfg.url },
|
|
155
|
+
windowsHide: true,
|
|
156
|
+
});
|
|
157
|
+
child.on("exit", (code) => {
|
|
158
|
+
process.exitCode = code || 0;
|
|
159
|
+
resolve();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function stats(target) {
|
|
165
|
+
const cfg = readConfig();
|
|
166
|
+
if (!cfg.url) {
|
|
167
|
+
console.error("No FalkorDB configured. Run `relay graph enable` first.");
|
|
168
|
+
process.exitCode = 1;
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const { FalkorClient } = require("../backend/lib/falkor/client");
|
|
172
|
+
const { loadRegistry } = require("../backend/lib/store");
|
|
173
|
+
const client = new FalkorClient(cfg);
|
|
174
|
+
try {
|
|
175
|
+
const db = await client.connection();
|
|
176
|
+
const regProjects = loadRegistry().projects || [];
|
|
177
|
+
let selected = target && target !== "all"
|
|
178
|
+
? regProjects.filter((p) => p.id === target || p.name === target)
|
|
179
|
+
: [...regProjects];
|
|
180
|
+
if (!selected.length && target && target !== "all") {
|
|
181
|
+
selected = [{ id: target, name: target }];
|
|
182
|
+
}
|
|
183
|
+
if (!selected.length) {
|
|
184
|
+
const prefix = cfg.prefix || "relay_";
|
|
185
|
+
const allGraphs = (await db.list()).filter((g) => g.startsWith(prefix));
|
|
186
|
+
selected = allGraphs.map((g) => ({ id: g.slice(prefix.length), name: g.slice(prefix.length) }));
|
|
187
|
+
}
|
|
188
|
+
if (!selected.length) {
|
|
189
|
+
console.log("No matching registered projects or FalkorDB graphs.");
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const p of selected) {
|
|
193
|
+
const gName = client.graphName(p.id);
|
|
194
|
+
console.log(`\n${p.name} (${p.id}) -> graph ${gName}`);
|
|
195
|
+
try {
|
|
196
|
+
const nodes = await client.read(p.id, "MATCH (n) RETURN labels(n)[0] AS label, count(n) AS count ORDER BY count DESC");
|
|
197
|
+
const edges = await client.read(p.id, "MATCH ()-[r]->() RETURN type(r) AS type, count(r) AS count ORDER BY count DESC");
|
|
198
|
+
const span = await client.read(
|
|
199
|
+
p.id,
|
|
200
|
+
"MATCH (t:Turn) RETURN min(t.ts) AS oldest, max(t.ts) AS newest, count(t.text) AS withText, count(t) AS total"
|
|
201
|
+
);
|
|
202
|
+
const meta = await client.read(
|
|
203
|
+
p.id,
|
|
204
|
+
"MATCH (m:Meta {key:'meta'}) RETURN m.schemaVersion AS s, m.ingestVersion AS i, coalesce(m.migrations, []) AS migs, m.lastIngestAt AS lastIngestAt"
|
|
205
|
+
);
|
|
206
|
+
let memBytes = null;
|
|
207
|
+
try {
|
|
208
|
+
const g = db.selectGraph(gName);
|
|
209
|
+
if (typeof g.memoryUsage === "function") {
|
|
210
|
+
const mem = await g.memoryUsage();
|
|
211
|
+
memBytes = Array.isArray(mem) ? mem[1] : mem?.total_graph_sz_mb || mem;
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
/* GRAPH.MEMORY USAGE optional on some server builds */
|
|
215
|
+
}
|
|
216
|
+
const nodeStr = nodes.map((r) => `${r.label}:${r.count}`).join(" ") || "empty";
|
|
217
|
+
const edgeStr = edges.map((r) => `${r.type}:${r.count}`).join(" ") || "none";
|
|
218
|
+
console.log(` nodes: ${nodeStr}`);
|
|
219
|
+
console.log(` edges: ${edgeStr}`);
|
|
220
|
+
if (span[0]?.total) {
|
|
221
|
+
const oldest = span[0].oldest ? new Date(Number(span[0].oldest)).toISOString() : "n/a";
|
|
222
|
+
const newest = span[0].newest ? new Date(Number(span[0].newest)).toISOString() : "n/a";
|
|
223
|
+
console.log(` turns: ${span[0].withText}/${span[0].total} with text (${oldest} .. ${newest})`);
|
|
224
|
+
}
|
|
225
|
+
if (meta[0]) {
|
|
226
|
+
console.log(` schema: v${meta[0].s} (ingest v${meta[0].i}), migrations: ${(meta[0].migs || []).join(", ") || "none"}`);
|
|
227
|
+
}
|
|
228
|
+
if (memBytes != null) {
|
|
229
|
+
console.log(` memory: ${typeof memBytes === "number" ? `${(memBytes / 1024).toFixed(1)} KB` : JSON.stringify(memBytes)}`);
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.log(` (no graph or unreachable: ${err.message || err})`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error(`Cannot connect to FalkorDB at ${mask(cfg.url)}: ${err.message || err}`);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
} finally {
|
|
239
|
+
await client.close();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function seed(target) {
|
|
244
|
+
const cfg = readConfig();
|
|
245
|
+
if (!cfg.url) {
|
|
246
|
+
console.error("No FalkorDB configured. Run `relay graph enable` first.");
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
return Promise.resolve();
|
|
249
|
+
}
|
|
250
|
+
return new Promise((resolve) => {
|
|
251
|
+
const args = [path.join(root, "scripts", "falkor-seed-demo.js")];
|
|
252
|
+
if (target) args.push(target);
|
|
253
|
+
const child = spawn(process.execPath, args, {
|
|
254
|
+
stdio: "inherit",
|
|
255
|
+
env: { ...process.env, RELAY_FALKOR_URL: cfg.url },
|
|
256
|
+
windowsHide: true,
|
|
257
|
+
});
|
|
258
|
+
child.on("exit", (code) => {
|
|
259
|
+
process.exitCode = code || 0;
|
|
260
|
+
resolve();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function graph(args) {
|
|
266
|
+
const [sub, arg] = args;
|
|
267
|
+
if (sub === "up") return up();
|
|
268
|
+
if (sub === "enable") return enable(arg);
|
|
269
|
+
if (sub === "disable") return disable();
|
|
270
|
+
if (sub === "status") return status();
|
|
271
|
+
if (sub === "stats") return stats(arg);
|
|
272
|
+
if (sub === "rebuild") return rebuild(arg);
|
|
273
|
+
if (sub === "seed") return seed(arg);
|
|
274
|
+
help();
|
|
275
|
+
if (sub && sub !== "help") process.exitCode = 1;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = { graph };
|
package/cli/relay.js
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { spawn } = require("child_process");
|
|
5
|
+
|
|
6
|
+
const root = path.join(__dirname, "..");
|
|
7
|
+
const cmd = process.argv[2] || "help";
|
|
8
|
+
const rest = process.argv.slice(3);
|
|
9
|
+
|
|
10
|
+
function help() {
|
|
11
|
+
console.log(`Relay — GitHub Desktop for AI agents
|
|
12
|
+
|
|
13
|
+
relay login Sign in with GitHub (uses gh CLI, or device flow if GITHUB_CLIENT_ID is set)
|
|
14
|
+
relay logout Clear the local session
|
|
15
|
+
relay whoami Show the signed-in GitHub user
|
|
16
|
+
relay clone <url> [dir] git clone + register workspace + install agent hooks
|
|
17
|
+
relay add <path> Attach an existing local repo
|
|
18
|
+
relay init [path] Wire hooks, MCP, and relay-os instructions (default: cwd)
|
|
19
|
+
relay serve Start API on 127.0.0.1:3001 and Mission Control on :3002
|
|
20
|
+
relay serve --no-ui Start API + coordinator only
|
|
21
|
+
relay serve --static Serve the prebuilt Mission Control from the API port (default for npm installs)
|
|
22
|
+
relay push Send your dirty working-tree files to the shared room
|
|
23
|
+
relay pull Apply the host's current dirty files onto this clone
|
|
24
|
+
relay status Health check
|
|
25
|
+
relay mcp-url Print the MCP config for this room's shared context endpoint
|
|
26
|
+
relay doctor Why the Coordinator board is empty (run it on the machine that sees nothing)
|
|
27
|
+
relay graph <cmd> Memory graph (FalkorDB): up | enable [url] | disable | status | stats | rebuild [id|all]
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function killTree(child) {
|
|
32
|
+
if (!child?.pid) return;
|
|
33
|
+
try {
|
|
34
|
+
if (process.platform === "win32") {
|
|
35
|
+
spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"], {
|
|
36
|
+
stdio: "ignore",
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
shell: false,
|
|
39
|
+
});
|
|
40
|
+
} else {
|
|
41
|
+
child.kill("SIGTERM");
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
/* already gone */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function apiPort(args) {
|
|
49
|
+
const idx = args.indexOf("--port");
|
|
50
|
+
if (idx >= 0) return Number(args[idx + 1]);
|
|
51
|
+
return Number(process.env.RELAY_PORT || 3001);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function waitForApi(url, ms = 30000) {
|
|
55
|
+
const start = Date.now();
|
|
56
|
+
while (Date.now() - start < ms) {
|
|
57
|
+
try {
|
|
58
|
+
const r = await fetch(url);
|
|
59
|
+
if (r.ok) return true;
|
|
60
|
+
} catch {
|
|
61
|
+
/* still booting */
|
|
62
|
+
}
|
|
63
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function serve(args) {
|
|
69
|
+
const noUi = args.includes("--no-ui");
|
|
70
|
+
const forwarded = args.filter((a) => a !== "--no-ui" && a !== "--static");
|
|
71
|
+
const uiPort = process.env.RELAY_UI_PORT || "3002";
|
|
72
|
+
const port = apiPort(forwarded);
|
|
73
|
+
const apiOrigin = `http://127.0.0.1:${port}`;
|
|
74
|
+
const children = [];
|
|
75
|
+
// An npm install ships Mission Control prebuilt (mission-control/out) and no Next.js,
|
|
76
|
+
// so the API serves the UI itself; a source checkout runs `next dev` as before.
|
|
77
|
+
const nextBin = path.join(root, "node_modules", "next", "dist", "bin", "next");
|
|
78
|
+
const hasStaticUi = fs.existsSync(path.join(root, "mission-control", "out", "index.html"));
|
|
79
|
+
const staticUi = !noUi && hasStaticUi && (args.includes("--static") || !fs.existsSync(nextBin));
|
|
80
|
+
const uiOrigin = staticUi ? `http://localhost:${port}` : `http://localhost:${uiPort}`;
|
|
81
|
+
|
|
82
|
+
const api = spawn(process.execPath, [path.join(root, "backend", "server.js"), ...forwarded], {
|
|
83
|
+
stdio: "inherit",
|
|
84
|
+
cwd: root,
|
|
85
|
+
windowsHide: false,
|
|
86
|
+
shell: false,
|
|
87
|
+
env: {
|
|
88
|
+
...process.env,
|
|
89
|
+
RELAY_PORT: String(port),
|
|
90
|
+
RELAY_UI_ORIGIN: process.env.RELAY_UI_ORIGIN || uiOrigin,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
children.push(api);
|
|
94
|
+
|
|
95
|
+
let shuttingDown = false;
|
|
96
|
+
function shutdown(code = 0) {
|
|
97
|
+
if (shuttingDown) return;
|
|
98
|
+
shuttingDown = true;
|
|
99
|
+
for (const child of children) killTree(child);
|
|
100
|
+
process.exit(code);
|
|
101
|
+
}
|
|
102
|
+
process.on("SIGINT", () => shutdown(0));
|
|
103
|
+
process.on("SIGTERM", () => shutdown(0));
|
|
104
|
+
api.on("exit", (code) => {
|
|
105
|
+
if (!shuttingDown) shutdown(code || 0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (staticUi) {
|
|
109
|
+
if (await waitForApi(`${apiOrigin}/api/health`)) console.log(`[relay] Mission Control → ${uiOrigin}`);
|
|
110
|
+
} else if (!noUi) {
|
|
111
|
+
if (!fs.existsSync(nextBin)) {
|
|
112
|
+
console.error("Mission Control is not installed — run npm install");
|
|
113
|
+
shutdown(1);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const ready = await waitForApi(`${apiOrigin}/api/health`);
|
|
117
|
+
if (!ready) {
|
|
118
|
+
console.error("[relay] API did not become ready on", apiOrigin);
|
|
119
|
+
shutdown(1);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const ui = spawn(process.execPath, [nextBin, "dev", "--port", String(uiPort)], {
|
|
123
|
+
stdio: "inherit",
|
|
124
|
+
cwd: path.join(root, "mission-control"),
|
|
125
|
+
windowsHide: false,
|
|
126
|
+
shell: false,
|
|
127
|
+
env: {
|
|
128
|
+
...process.env,
|
|
129
|
+
NEXT_PUBLIC_RELAY_API: apiOrigin,
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
children.push(ui);
|
|
133
|
+
ui.on("exit", (code) => {
|
|
134
|
+
if (!shuttingDown) shutdown(code || 0);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Walks the exact chain the Coordinator board depends on and says which link is
|
|
141
|
+
* broken. Locks and the board are different subsystems — a hook claims straight
|
|
142
|
+
* against the host over HTTP, while the board is fed by the lock mirror and SSE
|
|
143
|
+
* — so "locking works but I see nothing" is a normal, and otherwise invisible,
|
|
144
|
+
* way for this to fail.
|
|
145
|
+
*/
|
|
146
|
+
async function doctor() {
|
|
147
|
+
const api = `http://127.0.0.1:${apiPort(rest)}`;
|
|
148
|
+
const ok = (m) => console.log(` ok ${m}`);
|
|
149
|
+
const bad = (m) => console.log(` BROKE ${m}`);
|
|
150
|
+
const info = (m) => console.log(` ${m}`);
|
|
151
|
+
|
|
152
|
+
console.log("relay doctor — room + Coordinator board\n");
|
|
153
|
+
let hostLockCount = 0;
|
|
154
|
+
|
|
155
|
+
let health;
|
|
156
|
+
try {
|
|
157
|
+
health = await fetch(`${api}/api/health`).then((r) => r.json());
|
|
158
|
+
ok(`local API on ${api}`);
|
|
159
|
+
} catch {
|
|
160
|
+
bad(`no local API on ${api} — run \`relay serve\` here`);
|
|
161
|
+
process.exitCode = 1;
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const g = await fetch(`${api}/api/graph/health`).then((r) => r.json());
|
|
167
|
+
if (!g.running) info("memory graph off — `relay graph up && relay graph enable` to turn it on");
|
|
168
|
+
else if (g.breaker?.state === "closed") ok(`memory graph connected (${g.url})`);
|
|
169
|
+
else bad(`memory graph cannot reach FalkorDB at ${g.url}${g.breaker?.lastError ? ` (${g.breaker.lastError})` : ""} — \`relay graph status\``);
|
|
170
|
+
} catch {
|
|
171
|
+
info("memory graph status unavailable (older relay serve?)");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const room = health.room;
|
|
175
|
+
if (!room?.url) {
|
|
176
|
+
info("no room joined on this machine — the board shows only local agents.");
|
|
177
|
+
info("Host: Team tab -> Invite next to a teammate. Guest: Join on the Team tab.");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
ok(`room joined as ${room.role} -> ${room.url}`);
|
|
181
|
+
if (room.role === "host") {
|
|
182
|
+
info(`sharing ${room.hostProjectName || "?"} (${room.hostWorkspacePath || "?"})`);
|
|
183
|
+
info("A host reads its own lock table, so the board is local by definition.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (room.role === "guest") {
|
|
187
|
+
if (!room.hostProjectId) {
|
|
188
|
+
bad("room.json has no hostProjectId — the lock mirror never starts, so the");
|
|
189
|
+
info("board stays empty even while your locks work. Re-join with the invite link.");
|
|
190
|
+
} else {
|
|
191
|
+
ok(`mirroring host project ${room.hostProjectId}`);
|
|
192
|
+
}
|
|
193
|
+
const token = require("../backend/lib/room").loadRoom()?.memberToken;
|
|
194
|
+
const hit = async (p) => {
|
|
195
|
+
const r = await fetch(`${String(room.url).replace(/\/$/, "")}${p}`, {
|
|
196
|
+
headers: {
|
|
197
|
+
"ngrok-skip-browser-warning": "relay",
|
|
198
|
+
...(token ? { "x-relay-room-token": token } : {}),
|
|
199
|
+
},
|
|
200
|
+
signal: AbortSignal.timeout(6000),
|
|
201
|
+
});
|
|
202
|
+
if (!r.ok) throw new Error(`${r.status}`);
|
|
203
|
+
return r.json();
|
|
204
|
+
};
|
|
205
|
+
try {
|
|
206
|
+
await hit("/api/health");
|
|
207
|
+
ok("host answers through the tunnel");
|
|
208
|
+
} catch (err) {
|
|
209
|
+
bad(`cannot reach the host (${err.message}) — board falls back to local locks`);
|
|
210
|
+
info("Host is unreachable — Relay will look up their current tunnel and reconnect.");
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const remote = await hit(`/api/locks?projectId=${encodeURIComponent(room.hostProjectId || "")}`);
|
|
214
|
+
hostLockCount = (remote.locks || []).length;
|
|
215
|
+
ok(`host reports ${hostLockCount} lock(s) for the shared project`);
|
|
216
|
+
} catch (err) {
|
|
217
|
+
bad(`host refused /api/locks (${err.message})` + (token ? "" : " — no member token stored"));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// The mirror polls on its own clock, so a lock claimed a moment ago has not
|
|
222
|
+
// necessarily landed yet. Give it a few beats before calling anything broken.
|
|
223
|
+
let mine = await fetch(`${api}/api/locks`).then((r) => r.json());
|
|
224
|
+
for (let i = 0; i < 8 && hostLockCount > 0 && !(mine.locks || []).length; i += 1) {
|
|
225
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
226
|
+
mine = await fetch(`${api}/api/locks`).then((r) => r.json());
|
|
227
|
+
}
|
|
228
|
+
if (hostLockCount > 0 && !(mine.locks || []).length) {
|
|
229
|
+
bad("the host holds locks but this machine mirrors none of them.");
|
|
230
|
+
info("Locks still arbitrate correctly (hooks call the host directly); it is only");
|
|
231
|
+
info("the board that is fed by the mirror. Restart `relay serve` here — and if it");
|
|
232
|
+
info("comes back, report it: the mirror is not committing its polls.");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const localKeys = new Set(Object.values(mine.raw?.locks || {}).map((l) => `${l.filePath}::${l.agentId}`));
|
|
236
|
+
const fromHost = (mine.locks || []).filter((l) => !localKeys.has(`${l.filePath}::${l.agentId}`));
|
|
237
|
+
console.log("");
|
|
238
|
+
console.log(` board would render ${(mine.locks || []).length} lock(s):`);
|
|
239
|
+
info(`${fromHost.length} mirrored from the room, ${localKeys.size} claimed on this machine`);
|
|
240
|
+
for (const l of mine.locks || []) {
|
|
241
|
+
info(`${l.filePath} ${l.holder?.login ? "@" + l.holder.login : l.agentId} ${l.mode || "write"}`);
|
|
242
|
+
}
|
|
243
|
+
if (!(mine.locks || []).length) {
|
|
244
|
+
console.log("");
|
|
245
|
+
info("Nothing is locked anywhere right now — an empty board here is correct.");
|
|
246
|
+
info("Have someone start an agent edit, then run this again.");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function main() {
|
|
251
|
+
if (cmd === "help" || cmd === "-h" || cmd === "--help") {
|
|
252
|
+
help();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (cmd === "doctor") {
|
|
256
|
+
await doctor();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (cmd === "graph") {
|
|
260
|
+
await require("./graph").graph(rest);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (cmd === "serve") {
|
|
264
|
+
await serve(rest);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (cmd === "push" || cmd === "pull") {
|
|
268
|
+
try {
|
|
269
|
+
const r = await fetch(`http://127.0.0.1:3001/api/${cmd}`, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "Content-Type": "application/json" },
|
|
272
|
+
body: "{}",
|
|
273
|
+
});
|
|
274
|
+
const body = await r.json();
|
|
275
|
+
if (!r.ok) {
|
|
276
|
+
console.error(body.hint || body.error || `${cmd} failed`);
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const files = body.files || [];
|
|
281
|
+
console.log(files.length ? `${cmd} ${files.length} file(s)` : `${cmd}: nothing to ${cmd}`);
|
|
282
|
+
for (const file of files) console.log(` ${file}`);
|
|
283
|
+
} catch {
|
|
284
|
+
console.error("offline — run relay serve");
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (cmd === "status") {
|
|
291
|
+
try {
|
|
292
|
+
const r = await fetch("http://127.0.0.1:3001/api/health");
|
|
293
|
+
console.log(JSON.stringify(await r.json(), null, 2));
|
|
294
|
+
} catch {
|
|
295
|
+
console.error("offline — run relay serve");
|
|
296
|
+
process.exitCode = 1;
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const gh = require("../backend/lib/githubLogin");
|
|
302
|
+
const clone = require("../backend/lib/cloneRepo");
|
|
303
|
+
|
|
304
|
+
if (cmd === "whoami") {
|
|
305
|
+
const { user, session } = gh.whoami();
|
|
306
|
+
if (!user) {
|
|
307
|
+
console.log("not signed in — run relay login");
|
|
308
|
+
process.exitCode = 1;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
console.log(`${user.name} (@${user.login})`);
|
|
312
|
+
if (session.projectId) console.log(`project ${session.projectId}`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (cmd === "logout") {
|
|
317
|
+
gh.logout();
|
|
318
|
+
console.log("signed out");
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (cmd === "login") {
|
|
323
|
+
try {
|
|
324
|
+
const user = await gh.loginFromGh();
|
|
325
|
+
console.log(`signed in as ${user.name} (@${user.login}) via GitHub CLI`);
|
|
326
|
+
return;
|
|
327
|
+
} catch (err) {
|
|
328
|
+
if (process.env.GITHUB_CLIENT_ID) {
|
|
329
|
+
const device = await gh.loginDeviceFlow();
|
|
330
|
+
console.log(`Open ${device.verification_uri} and enter code: ${device.user_code}`);
|
|
331
|
+
const user = await gh.pollDevice(device);
|
|
332
|
+
console.log(`signed in as ${user.name} (@${user.login})`);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
console.error(err.message || err);
|
|
336
|
+
console.error("Install GitHub CLI and run: gh auth login");
|
|
337
|
+
console.error("Or set GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET and use Mission Control → Continue with GitHub.");
|
|
338
|
+
process.exitCode = 1;
|
|
339
|
+
}
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (cmd === "clone") {
|
|
344
|
+
const url = rest[0];
|
|
345
|
+
const dir = rest[1];
|
|
346
|
+
const { project, path: dest } = await clone.cloneRepo(url, dir);
|
|
347
|
+
console.log(`cloned ${url}`);
|
|
348
|
+
console.log(`workspace ${dest}`);
|
|
349
|
+
console.log(`registered ${project.name} (${project.id})`);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (cmd === "add") {
|
|
354
|
+
const project = clone.addLocal(rest[0], rest[1]);
|
|
355
|
+
console.log(`added ${project.path} as ${project.name}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (cmd === "init") {
|
|
360
|
+
const target = path.resolve(rest[0] || process.cwd());
|
|
361
|
+
if (!fs.existsSync(target)) {
|
|
362
|
+
console.error(`path not found: ${target}`);
|
|
363
|
+
process.exitCode = 1;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const { installProjectHooks, installGlobalHooks } = require("../backend/lib/installHooks");
|
|
367
|
+
try {
|
|
368
|
+
installProjectHooks(target);
|
|
369
|
+
installGlobalHooks();
|
|
370
|
+
const existing = clone.addLocal(target, path.basename(target));
|
|
371
|
+
console.log(`Relay initialized in ${existing.path}`);
|
|
372
|
+
console.log("Hooks + MCP wired for Cursor, Claude Code, Codex, Copilot CLI, Antigravity.");
|
|
373
|
+
console.log("Run `relay serve`, then say `/relay ask` in any agent — it calls MCP relay_room_brief.");
|
|
374
|
+
} catch (err) {
|
|
375
|
+
console.error(err.message || err);
|
|
376
|
+
process.exitCode = 1;
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (cmd === "mcp-url") {
|
|
382
|
+
const room = require("../backend/lib/room").loadRoom();
|
|
383
|
+
if (!room?.url) {
|
|
384
|
+
console.log("Not in a room. `relay serve`, then Share in Mission Control, or join with an invite link.");
|
|
385
|
+
console.log("Locally your agents already reach relay over stdio — nothing to configure.");
|
|
386
|
+
process.exitCode = 1;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const config = {
|
|
390
|
+
mcpServers: {
|
|
391
|
+
"relay-room": {
|
|
392
|
+
type: "http",
|
|
393
|
+
url: `${String(room.url).replace(/\/$/, "")}/mcp`,
|
|
394
|
+
headers: {
|
|
395
|
+
"ngrok-skip-browser-warning": "relay",
|
|
396
|
+
...(room.memberToken ? { "x-relay-room-token": room.memberToken } : {}),
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
console.log(`Room MCP endpoint (${room.role}) — read-only for anyone off this machine.\n`);
|
|
402
|
+
console.log(JSON.stringify(config, null, 2));
|
|
403
|
+
console.log(`\nPaste into the agent's MCP config. It exposes the room's context (chat history,
|
|
404
|
+
recent edits, locks, conflicts) to any MCP client — including one on a machine
|
|
405
|
+
with no relay installed. Locks and other writes stay on each member's own relay.`);
|
|
406
|
+
if (!room.memberToken && room.role === "guest") {
|
|
407
|
+
console.log("\nNo member token on file — if the host made the room invite-only, re-join with the invite link.");
|
|
408
|
+
}
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
help();
|
|
413
|
+
process.exitCode = 1;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
main().catch((err) => {
|
|
417
|
+
console.error(err.message || err);
|
|
418
|
+
process.exit(1);
|
|
419
|
+
});
|