@silicaclaw/cli 1.0.0-beta.9 → 2026.3.18-3
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/CHANGELOG.md +100 -0
- package/INSTALL.md +24 -13
- package/README.md +62 -18
- package/VERSION +1 -1
- package/apps/local-console/package.json +1 -0
- package/apps/local-console/public/index.html +2083 -473
- package/apps/local-console/src/server.ts +108 -31
- package/apps/public-explorer/public/index.html +283 -61
- package/docs/CLOUDFLARE_RELAY.md +61 -0
- package/docs/NEW_USER_INSTALL.md +166 -0
- package/docs/NEW_USER_OPERATIONS.md +265 -0
- package/package.json +2 -2
- package/packages/core/dist/socialConfig.d.ts +1 -1
- package/packages/core/dist/socialConfig.js +7 -6
- package/packages/core/dist/socialResolver.js +15 -5
- package/packages/core/src/socialConfig.ts +8 -7
- package/packages/core/src/socialResolver.ts +17 -5
- package/packages/network/dist/index.d.ts +1 -0
- package/packages/network/dist/index.js +1 -0
- package/packages/network/dist/relayPreview.d.ts +166 -0
- package/packages/network/dist/relayPreview.js +430 -0
- package/packages/network/src/index.ts +1 -0
- package/packages/network/src/relayPreview.ts +552 -0
- package/packages/storage/dist/socialRuntimeRepo.js +4 -4
- package/packages/storage/src/socialRuntimeRepo.ts +4 -4
- package/scripts/quickstart.sh +241 -38
- package/scripts/silicaclaw-cli.mjs +418 -56
- package/scripts/silicaclaw-gateway.mjs +231 -46
- package/scripts/webrtc-signaling-server.mjs +89 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import { existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -13,6 +13,72 @@ const ROOT_DIR = resolve(__dirname, "..");
|
|
|
13
13
|
const argv = process.argv.slice(2);
|
|
14
14
|
const cmd = String(argv[0] || "help").toLowerCase();
|
|
15
15
|
|
|
16
|
+
const COLOR = {
|
|
17
|
+
reset: "\x1b[0m",
|
|
18
|
+
bold: "\x1b[1m",
|
|
19
|
+
dim: "\x1b[2m",
|
|
20
|
+
orange: "\x1b[38;5;208m",
|
|
21
|
+
green: "\x1b[32m",
|
|
22
|
+
yellow: "\x1b[33m",
|
|
23
|
+
red: "\x1b[31m",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function useColor() {
|
|
27
|
+
return Boolean(process.stdout.isTTY && !process.env.NO_COLOR);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function paint(text, ...styles) {
|
|
31
|
+
if (!useColor() || styles.length === 0) return text;
|
|
32
|
+
return `${styles.join("")}${text}${COLOR.reset}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function displayVersion(raw) {
|
|
36
|
+
const text = String(raw || "unknown").trim() || "unknown";
|
|
37
|
+
return text.startsWith("v") ? text : `v${text}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function headline() {
|
|
41
|
+
const pkgPath = join(ROOT_DIR, "package.json");
|
|
42
|
+
let version = "unknown";
|
|
43
|
+
if (existsSync(pkgPath)) {
|
|
44
|
+
try {
|
|
45
|
+
const pkg = JSON.parse(String(readFileSync(pkgPath, "utf8")));
|
|
46
|
+
version = String(pkg.version || "unknown");
|
|
47
|
+
} catch {
|
|
48
|
+
version = "unknown";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
console.log(`${paint("SilicaClaw", COLOR.bold, COLOR.orange)} ${paint(displayVersion(version), COLOR.dim)}`);
|
|
52
|
+
console.log(paint("Public identity and discovery for OpenClaw agents.", COLOR.dim));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function kv(label, value) {
|
|
56
|
+
console.log(`${paint(label.padEnd(14), COLOR.dim)} ${value}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function section(title) {
|
|
60
|
+
console.log(paint(title, COLOR.bold));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readJson(file) {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(String(readFileSync(file, "utf8")));
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function isSilicaClawDir(dir) {
|
|
72
|
+
const pkgPath = join(dir, "package.json");
|
|
73
|
+
if (!existsSync(pkgPath)) return false;
|
|
74
|
+
const pkg = readJson(pkgPath);
|
|
75
|
+
if (!pkg || typeof pkg !== "object") return false;
|
|
76
|
+
const name = String(pkg.name || "");
|
|
77
|
+
if (name === "@silicaclaw/cli" || name === "silicaclaw") return true;
|
|
78
|
+
const scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
|
|
79
|
+
return Boolean(scripts.gateway || scripts["local-console"] || scripts["public-explorer"]);
|
|
80
|
+
}
|
|
81
|
+
|
|
16
82
|
function parseFlag(name, fallback = "") {
|
|
17
83
|
const prefix = `--${name}=`;
|
|
18
84
|
for (const item of argv) {
|
|
@@ -27,17 +93,17 @@ function hasFlag(name) {
|
|
|
27
93
|
|
|
28
94
|
function detectAppDir() {
|
|
29
95
|
const envDir = process.env.SILICACLAW_APP_DIR;
|
|
30
|
-
if (envDir &&
|
|
96
|
+
if (envDir && isSilicaClawDir(envDir)) {
|
|
31
97
|
return resolve(envDir);
|
|
32
98
|
}
|
|
33
99
|
|
|
34
100
|
const cwd = process.cwd();
|
|
35
|
-
if (
|
|
101
|
+
if (isSilicaClawDir(cwd)) {
|
|
36
102
|
return resolve(cwd);
|
|
37
103
|
}
|
|
38
104
|
|
|
39
105
|
const homeCandidate = join(homedir(), "silicaclaw");
|
|
40
|
-
if (
|
|
106
|
+
if (isSilicaClawDir(homeCandidate)) {
|
|
41
107
|
return resolve(homeCandidate);
|
|
42
108
|
}
|
|
43
109
|
|
|
@@ -97,7 +163,7 @@ async function stopPid(pid, name) {
|
|
|
97
163
|
}
|
|
98
164
|
}
|
|
99
165
|
if (isRunning(pid)) {
|
|
100
|
-
console.error(
|
|
166
|
+
console.error(`${paint("Failed to stop", COLOR.bold, COLOR.red)} ${name} (pid=${pid})`);
|
|
101
167
|
}
|
|
102
168
|
}
|
|
103
169
|
|
|
@@ -109,7 +175,7 @@ function parseMode(raw) {
|
|
|
109
175
|
|
|
110
176
|
function adapterForMode(mode) {
|
|
111
177
|
if (mode === "lan") return "real-preview";
|
|
112
|
-
if (mode === "global-preview") return "
|
|
178
|
+
if (mode === "global-preview") return "relay-preview";
|
|
113
179
|
return "local-event-bus";
|
|
114
180
|
}
|
|
115
181
|
|
|
@@ -153,53 +219,166 @@ function readState() {
|
|
|
153
219
|
}
|
|
154
220
|
|
|
155
221
|
function printHelp() {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
silicaclaw gateway
|
|
161
|
-
silicaclaw gateway
|
|
162
|
-
silicaclaw gateway
|
|
163
|
-
silicaclaw gateway
|
|
164
|
-
silicaclaw gateway logs [local-console|signaling]
|
|
165
|
-
|
|
166
|
-
Notes:
|
|
167
|
-
- Default app dir: current directory; fallback: ~/silicaclaw
|
|
168
|
-
- State dir: .silicaclaw/gateway
|
|
169
|
-
- global-preview + localhost signaling URL will auto-start signaling server
|
|
170
|
-
`.trim());
|
|
222
|
+
headline();
|
|
223
|
+
console.log("");
|
|
224
|
+
section("Gateway");
|
|
225
|
+
kv("Start", "silicaclaw gateway start");
|
|
226
|
+
kv("Stop", "silicaclaw gateway stop");
|
|
227
|
+
kv("Restart", "silicaclaw gateway restart");
|
|
228
|
+
kv("Status", "silicaclaw gateway status");
|
|
229
|
+
kv("Logs", "silicaclaw gateway logs local-console");
|
|
171
230
|
}
|
|
172
231
|
|
|
173
|
-
function
|
|
232
|
+
function buildStatusPayload() {
|
|
174
233
|
const localPid = readPid(CONSOLE_PID_FILE);
|
|
175
234
|
const sigPid = readPid(SIGNALING_PID_FILE);
|
|
176
235
|
const state = readState();
|
|
177
|
-
const
|
|
236
|
+
const localListener = listeningProcessOnPort(4310);
|
|
237
|
+
const signalingListener = listeningProcessOnPort(4510);
|
|
238
|
+
return {
|
|
178
239
|
app_dir: APP_DIR,
|
|
179
240
|
mode: state?.mode || "unknown",
|
|
180
241
|
adapter: state?.adapter || "unknown",
|
|
181
242
|
local_console: {
|
|
182
243
|
pid: localPid,
|
|
183
|
-
running:
|
|
244
|
+
running: Boolean(localListener),
|
|
184
245
|
log_file: CONSOLE_LOG_FILE,
|
|
185
246
|
},
|
|
186
247
|
signaling: {
|
|
187
248
|
pid: sigPid,
|
|
188
|
-
running:
|
|
249
|
+
running: Boolean(signalingListener),
|
|
189
250
|
log_file: SIGNALING_LOG_FILE,
|
|
190
251
|
url: state?.signaling_url || null,
|
|
191
252
|
room: state?.room || null,
|
|
192
253
|
},
|
|
193
254
|
updated_at: state?.updated_at || null,
|
|
194
255
|
};
|
|
195
|
-
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function showStatusJson() {
|
|
259
|
+
console.log(JSON.stringify(buildStatusPayload(), null, 2));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function showStatusHuman() {
|
|
263
|
+
const payload = buildStatusPayload();
|
|
264
|
+
headline();
|
|
265
|
+
console.log("");
|
|
266
|
+
kv(
|
|
267
|
+
"Status",
|
|
268
|
+
payload.local_console.running
|
|
269
|
+
? `${paint("running", COLOR.green)} · ${payload.mode}`
|
|
270
|
+
: `${paint("stopped", COLOR.dim)} · ${payload.mode}`
|
|
271
|
+
);
|
|
272
|
+
if (payload.mode === "global-preview") {
|
|
273
|
+
kv("Relay", payload.signaling.url || "https://relay.silicaclaw.com");
|
|
274
|
+
kv("Room", payload.signaling.room || "silicaclaw-global-preview");
|
|
275
|
+
}
|
|
276
|
+
if (payload.local_console.running) kv("Open", "http://localhost:4310");
|
|
277
|
+
return payload;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function printConnectionSummary(status, verb = "Started") {
|
|
281
|
+
if (!status?.local_console?.running) return;
|
|
282
|
+
headline();
|
|
283
|
+
console.log("");
|
|
284
|
+
kv("Status", `${verb.toLowerCase()} · ${status.mode}`);
|
|
285
|
+
kv("Console", `http://localhost:4310`);
|
|
286
|
+
if (status.mode === "global-preview") {
|
|
287
|
+
const signalingUrl = status?.signaling?.url || "https://relay.silicaclaw.com";
|
|
288
|
+
const room = status?.signaling?.room || "silicaclaw-global-preview";
|
|
289
|
+
kv("Relay", signalingUrl);
|
|
290
|
+
kv("Room", room);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function listeningProcessOnPort(port) {
|
|
295
|
+
try {
|
|
296
|
+
const pidRes = spawnSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
297
|
+
encoding: "utf8",
|
|
298
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
299
|
+
});
|
|
300
|
+
const pid = String(pidRes.stdout || "")
|
|
301
|
+
.split(/\r?\n/)
|
|
302
|
+
.map((s) => s.trim())
|
|
303
|
+
.find(Boolean);
|
|
304
|
+
if (!pid) return null;
|
|
305
|
+
|
|
306
|
+
const cmdRes = spawnSync("ps", ["-p", pid, "-o", "command="], {
|
|
307
|
+
encoding: "utf8",
|
|
308
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
309
|
+
});
|
|
310
|
+
const command = String(cmdRes.stdout || "").trim() || "unknown";
|
|
311
|
+
return { pid, command };
|
|
312
|
+
} catch {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function normalizePathForMatch(value) {
|
|
318
|
+
return String(value || "").replace(/\\/g, "/");
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isOwnedListener(listener, kind) {
|
|
322
|
+
if (!listener?.command) return false;
|
|
323
|
+
const command = normalizePathForMatch(listener.command).toLowerCase();
|
|
324
|
+
const appDir = normalizePathForMatch(APP_DIR).toLowerCase();
|
|
325
|
+
const rootDir = normalizePathForMatch(ROOT_DIR).toLowerCase();
|
|
326
|
+
const inWorkspace = command.includes(appDir) || command.includes(rootDir);
|
|
327
|
+
if (!inWorkspace) return false;
|
|
328
|
+
if (kind === "local-console") {
|
|
329
|
+
return (
|
|
330
|
+
command.includes("@silicaclaw/local-console") ||
|
|
331
|
+
command.includes("/apps/local-console/") ||
|
|
332
|
+
command.includes("src/server.ts") ||
|
|
333
|
+
command.includes("dist/server.js")
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
if (kind === "signaling") {
|
|
337
|
+
return command.includes("webrtc-signaling-server.mjs") || command.includes("npm run webrtc-signaling");
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function stopOwnedListener(port, kind) {
|
|
343
|
+
const listener = listeningProcessOnPort(port);
|
|
344
|
+
if (!listener || !isOwnedListener(listener, kind)) return false;
|
|
345
|
+
await stopPid(Number(listener.pid), kind);
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function printStopSummary() {
|
|
350
|
+
const localListener = listeningProcessOnPort(4310);
|
|
351
|
+
const signalingListener = listeningProcessOnPort(4510);
|
|
352
|
+
headline();
|
|
353
|
+
console.log("");
|
|
354
|
+
kv("Status", "stopped");
|
|
355
|
+
if (!localListener) {
|
|
356
|
+
kv("Console", paint("stopped", COLOR.green));
|
|
357
|
+
} else {
|
|
358
|
+
kv("Console", `${paint("still busy", COLOR.yellow)} (pid=${localListener.pid})`);
|
|
359
|
+
kv("Inspect", "lsof -nP -iTCP:4310 -sTCP:LISTEN");
|
|
360
|
+
kv("Stop pid", `kill ${localListener.pid}`);
|
|
361
|
+
}
|
|
362
|
+
if (!signalingListener) {
|
|
363
|
+
kv("Relay port", paint("stopped", COLOR.green));
|
|
364
|
+
} else {
|
|
365
|
+
kv("Relay port", `${paint("still busy", COLOR.yellow)} (pid=${signalingListener.pid})`);
|
|
366
|
+
kv("Inspect", "lsof -nP -iTCP:4510 -sTCP:LISTEN");
|
|
367
|
+
kv("Stop pid", `kill ${signalingListener.pid}`);
|
|
368
|
+
}
|
|
196
369
|
}
|
|
197
370
|
|
|
198
371
|
function tailFile(file, lines = 80) {
|
|
199
372
|
if (!existsSync(file)) {
|
|
200
|
-
|
|
373
|
+
headline();
|
|
374
|
+
console.log("");
|
|
375
|
+
kv("Logs", "not found");
|
|
201
376
|
return;
|
|
202
377
|
}
|
|
378
|
+
headline();
|
|
379
|
+
console.log("");
|
|
380
|
+
kv("Logs", file);
|
|
381
|
+
console.log("");
|
|
203
382
|
const text = String(readFileSync(file, "utf8"));
|
|
204
383
|
const out = text.split(/\r?\n/).slice(-lines).join("\n");
|
|
205
384
|
console.log(out);
|
|
@@ -210,29 +389,29 @@ async function stopAll() {
|
|
|
210
389
|
const sigPid = readPid(SIGNALING_PID_FILE);
|
|
211
390
|
await stopPid(localPid, "local-console");
|
|
212
391
|
await stopPid(sigPid, "signaling");
|
|
392
|
+
await stopOwnedListener(4310, "local-console");
|
|
393
|
+
await stopOwnedListener(4510, "signaling");
|
|
213
394
|
removeFileIfExists(CONSOLE_PID_FILE);
|
|
214
395
|
removeFileIfExists(SIGNALING_PID_FILE);
|
|
215
396
|
writeState({
|
|
216
397
|
...(readState() || {}),
|
|
217
398
|
updated_at: Date.now(),
|
|
218
399
|
});
|
|
219
|
-
console.log("gateway stopped");
|
|
220
400
|
}
|
|
221
401
|
|
|
222
402
|
function startAll() {
|
|
223
403
|
ensureStateDir();
|
|
224
404
|
|
|
225
|
-
const mode = parseMode(parseFlag("mode", process.env.NETWORK_MODE || "
|
|
405
|
+
const mode = parseMode(parseFlag("mode", process.env.NETWORK_MODE || "global-preview"));
|
|
226
406
|
const adapter = adapterForMode(mode);
|
|
227
|
-
const signalingUrl = parseFlag("signaling-url", process.env.WEBRTC_SIGNALING_URL || "
|
|
228
|
-
const room = parseFlag("room", process.env.WEBRTC_ROOM || "silicaclaw-
|
|
407
|
+
const signalingUrl = parseFlag("signaling-url", process.env.WEBRTC_SIGNALING_URL || "https://relay.silicaclaw.com");
|
|
408
|
+
const room = parseFlag("room", process.env.WEBRTC_ROOM || "silicaclaw-global-preview");
|
|
229
409
|
const shouldDisableSignaling = hasFlag("no-signaling");
|
|
230
410
|
|
|
231
411
|
const currentLocalPid = readPid(CONSOLE_PID_FILE);
|
|
232
412
|
const currentSigPid = readPid(SIGNALING_PID_FILE);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
} else {
|
|
413
|
+
let localPid = currentLocalPid;
|
|
414
|
+
if (!isRunning(currentLocalPid)) {
|
|
236
415
|
removeFileIfExists(CONSOLE_PID_FILE);
|
|
237
416
|
const env = {
|
|
238
417
|
NETWORK_ADAPTER: adapter,
|
|
@@ -240,8 +419,7 @@ function startAll() {
|
|
|
240
419
|
WEBRTC_SIGNALING_URL: signalingUrl,
|
|
241
420
|
WEBRTC_ROOM: room,
|
|
242
421
|
};
|
|
243
|
-
|
|
244
|
-
console.log(`local-console started (pid=${pid})`);
|
|
422
|
+
localPid = spawnBackground("npm", ["run", "local-console"], env, CONSOLE_LOG_FILE, CONSOLE_PID_FILE);
|
|
245
423
|
}
|
|
246
424
|
|
|
247
425
|
const { host, port } = parseUrlHostPort(signalingUrl);
|
|
@@ -250,19 +428,17 @@ function startAll() {
|
|
|
250
428
|
!shouldDisableSignaling &&
|
|
251
429
|
(host === "localhost" || host === "127.0.0.1");
|
|
252
430
|
|
|
431
|
+
let signalingPid = currentSigPid;
|
|
253
432
|
if (shouldAutoStartSignaling) {
|
|
254
|
-
if (isRunning(currentSigPid)) {
|
|
255
|
-
console.log(`signaling already running (pid=${currentSigPid})`);
|
|
256
|
-
} else {
|
|
433
|
+
if (!isRunning(currentSigPid)) {
|
|
257
434
|
removeFileIfExists(SIGNALING_PID_FILE);
|
|
258
|
-
|
|
435
|
+
signalingPid = spawnBackground(
|
|
259
436
|
"npm",
|
|
260
437
|
["run", "webrtc-signaling"],
|
|
261
438
|
{ PORT: String(port) },
|
|
262
439
|
SIGNALING_LOG_FILE,
|
|
263
440
|
SIGNALING_PID_FILE,
|
|
264
441
|
);
|
|
265
|
-
console.log(`signaling started (pid=${pid}, port=${port})`);
|
|
266
442
|
}
|
|
267
443
|
}
|
|
268
444
|
|
|
@@ -274,6 +450,7 @@ function startAll() {
|
|
|
274
450
|
room,
|
|
275
451
|
updated_at: Date.now(),
|
|
276
452
|
});
|
|
453
|
+
return { localPid, signalingPid: shouldAutoStartSignaling ? signalingPid : null };
|
|
277
454
|
}
|
|
278
455
|
|
|
279
456
|
async function main() {
|
|
@@ -282,23 +459,29 @@ async function main() {
|
|
|
282
459
|
return;
|
|
283
460
|
}
|
|
284
461
|
if (cmd === "status") {
|
|
285
|
-
|
|
462
|
+
if (hasFlag("json")) {
|
|
463
|
+
showStatusJson();
|
|
464
|
+
} else {
|
|
465
|
+
showStatusHuman();
|
|
466
|
+
}
|
|
286
467
|
return;
|
|
287
468
|
}
|
|
288
469
|
if (cmd === "start") {
|
|
289
470
|
startAll();
|
|
290
|
-
|
|
471
|
+
const status = buildStatusPayload();
|
|
472
|
+
printConnectionSummary(status, "Started");
|
|
291
473
|
return;
|
|
292
474
|
}
|
|
293
475
|
if (cmd === "stop") {
|
|
294
476
|
await stopAll();
|
|
295
|
-
|
|
477
|
+
printStopSummary();
|
|
296
478
|
return;
|
|
297
479
|
}
|
|
298
480
|
if (cmd === "restart") {
|
|
299
481
|
await stopAll();
|
|
300
482
|
startAll();
|
|
301
|
-
|
|
483
|
+
const status = buildStatusPayload();
|
|
484
|
+
printConnectionSummary(status, "Restarted");
|
|
302
485
|
return;
|
|
303
486
|
}
|
|
304
487
|
if (cmd === "logs") {
|
|
@@ -315,7 +498,9 @@ async function main() {
|
|
|
315
498
|
}
|
|
316
499
|
|
|
317
500
|
main().catch((error) => {
|
|
501
|
+
headline();
|
|
502
|
+
console.log("");
|
|
503
|
+
console.error(paint("Gateway command failed", COLOR.bold, COLOR.red));
|
|
318
504
|
console.error(error?.message || String(error));
|
|
319
505
|
process.exit(1);
|
|
320
506
|
});
|
|
321
|
-
|
|
@@ -5,8 +5,9 @@ import { randomUUID, createHash } from 'crypto';
|
|
|
5
5
|
const port = Number(process.env.PORT || process.env.WEBRTC_SIGNALING_PORT || 4510);
|
|
6
6
|
const PEER_STALE_MS = Number(process.env.WEBRTC_SIGNALING_PEER_STALE_MS || 120000);
|
|
7
7
|
const SIGNAL_DEDUPE_WINDOW_MS = Number(process.env.WEBRTC_SIGNALING_DEDUPE_WINDOW_MS || 60000);
|
|
8
|
+
const TOUCH_WRITE_INTERVAL_MS = Number(process.env.WEBRTC_SIGNALING_TOUCH_WRITE_INTERVAL_MS || 30000);
|
|
8
9
|
|
|
9
|
-
/** @type {Map<string, {peers: Map<string, {last_seen_at:number}>, queues: Map<string, any[]>, signal_fingerprints: Map<string, number>}>} */
|
|
10
|
+
/** @type {Map<string, {peers: Map<string, {last_seen_at:number}>, queues: Map<string, any[]>, relay_queues: Map<string, any[]>, signal_fingerprints: Map<string, number>}>} */
|
|
10
11
|
const rooms = new Map();
|
|
11
12
|
|
|
12
13
|
const counters = {
|
|
@@ -24,6 +25,7 @@ function getRoom(roomId) {
|
|
|
24
25
|
rooms.set(id, {
|
|
25
26
|
peers: new Map(),
|
|
26
27
|
queues: new Map(),
|
|
28
|
+
relay_queues: new Map(),
|
|
27
29
|
signal_fingerprints: new Map(),
|
|
28
30
|
});
|
|
29
31
|
}
|
|
@@ -73,6 +75,7 @@ function cleanupRoom(roomId) {
|
|
|
73
75
|
if (peer.last_seen_at < threshold) {
|
|
74
76
|
room.peers.delete(peerId);
|
|
75
77
|
room.queues.delete(peerId);
|
|
78
|
+
room.relay_queues.delete(peerId);
|
|
76
79
|
counters.stale_peers_cleaned_total += 1;
|
|
77
80
|
}
|
|
78
81
|
}
|
|
@@ -88,14 +91,21 @@ function cleanupRoom(roomId) {
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
function touchPeer(room, peerId) {
|
|
94
|
+
const ts = now();
|
|
95
|
+
const previous = room.peers.get(peerId)?.last_seen_at || 0;
|
|
96
|
+
const shouldWrite = !previous || ts - previous >= TOUCH_WRITE_INTERVAL_MS;
|
|
91
97
|
if (!room.peers.has(peerId)) {
|
|
92
|
-
room.peers.set(peerId, { last_seen_at:
|
|
98
|
+
room.peers.set(peerId, { last_seen_at: ts });
|
|
93
99
|
} else {
|
|
94
|
-
room.peers.get(peerId).last_seen_at =
|
|
100
|
+
room.peers.get(peerId).last_seen_at = shouldWrite ? ts : previous;
|
|
95
101
|
}
|
|
96
102
|
if (!room.queues.has(peerId)) {
|
|
97
103
|
room.queues.set(peerId, []);
|
|
98
104
|
}
|
|
105
|
+
if (!room.relay_queues.has(peerId)) {
|
|
106
|
+
room.relay_queues.set(peerId, []);
|
|
107
|
+
}
|
|
108
|
+
return shouldWrite;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
function isValidSignalPayload(body) {
|
|
@@ -148,7 +158,35 @@ const server = http.createServer(async (req, res) => {
|
|
|
148
158
|
const roomId = String(url.searchParams.get('room') || 'silicaclaw-room');
|
|
149
159
|
const room = getRoom(roomId);
|
|
150
160
|
cleanupRoom(roomId);
|
|
151
|
-
return json(res, 200, {
|
|
161
|
+
return json(res, 200, {
|
|
162
|
+
ok: true,
|
|
163
|
+
room: roomId,
|
|
164
|
+
peer_count: room.peers.size,
|
|
165
|
+
peers: Array.from(room.peers.keys()),
|
|
166
|
+
peer_details: Array.from(room.peers.entries()).map(([peerId, peer]) => ({
|
|
167
|
+
peer_id: peerId,
|
|
168
|
+
last_seen_at: peer.last_seen_at,
|
|
169
|
+
signal_queue_size: (room.queues.get(peerId) || []).length,
|
|
170
|
+
relay_queue_size: (room.relay_queues.get(peerId) || []).length,
|
|
171
|
+
})),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (req.method === 'GET' && url.pathname === '/room') {
|
|
176
|
+
const roomId = String(url.searchParams.get('room') || 'silicaclaw-room');
|
|
177
|
+
const room = getRoom(roomId);
|
|
178
|
+
cleanupRoom(roomId);
|
|
179
|
+
return json(res, 200, {
|
|
180
|
+
ok: true,
|
|
181
|
+
room: roomId,
|
|
182
|
+
peer_count: room.peers.size,
|
|
183
|
+
peers: Array.from(room.peers.entries()).map(([peerId, peer]) => ({
|
|
184
|
+
peer_id: peerId,
|
|
185
|
+
last_seen_at: peer.last_seen_at,
|
|
186
|
+
signal_queue_size: (room.queues.get(peerId) || []).length,
|
|
187
|
+
relay_queue_size: (room.relay_queues.get(peerId) || []).length,
|
|
188
|
+
})),
|
|
189
|
+
});
|
|
152
190
|
}
|
|
153
191
|
|
|
154
192
|
if (req.method === 'GET' && url.pathname === '/poll') {
|
|
@@ -165,7 +203,24 @@ const server = http.createServer(async (req, res) => {
|
|
|
165
203
|
|
|
166
204
|
const queue = room.queues.get(peerId) || [];
|
|
167
205
|
room.queues.set(peerId, []);
|
|
168
|
-
return json(res, 200, { ok: true, messages: queue });
|
|
206
|
+
return json(res, 200, { ok: true, messages: queue, peers: Array.from(room.peers.keys()) });
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (req.method === 'GET' && url.pathname === '/relay/poll') {
|
|
210
|
+
const roomId = String(url.searchParams.get('room') || 'silicaclaw-room');
|
|
211
|
+
const peerId = String(url.searchParams.get('peer_id') || '');
|
|
212
|
+
if (!peerId) {
|
|
213
|
+
counters.invalid_payload_total += 1;
|
|
214
|
+
return json(res, 400, { ok: false, error: 'missing_peer_id' });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const room = getRoom(roomId);
|
|
218
|
+
touchPeer(room, peerId);
|
|
219
|
+
cleanupRoom(roomId);
|
|
220
|
+
|
|
221
|
+
const queue = room.relay_queues.get(peerId) || [];
|
|
222
|
+
room.relay_queues.set(peerId, []);
|
|
223
|
+
return json(res, 200, { ok: true, messages: queue, peers: Array.from(room.peers.keys()) });
|
|
169
224
|
}
|
|
170
225
|
|
|
171
226
|
if (req.method === 'POST' && url.pathname === '/join') {
|
|
@@ -193,6 +248,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
193
248
|
if (peerId) {
|
|
194
249
|
room.peers.delete(peerId);
|
|
195
250
|
room.queues.delete(peerId);
|
|
251
|
+
room.relay_queues.delete(peerId);
|
|
196
252
|
counters.leave_total += 1;
|
|
197
253
|
} else {
|
|
198
254
|
counters.invalid_payload_total += 1;
|
|
@@ -240,6 +296,34 @@ const server = http.createServer(async (req, res) => {
|
|
|
240
296
|
return json(res, 200, { ok: true });
|
|
241
297
|
}
|
|
242
298
|
|
|
299
|
+
if (req.method === 'POST' && url.pathname === '/relay/publish') {
|
|
300
|
+
const body = await parseBody(req);
|
|
301
|
+
const roomId = String(body.room || 'silicaclaw-room');
|
|
302
|
+
const peerId = String(body.peer_id || '');
|
|
303
|
+
const envelope = body.envelope;
|
|
304
|
+
if (!peerId || typeof envelope !== 'object' || envelope === null) {
|
|
305
|
+
counters.invalid_payload_total += 1;
|
|
306
|
+
return json(res, 400, { ok: false, error: 'invalid_relay_payload' });
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const room = getRoom(roomId);
|
|
310
|
+
touchPeer(room, peerId);
|
|
311
|
+
|
|
312
|
+
for (const targetPeerId of room.peers.keys()) {
|
|
313
|
+
if (targetPeerId === peerId) continue;
|
|
314
|
+
if (!room.relay_queues.has(targetPeerId)) room.relay_queues.set(targetPeerId, []);
|
|
315
|
+
room.relay_queues.get(targetPeerId).push({
|
|
316
|
+
id: String(body.id || randomUUID()),
|
|
317
|
+
room: roomId,
|
|
318
|
+
from_peer_id: peerId,
|
|
319
|
+
envelope,
|
|
320
|
+
at: now(),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return json(res, 200, { ok: true, delivered_to: Math.max(0, room.peers.size - 1) });
|
|
325
|
+
}
|
|
326
|
+
|
|
243
327
|
return json(res, 404, { ok: false, error: 'not_found' });
|
|
244
328
|
});
|
|
245
329
|
|