@silicaclaw/cli 1.0.0-beta.8 → 2026.3.18-2
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 +93 -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 +2113 -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 +269 -43
- package/scripts/silicaclaw-cli.mjs +418 -56
- package/scripts/silicaclaw-gateway.mjs +197 -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,134 @@ 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 printStopSummary() {
|
|
318
|
+
const localListener = listeningProcessOnPort(4310);
|
|
319
|
+
const signalingListener = listeningProcessOnPort(4510);
|
|
320
|
+
headline();
|
|
321
|
+
console.log("");
|
|
322
|
+
kv("Status", "stopped");
|
|
323
|
+
if (!localListener) {
|
|
324
|
+
kv("Console", paint("stopped", COLOR.green));
|
|
325
|
+
} else {
|
|
326
|
+
kv("Console", `${paint("still busy", COLOR.yellow)} (pid=${localListener.pid})`);
|
|
327
|
+
kv("Inspect", "lsof -nP -iTCP:4310 -sTCP:LISTEN");
|
|
328
|
+
kv("Stop pid", `kill ${localListener.pid}`);
|
|
329
|
+
}
|
|
330
|
+
if (!signalingListener) {
|
|
331
|
+
kv("Relay port", paint("stopped", COLOR.green));
|
|
332
|
+
} else {
|
|
333
|
+
kv("Relay port", `${paint("still busy", COLOR.yellow)} (pid=${signalingListener.pid})`);
|
|
334
|
+
kv("Inspect", "lsof -nP -iTCP:4510 -sTCP:LISTEN");
|
|
335
|
+
kv("Stop pid", `kill ${signalingListener.pid}`);
|
|
336
|
+
}
|
|
196
337
|
}
|
|
197
338
|
|
|
198
339
|
function tailFile(file, lines = 80) {
|
|
199
340
|
if (!existsSync(file)) {
|
|
200
|
-
|
|
341
|
+
headline();
|
|
342
|
+
console.log("");
|
|
343
|
+
kv("Logs", "not found");
|
|
201
344
|
return;
|
|
202
345
|
}
|
|
346
|
+
headline();
|
|
347
|
+
console.log("");
|
|
348
|
+
kv("Logs", file);
|
|
349
|
+
console.log("");
|
|
203
350
|
const text = String(readFileSync(file, "utf8"));
|
|
204
351
|
const out = text.split(/\r?\n/).slice(-lines).join("\n");
|
|
205
352
|
console.log(out);
|
|
@@ -216,23 +363,21 @@ async function stopAll() {
|
|
|
216
363
|
...(readState() || {}),
|
|
217
364
|
updated_at: Date.now(),
|
|
218
365
|
});
|
|
219
|
-
console.log("gateway stopped");
|
|
220
366
|
}
|
|
221
367
|
|
|
222
368
|
function startAll() {
|
|
223
369
|
ensureStateDir();
|
|
224
370
|
|
|
225
|
-
const mode = parseMode(parseFlag("mode", process.env.NETWORK_MODE || "
|
|
371
|
+
const mode = parseMode(parseFlag("mode", process.env.NETWORK_MODE || "global-preview"));
|
|
226
372
|
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-
|
|
373
|
+
const signalingUrl = parseFlag("signaling-url", process.env.WEBRTC_SIGNALING_URL || "https://relay.silicaclaw.com");
|
|
374
|
+
const room = parseFlag("room", process.env.WEBRTC_ROOM || "silicaclaw-global-preview");
|
|
229
375
|
const shouldDisableSignaling = hasFlag("no-signaling");
|
|
230
376
|
|
|
231
377
|
const currentLocalPid = readPid(CONSOLE_PID_FILE);
|
|
232
378
|
const currentSigPid = readPid(SIGNALING_PID_FILE);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
} else {
|
|
379
|
+
let localPid = currentLocalPid;
|
|
380
|
+
if (!isRunning(currentLocalPid)) {
|
|
236
381
|
removeFileIfExists(CONSOLE_PID_FILE);
|
|
237
382
|
const env = {
|
|
238
383
|
NETWORK_ADAPTER: adapter,
|
|
@@ -240,8 +385,7 @@ function startAll() {
|
|
|
240
385
|
WEBRTC_SIGNALING_URL: signalingUrl,
|
|
241
386
|
WEBRTC_ROOM: room,
|
|
242
387
|
};
|
|
243
|
-
|
|
244
|
-
console.log(`local-console started (pid=${pid})`);
|
|
388
|
+
localPid = spawnBackground("npm", ["run", "local-console"], env, CONSOLE_LOG_FILE, CONSOLE_PID_FILE);
|
|
245
389
|
}
|
|
246
390
|
|
|
247
391
|
const { host, port } = parseUrlHostPort(signalingUrl);
|
|
@@ -250,19 +394,17 @@ function startAll() {
|
|
|
250
394
|
!shouldDisableSignaling &&
|
|
251
395
|
(host === "localhost" || host === "127.0.0.1");
|
|
252
396
|
|
|
397
|
+
let signalingPid = currentSigPid;
|
|
253
398
|
if (shouldAutoStartSignaling) {
|
|
254
|
-
if (isRunning(currentSigPid)) {
|
|
255
|
-
console.log(`signaling already running (pid=${currentSigPid})`);
|
|
256
|
-
} else {
|
|
399
|
+
if (!isRunning(currentSigPid)) {
|
|
257
400
|
removeFileIfExists(SIGNALING_PID_FILE);
|
|
258
|
-
|
|
401
|
+
signalingPid = spawnBackground(
|
|
259
402
|
"npm",
|
|
260
403
|
["run", "webrtc-signaling"],
|
|
261
404
|
{ PORT: String(port) },
|
|
262
405
|
SIGNALING_LOG_FILE,
|
|
263
406
|
SIGNALING_PID_FILE,
|
|
264
407
|
);
|
|
265
|
-
console.log(`signaling started (pid=${pid}, port=${port})`);
|
|
266
408
|
}
|
|
267
409
|
}
|
|
268
410
|
|
|
@@ -274,6 +416,7 @@ function startAll() {
|
|
|
274
416
|
room,
|
|
275
417
|
updated_at: Date.now(),
|
|
276
418
|
});
|
|
419
|
+
return { localPid, signalingPid: shouldAutoStartSignaling ? signalingPid : null };
|
|
277
420
|
}
|
|
278
421
|
|
|
279
422
|
async function main() {
|
|
@@ -282,23 +425,29 @@ async function main() {
|
|
|
282
425
|
return;
|
|
283
426
|
}
|
|
284
427
|
if (cmd === "status") {
|
|
285
|
-
|
|
428
|
+
if (hasFlag("json")) {
|
|
429
|
+
showStatusJson();
|
|
430
|
+
} else {
|
|
431
|
+
showStatusHuman();
|
|
432
|
+
}
|
|
286
433
|
return;
|
|
287
434
|
}
|
|
288
435
|
if (cmd === "start") {
|
|
289
436
|
startAll();
|
|
290
|
-
|
|
437
|
+
const status = buildStatusPayload();
|
|
438
|
+
printConnectionSummary(status, "Started");
|
|
291
439
|
return;
|
|
292
440
|
}
|
|
293
441
|
if (cmd === "stop") {
|
|
294
442
|
await stopAll();
|
|
295
|
-
|
|
443
|
+
printStopSummary();
|
|
296
444
|
return;
|
|
297
445
|
}
|
|
298
446
|
if (cmd === "restart") {
|
|
299
447
|
await stopAll();
|
|
300
448
|
startAll();
|
|
301
|
-
|
|
449
|
+
const status = buildStatusPayload();
|
|
450
|
+
printConnectionSummary(status, "Restarted");
|
|
302
451
|
return;
|
|
303
452
|
}
|
|
304
453
|
if (cmd === "logs") {
|
|
@@ -315,7 +464,9 @@ async function main() {
|
|
|
315
464
|
}
|
|
316
465
|
|
|
317
466
|
main().catch((error) => {
|
|
467
|
+
headline();
|
|
468
|
+
console.log("");
|
|
469
|
+
console.error(paint("Gateway command failed", COLOR.bold, COLOR.red));
|
|
318
470
|
console.error(error?.message || String(error));
|
|
319
471
|
process.exit(1);
|
|
320
472
|
});
|
|
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
|
|