@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.
@@ -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-3",
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
  },
@@ -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
- let localPid = currentLocalPid;
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("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
+ );
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;