@silicaclaw/cli 2026.3.18-3 → 2026.3.18-4
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 +10 -0
- package/VERSION +1 -1
- package/apps/local-console/package.json +1 -1
- package/apps/local-console/public/index.html +793 -224
- package/apps/local-console/src/server.ts +154 -1
- package/package.json +1 -1
- package/scripts/silicaclaw-gateway.mjs +76 -9
|
@@ -64,6 +64,7 @@ const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTE
|
|
|
64
64
|
const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
|
|
65
65
|
const NETWORK_UDP_BROADCAST_ADDRESS = process.env.NETWORK_UDP_BROADCAST_ADDRESS || "255.255.255.255";
|
|
66
66
|
const NETWORK_PEER_ID = process.env.NETWORK_PEER_ID;
|
|
67
|
+
const NETWORK_MODE = process.env.NETWORK_MODE || "";
|
|
67
68
|
const WEBRTC_SIGNALING_URL = process.env.WEBRTC_SIGNALING_URL || "https://relay.silicaclaw.com";
|
|
68
69
|
const WEBRTC_SIGNALING_URLS = process.env.WEBRTC_SIGNALING_URLS || "";
|
|
69
70
|
const WEBRTC_ROOM = process.env.WEBRTC_ROOM || "silicaclaw-global-preview";
|
|
@@ -295,7 +296,19 @@ class LocalNodeService {
|
|
|
295
296
|
await this.network.stop();
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
private ensureLocalDirectoryBaseline(): void {
|
|
300
|
+
if (this.profile) {
|
|
301
|
+
this.directory = ingestProfileRecord(this.directory, { type: "profile", profile: this.profile });
|
|
302
|
+
}
|
|
303
|
+
if (this.identity && this.profile?.public_enabled && this.broadcastEnabled) {
|
|
304
|
+
const currentSeenAt = this.directory.presence[this.identity.agent_id] ?? 0;
|
|
305
|
+
const baselineSeenAt = Math.max(currentSeenAt, this.lastBroadcastAt || Date.now());
|
|
306
|
+
this.directory = ingestPresenceRecord(this.directory, signPresence(this.identity, baselineSeenAt));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
298
310
|
getOverview() {
|
|
311
|
+
this.ensureLocalDirectoryBaseline();
|
|
299
312
|
this.compactCacheInMemory();
|
|
300
313
|
const profiles = Object.values(this.directory.profiles);
|
|
301
314
|
const onlineCount = profiles.filter((profile) =>
|
|
@@ -773,11 +786,13 @@ class LocalNodeService {
|
|
|
773
786
|
}
|
|
774
787
|
|
|
775
788
|
getDirectory(): DirectoryState {
|
|
789
|
+
this.ensureLocalDirectoryBaseline();
|
|
776
790
|
this.compactCacheInMemory();
|
|
777
791
|
return this.directory;
|
|
778
792
|
}
|
|
779
793
|
|
|
780
794
|
search(keyword: string): PublicProfileSummary[] {
|
|
795
|
+
this.ensureLocalDirectoryBaseline();
|
|
781
796
|
this.compactCacheInMemory();
|
|
782
797
|
return searchDirectory(this.directory, keyword, { presenceTTLms: PRESENCE_TTL_MS }).map((profile) => {
|
|
783
798
|
const lastSeenAt = this.directory.presence[profile.agent_id] ?? 0;
|
|
@@ -1383,7 +1398,13 @@ class LocalNodeService {
|
|
|
1383
1398
|
}
|
|
1384
1399
|
|
|
1385
1400
|
private applyResolvedNetworkConfig(): void {
|
|
1386
|
-
|
|
1401
|
+
const modeEnv = String(NETWORK_MODE || "").trim();
|
|
1402
|
+
const resolvedMode =
|
|
1403
|
+
modeEnv === "local" || modeEnv === "lan" || modeEnv === "global-preview"
|
|
1404
|
+
? modeEnv
|
|
1405
|
+
: this.socialConfig.network.mode || "lan";
|
|
1406
|
+
|
|
1407
|
+
this.networkMode = resolvedMode;
|
|
1387
1408
|
this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || "silicaclaw.preview";
|
|
1388
1409
|
this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT || 44123);
|
|
1389
1410
|
|
|
@@ -1503,10 +1524,76 @@ function resolveLocalConsoleStaticDir(): string {
|
|
|
1503
1524
|
return candidates[0];
|
|
1504
1525
|
}
|
|
1505
1526
|
|
|
1527
|
+
function escapeHtml(text: string): string {
|
|
1528
|
+
return String(text)
|
|
1529
|
+
.replace(/&/g, "&")
|
|
1530
|
+
.replace(/</g, "<")
|
|
1531
|
+
.replace(/>/g, ">")
|
|
1532
|
+
.replace(/"/g, """)
|
|
1533
|
+
.replace(/'/g, "'");
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function shortId(id: string): string {
|
|
1537
|
+
if (!id) return "-";
|
|
1538
|
+
return `${id.slice(0, 10)}...${id.slice(-6)}`;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
function ago(ts: number | null | undefined): string {
|
|
1542
|
+
if (!ts) return "-";
|
|
1543
|
+
const seconds = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
1544
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
1545
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
1546
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function renderBootstrapScript(payload: unknown): string {
|
|
1550
|
+
const encoded = JSON.stringify(payload).replace(/</g, "\\u003c");
|
|
1551
|
+
return `
|
|
1552
|
+
<script>
|
|
1553
|
+
(() => {
|
|
1554
|
+
const data = ${encoded};
|
|
1555
|
+
if (!data) return;
|
|
1556
|
+
const setText = (id, value) => {
|
|
1557
|
+
const el = document.getElementById(id);
|
|
1558
|
+
if (el) el.textContent = value;
|
|
1559
|
+
};
|
|
1560
|
+
const setHtml = (id, value) => {
|
|
1561
|
+
const el = document.getElementById(id);
|
|
1562
|
+
if (el) el.innerHTML = value;
|
|
1563
|
+
};
|
|
1564
|
+
if (data.integrationStatusText) {
|
|
1565
|
+
const bar = document.getElementById('integrationStatusBar');
|
|
1566
|
+
if (bar) {
|
|
1567
|
+
bar.textContent = data.integrationStatusText;
|
|
1568
|
+
if (data.integrationStatusClassName) bar.className = data.integrationStatusClassName;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
setText('socialStatusLine', data.socialStatusLineText || '');
|
|
1572
|
+
setText('socialStatusSubline', data.socialStatusSublineText || '');
|
|
1573
|
+
setText('brandVersion', data.brandVersionText || '-');
|
|
1574
|
+
setText('snapshot', data.snapshotText || '');
|
|
1575
|
+
setText('heroMode', data.heroModeText || '-');
|
|
1576
|
+
setText('heroAdapter', data.heroAdapterText || '-');
|
|
1577
|
+
setText('heroRelay', data.heroRelayText || '-');
|
|
1578
|
+
setText('heroRoom', data.heroRoomText || '-');
|
|
1579
|
+
setText('pillAdapter', data.pillAdapterText || 'adapter: -');
|
|
1580
|
+
const pillBroadcast = document.getElementById('pillBroadcast');
|
|
1581
|
+
if (pillBroadcast) {
|
|
1582
|
+
pillBroadcast.textContent = data.pillBroadcastText || 'broadcast: -';
|
|
1583
|
+
if (data.pillBroadcastClassName) pillBroadcast.className = data.pillBroadcastClassName;
|
|
1584
|
+
}
|
|
1585
|
+
setHtml('overviewCards', data.overviewCardsHtml || '');
|
|
1586
|
+
setText('agentsCountHint', data.agentsCountHintText || '0 agents');
|
|
1587
|
+
setHtml('agentsWrap', data.agentsWrapHtml || '<div class="label">No discovered agents yet.</div>');
|
|
1588
|
+
})();
|
|
1589
|
+
</script>`;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1506
1592
|
async function main() {
|
|
1507
1593
|
const app = express();
|
|
1508
1594
|
const port = Number(process.env.PORT || 4310);
|
|
1509
1595
|
const staticDir = resolveLocalConsoleStaticDir();
|
|
1596
|
+
const staticIndexFile = resolve(staticDir, "index.html");
|
|
1510
1597
|
|
|
1511
1598
|
const node = new LocalNodeService();
|
|
1512
1599
|
await node.start();
|
|
@@ -1708,6 +1795,72 @@ async function main() {
|
|
|
1708
1795
|
sendOk(res, { ok: true });
|
|
1709
1796
|
});
|
|
1710
1797
|
|
|
1798
|
+
app.get(["/", "/index.html"], (_req, res) => {
|
|
1799
|
+
const overview = node.getOverview();
|
|
1800
|
+
const discovered = node.search("");
|
|
1801
|
+
const network = node.getNetworkConfig();
|
|
1802
|
+
const integration = node.getIntegrationStatus();
|
|
1803
|
+
const overviewCardsHtml = [
|
|
1804
|
+
["Discovered", overview.discovered_count],
|
|
1805
|
+
["Online", overview.online_count],
|
|
1806
|
+
["Offline", overview.offline_count],
|
|
1807
|
+
["Presence TTL", `${Math.floor(overview.presence_ttl_ms / 1000)}s`],
|
|
1808
|
+
]
|
|
1809
|
+
.map(
|
|
1810
|
+
([k, v]) => `<div class="card"><div class="label">${escapeHtml(String(k))}</div><div class="value">${escapeHtml(String(v))}</div></div>`
|
|
1811
|
+
)
|
|
1812
|
+
.join("");
|
|
1813
|
+
const agentsWrapHtml =
|
|
1814
|
+
discovered.length === 0
|
|
1815
|
+
? `<div class="label">No discovered agents yet.</div>`
|
|
1816
|
+
: `
|
|
1817
|
+
<table class="table">
|
|
1818
|
+
<thead><tr><th>Name</th><th>Agent ID</th><th>Status</th><th>Updated</th></tr></thead>
|
|
1819
|
+
<tbody>
|
|
1820
|
+
${discovered
|
|
1821
|
+
.map(
|
|
1822
|
+
(agent) => `
|
|
1823
|
+
<tr>
|
|
1824
|
+
<td>${escapeHtml(agent.display_name || "Unnamed")}</td>
|
|
1825
|
+
<td class="mono">${escapeHtml(shortId(agent.agent_id || ""))}</td>
|
|
1826
|
+
<td class="${agent.online ? "online" : "offline"}">${agent.online ? "online" : "offline"}</td>
|
|
1827
|
+
<td>${escapeHtml(ago(agent.updated_at))}</td>
|
|
1828
|
+
</tr>`
|
|
1829
|
+
)
|
|
1830
|
+
.join("")}
|
|
1831
|
+
</tbody>
|
|
1832
|
+
</table>
|
|
1833
|
+
`;
|
|
1834
|
+
const payload = {
|
|
1835
|
+
brandVersionText: overview.app_version ? `v${overview.app_version}` : "-",
|
|
1836
|
+
snapshotText: [
|
|
1837
|
+
`app_version: ${overview.app_version || "-"}`,
|
|
1838
|
+
`agent_id: ${overview.agent_id || "-"}`,
|
|
1839
|
+
`public_enabled: ${overview.public_enabled}`,
|
|
1840
|
+
`broadcast_enabled: ${overview.broadcast_enabled}`,
|
|
1841
|
+
`last_broadcast: ${ago(overview.last_broadcast_at)}`,
|
|
1842
|
+
].join("\n"),
|
|
1843
|
+
heroModeText: overview.social?.network_mode || "-",
|
|
1844
|
+
heroAdapterText: network.adapter || "-",
|
|
1845
|
+
heroRelayText: network.adapter_extra?.signaling_url || "-",
|
|
1846
|
+
heroRoomText: network.adapter_extra?.room || "-",
|
|
1847
|
+
pillAdapterText: `adapter: ${network.adapter || "-"}`,
|
|
1848
|
+
pillBroadcastText: overview.broadcast_enabled ? "broadcast: running" : "broadcast: paused",
|
|
1849
|
+
pillBroadcastClassName: `pill ${overview.broadcast_enabled ? "ok" : "warn"}`,
|
|
1850
|
+
overviewCardsHtml,
|
|
1851
|
+
agentsCountHintText: `${discovered.length} agents discovered`,
|
|
1852
|
+
agentsWrapHtml,
|
|
1853
|
+
integrationStatusText: `Connected to SilicaClaw: ${integration.connected_to_silicaclaw ? "yes" : "no"} · Network mode: ${integration.network_mode || "-"} · Public discovery: ${integration.public_enabled ? "enabled" : "disabled"}`,
|
|
1854
|
+
integrationStatusClassName: `integration-strip ${integration.connected_to_silicaclaw && integration.public_enabled ? "ok" : "warn"}`,
|
|
1855
|
+
socialStatusLineText: integration.status_line || "",
|
|
1856
|
+
socialStatusSublineText: `Connected to SilicaClaw · ${integration.public_enabled ? "Public discovery enabled" : "Public discovery disabled"} · mode ${integration.network_mode || "-"}`,
|
|
1857
|
+
};
|
|
1858
|
+
let html = readFileSync(staticIndexFile, "utf8");
|
|
1859
|
+
html = html.replace("</body>", `${renderBootstrapScript(payload)}\n</body>`);
|
|
1860
|
+
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
1861
|
+
res.send(html);
|
|
1862
|
+
});
|
|
1863
|
+
|
|
1711
1864
|
app.use(express.static(staticDir));
|
|
1712
1865
|
|
|
1713
1866
|
app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
package/package.json
CHANGED
|
@@ -111,6 +111,7 @@ function detectAppDir() {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
const APP_DIR = detectAppDir();
|
|
114
|
+
const LOCAL_CONSOLE_DIR = join(APP_DIR, "apps", "local-console");
|
|
114
115
|
const STATE_DIR = join(APP_DIR, ".silicaclaw", "gateway");
|
|
115
116
|
const CONSOLE_PID_FILE = join(STATE_DIR, "local-console.pid");
|
|
116
117
|
const CONSOLE_LOG_FILE = join(STATE_DIR, "local-console.log");
|
|
@@ -191,10 +192,10 @@ function parseUrlHostPort(url) {
|
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
|
|
194
|
-
function spawnBackground(command, args, env, logFile, pidFile) {
|
|
195
|
+
function spawnBackground(command, args, env, logFile, pidFile, cwd = APP_DIR) {
|
|
195
196
|
const outFd = openSync(logFile, "a");
|
|
196
197
|
const child = spawn(command, args, {
|
|
197
|
-
cwd
|
|
198
|
+
cwd,
|
|
198
199
|
env: { ...process.env, ...env },
|
|
199
200
|
detached: true,
|
|
200
201
|
stdio: ["ignore", outFd, outFd],
|
|
@@ -240,7 +241,7 @@ function buildStatusPayload() {
|
|
|
240
241
|
mode: state?.mode || "unknown",
|
|
241
242
|
adapter: state?.adapter || "unknown",
|
|
242
243
|
local_console: {
|
|
243
|
-
pid: localPid,
|
|
244
|
+
pid: Number(localListener?.pid || localPid || 0) || null,
|
|
244
245
|
running: Boolean(localListener),
|
|
245
246
|
log_file: CONSOLE_LOG_FILE,
|
|
246
247
|
},
|
|
@@ -318,6 +319,26 @@ function normalizePathForMatch(value) {
|
|
|
318
319
|
return String(value || "").replace(/\\/g, "/");
|
|
319
320
|
}
|
|
320
321
|
|
|
322
|
+
function sleep(ms) {
|
|
323
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function waitForPort(port, timeoutMs = 5000) {
|
|
327
|
+
const startedAt = Date.now();
|
|
328
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
329
|
+
const listener = listeningProcessOnPort(port);
|
|
330
|
+
if (listener) return listener;
|
|
331
|
+
await sleep(200);
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function tailText(file, lines = 20) {
|
|
337
|
+
if (!existsSync(file)) return "";
|
|
338
|
+
const text = String(readFileSync(file, "utf8"));
|
|
339
|
+
return text.split(/\r?\n/).slice(-lines).join("\n").trim();
|
|
340
|
+
}
|
|
341
|
+
|
|
321
342
|
function isOwnedListener(listener, kind) {
|
|
322
343
|
if (!listener?.command) return false;
|
|
323
344
|
const command = normalizePathForMatch(listener.command).toLowerCase();
|
|
@@ -343,6 +364,14 @@ async function stopOwnedListener(port, kind) {
|
|
|
343
364
|
const listener = listeningProcessOnPort(port);
|
|
344
365
|
if (!listener || !isOwnedListener(listener, kind)) return false;
|
|
345
366
|
await stopPid(Number(listener.pid), kind);
|
|
367
|
+
const remaining = listeningProcessOnPort(port);
|
|
368
|
+
if (remaining && Number(remaining.pid) === Number(listener.pid) && isOwnedListener(remaining, kind)) {
|
|
369
|
+
try {
|
|
370
|
+
spawnSync("kill", ["-9", String(remaining.pid)], { stdio: ["ignore", "ignore", "ignore"] });
|
|
371
|
+
} catch {
|
|
372
|
+
// ignore hard-kill fallback failures
|
|
373
|
+
}
|
|
374
|
+
}
|
|
346
375
|
return true;
|
|
347
376
|
}
|
|
348
377
|
|
|
@@ -399,7 +428,7 @@ async function stopAll() {
|
|
|
399
428
|
});
|
|
400
429
|
}
|
|
401
430
|
|
|
402
|
-
function startAll() {
|
|
431
|
+
async function startAll() {
|
|
403
432
|
ensureStateDir();
|
|
404
433
|
|
|
405
434
|
const mode = parseMode(parseFlag("mode", process.env.NETWORK_MODE || "global-preview"));
|
|
@@ -410,8 +439,13 @@ function startAll() {
|
|
|
410
439
|
|
|
411
440
|
const currentLocalPid = readPid(CONSOLE_PID_FILE);
|
|
412
441
|
const currentSigPid = readPid(SIGNALING_PID_FILE);
|
|
413
|
-
|
|
414
|
-
if (!isRunning(currentLocalPid)) {
|
|
442
|
+
const currentListener = listeningProcessOnPort(4310);
|
|
443
|
+
if (currentListener && isOwnedListener(currentListener, "local-console") && !isRunning(currentLocalPid)) {
|
|
444
|
+
writeFileSync(CONSOLE_PID_FILE, String(currentListener.pid));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let localPid = readPid(CONSOLE_PID_FILE);
|
|
448
|
+
if (!isRunning(localPid)) {
|
|
415
449
|
removeFileIfExists(CONSOLE_PID_FILE);
|
|
416
450
|
const env = {
|
|
417
451
|
NETWORK_ADAPTER: adapter,
|
|
@@ -419,7 +453,14 @@ function startAll() {
|
|
|
419
453
|
WEBRTC_SIGNALING_URL: signalingUrl,
|
|
420
454
|
WEBRTC_ROOM: room,
|
|
421
455
|
};
|
|
422
|
-
localPid = spawnBackground(
|
|
456
|
+
localPid = spawnBackground(
|
|
457
|
+
process.execPath,
|
|
458
|
+
["--import", "tsx", "src/server.ts"],
|
|
459
|
+
env,
|
|
460
|
+
CONSOLE_LOG_FILE,
|
|
461
|
+
CONSOLE_PID_FILE,
|
|
462
|
+
LOCAL_CONSOLE_DIR,
|
|
463
|
+
);
|
|
423
464
|
}
|
|
424
465
|
|
|
425
466
|
const { host, port } = parseUrlHostPort(signalingUrl);
|
|
@@ -467,7 +508,20 @@ async function main() {
|
|
|
467
508
|
return;
|
|
468
509
|
}
|
|
469
510
|
if (cmd === "start") {
|
|
470
|
-
startAll();
|
|
511
|
+
await startAll();
|
|
512
|
+
const listener = await waitForPort(4310, 15000);
|
|
513
|
+
if (!listener) {
|
|
514
|
+
headline();
|
|
515
|
+
console.log("");
|
|
516
|
+
kv("Status", paint("failed to start", COLOR.red));
|
|
517
|
+
const recent = tailText(CONSOLE_LOG_FILE, 18);
|
|
518
|
+
if (recent) {
|
|
519
|
+
console.log("");
|
|
520
|
+
console.log(recent);
|
|
521
|
+
}
|
|
522
|
+
process.exitCode = 1;
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
471
525
|
const status = buildStatusPayload();
|
|
472
526
|
printConnectionSummary(status, "Started");
|
|
473
527
|
return;
|
|
@@ -479,7 +533,20 @@ async function main() {
|
|
|
479
533
|
}
|
|
480
534
|
if (cmd === "restart") {
|
|
481
535
|
await stopAll();
|
|
482
|
-
startAll();
|
|
536
|
+
await startAll();
|
|
537
|
+
const listener = await waitForPort(4310, 15000);
|
|
538
|
+
if (!listener) {
|
|
539
|
+
headline();
|
|
540
|
+
console.log("");
|
|
541
|
+
kv("Status", paint("failed to restart", COLOR.red));
|
|
542
|
+
const recent = tailText(CONSOLE_LOG_FILE, 18);
|
|
543
|
+
if (recent) {
|
|
544
|
+
console.log("");
|
|
545
|
+
console.log(recent);
|
|
546
|
+
}
|
|
547
|
+
process.exitCode = 1;
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
483
550
|
const status = buildStatusPayload();
|
|
484
551
|
printConnectionSummary(status, "Restarted");
|
|
485
552
|
return;
|