@silicaclaw/cli 2026.3.19-1 → 2026.3.19-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.
Files changed (33) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/INSTALL.md +31 -0
  3. package/README.md +28 -0
  4. package/VERSION +1 -1
  5. package/apps/local-console/public/index.html +1327 -245
  6. package/apps/local-console/src/server.ts +439 -10
  7. package/docs/OPENCLAW_BRIDGE.md +85 -0
  8. package/docs/OPENCLAW_BRIDGE_ZH.md +90 -0
  9. package/openclaw-skills/silicaclaw-broadcast/SKILL.md +89 -0
  10. package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -0
  11. package/openclaw-skills/silicaclaw-broadcast/agents/openai.yaml +6 -0
  12. package/openclaw-skills/silicaclaw-broadcast/manifest.json +34 -0
  13. package/openclaw-skills/silicaclaw-broadcast/references/computer-control-via-openclaw.md +41 -0
  14. package/openclaw-skills/silicaclaw-broadcast/references/owner-dispatch-adapter.md +81 -0
  15. package/openclaw-skills/silicaclaw-broadcast/references/owner-forwarding-policy.md +48 -0
  16. package/openclaw-skills/silicaclaw-broadcast/scripts/bridge-client.mjs +59 -0
  17. package/openclaw-skills/silicaclaw-broadcast/scripts/owner-dispatch-adapter-demo.mjs +12 -0
  18. package/openclaw-skills/silicaclaw-broadcast/scripts/owner-forwarder-demo.mjs +111 -0
  19. package/openclaw-skills/silicaclaw-broadcast/scripts/send-to-owner-via-openclaw.mjs +69 -0
  20. package/package.json +2 -1
  21. package/packages/core/dist/socialConfig.js +1 -1
  22. package/packages/core/dist/socialTemplate.js +1 -1
  23. package/packages/core/src/socialConfig.ts +1 -1
  24. package/packages/core/src/socialTemplate.ts +1 -1
  25. package/packages/network/dist/relayPreview.js +16 -4
  26. package/packages/network/src/relayPreview.ts +17 -4
  27. package/scripts/functional-check.mjs +29 -0
  28. package/scripts/install-openclaw-skill.mjs +54 -0
  29. package/scripts/openclaw-bridge-adapter.mjs +7 -0
  30. package/scripts/openclaw-bridge-client.mjs +42 -0
  31. package/scripts/pack-openclaw-skill.mjs +58 -0
  32. package/scripts/silicaclaw-cli.mjs +18 -0
  33. package/scripts/validate-openclaw-skill.mjs +74 -0
@@ -1,9 +1,11 @@
1
1
  import express, { NextFunction, Request, Response } from "express";
2
2
  import cors from "cors";
3
+ import { execFile, spawnSync } from "child_process";
3
4
  import { resolve } from "path";
4
- import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
5
+ import { accessSync, constants, copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
5
6
  import { createHash } from "crypto";
6
7
  import { hostname } from "os";
8
+ import { promisify } from "util";
7
9
  import {
8
10
  AgentIdentity,
9
11
  DirectoryState,
@@ -106,6 +108,8 @@ const SOCIAL_MESSAGE_BLOCKED_AGENT_IDS = new Set(
106
108
  const SOCIAL_MESSAGE_BLOCKED_TERMS = dedupeStrings(parseListEnv(process.env.SOCIAL_MESSAGE_BLOCKED_TERMS || ""))
107
109
  .map((term) => term.trim().toLowerCase())
108
110
  .filter(Boolean);
111
+ const execFileAsync = promisify(execFile);
112
+ const OPENCLAW_SKILL_NAME = "silicaclaw-broadcast";
109
113
 
110
114
  function readWorkspaceVersion(workspaceRoot: string): string {
111
115
  const pkgFile = resolve(workspaceRoot, "package.json");
@@ -144,6 +148,193 @@ function resolveStorageRoot(workspaceRoot: string, cwd = process.cwd()): string
144
148
  return cwd;
145
149
  }
146
150
 
151
+ function resolveExecutableInPath(binName: string): string | null {
152
+ const pathValue = String(process.env.PATH || "").trim();
153
+ if (!pathValue) return null;
154
+ const pathEntries = pathValue.split(":").map((item) => item.trim()).filter(Boolean);
155
+ for (const entry of pathEntries) {
156
+ const candidate = resolve(entry, binName);
157
+ if (!existsSync(candidate)) continue;
158
+ try {
159
+ accessSync(candidate, constants.X_OK);
160
+ return candidate;
161
+ } catch {
162
+ // ignore non-executable matches
163
+ }
164
+ }
165
+ return null;
166
+ }
167
+
168
+ function existingPathOrNull(filePath: string): string | null {
169
+ return existsSync(filePath) ? filePath : null;
170
+ }
171
+
172
+ function detectOpenClawInstallation(workspaceRoot: string) {
173
+ const workspaceDir = resolve(workspaceRoot, ".openclaw");
174
+ const homeDir = resolve(process.env.HOME || "", ".openclaw");
175
+ const commandPath = resolveExecutableInPath("openclaw");
176
+
177
+ const workspaceIdentityPath = existingPathOrNull(resolve(workspaceDir, "identity.json"));
178
+ const workspaceProfilePath = existingPathOrNull(resolve(workspaceDir, "profile.json"));
179
+ const workspaceSocialPath = existingPathOrNull(resolve(workspaceDir, "social.md"));
180
+ const workspaceSkillsPath = existingPathOrNull(resolve(workspaceDir, "skills"));
181
+ const homeIdentityPath = existingPathOrNull(resolve(homeDir, "identity.json"));
182
+ const homeProfilePath = existingPathOrNull(resolve(homeDir, "profile.json"));
183
+ const homeSocialPath = existingPathOrNull(resolve(homeDir, "social.md"));
184
+ const homeSkillsPath = existingPathOrNull(resolve(homeDir, "skills"));
185
+
186
+ const workspaceDetected = Boolean(
187
+ existsSync(workspaceDir) ||
188
+ workspaceIdentityPath ||
189
+ workspaceProfilePath ||
190
+ workspaceSocialPath ||
191
+ workspaceSkillsPath
192
+ );
193
+ const homeDetected = Boolean(
194
+ existsSync(homeDir) || homeIdentityPath || homeProfilePath || homeSocialPath || homeSkillsPath
195
+ );
196
+
197
+ return {
198
+ detected: Boolean(commandPath || workspaceDetected || homeDetected),
199
+ detection_mode: commandPath
200
+ ? "command"
201
+ : workspaceDetected
202
+ ? "workspace"
203
+ : homeDetected
204
+ ? "home"
205
+ : "not_found",
206
+ command_path: commandPath,
207
+ workspace_dir: workspaceDir,
208
+ home_dir: homeDir,
209
+ workspace_dir_exists: existsSync(workspaceDir),
210
+ home_dir_exists: existsSync(homeDir),
211
+ workspace_identity_path: workspaceIdentityPath,
212
+ workspace_profile_path: workspaceProfilePath,
213
+ workspace_social_path: workspaceSocialPath,
214
+ workspace_skills_path: workspaceSkillsPath,
215
+ home_identity_path: homeIdentityPath,
216
+ home_profile_path: homeProfilePath,
217
+ home_social_path: homeSocialPath,
218
+ home_skills_path: homeSkillsPath,
219
+ } as const;
220
+ }
221
+
222
+ function detectOpenClawRuntime() {
223
+ const result = spawnSync("ps", ["-Ao", "pid=,ppid=,command="], {
224
+ encoding: "utf8",
225
+ });
226
+ const stdout = String(result.stdout || "");
227
+ const lines = stdout
228
+ .split("\n")
229
+ .map((line) => line.trim())
230
+ .filter(Boolean);
231
+
232
+ const processes = lines
233
+ .map((line) => {
234
+ const match = line.match(/^(\d+)\s+(\d+)\s+(.+)$/);
235
+ if (!match) return null;
236
+ const command = match[3] || "";
237
+ const lower = command.toLowerCase();
238
+ const isOpenClaw =
239
+ lower.includes(" openclaw ") ||
240
+ lower.endsWith(" openclaw") ||
241
+ lower.includes("/openclaw ") ||
242
+ lower.includes("openclaw.mjs") ||
243
+ lower.includes("openclaw gateway") ||
244
+ lower.includes("openclaw agent") ||
245
+ lower.includes("openclaw message");
246
+ if (!isOpenClaw) return null;
247
+ return {
248
+ pid: Number(match[1]),
249
+ ppid: Number(match[2]),
250
+ command,
251
+ };
252
+ })
253
+ .filter((item): item is { pid: number; ppid: number; command: string } => Boolean(item));
254
+
255
+ return {
256
+ running: processes.length > 0,
257
+ process_count: processes.length,
258
+ processes: processes.slice(0, 10),
259
+ detection_error: result.status === 0 ? null : String(result.stderr || "ps failed"),
260
+ } as const;
261
+ }
262
+
263
+ function detectOpenClawSkillInstallation() {
264
+ const homeDir = resolve(process.env.HOME || "", ".openclaw");
265
+ const workspaceSkillRoot = resolve(homeDir, "workspace", "skills");
266
+ const legacySkillRoot = resolve(homeDir, "skills");
267
+ const workspaceSkillPath = resolve(workspaceSkillRoot, OPENCLAW_SKILL_NAME, "SKILL.md");
268
+ const legacySkillPath = resolve(legacySkillRoot, OPENCLAW_SKILL_NAME, "SKILL.md");
269
+ const workspaceInstalled = existsSync(workspaceSkillPath);
270
+ const legacyInstalled = existsSync(legacySkillPath);
271
+
272
+ return {
273
+ installed: workspaceInstalled || legacyInstalled,
274
+ install_mode: workspaceInstalled ? "workspace" : legacyInstalled ? "legacy" : "not_installed",
275
+ workspace_skill_root: workspaceSkillRoot,
276
+ legacy_skill_root: legacySkillRoot,
277
+ workspace_skill_path: workspaceInstalled ? workspaceSkillPath : null,
278
+ legacy_skill_path: legacyInstalled ? legacySkillPath : null,
279
+ } as const;
280
+ }
281
+
282
+ function detectOwnerDeliveryStatus(params: {
283
+ workspaceRoot: string;
284
+ connectedToSilicaclaw: boolean;
285
+ openclawRunning: boolean;
286
+ skillInstalled: boolean;
287
+ }) {
288
+ const forwardCommand = String(process.env.OPENCLAW_OWNER_FORWARD_CMD || "").trim();
289
+ const ownerChannel = String(process.env.OPENCLAW_OWNER_CHANNEL || "").trim();
290
+ const ownerTarget = String(process.env.OPENCLAW_OWNER_TARGET || "").trim();
291
+ const ownerAccount = String(process.env.OPENCLAW_OWNER_ACCOUNT || "").trim();
292
+ const explicitOpenClawBin = String(process.env.OPENCLAW_BIN || "").trim();
293
+ const configuredSourceDir = String(process.env.OPENCLAW_SOURCE_DIR || "").trim();
294
+ const defaultSourceDir = resolve(params.workspaceRoot, "..", "openclaw");
295
+ const openclawSourceDir = configuredSourceDir || defaultSourceDir;
296
+ const openclawSourceEntry = existingPathOrNull(resolve(openclawSourceDir, "openclaw.mjs"));
297
+ const openclawCommandResolvable = Boolean(explicitOpenClawBin || resolveExecutableInPath("openclaw") || openclawSourceEntry);
298
+ const bridgeMessagesReadable = params.connectedToSilicaclaw && params.openclawRunning && params.skillInstalled;
299
+ const forwardCommandConfigured = Boolean(forwardCommand);
300
+ const ownerRouteConfigured = Boolean(ownerChannel && ownerTarget);
301
+ const ready =
302
+ bridgeMessagesReadable && forwardCommandConfigured && ownerRouteConfigured && openclawCommandResolvable;
303
+
304
+ let reason = "";
305
+ if (!params.connectedToSilicaclaw) {
306
+ reason = "SilicaClaw social bridge is not connected yet, so there is no broadcast stream for OpenClaw to learn.";
307
+ } else if (!params.openclawRunning) {
308
+ reason = "OpenClaw is not running on this machine yet, so broadcast learning and owner forwarding are idle.";
309
+ } else if (!params.skillInstalled) {
310
+ reason = "OpenClaw is running, but the silicaclaw-broadcast skill is not installed yet.";
311
+ } else if (!forwardCommandConfigured) {
312
+ reason = "Broadcast learning is ready, but OPENCLAW_OWNER_FORWARD_CMD is not configured yet.";
313
+ } else if (!ownerRouteConfigured) {
314
+ reason = "The owner forward command exists, but OPENCLAW_OWNER_CHANNEL and OPENCLAW_OWNER_TARGET are still missing.";
315
+ } else if (!openclawCommandResolvable) {
316
+ reason = "Owner forwarding is configured, but no runnable OpenClaw CLI or source checkout was found.";
317
+ } else {
318
+ reason = "This machine can read SilicaClaw broadcasts and route owner summaries through OpenClaw.";
319
+ }
320
+
321
+ return {
322
+ supported: bridgeMessagesReadable,
323
+ mode: "public-broadcast-via-openclaw" as const,
324
+ send_to_owner_via_openclaw: ready,
325
+ bridge_messages_readable: bridgeMessagesReadable,
326
+ forward_command_configured: forwardCommandConfigured,
327
+ openclaw_command_resolvable: openclawCommandResolvable,
328
+ ready,
329
+ forward_command: forwardCommand || null,
330
+ owner_channel: ownerChannel || null,
331
+ owner_target: ownerTarget || null,
332
+ owner_account: ownerAccount || null,
333
+ openclaw_source_dir: openclawSourceEntry ? openclawSourceDir : null,
334
+ reason,
335
+ };
336
+ }
337
+
147
338
  function hasMeaningfulJson(filePath: string): boolean {
148
339
  if (!existsSync(filePath)) return false;
149
340
  try {
@@ -245,14 +436,93 @@ type OpenClawBridgeStatus = {
245
436
  identity_source: "silicaclaw-existing" | "openclaw-existing" | "silicaclaw-generated";
246
437
  openclaw_identity_source_path: string | null;
247
438
  social_source_path: string | null;
439
+ openclaw_installation: {
440
+ detected: boolean;
441
+ detection_mode: "command" | "workspace" | "home" | "not_found";
442
+ command_path: string | null;
443
+ workspace_dir: string;
444
+ home_dir: string;
445
+ workspace_dir_exists: boolean;
446
+ home_dir_exists: boolean;
447
+ workspace_identity_path: string | null;
448
+ workspace_profile_path: string | null;
449
+ workspace_social_path: string | null;
450
+ workspace_skills_path: string | null;
451
+ home_identity_path: string | null;
452
+ home_profile_path: string | null;
453
+ home_social_path: string | null;
454
+ home_skills_path: string | null;
455
+ };
456
+ openclaw_runtime: {
457
+ running: boolean;
458
+ process_count: number;
459
+ processes: Array<{
460
+ pid: number;
461
+ ppid: number;
462
+ command: string;
463
+ }>;
464
+ detection_error: string | null;
465
+ };
466
+ skill_learning: {
467
+ available: boolean;
468
+ installed: boolean;
469
+ install_mode: "workspace" | "legacy" | "not_installed";
470
+ installed_skill_path: string | null;
471
+ install_action: {
472
+ supported: boolean;
473
+ endpoint: string;
474
+ recommended_command: string;
475
+ };
476
+ skills: Array<{
477
+ key: "get_profile" | "list_messages" | "watch_messages" | "send_message";
478
+ summary: string;
479
+ endpoint: string;
480
+ }>;
481
+ };
482
+ owner_delivery: {
483
+ supported: boolean;
484
+ mode: "public-broadcast-via-openclaw";
485
+ send_to_owner_via_openclaw: boolean;
486
+ bridge_messages_readable: boolean;
487
+ forward_command_configured: boolean;
488
+ openclaw_command_resolvable: boolean;
489
+ ready: boolean;
490
+ forward_command: string | null;
491
+ owner_channel: string | null;
492
+ owner_target: string | null;
493
+ owner_account: string | null;
494
+ openclaw_source_dir: string | null;
495
+ reason: string;
496
+ };
248
497
  endpoints: {
249
498
  status: string;
250
499
  profile: string;
251
500
  messages: string;
252
501
  send_message: string;
502
+ install_skill: string;
253
503
  };
254
504
  };
255
505
 
506
+ type OpenClawBridgeConfigView = {
507
+ bridge_api_base: string;
508
+ openclaw_detected: boolean;
509
+ openclaw_running: boolean;
510
+ openclaw_workspace_skill_dir: string;
511
+ openclaw_legacy_skill_dir: string;
512
+ silicaclaw_env_template_path: string;
513
+ recommended_skill_name: string;
514
+ recommended_install_command: string;
515
+ recommended_owner_forward_env: {
516
+ OPENCLAW_SOURCE_DIR: string;
517
+ OPENCLAW_OWNER_CHANNEL: string;
518
+ OPENCLAW_OWNER_TARGET: string;
519
+ OPENCLAW_OWNER_ACCOUNT: string;
520
+ OPENCLAW_OWNER_FORWARD_CMD: string;
521
+ };
522
+ owner_forward_command_example: string;
523
+ notes: string[];
524
+ };
525
+
256
526
  export class LocalNodeService {
257
527
  private workspaceRoot: string;
258
528
  private storageRoot: string;
@@ -276,6 +546,9 @@ export class LocalNodeService {
276
546
  private broadcastCount = 0;
277
547
  private lastMessageAt = 0;
278
548
  private lastBroadcastAt = 0;
549
+ private lastBroadcastErrorAt = 0;
550
+ private lastBroadcastError: string | null = null;
551
+ private broadcastFailureCount = 0;
279
552
  private broadcaster: NodeJS.Timeout | null = null;
280
553
  private subscriptionsBound = false;
281
554
  private broadcastEnabled = true;
@@ -294,7 +567,7 @@ export class LocalNodeService {
294
567
 
295
568
  private network: NetworkAdapter;
296
569
  private adapterMode: "mock" | "local-event-bus" | "real-preview" | "webrtc-preview" | "relay-preview";
297
- private networkMode: "local" | "lan" | "global-preview" = "lan";
570
+ private networkMode: "local" | "lan" | "global-preview" = "global-preview";
298
571
  private networkNamespace: string;
299
572
  private networkPort: number | null;
300
573
  private socialConfig: SocialConfig;
@@ -414,6 +687,9 @@ export class LocalNodeService {
414
687
  public_enabled: Boolean(this.profile?.public_enabled),
415
688
  broadcast_enabled: this.broadcastEnabled,
416
689
  last_broadcast_at: this.lastBroadcastAt,
690
+ last_broadcast_error_at: this.lastBroadcastErrorAt,
691
+ last_broadcast_error: this.lastBroadcastError,
692
+ broadcast_failure_count: this.broadcastFailureCount,
417
693
  discovered_count: profiles.length,
418
694
  online_count: onlineCount,
419
695
  offline_count: Math.max(0, profiles.length - onlineCount),
@@ -447,8 +723,11 @@ export class LocalNodeService {
447
723
  mode: this.networkMode,
448
724
  received_count: this.receivedCount,
449
725
  broadcast_count: this.broadcastCount,
726
+ broadcast_failure_count: this.broadcastFailureCount,
450
727
  last_message_at: this.lastMessageAt,
451
728
  last_broadcast_at: this.lastBroadcastAt,
729
+ last_broadcast_error_at: this.lastBroadcastErrorAt,
730
+ last_broadcast_error: this.lastBroadcastError,
452
731
  received_by_topic: this.receivedByTopic,
453
732
  published_by_topic: this.publishedByTopic,
454
733
  peers_discovered: peerCount,
@@ -561,8 +840,11 @@ export class LocalNodeService {
561
840
  message_counters: {
562
841
  received_total: this.receivedCount,
563
842
  broadcast_total: this.broadcastCount,
843
+ broadcast_failures_total: this.broadcastFailureCount,
564
844
  last_message_at: this.lastMessageAt,
565
845
  last_broadcast_at: this.lastBroadcastAt,
846
+ last_broadcast_error_at: this.lastBroadcastErrorAt,
847
+ last_broadcast_error: this.lastBroadcastError,
566
848
  received_by_topic: this.receivedByTopic,
567
849
  published_by_topic: this.publishedByTopic,
568
850
  },
@@ -977,6 +1259,15 @@ export class LocalNodeService {
977
1259
 
978
1260
  getOpenClawBridgeStatus(): OpenClawBridgeStatus {
979
1261
  const integration = this.getIntegrationStatus();
1262
+ const openclawInstallation = detectOpenClawInstallation(this.workspaceRoot);
1263
+ const openclawRuntime = detectOpenClawRuntime();
1264
+ const skillInstallation = detectOpenClawSkillInstallation();
1265
+ const ownerDelivery = detectOwnerDeliveryStatus({
1266
+ workspaceRoot: this.workspaceRoot,
1267
+ connectedToSilicaclaw: integration.connected_to_silicaclaw,
1268
+ openclawRunning: openclawRuntime.running,
1269
+ skillInstalled: skillInstallation.installed,
1270
+ });
980
1271
  return {
981
1272
  enabled: this.socialConfig.enabled,
982
1273
  connected_to_silicaclaw: integration.connected_to_silicaclaw,
@@ -989,15 +1280,66 @@ export class LocalNodeService {
989
1280
  identity_source: this.resolvedIdentitySource,
990
1281
  openclaw_identity_source_path: this.resolvedOpenClawIdentityPath,
991
1282
  social_source_path: this.socialSourcePath,
1283
+ openclaw_installation: openclawInstallation,
1284
+ openclaw_runtime: openclawRuntime,
1285
+ skill_learning: {
1286
+ available: integration.connected_to_silicaclaw && openclawRuntime.running,
1287
+ installed: skillInstallation.installed,
1288
+ install_mode: skillInstallation.install_mode,
1289
+ installed_skill_path: skillInstallation.workspace_skill_path || skillInstallation.legacy_skill_path,
1290
+ install_action: {
1291
+ supported: true,
1292
+ endpoint: "/api/openclaw/bridge/skill-install",
1293
+ recommended_command: "silicaclaw openclaw-skill-install",
1294
+ },
1295
+ skills: [
1296
+ {
1297
+ key: "get_profile",
1298
+ summary: "Read SilicaClaw identity/profile so OpenClaw can align its runtime persona.",
1299
+ endpoint: "/api/openclaw/bridge/profile",
1300
+ },
1301
+ {
1302
+ key: "list_messages",
1303
+ summary: "Read recent public broadcast messages observed by this SilicaClaw node.",
1304
+ endpoint: "/api/openclaw/bridge/messages",
1305
+ },
1306
+ {
1307
+ key: "watch_messages",
1308
+ summary: "Poll the recent broadcast feed so OpenClaw can learn from new public messages.",
1309
+ endpoint: "/api/openclaw/bridge/messages",
1310
+ },
1311
+ {
1312
+ key: "send_message",
1313
+ summary: "Publish a signed public broadcast through SilicaClaw on behalf of OpenClaw.",
1314
+ endpoint: "/api/openclaw/bridge/message",
1315
+ },
1316
+ ],
1317
+ },
1318
+ owner_delivery: ownerDelivery,
992
1319
  endpoints: {
993
1320
  status: "/api/openclaw/bridge",
994
1321
  profile: "/api/openclaw/bridge/profile",
995
1322
  messages: "/api/openclaw/bridge/messages",
996
1323
  send_message: "/api/openclaw/bridge/message",
1324
+ install_skill: "/api/openclaw/bridge/skill-install",
997
1325
  },
998
1326
  };
999
1327
  }
1000
1328
 
1329
+ async installOpenClawSkill() {
1330
+ const scriptPath = resolve(this.workspaceRoot, "scripts", "install-openclaw-skill.mjs");
1331
+ const { stdout } = await execFileAsync(process.execPath, [scriptPath], {
1332
+ cwd: this.workspaceRoot,
1333
+ env: process.env,
1334
+ maxBuffer: 1024 * 1024,
1335
+ });
1336
+ const parsed = JSON.parse(String(stdout || "{}"));
1337
+ return {
1338
+ ...parsed,
1339
+ bridge: this.getOpenClawBridgeStatus(),
1340
+ };
1341
+ }
1342
+
1001
1343
  getOpenClawBridgeProfile() {
1002
1344
  return {
1003
1345
  identity: this.getIdentity(),
@@ -1008,6 +1350,44 @@ export class LocalNodeService {
1008
1350
  };
1009
1351
  }
1010
1352
 
1353
+ getOpenClawBridgeConfig(): OpenClawBridgeConfigView {
1354
+ const homeDir = resolve(process.env.HOME || "", ".openclaw");
1355
+ const workspaceSkillDir = resolve(homeDir, "workspace", "skills");
1356
+ const legacySkillDir = resolve(homeDir, "skills");
1357
+ const openclawSourceDir = resolve(this.workspaceRoot, "..", "openclaw");
1358
+
1359
+ return {
1360
+ bridge_api_base: "http://localhost:4310",
1361
+ openclaw_detected: detectOpenClawInstallation(this.workspaceRoot).detected,
1362
+ openclaw_running: detectOpenClawRuntime().running,
1363
+ openclaw_workspace_skill_dir: workspaceSkillDir,
1364
+ openclaw_legacy_skill_dir: legacySkillDir,
1365
+ silicaclaw_env_template_path: resolve(this.workspaceRoot, "openclaw-owner-forward.env.example"),
1366
+ recommended_skill_name: "silicaclaw-broadcast",
1367
+ recommended_install_command: "silicaclaw openclaw-skill-install",
1368
+ recommended_owner_forward_env: {
1369
+ OPENCLAW_SOURCE_DIR: openclawSourceDir,
1370
+ OPENCLAW_OWNER_CHANNEL: "<channel>",
1371
+ OPENCLAW_OWNER_TARGET: "<target>",
1372
+ OPENCLAW_OWNER_ACCOUNT: "",
1373
+ OPENCLAW_OWNER_FORWARD_CMD: "node scripts/send-to-owner-via-openclaw.mjs",
1374
+ },
1375
+ owner_forward_command_example: [
1376
+ `OPENCLAW_SOURCE_DIR='${openclawSourceDir}'`,
1377
+ "OPENCLAW_OWNER_CHANNEL='<channel>'",
1378
+ "OPENCLAW_OWNER_TARGET='<target>'",
1379
+ "OPENCLAW_OWNER_FORWARD_CMD='node scripts/send-to-owner-via-openclaw.mjs'",
1380
+ "node scripts/owner-forwarder-demo.mjs",
1381
+ ].join(" "),
1382
+ notes: [
1383
+ "Install and maintain the skill from SilicaClaw; do not edit OpenClaw core source for this integration.",
1384
+ "OpenClaw learns broadcasts via the installed skill under ~/.openclaw/workspace/skills/.",
1385
+ "Owner delivery runs through OpenClaw's own message channel stack after the skill forwards a summary.",
1386
+ "Sensitive computer control still requires OpenClaw's own owner approval and node permission flow.",
1387
+ ],
1388
+ };
1389
+ }
1390
+
1011
1391
  getRuntimeMessageGovernance() {
1012
1392
  return this.messageGovernance;
1013
1393
  }
@@ -1050,7 +1430,7 @@ export class LocalNodeService {
1050
1430
  return this.getMessageGovernanceView();
1051
1431
  }
1052
1432
 
1053
- async sendSocialMessage(body: string, topic = DEFAULT_SOCIAL_MESSAGE_CHANNEL): Promise<{ sent: boolean; reason: string; message?: SocialMessageView }> {
1433
+ async sendSocialMessage(body: string, topic = DEFAULT_SOCIAL_MESSAGE_CHANNEL): Promise<{ sent: boolean; reason: string; message?: SocialMessageView; error?: string }> {
1054
1434
  if (!this.identity || !this.profile) {
1055
1435
  return { sent: false, reason: "missing_identity_or_profile" };
1056
1436
  }
@@ -1098,7 +1478,22 @@ export class LocalNodeService {
1098
1478
 
1099
1479
  this.recordTimestamp(this.outboundMessageTimestamps, this.messageGovernance.send_window_ms, message.created_at);
1100
1480
  this.ingestSocialMessage(message);
1101
- await this.publish(SOCIAL_MESSAGE_TOPIC, message);
1481
+ try {
1482
+ await this.publish(SOCIAL_MESSAGE_TOPIC, message);
1483
+ } catch (error) {
1484
+ const messageText = error instanceof Error ? error.message : String(error);
1485
+ this.lastBroadcastErrorAt = Date.now();
1486
+ this.lastBroadcastError = messageText;
1487
+ this.broadcastFailureCount += 1;
1488
+ await this.persistSocialMessages();
1489
+ await this.log("error", `Social message broadcast failed (${message.message_id.slice(0, 10)}): ${messageText}`);
1490
+ return {
1491
+ sent: false,
1492
+ reason: "publish_failed",
1493
+ error: messageText,
1494
+ message: this.getSocialMessages(1).items[0],
1495
+ };
1496
+ }
1102
1497
  await this.persistSocialMessages();
1103
1498
  await this.log("info", `Social message broadcast (${message.message_id.slice(0, 10)})`);
1104
1499
 
@@ -1219,7 +1614,7 @@ export class LocalNodeService {
1219
1614
  return { broadcast_enabled: this.broadcastEnabled };
1220
1615
  }
1221
1616
 
1222
- async broadcastNow(reason = "manual"): Promise<{ sent: boolean; reason: string }> {
1617
+ async broadcastNow(reason = "manual"): Promise<{ sent: boolean; reason: string; error?: string }> {
1223
1618
  if (!this.identity || !this.profile) {
1224
1619
  return { sent: false, reason: "missing_identity_or_profile" };
1225
1620
  }
@@ -1237,14 +1632,25 @@ export class LocalNodeService {
1237
1632
  const presenceRecord = signPresence(this.identity, Date.now());
1238
1633
  const indexRecords = buildIndexRecords(this.profile);
1239
1634
 
1240
- await this.publish("profile", profileRecord);
1241
- await this.publish("presence", presenceRecord);
1242
- for (const record of indexRecords) {
1243
- await this.publish("index", record);
1635
+ try {
1636
+ await this.publish("profile", profileRecord);
1637
+ await this.publish("presence", presenceRecord);
1638
+ for (const record of indexRecords) {
1639
+ await this.publish("index", record);
1640
+ }
1641
+ } catch (error) {
1642
+ const message = error instanceof Error ? error.message : String(error);
1643
+ this.lastBroadcastErrorAt = Date.now();
1644
+ this.lastBroadcastError = message;
1645
+ this.broadcastFailureCount += 1;
1646
+ await this.log("error", `Broadcast failed (reason=${reason}): ${message}`);
1647
+ return { sent: false, reason: "publish_failed", error: message };
1244
1648
  }
1245
1649
 
1246
1650
  this.lastBroadcastAt = Date.now();
1247
1651
  this.broadcastCount += 1;
1652
+ this.lastBroadcastError = null;
1653
+ this.lastBroadcastErrorAt = 0;
1248
1654
 
1249
1655
  this.directory = ingestProfileRecord(this.directory, profileRecord);
1250
1656
  this.directory = ingestPresenceRecord(this.directory, presenceRecord);
@@ -1785,7 +2191,7 @@ export class LocalNodeService {
1785
2191
  this.socialConfig.network.mode ||
1786
2192
  (modeEnv === "local" || modeEnv === "lan" || modeEnv === "global-preview"
1787
2193
  ? modeEnv
1788
- : "lan");
2194
+ : "global-preview");
1789
2195
 
1790
2196
  this.networkMode = resolvedMode;
1791
2197
  this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || "silicaclaw.preview";
@@ -2386,6 +2792,10 @@ export async function main() {
2386
2792
  sendOk(res, node.getOpenClawBridgeStatus());
2387
2793
  });
2388
2794
 
2795
+ app.get("/api/openclaw/bridge/config", (_req, res) => {
2796
+ sendOk(res, node.getOpenClawBridgeConfig());
2797
+ });
2798
+
2389
2799
  app.get("/api/openclaw/bridge/profile", (_req, res) => {
2390
2800
  sendOk(res, node.getOpenClawBridgeProfile());
2391
2801
  });
@@ -2408,6 +2818,25 @@ export async function main() {
2408
2818
  })
2409
2819
  );
2410
2820
 
2821
+ app.post(
2822
+ "/api/openclaw/bridge/skill-install",
2823
+ asyncRoute(async (_req, res) => {
2824
+ try {
2825
+ const result = await node.installOpenClawSkill();
2826
+ sendOk(res, result, {
2827
+ message: "OpenClaw skill installed",
2828
+ });
2829
+ } catch (error) {
2830
+ sendError(
2831
+ res,
2832
+ 500,
2833
+ "OPENCLAW_SKILL_INSTALL_FAILED",
2834
+ error instanceof Error ? error.message : "OpenClaw skill install failed"
2835
+ );
2836
+ }
2837
+ })
2838
+ );
2839
+
2411
2840
  app.post(
2412
2841
  "/api/cache/refresh",
2413
2842
  asyncRoute(async (_req, res) => {