@silicaclaw/cli 2026.3.18-2 → 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.
@@ -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
- this.networkMode = this.socialConfig.network.mode || "lan";
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, "&lt;")
1531
+ .replace(/>/g, "&gt;")
1532
+ .replace(/"/g, "&quot;")
1533
+ .replace(/'/g, "&#39;");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silicaclaw/cli",
3
- "version": "2026.3.18-2",
3
+ "version": "2026.3.18-4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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: APP_DIR,
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
  },
@@ -314,6 +315,66 @@ function listeningProcessOnPort(port) {
314
315
  }
315
316
  }
316
317
 
318
+ function normalizePathForMatch(value) {
319
+ return String(value || "").replace(/\\/g, "/");
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
+
342
+ function isOwnedListener(listener, kind) {
343
+ if (!listener?.command) return false;
344
+ const command = normalizePathForMatch(listener.command).toLowerCase();
345
+ const appDir = normalizePathForMatch(APP_DIR).toLowerCase();
346
+ const rootDir = normalizePathForMatch(ROOT_DIR).toLowerCase();
347
+ const inWorkspace = command.includes(appDir) || command.includes(rootDir);
348
+ if (!inWorkspace) return false;
349
+ if (kind === "local-console") {
350
+ return (
351
+ command.includes("@silicaclaw/local-console") ||
352
+ command.includes("/apps/local-console/") ||
353
+ command.includes("src/server.ts") ||
354
+ command.includes("dist/server.js")
355
+ );
356
+ }
357
+ if (kind === "signaling") {
358
+ return command.includes("webrtc-signaling-server.mjs") || command.includes("npm run webrtc-signaling");
359
+ }
360
+ return false;
361
+ }
362
+
363
+ async function stopOwnedListener(port, kind) {
364
+ const listener = listeningProcessOnPort(port);
365
+ if (!listener || !isOwnedListener(listener, kind)) return false;
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
+ }
375
+ return true;
376
+ }
377
+
317
378
  function printStopSummary() {
318
379
  const localListener = listeningProcessOnPort(4310);
319
380
  const signalingListener = listeningProcessOnPort(4510);
@@ -357,6 +418,8 @@ async function stopAll() {
357
418
  const sigPid = readPid(SIGNALING_PID_FILE);
358
419
  await stopPid(localPid, "local-console");
359
420
  await stopPid(sigPid, "signaling");
421
+ await stopOwnedListener(4310, "local-console");
422
+ await stopOwnedListener(4510, "signaling");
360
423
  removeFileIfExists(CONSOLE_PID_FILE);
361
424
  removeFileIfExists(SIGNALING_PID_FILE);
362
425
  writeState({
@@ -365,7 +428,7 @@ async function stopAll() {
365
428
  });
366
429
  }
367
430
 
368
- function startAll() {
431
+ async function startAll() {
369
432
  ensureStateDir();
370
433
 
371
434
  const mode = parseMode(parseFlag("mode", process.env.NETWORK_MODE || "global-preview"));
@@ -376,8 +439,13 @@ function startAll() {
376
439
 
377
440
  const currentLocalPid = readPid(CONSOLE_PID_FILE);
378
441
  const currentSigPid = readPid(SIGNALING_PID_FILE);
379
- let localPid = currentLocalPid;
380
- 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)) {
381
449
  removeFileIfExists(CONSOLE_PID_FILE);
382
450
  const env = {
383
451
  NETWORK_ADAPTER: adapter,
@@ -385,7 +453,14 @@ function startAll() {
385
453
  WEBRTC_SIGNALING_URL: signalingUrl,
386
454
  WEBRTC_ROOM: room,
387
455
  };
388
- localPid = spawnBackground("npm", ["run", "local-console"], env, CONSOLE_LOG_FILE, CONSOLE_PID_FILE);
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
+ );
389
464
  }
390
465
 
391
466
  const { host, port } = parseUrlHostPort(signalingUrl);
@@ -433,7 +508,20 @@ async function main() {
433
508
  return;
434
509
  }
435
510
  if (cmd === "start") {
436
- 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
+ }
437
525
  const status = buildStatusPayload();
438
526
  printConnectionSummary(status, "Started");
439
527
  return;
@@ -445,7 +533,20 @@ async function main() {
445
533
  }
446
534
  if (cmd === "restart") {
447
535
  await stopAll();
448
- 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
+ }
449
550
  const status = buildStatusPayload();
450
551
  printConnectionSummary(status, "Restarted");
451
552
  return;