@silicaclaw/cli 2026.3.20-5 → 2026.3.20-7

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 CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## v1.0 beta - 2026-03-20
4
4
 
5
+ ### 2026.3.20-7
6
+
7
+ - release build:
8
+ - prepared another fresh latest-channel package build without publishing
9
+ - regenerated the npm tarball through the verified release packing workflow
10
+
11
+ ### 2026.3.20-6
12
+
13
+ - release build:
14
+ - prepared another fresh latest-channel package build without publishing
15
+ - regenerated the npm tarball through the verified release packing workflow
16
+
5
17
  ### 2026.3.20-5
6
18
 
7
19
  - release build:
package/INSTALL.md CHANGED
@@ -211,9 +211,9 @@ npx clawhub publish openclaw-skills/silicaclaw-broadcast \
211
211
  npx clawhub publish openclaw-skills/silicaclaw-owner-push \
212
212
  --slug silicaclaw-owner-push \
213
213
  --name "SilicaClaw Owner Push" \
214
- --version 2026.3.20-beta.2 \
214
+ --version 2026.3.20-beta.3 \
215
215
  --tags latest \
216
- --changelog "Added latest-only owner push behavior with timestamp cursor state so only the newest qualifying broadcast is pushed and older messages are skipped."
216
+ --changelog "Added single-instance lock protection so owner push avoids duplicate notifications when multiple forwarders start at the same time."
217
217
  ```
218
218
 
219
219
  ClawHub expects each skill version to be valid semver, so use the versions from each skill's `manifest.json` and `VERSION`, not the npm CLI version format.
package/README.md CHANGED
@@ -283,9 +283,9 @@ npx clawhub publish openclaw-skills/silicaclaw-broadcast \
283
283
  npx clawhub publish openclaw-skills/silicaclaw-owner-push \
284
284
  --slug silicaclaw-owner-push \
285
285
  --name "SilicaClaw Owner Push" \
286
- --version 2026.3.20-beta.2 \
286
+ --version 2026.3.20-beta.3 \
287
287
  --tags latest \
288
- --changelog "Added latest-only owner push behavior with timestamp cursor state so only the newest qualifying broadcast is pushed and older messages are skipped."
288
+ --changelog "Added single-instance lock protection so owner push avoids duplicate notifications when multiple forwarders start at the same time."
289
289
  ```
290
290
 
291
291
  ClawHub publishes the OpenClaw skill folders, not the npm CLI package.
package/VERSION CHANGED
@@ -1 +1 @@
1
- v2026.3.20-5
1
+ v2026.3.20-7
@@ -72,11 +72,17 @@ type OpenClawBridgeStatus = {
72
72
  gateway_url: string;
73
73
  gateway_port: number;
74
74
  gateway_reachable: boolean;
75
+ status_command: string | null;
76
+ status_ok: boolean;
77
+ status_summary: string | null;
78
+ gateway_probe_command: string | null;
79
+ gateway_probe_ok: boolean;
80
+ gateway_probe_summary: string | null;
75
81
  configured_gateway_url: string;
76
82
  configured_gateway_port: number;
77
83
  configured_gateway_bind: string | null;
78
84
  configured_gateway_config_path: string | null;
79
- detection_mode: "process" | "gateway" | "process+gateway" | "not_running";
85
+ detection_mode: "gateway-probe" | "gateway-probe+process" | "gateway-probe+gateway" | "gateway-probe+process+gateway" | "process" | "gateway" | "process+gateway" | "not_running";
80
86
  };
81
87
  skill_learning: {
82
88
  available: boolean;
@@ -204,11 +210,15 @@ export declare class LocalNodeService {
204
210
  private networkReconnectTimer;
205
211
  private networkReconnectDelayMs;
206
212
  private appVersion;
213
+ private openclawRuntimeCache;
214
+ private openclawBridgeStatusCache;
207
215
  constructor(options?: {
208
216
  workspaceRoot?: string;
209
217
  projectRoot?: string;
210
218
  storageRoot?: string;
211
219
  });
220
+ private getCachedOpenClawRuntime;
221
+ private invalidateOpenClawCaches;
212
222
  start(): Promise<void>;
213
223
  stop(): Promise<void>;
214
224
  private ensureLocalDirectoryBaseline;
@@ -636,7 +646,7 @@ export declare class LocalNodeService {
636
646
  openclaw: {
637
647
  detected: boolean;
638
648
  running: boolean;
639
- detection_mode: "process+gateway" | "gateway" | "process" | "not_running";
649
+ detection_mode: "gateway" | "gateway-probe+process+gateway" | "gateway-probe+gateway" | "gateway-probe+process" | "gateway-probe" | "process+gateway" | "process" | "not_running";
640
650
  gateway_url: string;
641
651
  workspace_install_root: string;
642
652
  legacy_install_root: string;
@@ -36,6 +36,8 @@ const DEFAULT_GLOBAL_ROOM = silicaclaw_defaults_json_1.default.network.global_pr
36
36
  const DEFAULT_BRIDGE_API_BASE = silicaclaw_defaults_json_1.default.bridge.api_base;
37
37
  const OPENCLAW_GATEWAY_PORT = silicaclaw_defaults_json_1.default.ports.openclaw_gateway;
38
38
  const OPENCLAW_GATEWAY_URL = `http://${OPENCLAW_GATEWAY_HOST}:${OPENCLAW_GATEWAY_PORT}/`;
39
+ const OPENCLAW_RUNTIME_CACHE_MS = 15_000;
40
+ const OPENCLAW_BRIDGE_STATUS_CACHE_MS = 5_000;
39
41
  const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
40
42
  const DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT = Number(process.env.DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT || 1000);
41
43
  const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
@@ -142,6 +144,9 @@ function compareVersionTokens(left, right) {
142
144
  function userNpmCacheDir() {
143
145
  return (0, path_1.resolve)((0, os_1.homedir)(), ".silicaclaw", "npm-cache");
144
146
  }
147
+ function userShimPath() {
148
+ return (0, path_1.resolve)((0, os_1.homedir)(), ".silicaclaw", "bin", "silicaclaw");
149
+ }
145
150
  function resolveWorkspaceRoot(cwd = process.cwd()) {
146
151
  if ((0, fs_1.existsSync)((0, path_1.resolve)(cwd, "apps", "local-console", "package.json"))) {
147
152
  return cwd;
@@ -389,8 +394,71 @@ function readOpenClawConfiguredGateway(workspaceRoot) {
389
394
  gateway_url: OPENCLAW_GATEWAY_URL,
390
395
  };
391
396
  }
397
+ function resolveOpenClawStatusCommand(workspaceRoot) {
398
+ const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
399
+ if (explicitBin) {
400
+ return { cmd: explicitBin, args: ["status"] };
401
+ }
402
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
403
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
404
+ const sourceDir = configuredSourceDir || defaultSourceDir;
405
+ const sourceEntry = existingPathOrNull((0, path_1.resolve)(sourceDir, "openclaw.mjs"));
406
+ if (sourceEntry) {
407
+ return { cmd: process.execPath, args: [sourceEntry, "status"] };
408
+ }
409
+ const commandPath = resolveExecutableInPath("openclaw");
410
+ if (commandPath) {
411
+ return { cmd: commandPath, args: ["status"] };
412
+ }
413
+ return null;
414
+ }
415
+ function resolveOpenClawGatewayProbeCommand(workspaceRoot) {
416
+ const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
417
+ if (explicitBin) {
418
+ return { cmd: explicitBin, args: ["gateway", "probe"] };
419
+ }
420
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
421
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
422
+ const sourceDir = configuredSourceDir || defaultSourceDir;
423
+ const sourceEntry = existingPathOrNull((0, path_1.resolve)(sourceDir, "openclaw.mjs"));
424
+ if (sourceEntry) {
425
+ return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] };
426
+ }
427
+ const commandPath = resolveExecutableInPath("openclaw");
428
+ if (commandPath) {
429
+ return { cmd: commandPath, args: ["gateway", "probe"] };
430
+ }
431
+ return null;
432
+ }
392
433
  function detectOpenClawRuntime(workspaceRoot) {
393
434
  const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
435
+ const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
436
+ const gatewayProbeCommand = resolveOpenClawGatewayProbeCommand(workspaceRoot);
437
+ const statusProbe = statusCommand
438
+ ? (0, child_process_1.spawnSync)(statusCommand.cmd, statusCommand.args, {
439
+ encoding: "utf8",
440
+ env: process.env,
441
+ })
442
+ : null;
443
+ const statusStdout = String(statusProbe?.stdout || "");
444
+ const statusStderr = String(statusProbe?.stderr || "");
445
+ const statusLooksConfigured = Boolean(statusCommand &&
446
+ statusProbe &&
447
+ statusProbe.status === 0 &&
448
+ (/\bChannels\b/i.test(statusStdout) ||
449
+ /\bSessions\b/i.test(statusStdout) ||
450
+ /\bNext steps:\b/i.test(statusStdout)));
451
+ const gatewayStatusProbe = gatewayProbeCommand
452
+ ? (0, child_process_1.spawnSync)(gatewayProbeCommand.cmd, gatewayProbeCommand.args, {
453
+ encoding: "utf8",
454
+ env: process.env,
455
+ })
456
+ : null;
457
+ const gatewayStatusStdout = String(gatewayStatusProbe?.stdout || "");
458
+ const gatewayStatusStderr = String(gatewayStatusProbe?.stderr || "");
459
+ const gatewayProbeOk = Boolean(gatewayProbeCommand &&
460
+ gatewayStatusProbe &&
461
+ gatewayStatusProbe.status === 0);
394
462
  const result = (0, child_process_1.spawnSync)("ps", ["-Ao", "pid=,ppid=,command="], {
395
463
  encoding: "utf8",
396
464
  });
@@ -473,6 +541,12 @@ function detectOpenClawRuntime(workspaceRoot) {
473
541
  const allProcesses = Array.from(combinedProcesses.values());
474
542
  const gatewayReachable = gatewayListeners.length > 0;
475
543
  const detectionNotes = [];
544
+ if (statusProbe && statusProbe.status !== 0) {
545
+ detectionNotes.push(String(statusStderr || "openclaw status failed").trim());
546
+ }
547
+ if (gatewayStatusProbe && gatewayStatusProbe.status !== 0) {
548
+ detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
549
+ }
476
550
  if (result.status !== 0)
477
551
  detectionNotes.push(String(result.stderr || "ps failed").trim());
478
552
  if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
@@ -481,24 +555,52 @@ function detectOpenClawRuntime(workspaceRoot) {
481
555
  const gatewayPort = preferredListener?.port || configuredGateway.gateway_port;
482
556
  const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
483
557
  return {
484
- running: allProcesses.length > 0 || gatewayReachable,
558
+ running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
485
559
  process_count: allProcesses.length,
486
560
  processes: allProcesses.slice(0, 10),
487
561
  detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
488
562
  gateway_url: gatewayUrl,
489
563
  gateway_port: gatewayPort,
490
564
  gateway_reachable: gatewayReachable,
565
+ status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
566
+ status_ok: statusLooksConfigured,
567
+ status_summary: statusLooksConfigured
568
+ ? statusStdout
569
+ .split("\n")
570
+ .map((line) => line.trim())
571
+ .filter(Boolean)
572
+ .slice(0, 6)
573
+ .join(" | ")
574
+ : null,
575
+ gateway_probe_command: gatewayProbeCommand ? [gatewayProbeCommand.cmd, ...gatewayProbeCommand.args].join(" ") : null,
576
+ gateway_probe_ok: gatewayProbeOk,
577
+ gateway_probe_summary: gatewayProbeOk
578
+ ? gatewayStatusStdout
579
+ .split("\n")
580
+ .map((line) => line.trim())
581
+ .filter(Boolean)
582
+ .slice(0, 4)
583
+ .join(" | ")
584
+ : null,
491
585
  configured_gateway_url: configuredGateway.gateway_url,
492
586
  configured_gateway_port: configuredGateway.gateway_port,
493
587
  configured_gateway_bind: configuredGateway.gateway_bind,
494
588
  configured_gateway_config_path: configuredGateway.config_path,
495
- detection_mode: processes.length > 0 && gatewayReachable
496
- ? "process+gateway"
497
- : gatewayReachable
498
- ? "gateway"
499
- : processes.length > 0
500
- ? "process"
501
- : "not_running",
589
+ detection_mode: gatewayProbeOk
590
+ ? (processes.length > 0 && gatewayReachable
591
+ ? "gateway-probe+process+gateway"
592
+ : gatewayReachable
593
+ ? "gateway-probe+gateway"
594
+ : processes.length > 0
595
+ ? "gateway-probe+process"
596
+ : "gateway-probe")
597
+ : processes.length > 0 && gatewayReachable
598
+ ? "process+gateway"
599
+ : gatewayReachable
600
+ ? "gateway"
601
+ : processes.length > 0
602
+ ? "process"
603
+ : "not_running",
502
604
  };
503
605
  }
504
606
  function detectOpenClawSkillInstallation() {
@@ -755,6 +857,8 @@ class LocalNodeService {
755
857
  networkReconnectTimer = null;
756
858
  networkReconnectDelayMs = 5_000;
757
859
  appVersion = "unknown";
860
+ openclawRuntimeCache = null;
861
+ openclawBridgeStatusCache = null;
758
862
  constructor(options) {
759
863
  this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
760
864
  this.projectRoot = options?.projectRoot || resolveProjectRoot(this.workspaceRoot);
@@ -795,6 +899,22 @@ class LocalNodeService {
795
899
  this.adapterMode = resolved.mode;
796
900
  this.networkPort = resolved.port;
797
901
  }
902
+ getCachedOpenClawRuntime() {
903
+ const now = Date.now();
904
+ if (this.openclawRuntimeCache && this.openclawRuntimeCache.expiresAt > now) {
905
+ return this.openclawRuntimeCache.value;
906
+ }
907
+ const value = detectOpenClawRuntime(this.projectRoot);
908
+ this.openclawRuntimeCache = {
909
+ value,
910
+ expiresAt: now + OPENCLAW_RUNTIME_CACHE_MS,
911
+ };
912
+ return value;
913
+ }
914
+ invalidateOpenClawCaches() {
915
+ this.openclawRuntimeCache = null;
916
+ this.openclawBridgeStatusCache = null;
917
+ }
798
918
  async start() {
799
919
  await this.hydrateFromDisk();
800
920
  this.bindNetworkSubscriptions();
@@ -1187,8 +1307,10 @@ class LocalNodeService {
1187
1307
  reason: status.check_error || "already_current",
1188
1308
  };
1189
1309
  }
1310
+ const shimPath = userShimPath();
1190
1311
  const scriptPath = (0, path_1.resolve)(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
1191
- if (!(0, fs_1.existsSync)(scriptPath)) {
1312
+ const useShim = (0, fs_1.existsSync)(shimPath);
1313
+ if (!useShim && !(0, fs_1.existsSync)(scriptPath)) {
1192
1314
  return {
1193
1315
  started: false,
1194
1316
  target_version: status.latest_version,
@@ -1196,7 +1318,9 @@ class LocalNodeService {
1196
1318
  reason: "missing_cli_script",
1197
1319
  };
1198
1320
  }
1199
- const child = (0, child_process_1.spawn)(process.execPath, [scriptPath, "update"], {
1321
+ const command = useShim ? shimPath : process.execPath;
1322
+ const args = useShim ? ["update"] : [scriptPath, "update"];
1323
+ const child = (0, child_process_1.spawn)(command, args, {
1200
1324
  cwd: this.projectRoot,
1201
1325
  detached: true,
1202
1326
  stdio: "ignore",
@@ -1490,9 +1614,13 @@ class LocalNodeService {
1490
1614
  };
1491
1615
  }
1492
1616
  getOpenClawBridgeStatus() {
1617
+ const now = Date.now();
1618
+ if (this.openclawBridgeStatusCache && this.openclawBridgeStatusCache.expiresAt > now) {
1619
+ return this.openclawBridgeStatusCache.value;
1620
+ }
1493
1621
  const integration = this.getIntegrationStatus();
1494
1622
  const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
1495
- const openclawRuntime = detectOpenClawRuntime(this.projectRoot);
1623
+ const openclawRuntime = this.getCachedOpenClawRuntime();
1496
1624
  const skillInstallation = detectOpenClawSkillInstallation();
1497
1625
  const ownerDelivery = detectOwnerDeliveryStatus({
1498
1626
  workspaceRoot: this.projectRoot,
@@ -1500,7 +1628,7 @@ class LocalNodeService {
1500
1628
  openclawRunning: openclawRuntime.running,
1501
1629
  skillInstalled: skillInstallation.installed,
1502
1630
  });
1503
- return {
1631
+ const value = {
1504
1632
  enabled: this.socialConfig.enabled,
1505
1633
  connected_to_silicaclaw: integration.connected_to_silicaclaw,
1506
1634
  public_enabled: integration.public_enabled,
@@ -1556,6 +1684,11 @@ class LocalNodeService {
1556
1684
  install_skill: "/api/openclaw/bridge/skill-install",
1557
1685
  },
1558
1686
  };
1687
+ this.openclawBridgeStatusCache = {
1688
+ value,
1689
+ expiresAt: now + OPENCLAW_BRIDGE_STATUS_CACHE_MS,
1690
+ };
1691
+ return value;
1559
1692
  }
1560
1693
  async installOpenClawSkill(skillName) {
1561
1694
  const scriptPath = (0, path_1.resolve)(this.workspaceRoot, "scripts", "install-openclaw-skill.mjs");
@@ -1569,6 +1702,7 @@ class LocalNodeService {
1569
1702
  maxBuffer: 1024 * 1024,
1570
1703
  });
1571
1704
  const parsed = JSON.parse(String(stdout || "{}"));
1705
+ this.invalidateOpenClawCaches();
1572
1706
  return {
1573
1707
  ...parsed,
1574
1708
  bridge: this.getOpenClawBridgeStatus(),
@@ -1588,7 +1722,7 @@ class LocalNodeService {
1588
1722
  const workspaceSkillDir = (0, path_1.resolve)(homeDir, "workspace", "skills");
1589
1723
  const legacySkillDir = (0, path_1.resolve)(homeDir, "skills");
1590
1724
  const openclawSourceDir = defaultOpenClawSourceDir(this.projectRoot);
1591
- const openclawRuntime = detectOpenClawRuntime(this.projectRoot);
1725
+ const openclawRuntime = this.getCachedOpenClawRuntime();
1592
1726
  return {
1593
1727
  bridge_api_base: DEFAULT_BRIDGE_API_BASE,
1594
1728
  openclaw_detected: detectOpenClawInstallation(this.projectRoot).detected,
@@ -446,6 +446,7 @@ export function createSocialController({
446
446
  [t("social.ownerForwardReady"), ownerDelivery.ready ? t("common.yes") : t("common.no")],
447
447
  [t("social.ownerForwardCommand"), ownerDelivery.forward_command_configured ? t("common.yes") : t("common.no")],
448
448
  [t("social.openclawDetectionMode"), bridge.openclaw_runtime?.detection_mode || "-"],
449
+ ["Gateway probe", bridge.openclaw_runtime?.gateway_probe_ok ? t("common.yes") : t("common.no")],
449
450
  [t("social.openclawGateway"), bridge.openclaw_runtime?.gateway_url || "-"],
450
451
  [t("social.installMode"), skillLearning.install_mode || "-"],
451
452
  [t("social.installedPath"), skillInstalled ? installedSkillPath : "-"],
@@ -86,7 +86,7 @@ export const TRANSLATIONS = {
86
86
  showLess: 'Show Less',
87
87
  showMoreCount: 'Show {count} More',
88
88
  openclawNotInstalled: 'OpenClaw Not Installed Here',
89
- openclawNotRunning: 'OpenClaw Not Running',
89
+ openclawNotRunning: 'OpenClaw Gateway Not Running',
90
90
  openclawSkillLearned: 'Skill Already Learned',
91
91
  silicaClawSkillsInstalled: 'Skills Already Installed',
92
92
  },
@@ -591,7 +591,7 @@ export const TRANSLATIONS = {
591
591
  openclawSkillNotInstalled: 'The SilicaClaw broadcast skill is not installed yet.',
592
592
  openclawSkillInstallFailed: 'OpenClaw skill installation failed',
593
593
  openclawRoleBroadcasterOnly: 'This computer is broadcaster-only. Install OpenClaw here to learn broadcasts.',
594
- openclawRoleNotRunning: 'OpenClaw is installed here, but not running yet.',
594
+ openclawRoleNotRunning: 'OpenClaw is configured here, but the gateway is not running yet.',
595
595
  openclawRoleReadyToLearn: 'OpenClaw is installed here. Learn the broadcast skill to keep going.',
596
596
  openclawRoleLearned: 'This computer is ready to learn broadcasts and forward useful updates.',
597
597
  openclawRoleLearningOnly: 'Broadcast learning works, but owner forwarding is not ready yet.',
@@ -694,7 +694,7 @@ export const TRANSLATIONS = {
694
694
  showLess: '收起',
695
695
  showMoreCount: '再显示 {count} 个',
696
696
  openclawNotInstalled: '本机未安装 OpenClaw',
697
- openclawNotRunning: 'OpenClaw 未启动',
697
+ openclawNotRunning: 'OpenClaw 网关未启动',
698
698
  openclawSkillLearned: '技能已学会',
699
699
  silicaClawSkillsInstalled: '技能已全部安装',
700
700
  },
@@ -1020,7 +1020,7 @@ export const TRANSLATIONS = {
1020
1020
  homeDegraded: '降级',
1021
1021
  homeRunning: '运行中',
1022
1022
  homeStopped: '未启动',
1023
- homeInstalledOnly: '仅检测到安装痕迹',
1023
+ homeInstalledOnly: '已检测到 OpenClaw 配置',
1024
1024
  homeGlobalReady: '全网预览已启用',
1025
1025
  homeNotGlobal: '当前不是全网预览',
1026
1026
  homeBriefNetwork: '网络',
@@ -1032,7 +1032,7 @@ export const TRANSLATIONS = {
1032
1032
  homeBriefActionBroadcast: '保持公开广播,让运行 OpenClaw 的机器可以学习这个代理。',
1033
1033
  homeBriefActionStabilize: '先修复 relay 或广播健康度,再依赖主人转发链路。',
1034
1034
  homeMetaRunning: '已经检测到本机 OpenClaw 进程。',
1035
- homeMetaNotRunning: '要开始学习广播,前提是本机 OpenClaw 正在运行。',
1035
+ homeMetaNotRunning: '要开始学习广播,前提是本机 OpenClaw 网关正在运行。',
1036
1036
  homeMetaGlobal: '当前默认使用全网 relay 预览模式。',
1037
1037
  homeMetaNotGlobal: '这台机器当前还没有走全网 relay 路径。',
1038
1038
  homeMetaPeers: '从本机视角看到 {online} 个在线代理,累计发现 {discovered} 个代理。',
@@ -1199,7 +1199,7 @@ export const TRANSLATIONS = {
1199
1199
  openclawSkillNotInstalled: 'SilicaClaw 广播技能还没有安装。',
1200
1200
  openclawSkillInstallFailed: 'OpenClaw 技能安装失败',
1201
1201
  openclawRoleBroadcasterOnly: '这台电脑当前只能广播。想学习广播,先在这里安装 OpenClaw。',
1202
- openclawRoleNotRunning: '这台电脑已经安装了 OpenClaw,但还没启动。',
1202
+ openclawRoleNotRunning: '这台电脑已经配置了 OpenClaw,但网关还没启动。',
1203
1203
  openclawRoleReadyToLearn: '这台电脑已经装了 OpenClaw。先学习广播技能,再继续。',
1204
1204
  openclawRoleLearned: '这台电脑已经可以学习广播,并转发有用更新。',
1205
1205
  openclawRoleLearningOnly: '已经能学习广播,但主人推送还没准备好。',
@@ -88,6 +88,8 @@ const DEFAULT_GLOBAL_ROOM = defaults.network.global_preview.room;
88
88
  const DEFAULT_BRIDGE_API_BASE = defaults.bridge.api_base;
89
89
  const OPENCLAW_GATEWAY_PORT = defaults.ports.openclaw_gateway;
90
90
  const OPENCLAW_GATEWAY_URL = `http://${OPENCLAW_GATEWAY_HOST}:${OPENCLAW_GATEWAY_PORT}/`;
91
+ const OPENCLAW_RUNTIME_CACHE_MS = 15_000;
92
+ const OPENCLAW_BRIDGE_STATUS_CACHE_MS = 5_000;
91
93
  const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
92
94
  const DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT = Number(process.env.DIRECTORY_REMOTE_PROFILE_SOFT_LIMIT || 1000);
93
95
  const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
@@ -200,6 +202,10 @@ function userNpmCacheDir(): string {
200
202
  return resolve(homedir(), ".silicaclaw", "npm-cache");
201
203
  }
202
204
 
205
+ function userShimPath(): string {
206
+ return resolve(homedir(), ".silicaclaw", "bin", "silicaclaw");
207
+ }
208
+
203
209
  function resolveWorkspaceRoot(cwd = process.cwd()): string {
204
210
  if (existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
205
211
  return cwd;
@@ -455,8 +461,85 @@ function readOpenClawConfiguredGateway(workspaceRoot: string) {
455
461
  } as const;
456
462
  }
457
463
 
464
+ function resolveOpenClawStatusCommand(workspaceRoot: string) {
465
+ const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
466
+ if (explicitBin) {
467
+ return { cmd: explicitBin, args: ["status"] } as const;
468
+ }
469
+
470
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
471
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
472
+ const sourceDir = configuredSourceDir || defaultSourceDir;
473
+ const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
474
+ if (sourceEntry) {
475
+ return { cmd: process.execPath, args: [sourceEntry, "status"] } as const;
476
+ }
477
+
478
+ const commandPath = resolveExecutableInPath("openclaw");
479
+ if (commandPath) {
480
+ return { cmd: commandPath, args: ["status"] } as const;
481
+ }
482
+
483
+ return null;
484
+ }
485
+
486
+ function resolveOpenClawGatewayProbeCommand(workspaceRoot: string) {
487
+ const explicitBin = String(process.env.OPENCLAW_BIN || "").trim();
488
+ if (explicitBin) {
489
+ return { cmd: explicitBin, args: ["gateway", "probe"] } as const;
490
+ }
491
+
492
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
493
+ const defaultSourceDir = defaultOpenClawSourceDir(workspaceRoot);
494
+ const sourceDir = configuredSourceDir || defaultSourceDir;
495
+ const sourceEntry = existingPathOrNull(resolve(sourceDir, "openclaw.mjs"));
496
+ if (sourceEntry) {
497
+ return { cmd: process.execPath, args: [sourceEntry, "gateway", "probe"] } as const;
498
+ }
499
+
500
+ const commandPath = resolveExecutableInPath("openclaw");
501
+ if (commandPath) {
502
+ return { cmd: commandPath, args: ["gateway", "probe"] } as const;
503
+ }
504
+
505
+ return null;
506
+ }
507
+
458
508
  function detectOpenClawRuntime(workspaceRoot: string) {
459
509
  const configuredGateway = readOpenClawConfiguredGateway(workspaceRoot);
510
+ const statusCommand = resolveOpenClawStatusCommand(workspaceRoot);
511
+ const gatewayProbeCommand = resolveOpenClawGatewayProbeCommand(workspaceRoot);
512
+ const statusProbe = statusCommand
513
+ ? spawnSync(statusCommand.cmd, statusCommand.args, {
514
+ encoding: "utf8",
515
+ env: process.env,
516
+ })
517
+ : null;
518
+ const statusStdout = String(statusProbe?.stdout || "");
519
+ const statusStderr = String(statusProbe?.stderr || "");
520
+ const statusLooksConfigured = Boolean(
521
+ statusCommand &&
522
+ statusProbe &&
523
+ statusProbe.status === 0 &&
524
+ (
525
+ /\bChannels\b/i.test(statusStdout) ||
526
+ /\bSessions\b/i.test(statusStdout) ||
527
+ /\bNext steps:\b/i.test(statusStdout)
528
+ )
529
+ );
530
+ const gatewayStatusProbe = gatewayProbeCommand
531
+ ? spawnSync(gatewayProbeCommand.cmd, gatewayProbeCommand.args, {
532
+ encoding: "utf8",
533
+ env: process.env,
534
+ })
535
+ : null;
536
+ const gatewayStatusStdout = String(gatewayStatusProbe?.stdout || "");
537
+ const gatewayStatusStderr = String(gatewayStatusProbe?.stderr || "");
538
+ const gatewayProbeOk = Boolean(
539
+ gatewayProbeCommand &&
540
+ gatewayStatusProbe &&
541
+ gatewayStatusProbe.status === 0
542
+ );
460
543
  const result = spawnSync("ps", ["-Ao", "pid=,ppid=,command="], {
461
544
  encoding: "utf8",
462
545
  });
@@ -540,6 +623,12 @@ function detectOpenClawRuntime(workspaceRoot: string) {
540
623
  const allProcesses = Array.from(combinedProcesses.values());
541
624
  const gatewayReachable = gatewayListeners.length > 0;
542
625
  const detectionNotes = [];
626
+ if (statusProbe && statusProbe.status !== 0) {
627
+ detectionNotes.push(String(statusStderr || "openclaw status failed").trim());
628
+ }
629
+ if (gatewayStatusProbe && gatewayStatusProbe.status !== 0) {
630
+ detectionNotes.push(String(gatewayStatusStderr || "openclaw gateway probe failed").trim());
631
+ }
543
632
  if (result.status !== 0) detectionNotes.push(String(result.stderr || "ps failed").trim());
544
633
  if (gatewayProbe.status !== 0 && gatewayLines.length === 0) {
545
634
  detectionNotes.push(String(gatewayProbe.stderr || "lsof failed").trim());
@@ -548,19 +637,49 @@ function detectOpenClawRuntime(workspaceRoot: string) {
548
637
  const gatewayUrl = `http://${OPENCLAW_GATEWAY_HOST}:${gatewayPort}/`;
549
638
 
550
639
  return {
551
- running: allProcesses.length > 0 || gatewayReachable,
640
+ running: gatewayProbeOk || allProcesses.length > 0 || gatewayReachable,
552
641
  process_count: allProcesses.length,
553
642
  processes: allProcesses.slice(0, 10),
554
643
  detection_error: detectionNotes.filter(Boolean).join(" | ") || null,
555
644
  gateway_url: gatewayUrl,
556
645
  gateway_port: gatewayPort,
557
646
  gateway_reachable: gatewayReachable,
647
+ status_command: statusCommand ? [statusCommand.cmd, ...statusCommand.args].join(" ") : null,
648
+ status_ok: statusLooksConfigured,
649
+ status_summary: statusLooksConfigured
650
+ ? statusStdout
651
+ .split("\n")
652
+ .map((line) => line.trim())
653
+ .filter(Boolean)
654
+ .slice(0, 6)
655
+ .join(" | ")
656
+ : null,
657
+ gateway_probe_command: gatewayProbeCommand ? [gatewayProbeCommand.cmd, ...gatewayProbeCommand.args].join(" ") : null,
658
+ gateway_probe_ok: gatewayProbeOk,
659
+ gateway_probe_summary: gatewayProbeOk
660
+ ? gatewayStatusStdout
661
+ .split("\n")
662
+ .map((line) => line.trim())
663
+ .filter(Boolean)
664
+ .slice(0, 4)
665
+ .join(" | ")
666
+ : null,
558
667
  configured_gateway_url: configuredGateway.gateway_url,
559
668
  configured_gateway_port: configuredGateway.gateway_port,
560
669
  configured_gateway_bind: configuredGateway.gateway_bind,
561
670
  configured_gateway_config_path: configuredGateway.config_path,
562
671
  detection_mode:
563
- processes.length > 0 && gatewayReachable
672
+ gatewayProbeOk
673
+ ? (
674
+ processes.length > 0 && gatewayReachable
675
+ ? "gateway-probe+process+gateway"
676
+ : gatewayReachable
677
+ ? "gateway-probe+gateway"
678
+ : processes.length > 0
679
+ ? "gateway-probe+process"
680
+ : "gateway-probe"
681
+ )
682
+ : processes.length > 0 && gatewayReachable
564
683
  ? "process+gateway"
565
684
  : gatewayReachable
566
685
  ? "gateway"
@@ -833,11 +952,17 @@ type OpenClawBridgeStatus = {
833
952
  gateway_url: string;
834
953
  gateway_port: number;
835
954
  gateway_reachable: boolean;
955
+ status_command: string | null;
956
+ status_ok: boolean;
957
+ status_summary: string | null;
958
+ gateway_probe_command: string | null;
959
+ gateway_probe_ok: boolean;
960
+ gateway_probe_summary: string | null;
836
961
  configured_gateway_url: string;
837
962
  configured_gateway_port: number;
838
963
  configured_gateway_bind: string | null;
839
964
  configured_gateway_config_path: string | null;
840
- detection_mode: "process" | "gateway" | "process+gateway" | "not_running";
965
+ detection_mode: "gateway-probe" | "gateway-probe+process" | "gateway-probe+gateway" | "gateway-probe+process+gateway" | "process" | "gateway" | "process+gateway" | "not_running";
841
966
  };
842
967
  skill_learning: {
843
968
  available: boolean;
@@ -978,6 +1103,8 @@ export class LocalNodeService {
978
1103
  private networkReconnectTimer: NodeJS.Timeout | null = null;
979
1104
  private networkReconnectDelayMs = 5_000;
980
1105
  private appVersion = "unknown";
1106
+ private openclawRuntimeCache: { value: ReturnType<typeof detectOpenClawRuntime>; expiresAt: number } | null = null;
1107
+ private openclawBridgeStatusCache: { value: OpenClawBridgeStatus; expiresAt: number } | null = null;
981
1108
 
982
1109
  constructor(options?: { workspaceRoot?: string; projectRoot?: string; storageRoot?: string }) {
983
1110
  this.workspaceRoot = options?.workspaceRoot || resolveWorkspaceRoot();
@@ -1023,6 +1150,24 @@ export class LocalNodeService {
1023
1150
  this.networkPort = resolved.port;
1024
1151
  }
1025
1152
 
1153
+ private getCachedOpenClawRuntime() {
1154
+ const now = Date.now();
1155
+ if (this.openclawRuntimeCache && this.openclawRuntimeCache.expiresAt > now) {
1156
+ return this.openclawRuntimeCache.value;
1157
+ }
1158
+ const value = detectOpenClawRuntime(this.projectRoot);
1159
+ this.openclawRuntimeCache = {
1160
+ value,
1161
+ expiresAt: now + OPENCLAW_RUNTIME_CACHE_MS,
1162
+ };
1163
+ return value;
1164
+ }
1165
+
1166
+ private invalidateOpenClawCaches() {
1167
+ this.openclawRuntimeCache = null;
1168
+ this.openclawBridgeStatusCache = null;
1169
+ }
1170
+
1026
1171
  async start(): Promise<void> {
1027
1172
  await this.hydrateFromDisk();
1028
1173
 
@@ -1431,8 +1576,10 @@ export class LocalNodeService {
1431
1576
  reason: status.check_error || "already_current",
1432
1577
  };
1433
1578
  }
1579
+ const shimPath = userShimPath();
1434
1580
  const scriptPath = resolve(this.workspaceRoot, "scripts", "silicaclaw-cli.mjs");
1435
- if (!existsSync(scriptPath)) {
1581
+ const useShim = existsSync(shimPath);
1582
+ if (!useShim && !existsSync(scriptPath)) {
1436
1583
  return {
1437
1584
  started: false,
1438
1585
  target_version: status.latest_version,
@@ -1440,7 +1587,9 @@ export class LocalNodeService {
1440
1587
  reason: "missing_cli_script",
1441
1588
  };
1442
1589
  }
1443
- const child = spawn(process.execPath, [scriptPath, "update"], {
1590
+ const command = useShim ? shimPath : process.execPath;
1591
+ const args = useShim ? ["update"] : [scriptPath, "update"];
1592
+ const child = spawn(command, args, {
1444
1593
  cwd: this.projectRoot,
1445
1594
  detached: true,
1446
1595
  stdio: "ignore",
@@ -1778,9 +1927,13 @@ export class LocalNodeService {
1778
1927
  }
1779
1928
 
1780
1929
  getOpenClawBridgeStatus(): OpenClawBridgeStatus {
1930
+ const now = Date.now();
1931
+ if (this.openclawBridgeStatusCache && this.openclawBridgeStatusCache.expiresAt > now) {
1932
+ return this.openclawBridgeStatusCache.value;
1933
+ }
1781
1934
  const integration = this.getIntegrationStatus();
1782
1935
  const openclawInstallation = detectOpenClawInstallation(this.projectRoot);
1783
- const openclawRuntime = detectOpenClawRuntime(this.projectRoot);
1936
+ const openclawRuntime = this.getCachedOpenClawRuntime();
1784
1937
  const skillInstallation = detectOpenClawSkillInstallation();
1785
1938
  const ownerDelivery = detectOwnerDeliveryStatus({
1786
1939
  workspaceRoot: this.projectRoot,
@@ -1788,7 +1941,7 @@ export class LocalNodeService {
1788
1941
  openclawRunning: openclawRuntime.running,
1789
1942
  skillInstalled: skillInstallation.installed,
1790
1943
  });
1791
- return {
1944
+ const value: OpenClawBridgeStatus = {
1792
1945
  enabled: this.socialConfig.enabled,
1793
1946
  connected_to_silicaclaw: integration.connected_to_silicaclaw,
1794
1947
  public_enabled: integration.public_enabled,
@@ -1844,6 +1997,11 @@ export class LocalNodeService {
1844
1997
  install_skill: "/api/openclaw/bridge/skill-install",
1845
1998
  },
1846
1999
  };
2000
+ this.openclawBridgeStatusCache = {
2001
+ value,
2002
+ expiresAt: now + OPENCLAW_BRIDGE_STATUS_CACHE_MS,
2003
+ };
2004
+ return value;
1847
2005
  }
1848
2006
 
1849
2007
  async installOpenClawSkill(skillName?: string) {
@@ -1858,6 +2016,7 @@ export class LocalNodeService {
1858
2016
  maxBuffer: 1024 * 1024,
1859
2017
  });
1860
2018
  const parsed = JSON.parse(String(stdout || "{}"));
2019
+ this.invalidateOpenClawCaches();
1861
2020
  return {
1862
2021
  ...parsed,
1863
2022
  bridge: this.getOpenClawBridgeStatus(),
@@ -1879,7 +2038,7 @@ export class LocalNodeService {
1879
2038
  const workspaceSkillDir = resolve(homeDir, "workspace", "skills");
1880
2039
  const legacySkillDir = resolve(homeDir, "skills");
1881
2040
  const openclawSourceDir = defaultOpenClawSourceDir(this.projectRoot);
1882
- const openclawRuntime = detectOpenClawRuntime(this.projectRoot);
2041
+ const openclawRuntime = this.getCachedOpenClawRuntime();
1883
2042
 
1884
2043
  return {
1885
2044
  bridge_api_base: DEFAULT_BRIDGE_API_BASE,
@@ -1 +1 @@
1
- 2026.3.20-beta.5
1
+ 2026.3.20-beta.7
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silicaclaw-broadcast",
3
- "version": "2026.3.20-beta.5",
3
+ "version": "2026.3.20-beta.7",
4
4
  "display_name": "SilicaClaw Broadcast",
5
5
  "description": "Official OpenClaw skill for a bounded local SilicaClaw broadcast workflow: read public broadcasts, publish public broadcasts, and optionally forward owner-relevant summaries through OpenClaw's native channel.",
6
6
  "entrypoints": {
@@ -1 +1 @@
1
- 2026.3.20-beta.2
1
+ 2026.3.20-beta.3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "silicaclaw-owner-push",
3
- "version": "2026.3.20-beta.2",
3
+ "version": "2026.3.20-beta.3",
4
4
  "display_name": "SilicaClaw Owner Push",
5
5
  "description": "Official OpenClaw skill for a bounded local SilicaClaw monitoring workflow: watch public broadcasts, filter owner-relevant updates, and push concise summaries through OpenClaw's native owner channel.",
6
6
  "entrypoints": {
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, resolve } from "node:path";
6
6
  import { spawn } from "node:child_process";
@@ -12,9 +12,11 @@ const OWNER_FORWARD_CMD = String(process.env.OPENCLAW_OWNER_FORWARD_CMD || "").t
12
12
  const STATE_PATH = resolve(
13
13
  String(process.env.OPENCLAW_OWNER_FORWARD_STATE_PATH || resolve(homedir(), ".openclaw", "workspace", "state", "silicaclaw-owner-push.json"))
14
14
  );
15
+ const LOCK_PATH = `${STATE_PATH}.lock`;
15
16
  const LATEST_ONLY = String(process.env.OPENCLAW_FORWARD_LATEST_ONLY || "true").trim().toLowerCase() !== "false";
16
17
  const ONCE = process.argv.includes("--once");
17
18
  const VERBOSE = process.argv.includes("--verbose");
19
+ let lockFd = null;
18
20
 
19
21
  function parseListEnv(name) {
20
22
  return String(process.env[name] || "")
@@ -89,6 +91,85 @@ function saveState(state) {
89
91
  writeFileSync(STATE_PATH, JSON.stringify(state, null, 2), "utf8");
90
92
  }
91
93
 
94
+ function isPidRunning(pid) {
95
+ if (!pid || !Number.isFinite(pid) || pid <= 0) return false;
96
+ try {
97
+ process.kill(pid, 0);
98
+ return true;
99
+ } catch {
100
+ return false;
101
+ }
102
+ }
103
+
104
+ function releaseLock() {
105
+ if (lockFd !== null) {
106
+ try {
107
+ closeSync(lockFd);
108
+ } catch {
109
+ // ignore
110
+ }
111
+ try {
112
+ rmSync(LOCK_PATH, { force: true });
113
+ } catch {
114
+ // ignore
115
+ }
116
+ lockFd = null;
117
+ }
118
+ }
119
+
120
+ function acquireLock() {
121
+ mkdirSync(dirname(LOCK_PATH), { recursive: true });
122
+ try {
123
+ lockFd = openSync(LOCK_PATH, "wx");
124
+ writeFileSync(lockFd, JSON.stringify({
125
+ pid: process.pid,
126
+ started_at: new Date().toISOString(),
127
+ state_path: STATE_PATH,
128
+ }, null, 2), "utf8");
129
+ process.on("exit", releaseLock);
130
+ process.on("SIGINT", () => {
131
+ releaseLock();
132
+ process.exit(130);
133
+ });
134
+ process.on("SIGTERM", () => {
135
+ releaseLock();
136
+ process.exit(143);
137
+ });
138
+ return;
139
+ } catch {
140
+ // fall through
141
+ }
142
+
143
+ try {
144
+ const existing = JSON.parse(readFileSync(LOCK_PATH, "utf8"));
145
+ const existingPid = Number(existing?.pid || 0) || 0;
146
+ if (isPidRunning(existingPid)) {
147
+ throw new Error(`owner push forwarder already running (pid=${existingPid})`);
148
+ }
149
+ } catch (error) {
150
+ if (error instanceof Error && error.message.includes("already running")) {
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ rmSync(LOCK_PATH, { force: true });
156
+ lockFd = openSync(LOCK_PATH, "wx");
157
+ writeFileSync(lockFd, JSON.stringify({
158
+ pid: process.pid,
159
+ started_at: new Date().toISOString(),
160
+ state_path: STATE_PATH,
161
+ }, null, 2), "utf8");
162
+ process.on("exit", releaseLock);
163
+ process.on("SIGINT", () => {
164
+ releaseLock();
165
+ process.exit(130);
166
+ });
167
+ process.on("SIGTERM", () => {
168
+ releaseLock();
169
+ process.exit(143);
170
+ });
171
+ }
172
+
92
173
  function trimState(state) {
93
174
  const recentIds = Array.isArray(state.seen_ids) ? state.seen_ids.slice(-500) : [];
94
175
  const pushedEntries = Object.entries(state.pushed_at || {}).slice(-500);
@@ -253,10 +334,12 @@ async function pollOnce(state) {
253
334
  }
254
335
 
255
336
  async function main() {
337
+ acquireLock();
256
338
  const state = loadState();
257
339
  if (VERBOSE) {
258
340
  console.log(`SilicaClaw owner push watching ${API_BASE}`);
259
341
  console.log(`State file: ${STATE_PATH}`);
342
+ console.log(`Lock file: ${LOCK_PATH}`);
260
343
  console.log(`Latest-only mode: ${LATEST_ONLY ? "on" : "off"}`);
261
344
  }
262
345
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silicaclaw/cli",
3
- "version": "2026.3.20-5",
3
+ "version": "2026.3.20-7",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"