@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.
- package/CHANGELOG.md +1 -1
- package/INSTALL.md +31 -0
- package/README.md +28 -0
- package/VERSION +1 -1
- package/apps/local-console/public/index.html +1327 -245
- package/apps/local-console/src/server.ts +439 -10
- package/docs/OPENCLAW_BRIDGE.md +85 -0
- package/docs/OPENCLAW_BRIDGE_ZH.md +90 -0
- package/openclaw-skills/silicaclaw-broadcast/SKILL.md +89 -0
- package/openclaw-skills/silicaclaw-broadcast/VERSION +1 -0
- package/openclaw-skills/silicaclaw-broadcast/agents/openai.yaml +6 -0
- package/openclaw-skills/silicaclaw-broadcast/manifest.json +34 -0
- package/openclaw-skills/silicaclaw-broadcast/references/computer-control-via-openclaw.md +41 -0
- package/openclaw-skills/silicaclaw-broadcast/references/owner-dispatch-adapter.md +81 -0
- package/openclaw-skills/silicaclaw-broadcast/references/owner-forwarding-policy.md +48 -0
- package/openclaw-skills/silicaclaw-broadcast/scripts/bridge-client.mjs +59 -0
- package/openclaw-skills/silicaclaw-broadcast/scripts/owner-dispatch-adapter-demo.mjs +12 -0
- package/openclaw-skills/silicaclaw-broadcast/scripts/owner-forwarder-demo.mjs +111 -0
- package/openclaw-skills/silicaclaw-broadcast/scripts/send-to-owner-via-openclaw.mjs +69 -0
- package/package.json +2 -1
- package/packages/core/dist/socialConfig.js +1 -1
- package/packages/core/dist/socialTemplate.js +1 -1
- package/packages/core/src/socialConfig.ts +1 -1
- package/packages/core/src/socialTemplate.ts +1 -1
- package/packages/network/dist/relayPreview.js +16 -4
- package/packages/network/src/relayPreview.ts +17 -4
- package/scripts/functional-check.mjs +29 -0
- package/scripts/install-openclaw-skill.mjs +54 -0
- package/scripts/openclaw-bridge-adapter.mjs +7 -0
- package/scripts/openclaw-bridge-client.mjs +42 -0
- package/scripts/pack-openclaw-skill.mjs +58 -0
- package/scripts/silicaclaw-cli.mjs +18 -0
- 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" = "
|
|
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
|
-
|
|
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
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
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
|
-
: "
|
|
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) => {
|