@silicaclaw/cli 1.0.0-beta.0
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/ARCHITECTURE.md +137 -0
- package/CHANGELOG.md +411 -0
- package/DEMO_GUIDE.md +89 -0
- package/INSTALL.md +156 -0
- package/README.md +244 -0
- package/RELEASE_NOTES_v1.0.md +65 -0
- package/ROADMAP.md +48 -0
- package/SOCIAL_MD_SPEC.md +122 -0
- package/VERSION +1 -0
- package/apps/local-console/package.json +23 -0
- package/apps/local-console/public/assets/README.md +5 -0
- package/apps/local-console/public/assets/silicaclaw-logo.png +0 -0
- package/apps/local-console/public/index.html +1602 -0
- package/apps/local-console/src/server.ts +1656 -0
- package/apps/local-console/src/socialRoutes.ts +90 -0
- package/apps/local-console/tsconfig.json +7 -0
- package/apps/public-explorer/package.json +20 -0
- package/apps/public-explorer/public/assets/README.md +5 -0
- package/apps/public-explorer/public/assets/silicaclaw-logo.png +0 -0
- package/apps/public-explorer/public/index.html +483 -0
- package/apps/public-explorer/src/server.ts +32 -0
- package/apps/public-explorer/tsconfig.json +7 -0
- package/docs/QUICK_START.md +48 -0
- package/docs/assets/README.md +8 -0
- package/docs/assets/banner.svg +25 -0
- package/docs/assets/silicaclaw-logo.png +0 -0
- package/docs/assets/silicaclaw-og.png +0 -0
- package/docs/release/GITHUB_RELEASE_v1.0-beta.md +143 -0
- package/docs/screenshots/README.md +8 -0
- package/docs/screenshots/v0.3.1-explorer-search.svg +9 -0
- package/docs/screenshots/v0.3.1-machine-a-network.svg +9 -0
- package/docs/screenshots/v0.3.1-machine-b-peers.svg +9 -0
- package/docs/screenshots/v0.3.1-stale-transition.svg +9 -0
- package/openclaw.social.md.example +28 -0
- package/package.json +64 -0
- package/packages/core/package.json +13 -0
- package/packages/core/src/crypto.ts +55 -0
- package/packages/core/src/directory.ts +171 -0
- package/packages/core/src/identity.ts +14 -0
- package/packages/core/src/index.ts +11 -0
- package/packages/core/src/indexing.ts +42 -0
- package/packages/core/src/presence.ts +24 -0
- package/packages/core/src/profile.ts +39 -0
- package/packages/core/src/publicProfileSummary.ts +180 -0
- package/packages/core/src/socialConfig.ts +440 -0
- package/packages/core/src/socialResolver.ts +281 -0
- package/packages/core/src/socialTemplate.ts +97 -0
- package/packages/core/src/types.ts +43 -0
- package/packages/core/tsconfig.json +7 -0
- package/packages/network/package.json +10 -0
- package/packages/network/src/abstractions/messageEnvelope.ts +80 -0
- package/packages/network/src/abstractions/peerDiscovery.ts +49 -0
- package/packages/network/src/abstractions/topicCodec.ts +4 -0
- package/packages/network/src/abstractions/transport.ts +40 -0
- package/packages/network/src/codec/jsonMessageEnvelopeCodec.ts +22 -0
- package/packages/network/src/codec/jsonTopicCodec.ts +11 -0
- package/packages/network/src/discovery/heartbeatPeerDiscovery.ts +173 -0
- package/packages/network/src/index.ts +16 -0
- package/packages/network/src/localEventBus.ts +61 -0
- package/packages/network/src/mock.ts +27 -0
- package/packages/network/src/realPreview.ts +436 -0
- package/packages/network/src/transport/udpLanBroadcastTransport.ts +173 -0
- package/packages/network/src/types.ts +6 -0
- package/packages/network/src/webrtcPreview.ts +1052 -0
- package/packages/network/tsconfig.json +7 -0
- package/packages/storage/package.json +13 -0
- package/packages/storage/src/index.ts +3 -0
- package/packages/storage/src/jsonRepo.ts +25 -0
- package/packages/storage/src/repos.ts +46 -0
- package/packages/storage/src/socialRuntimeRepo.ts +51 -0
- package/packages/storage/tsconfig.json +7 -0
- package/scripts/functional-check.mjs +165 -0
- package/scripts/install-logo.sh +53 -0
- package/scripts/quickstart.sh +144 -0
- package/scripts/silicaclaw-cli.mjs +88 -0
- package/scripts/webrtc-signaling-server.mjs +249 -0
- package/social.md.example +30 -0
|
@@ -0,0 +1,1656 @@
|
|
|
1
|
+
import express, { NextFunction, Request, Response } from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "fs";
|
|
5
|
+
import { createHash } from "crypto";
|
|
6
|
+
import { hostname } from "os";
|
|
7
|
+
import {
|
|
8
|
+
AgentIdentity,
|
|
9
|
+
DirectoryState,
|
|
10
|
+
IndexRefRecord,
|
|
11
|
+
PresenceRecord,
|
|
12
|
+
ProfileInput,
|
|
13
|
+
PublicProfile,
|
|
14
|
+
PublicProfileSummary,
|
|
15
|
+
SignedProfileRecord,
|
|
16
|
+
buildPublicProfileSummary,
|
|
17
|
+
buildIndexRecords,
|
|
18
|
+
cleanupExpiredPresence,
|
|
19
|
+
createDefaultProfileInput,
|
|
20
|
+
createEmptyDirectoryState,
|
|
21
|
+
createIdentity,
|
|
22
|
+
dedupeIndex,
|
|
23
|
+
ensureDefaultSocialMd,
|
|
24
|
+
ingestIndexRecord,
|
|
25
|
+
ingestPresenceRecord,
|
|
26
|
+
ingestProfileRecord,
|
|
27
|
+
isAgentOnline,
|
|
28
|
+
loadSocialConfig,
|
|
29
|
+
getSocialConfigSearchPaths,
|
|
30
|
+
resolveIdentityWithSocial,
|
|
31
|
+
resolveProfileInputWithSocial,
|
|
32
|
+
searchDirectory,
|
|
33
|
+
signPresence,
|
|
34
|
+
signProfile,
|
|
35
|
+
SocialConfig,
|
|
36
|
+
SocialRuntimeConfig,
|
|
37
|
+
generateSocialMdTemplate,
|
|
38
|
+
verifyPresence,
|
|
39
|
+
verifyProfile,
|
|
40
|
+
} from "@silicaclaw/core";
|
|
41
|
+
import {
|
|
42
|
+
HeartbeatPeerDiscovery,
|
|
43
|
+
LocalEventBusAdapter,
|
|
44
|
+
MockNetworkAdapter,
|
|
45
|
+
NetworkAdapter,
|
|
46
|
+
RealNetworkAdapterPreview,
|
|
47
|
+
UdpLanBroadcastTransport,
|
|
48
|
+
WebRTCPreviewAdapter,
|
|
49
|
+
} from "@silicaclaw/network";
|
|
50
|
+
import { CacheRepo, IdentityRepo, LogRepo, ProfileRepo, SocialRuntimeRepo } from "@silicaclaw/storage";
|
|
51
|
+
import { registerSocialRoutes } from "./socialRoutes";
|
|
52
|
+
|
|
53
|
+
const BROADCAST_INTERVAL_MS = 10_000;
|
|
54
|
+
const PRESENCE_TTL_MS = Number(process.env.PRESENCE_TTL_MS || 30_000);
|
|
55
|
+
const NETWORK_MAX_MESSAGE_BYTES = Number(process.env.NETWORK_MAX_MESSAGE_BYTES || 64 * 1024);
|
|
56
|
+
const NETWORK_DEDUPE_WINDOW_MS = Number(process.env.NETWORK_DEDUPE_WINDOW_MS || 90_000);
|
|
57
|
+
const NETWORK_DEDUPE_MAX_ENTRIES = Number(process.env.NETWORK_DEDUPE_MAX_ENTRIES || 10_000);
|
|
58
|
+
const NETWORK_MAX_FUTURE_DRIFT_MS = Number(process.env.NETWORK_MAX_FUTURE_DRIFT_MS || 30_000);
|
|
59
|
+
const NETWORK_MAX_PAST_DRIFT_MS = Number(process.env.NETWORK_MAX_PAST_DRIFT_MS || 120_000);
|
|
60
|
+
const NETWORK_HEARTBEAT_INTERVAL_MS = Number(process.env.NETWORK_HEARTBEAT_INTERVAL_MS || 12_000);
|
|
61
|
+
const NETWORK_PEER_STALE_AFTER_MS = Number(process.env.NETWORK_PEER_STALE_AFTER_MS || 45_000);
|
|
62
|
+
const NETWORK_PEER_REMOVE_AFTER_MS = Number(process.env.NETWORK_PEER_REMOVE_AFTER_MS || 180_000);
|
|
63
|
+
const NETWORK_UDP_BIND_ADDRESS = process.env.NETWORK_UDP_BIND_ADDRESS || "0.0.0.0";
|
|
64
|
+
const NETWORK_UDP_BROADCAST_ADDRESS = process.env.NETWORK_UDP_BROADCAST_ADDRESS || "255.255.255.255";
|
|
65
|
+
const NETWORK_PEER_ID = process.env.NETWORK_PEER_ID;
|
|
66
|
+
const WEBRTC_SIGNALING_URL = process.env.WEBRTC_SIGNALING_URL || "http://localhost:4510";
|
|
67
|
+
const WEBRTC_SIGNALING_URLS = process.env.WEBRTC_SIGNALING_URLS || "";
|
|
68
|
+
const WEBRTC_ROOM = process.env.WEBRTC_ROOM || "silicaclaw-room";
|
|
69
|
+
const WEBRTC_SEED_PEERS = process.env.WEBRTC_SEED_PEERS || "";
|
|
70
|
+
const WEBRTC_BOOTSTRAP_HINTS = process.env.WEBRTC_BOOTSTRAP_HINTS || "";
|
|
71
|
+
const PROFILE_VERSION = "v0.9";
|
|
72
|
+
|
|
73
|
+
function resolveWorkspaceRoot(cwd = process.cwd()): string {
|
|
74
|
+
if (existsSync(resolve(cwd, "apps", "local-console", "package.json"))) {
|
|
75
|
+
return cwd;
|
|
76
|
+
}
|
|
77
|
+
const candidate = resolve(cwd, "..", "..");
|
|
78
|
+
if (existsSync(resolve(candidate, "apps", "local-console", "package.json"))) {
|
|
79
|
+
return candidate;
|
|
80
|
+
}
|
|
81
|
+
return cwd;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveStorageRoot(workspaceRoot: string, cwd = process.cwd()): string {
|
|
85
|
+
const appRoot = resolve(workspaceRoot, "apps", "local-console");
|
|
86
|
+
if (existsSync(resolve(appRoot, "package.json"))) {
|
|
87
|
+
return appRoot;
|
|
88
|
+
}
|
|
89
|
+
return cwd;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function hasMeaningfulJson(filePath: string): boolean {
|
|
93
|
+
if (!existsSync(filePath)) return false;
|
|
94
|
+
try {
|
|
95
|
+
const raw = readFileSync(filePath, "utf8").trim();
|
|
96
|
+
if (!raw) return false;
|
|
97
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
98
|
+
if (parsed == null) return false;
|
|
99
|
+
if (Array.isArray(parsed)) return parsed.length > 0;
|
|
100
|
+
if (typeof parsed === "object") return Object.keys(parsed as Record<string, unknown>).length > 0;
|
|
101
|
+
return false;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function migrateLegacyDataIfNeeded(workspaceRoot: string, storageRoot: string): void {
|
|
108
|
+
const legacyDataDir = resolve(workspaceRoot, "data");
|
|
109
|
+
const targetDataDir = resolve(storageRoot, "data");
|
|
110
|
+
if (legacyDataDir === targetDataDir) return;
|
|
111
|
+
const files = ["identity.json", "profile.json", "cache.json", "logs.json"];
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
const src = resolve(legacyDataDir, file);
|
|
114
|
+
const dst = resolve(targetDataDir, file);
|
|
115
|
+
if (!existsSync(src)) continue;
|
|
116
|
+
if (hasMeaningfulJson(dst)) continue;
|
|
117
|
+
if (!hasMeaningfulJson(src)) continue;
|
|
118
|
+
mkdirSync(targetDataDir, { recursive: true });
|
|
119
|
+
copyFileSync(src, dst);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseListEnv(raw: string): string[] {
|
|
124
|
+
return raw
|
|
125
|
+
.split(/[,\n]/g)
|
|
126
|
+
.map((item) => item.trim())
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function dedupeStrings(values: string[]): string[] {
|
|
131
|
+
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean)));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
type ApiErrorShape = {
|
|
135
|
+
code: string;
|
|
136
|
+
message: string;
|
|
137
|
+
details?: unknown;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
type InitState = {
|
|
141
|
+
identity_auto_created: boolean;
|
|
142
|
+
profile_auto_created: boolean;
|
|
143
|
+
social_auto_created: boolean;
|
|
144
|
+
initialized_at: number;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type IntegrationStatusSummary = {
|
|
148
|
+
configured: boolean;
|
|
149
|
+
running: boolean;
|
|
150
|
+
discoverable: boolean;
|
|
151
|
+
network_mode: "local" | "lan" | "global-preview";
|
|
152
|
+
public_enabled: boolean;
|
|
153
|
+
agent_id: string;
|
|
154
|
+
display_name: string;
|
|
155
|
+
connected_to_silicaclaw: boolean;
|
|
156
|
+
configured_reason: string;
|
|
157
|
+
running_reason: string;
|
|
158
|
+
discoverable_reason: string;
|
|
159
|
+
status_line: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
class LocalNodeService {
|
|
163
|
+
private workspaceRoot: string;
|
|
164
|
+
private storageRoot: string;
|
|
165
|
+
private identityRepo: IdentityRepo;
|
|
166
|
+
private profileRepo: ProfileRepo;
|
|
167
|
+
private cacheRepo: CacheRepo;
|
|
168
|
+
private logRepo: LogRepo;
|
|
169
|
+
private socialRuntimeRepo: SocialRuntimeRepo;
|
|
170
|
+
|
|
171
|
+
private identity: AgentIdentity | null = null;
|
|
172
|
+
private profile: PublicProfile | null = null;
|
|
173
|
+
private directory: DirectoryState = createEmptyDirectoryState();
|
|
174
|
+
|
|
175
|
+
private receivedCount = 0;
|
|
176
|
+
private broadcastCount = 0;
|
|
177
|
+
private lastMessageAt = 0;
|
|
178
|
+
private lastBroadcastAt = 0;
|
|
179
|
+
private broadcaster: NodeJS.Timeout | null = null;
|
|
180
|
+
private broadcastEnabled = true;
|
|
181
|
+
|
|
182
|
+
private receivedByTopic: Record<string, number> = {};
|
|
183
|
+
private publishedByTopic: Record<string, number> = {};
|
|
184
|
+
|
|
185
|
+
private initState: InitState = {
|
|
186
|
+
identity_auto_created: false,
|
|
187
|
+
profile_auto_created: false,
|
|
188
|
+
social_auto_created: false,
|
|
189
|
+
initialized_at: 0,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
private network: NetworkAdapter;
|
|
193
|
+
private adapterMode: "mock" | "local-event-bus" | "real-preview" | "webrtc-preview";
|
|
194
|
+
private networkMode: "local" | "lan" | "global-preview" = "lan";
|
|
195
|
+
private networkNamespace: string;
|
|
196
|
+
private networkPort: number | null;
|
|
197
|
+
private socialConfig: SocialConfig;
|
|
198
|
+
private socialSourcePath: string | null = null;
|
|
199
|
+
private socialFound = false;
|
|
200
|
+
private socialParseError: string | null = null;
|
|
201
|
+
private socialRawFrontmatter: Record<string, unknown> | null = null;
|
|
202
|
+
private socialRuntime: SocialRuntimeConfig | null = null;
|
|
203
|
+
private socialNetworkRequiresRestart = false;
|
|
204
|
+
private resolvedIdentitySource: "silicaclaw-existing" | "openclaw-existing" | "silicaclaw-generated" =
|
|
205
|
+
"silicaclaw-existing";
|
|
206
|
+
private resolvedOpenClawIdentityPath: string | null = null;
|
|
207
|
+
private webrtcSignalingUrls: string[] = [];
|
|
208
|
+
private webrtcRoom = "silicaclaw-room";
|
|
209
|
+
private webrtcSeedPeers: string[] = [];
|
|
210
|
+
private webrtcBootstrapHints: string[] = [];
|
|
211
|
+
private webrtcBootstrapSources: string[] = [];
|
|
212
|
+
|
|
213
|
+
constructor() {
|
|
214
|
+
this.workspaceRoot = resolveWorkspaceRoot();
|
|
215
|
+
this.storageRoot = resolveStorageRoot(this.workspaceRoot);
|
|
216
|
+
migrateLegacyDataIfNeeded(this.workspaceRoot, this.storageRoot);
|
|
217
|
+
|
|
218
|
+
this.identityRepo = new IdentityRepo(this.storageRoot);
|
|
219
|
+
this.profileRepo = new ProfileRepo(this.storageRoot);
|
|
220
|
+
this.cacheRepo = new CacheRepo(this.storageRoot);
|
|
221
|
+
this.logRepo = new LogRepo(this.storageRoot);
|
|
222
|
+
this.socialRuntimeRepo = new SocialRuntimeRepo(this.storageRoot);
|
|
223
|
+
|
|
224
|
+
let loadedSocial = loadSocialConfig(this.workspaceRoot);
|
|
225
|
+
if (!loadedSocial.meta.found) {
|
|
226
|
+
ensureDefaultSocialMd(this.workspaceRoot, {
|
|
227
|
+
display_name: this.getDefaultDisplayName(),
|
|
228
|
+
bio: "Local AI agent connected to SilicaClaw",
|
|
229
|
+
tags: ["openclaw", "local-first"],
|
|
230
|
+
mode: "lan",
|
|
231
|
+
public_enabled: false,
|
|
232
|
+
});
|
|
233
|
+
loadedSocial = loadSocialConfig(this.workspaceRoot);
|
|
234
|
+
this.initState.social_auto_created = true;
|
|
235
|
+
}
|
|
236
|
+
this.socialConfig = loadedSocial.config;
|
|
237
|
+
this.socialSourcePath = loadedSocial.meta.source_path;
|
|
238
|
+
this.socialFound = loadedSocial.meta.found;
|
|
239
|
+
this.socialParseError = loadedSocial.meta.parse_error;
|
|
240
|
+
this.socialRawFrontmatter = loadedSocial.raw_frontmatter;
|
|
241
|
+
|
|
242
|
+
this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || "silicaclaw.preview";
|
|
243
|
+
this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT || 44123);
|
|
244
|
+
this.applyResolvedNetworkConfig();
|
|
245
|
+
const resolved = this.buildNetworkAdapter();
|
|
246
|
+
this.network = resolved.adapter;
|
|
247
|
+
this.adapterMode = resolved.mode;
|
|
248
|
+
this.networkPort = resolved.port;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async start(): Promise<void> {
|
|
252
|
+
await this.hydrateFromDisk();
|
|
253
|
+
|
|
254
|
+
await this.network.start();
|
|
255
|
+
this.bindNetworkSubscriptions();
|
|
256
|
+
|
|
257
|
+
this.startBroadcastLoop();
|
|
258
|
+
await this.log("info", "Local node started");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async stop(): Promise<void> {
|
|
262
|
+
if (this.broadcaster) {
|
|
263
|
+
clearInterval(this.broadcaster);
|
|
264
|
+
this.broadcaster = null;
|
|
265
|
+
}
|
|
266
|
+
await this.network.stop();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
getOverview() {
|
|
270
|
+
this.compactCacheInMemory();
|
|
271
|
+
const profiles = Object.values(this.directory.profiles);
|
|
272
|
+
const onlineCount = profiles.filter((profile) =>
|
|
273
|
+
isAgentOnline(this.directory.presence[profile.agent_id], Date.now(), PRESENCE_TTL_MS)
|
|
274
|
+
).length;
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
agent_id: this.identity?.agent_id ?? "",
|
|
278
|
+
public_enabled: Boolean(this.profile?.public_enabled),
|
|
279
|
+
broadcast_enabled: this.broadcastEnabled,
|
|
280
|
+
last_broadcast_at: this.lastBroadcastAt,
|
|
281
|
+
discovered_count: profiles.length,
|
|
282
|
+
online_count: onlineCount,
|
|
283
|
+
offline_count: Math.max(0, profiles.length - onlineCount),
|
|
284
|
+
init_state: this.initState,
|
|
285
|
+
presence_ttl_ms: PRESENCE_TTL_MS,
|
|
286
|
+
onboarding: this.getOnboardingSummary(),
|
|
287
|
+
social: {
|
|
288
|
+
found: this.socialFound,
|
|
289
|
+
enabled: this.socialConfig.enabled,
|
|
290
|
+
source_path: this.socialSourcePath,
|
|
291
|
+
network_mode: this.networkMode,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
getNetworkSummary() {
|
|
297
|
+
const diagnostics = this.getAdapterDiagnostics();
|
|
298
|
+
const peerCount = diagnostics?.peers.total ?? 0;
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
status: "running",
|
|
302
|
+
adapter: this.adapterMode,
|
|
303
|
+
mode: this.networkMode,
|
|
304
|
+
received_count: this.receivedCount,
|
|
305
|
+
broadcast_count: this.broadcastCount,
|
|
306
|
+
last_message_at: this.lastMessageAt,
|
|
307
|
+
last_broadcast_at: this.lastBroadcastAt,
|
|
308
|
+
received_by_topic: this.receivedByTopic,
|
|
309
|
+
published_by_topic: this.publishedByTopic,
|
|
310
|
+
peers_discovered: peerCount,
|
|
311
|
+
namespace: diagnostics?.namespace ?? this.networkNamespace,
|
|
312
|
+
port: this.networkPort,
|
|
313
|
+
components: diagnostics?.components ?? {
|
|
314
|
+
transport: "-",
|
|
315
|
+
discovery: "-",
|
|
316
|
+
envelope_codec: "-",
|
|
317
|
+
topic_codec: "-",
|
|
318
|
+
},
|
|
319
|
+
real_preview_stats: diagnostics?.stats ?? null,
|
|
320
|
+
real_preview_transport_stats: diagnostics?.transport_stats ?? null,
|
|
321
|
+
real_preview_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
322
|
+
webrtc_preview: diagnostics && diagnostics.adapter === "webrtc-preview"
|
|
323
|
+
? {
|
|
324
|
+
signaling_url: diagnostics.signaling_url ?? null,
|
|
325
|
+
signaling_endpoints: diagnostics.signaling_endpoints ?? [],
|
|
326
|
+
room: diagnostics.room ?? null,
|
|
327
|
+
bootstrap_sources: diagnostics.bootstrap_sources ?? [],
|
|
328
|
+
seed_peers_count: diagnostics.seed_peers_count ?? 0,
|
|
329
|
+
discovery_events_total: diagnostics.discovery_events_total ?? 0,
|
|
330
|
+
last_discovery_event_at: diagnostics.last_discovery_event_at ?? 0,
|
|
331
|
+
active_webrtc_peers: diagnostics.active_webrtc_peers ?? 0,
|
|
332
|
+
reconnect_attempts_total: diagnostics.reconnect_attempts_total ?? 0,
|
|
333
|
+
}
|
|
334
|
+
: null,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
getNetworkConfig() {
|
|
339
|
+
const diagnostics = this.getAdapterDiagnostics();
|
|
340
|
+
return {
|
|
341
|
+
adapter: this.adapterMode,
|
|
342
|
+
mode: this.networkMode,
|
|
343
|
+
namespace: diagnostics?.namespace ?? this.networkNamespace,
|
|
344
|
+
port: this.networkPort,
|
|
345
|
+
components: diagnostics?.components ?? {
|
|
346
|
+
transport: "-",
|
|
347
|
+
discovery: "-",
|
|
348
|
+
envelope_codec: "-",
|
|
349
|
+
topic_codec: "-",
|
|
350
|
+
},
|
|
351
|
+
limits: diagnostics?.limits ?? null,
|
|
352
|
+
adapter_config: diagnostics?.config ?? null,
|
|
353
|
+
adapter_extra: diagnostics && diagnostics.adapter === "webrtc-preview"
|
|
354
|
+
? {
|
|
355
|
+
signaling_url: diagnostics.signaling_url ?? null,
|
|
356
|
+
signaling_endpoints: diagnostics.signaling_endpoints ?? [],
|
|
357
|
+
room: diagnostics.room ?? null,
|
|
358
|
+
bootstrap_sources: diagnostics.bootstrap_sources ?? [],
|
|
359
|
+
seed_peers_count: diagnostics.seed_peers_count ?? 0,
|
|
360
|
+
discovery_events_total: diagnostics.discovery_events_total ?? 0,
|
|
361
|
+
last_discovery_event_at: diagnostics.last_discovery_event_at ?? 0,
|
|
362
|
+
connection_states_summary: diagnostics.connection_states_summary ?? null,
|
|
363
|
+
datachannel_states_summary: diagnostics.datachannel_states_summary ?? null,
|
|
364
|
+
}
|
|
365
|
+
: null,
|
|
366
|
+
env: {
|
|
367
|
+
NETWORK_ADAPTER: this.adapterMode,
|
|
368
|
+
NETWORK_NAMESPACE: this.networkNamespace,
|
|
369
|
+
NETWORK_PORT: this.networkPort,
|
|
370
|
+
NETWORK_MAX_MESSAGE_BYTES,
|
|
371
|
+
NETWORK_DEDUPE_WINDOW_MS,
|
|
372
|
+
NETWORK_DEDUPE_MAX_ENTRIES,
|
|
373
|
+
NETWORK_MAX_FUTURE_DRIFT_MS,
|
|
374
|
+
NETWORK_MAX_PAST_DRIFT_MS,
|
|
375
|
+
NETWORK_HEARTBEAT_INTERVAL_MS,
|
|
376
|
+
NETWORK_PEER_STALE_AFTER_MS,
|
|
377
|
+
NETWORK_PEER_REMOVE_AFTER_MS,
|
|
378
|
+
NETWORK_UDP_BIND_ADDRESS,
|
|
379
|
+
NETWORK_UDP_BROADCAST_ADDRESS,
|
|
380
|
+
NETWORK_PEER_ID: NETWORK_PEER_ID ?? null,
|
|
381
|
+
WEBRTC_SIGNALING_URLS,
|
|
382
|
+
WEBRTC_SIGNALING_URL,
|
|
383
|
+
WEBRTC_ROOM,
|
|
384
|
+
WEBRTC_SEED_PEERS,
|
|
385
|
+
WEBRTC_BOOTSTRAP_HINTS,
|
|
386
|
+
},
|
|
387
|
+
demo_mode:
|
|
388
|
+
this.adapterMode === "real-preview"
|
|
389
|
+
? "lan-preview"
|
|
390
|
+
: this.adapterMode === "webrtc-preview"
|
|
391
|
+
? "webrtc-preview"
|
|
392
|
+
: "local-process",
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
getNetworkStats() {
|
|
397
|
+
const diagnostics = this.getAdapterDiagnostics();
|
|
398
|
+
const peers: Array<{ status?: string }> = diagnostics?.peers?.items ?? [];
|
|
399
|
+
const online = peers.filter((peer: { status?: string }) => peer.status === "online").length;
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
adapter: this.adapterMode,
|
|
403
|
+
mode: this.networkMode,
|
|
404
|
+
message_counters: {
|
|
405
|
+
received_total: this.receivedCount,
|
|
406
|
+
broadcast_total: this.broadcastCount,
|
|
407
|
+
last_message_at: this.lastMessageAt,
|
|
408
|
+
last_broadcast_at: this.lastBroadcastAt,
|
|
409
|
+
received_by_topic: this.receivedByTopic,
|
|
410
|
+
published_by_topic: this.publishedByTopic,
|
|
411
|
+
},
|
|
412
|
+
peer_counters: {
|
|
413
|
+
total: peers.length,
|
|
414
|
+
online,
|
|
415
|
+
stale: Math.max(0, peers.length - online),
|
|
416
|
+
},
|
|
417
|
+
adapter_config: diagnostics?.config ?? null,
|
|
418
|
+
adapter_stats: diagnostics?.stats ?? null,
|
|
419
|
+
adapter_transport_stats: diagnostics?.transport_stats ?? null,
|
|
420
|
+
adapter_discovery_stats: diagnostics?.discovery_stats ?? null,
|
|
421
|
+
adapter_diagnostics_summary: diagnostics
|
|
422
|
+
? {
|
|
423
|
+
signaling_url: diagnostics.signaling_url ?? null,
|
|
424
|
+
signaling_endpoints: diagnostics.signaling_endpoints ?? [],
|
|
425
|
+
room: diagnostics.room ?? null,
|
|
426
|
+
bootstrap_sources: diagnostics.bootstrap_sources ?? [],
|
|
427
|
+
seed_peers_count: diagnostics.seed_peers_count ?? 0,
|
|
428
|
+
discovery_events_total: diagnostics.discovery_events_total ?? 0,
|
|
429
|
+
last_discovery_event_at: diagnostics.last_discovery_event_at ?? 0,
|
|
430
|
+
connection_states_summary: diagnostics.connection_states_summary ?? null,
|
|
431
|
+
datachannel_states_summary: diagnostics.datachannel_states_summary ?? null,
|
|
432
|
+
signaling_messages_sent_total: diagnostics.signaling_messages_sent_total ?? null,
|
|
433
|
+
signaling_messages_received_total: diagnostics.signaling_messages_received_total ?? null,
|
|
434
|
+
reconnect_attempts_total: diagnostics.reconnect_attempts_total ?? null,
|
|
435
|
+
active_webrtc_peers: diagnostics.active_webrtc_peers ?? null,
|
|
436
|
+
}
|
|
437
|
+
: null,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
getPeersSummary() {
|
|
442
|
+
const diagnostics = this.getAdapterDiagnostics();
|
|
443
|
+
if (!diagnostics) {
|
|
444
|
+
return {
|
|
445
|
+
adapter: this.adapterMode,
|
|
446
|
+
namespace: this.networkNamespace,
|
|
447
|
+
total: 0,
|
|
448
|
+
online: 0,
|
|
449
|
+
stale: 0,
|
|
450
|
+
items: [],
|
|
451
|
+
stats: null,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
return {
|
|
455
|
+
adapter: diagnostics.adapter,
|
|
456
|
+
namespace: diagnostics.namespace,
|
|
457
|
+
total: diagnostics.peers.total,
|
|
458
|
+
online: diagnostics.peers.online,
|
|
459
|
+
stale: diagnostics.peers.stale,
|
|
460
|
+
items: diagnostics.peers.items,
|
|
461
|
+
stats: diagnostics.stats,
|
|
462
|
+
components: diagnostics.components,
|
|
463
|
+
limits: diagnostics.limits,
|
|
464
|
+
diagnostics_summary: {
|
|
465
|
+
signaling_url: diagnostics.signaling_url ?? null,
|
|
466
|
+
signaling_endpoints: diagnostics.signaling_endpoints ?? [],
|
|
467
|
+
room: diagnostics.room ?? null,
|
|
468
|
+
bootstrap_sources: diagnostics.bootstrap_sources ?? [],
|
|
469
|
+
seed_peers_count: diagnostics.seed_peers_count ?? 0,
|
|
470
|
+
discovery_events_total: diagnostics.discovery_events_total ?? 0,
|
|
471
|
+
last_discovery_event_at: diagnostics.last_discovery_event_at ?? 0,
|
|
472
|
+
connection_states_summary: diagnostics.connection_states_summary ?? null,
|
|
473
|
+
datachannel_states_summary: diagnostics.datachannel_states_summary ?? null,
|
|
474
|
+
active_webrtc_peers: diagnostics.active_webrtc_peers ?? null,
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
getDiscoveryEvents() {
|
|
480
|
+
const diagnostics = this.getAdapterDiagnostics();
|
|
481
|
+
return {
|
|
482
|
+
adapter: this.adapterMode,
|
|
483
|
+
mode: this.networkMode,
|
|
484
|
+
namespace: diagnostics?.namespace ?? this.networkNamespace,
|
|
485
|
+
total: diagnostics?.discovery_events_total ?? 0,
|
|
486
|
+
last_event_at: diagnostics?.last_discovery_event_at ?? 0,
|
|
487
|
+
items: Array.isArray(diagnostics?.discovery_events) ? diagnostics.discovery_events : [],
|
|
488
|
+
bootstrap_sources: diagnostics?.bootstrap_sources ?? this.webrtcBootstrapSources,
|
|
489
|
+
signaling_endpoints: diagnostics?.signaling_endpoints ?? this.webrtcSignalingUrls,
|
|
490
|
+
seed_peers_count: diagnostics?.seed_peers_count ?? this.webrtcSeedPeers.length,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
getSocialConfigView() {
|
|
495
|
+
return {
|
|
496
|
+
found: this.socialFound,
|
|
497
|
+
source_path: this.socialSourcePath,
|
|
498
|
+
parse_error: this.socialParseError,
|
|
499
|
+
network_requires_restart: this.socialNetworkRequiresRestart,
|
|
500
|
+
social_config: this.socialConfig,
|
|
501
|
+
raw_frontmatter: this.socialRawFrontmatter,
|
|
502
|
+
runtime: this.socialRuntime,
|
|
503
|
+
init_state: this.initState,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
getRuntimePaths() {
|
|
508
|
+
return {
|
|
509
|
+
workspace_root: this.workspaceRoot,
|
|
510
|
+
storage_root: this.storageRoot,
|
|
511
|
+
data_dir: resolve(this.storageRoot, "data"),
|
|
512
|
+
social_runtime_path: resolve(this.storageRoot, ".silicaclaw", "social.runtime.json"),
|
|
513
|
+
local_console_public_dir: resolve(this.workspaceRoot, "apps", "local-console", "public"),
|
|
514
|
+
social_lookup_paths: getSocialConfigSearchPaths(this.workspaceRoot),
|
|
515
|
+
social_source_path: this.socialSourcePath,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
getIntegrationSummary() {
|
|
520
|
+
const status = this.getIntegrationStatus();
|
|
521
|
+
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
522
|
+
const identitySource = this.socialRuntime?.resolved_identity?.source ?? this.resolvedIdentitySource;
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
connected: status.connected_to_silicaclaw,
|
|
526
|
+
discoverable: status.discoverable,
|
|
527
|
+
summary_line: status.status_line,
|
|
528
|
+
social_md_found: this.socialFound,
|
|
529
|
+
social_md_source_path: this.socialSourcePath,
|
|
530
|
+
runtime_generated: runtimeGenerated,
|
|
531
|
+
reused_openclaw_identity: identitySource === "openclaw-existing",
|
|
532
|
+
openclaw_identity_source_path: this.resolvedOpenClawIdentityPath,
|
|
533
|
+
current_public_enabled: status.public_enabled,
|
|
534
|
+
current_network_mode: status.network_mode,
|
|
535
|
+
current_adapter: this.adapterMode,
|
|
536
|
+
current_namespace: this.networkNamespace,
|
|
537
|
+
current_broadcast_status: this.broadcastEnabled ? "running" : "paused",
|
|
538
|
+
configured_enabled: this.socialConfig.enabled,
|
|
539
|
+
configured_public_enabled: this.socialConfig.public_enabled,
|
|
540
|
+
configured_discoverable: this.socialConfig.discovery.discoverable,
|
|
541
|
+
configured: status.configured,
|
|
542
|
+
running: status.running,
|
|
543
|
+
configured_reason: status.configured_reason,
|
|
544
|
+
running_reason: status.running_reason,
|
|
545
|
+
discoverable_reason: status.discoverable_reason,
|
|
546
|
+
agent_id: status.agent_id,
|
|
547
|
+
display_name: status.display_name,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
getIntegrationStatus(): IntegrationStatusSummary {
|
|
552
|
+
const runtimeGenerated = Boolean(this.socialRuntime && this.socialRuntime.last_loaded_at > 0);
|
|
553
|
+
const connected = this.socialFound && runtimeGenerated && !this.socialParseError;
|
|
554
|
+
const configured = connected && this.socialConfig.enabled;
|
|
555
|
+
const running = configured && this.broadcastEnabled;
|
|
556
|
+
const publicEnabled = Boolean(this.profile?.public_enabled);
|
|
557
|
+
const discoveryEnabled =
|
|
558
|
+
this.socialConfig.discovery.discoverable &&
|
|
559
|
+
this.socialConfig.discovery.allow_profile_broadcast &&
|
|
560
|
+
this.socialConfig.discovery.allow_presence_broadcast;
|
|
561
|
+
const discoverable = running && publicEnabled && discoveryEnabled;
|
|
562
|
+
|
|
563
|
+
const configuredReason = configured
|
|
564
|
+
? "configured"
|
|
565
|
+
: !this.socialFound
|
|
566
|
+
? "social.md not found"
|
|
567
|
+
: this.socialParseError
|
|
568
|
+
? "social.md parse error"
|
|
569
|
+
: !this.socialConfig.enabled
|
|
570
|
+
? "integration disabled"
|
|
571
|
+
: "runtime not ready";
|
|
572
|
+
|
|
573
|
+
const runningReason = running
|
|
574
|
+
? "running"
|
|
575
|
+
: !configured
|
|
576
|
+
? "not configured"
|
|
577
|
+
: !this.broadcastEnabled
|
|
578
|
+
? "broadcast paused"
|
|
579
|
+
: "not running";
|
|
580
|
+
|
|
581
|
+
const discoverableReason = discoverable
|
|
582
|
+
? "discoverable"
|
|
583
|
+
: !running
|
|
584
|
+
? "not running"
|
|
585
|
+
: !publicEnabled
|
|
586
|
+
? "Public discovery is disabled"
|
|
587
|
+
: !this.socialConfig.discovery.discoverable
|
|
588
|
+
? "discovery disabled"
|
|
589
|
+
: !this.socialConfig.discovery.allow_profile_broadcast
|
|
590
|
+
? "profile broadcast disabled"
|
|
591
|
+
: !this.socialConfig.discovery.allow_presence_broadcast
|
|
592
|
+
? "presence broadcast disabled"
|
|
593
|
+
: "not discoverable";
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
configured,
|
|
597
|
+
running,
|
|
598
|
+
discoverable,
|
|
599
|
+
network_mode: this.networkMode,
|
|
600
|
+
public_enabled: publicEnabled,
|
|
601
|
+
agent_id: this.identity?.agent_id ?? "",
|
|
602
|
+
display_name: this.profile?.display_name ?? "",
|
|
603
|
+
connected_to_silicaclaw: connected,
|
|
604
|
+
configured_reason: configuredReason,
|
|
605
|
+
running_reason: runningReason,
|
|
606
|
+
discoverable_reason: discoverableReason,
|
|
607
|
+
status_line: `${connected ? "Connected to SilicaClaw" : "Not connected to SilicaClaw"} · ${publicEnabled ? "Public discovery enabled" : "Public discovery disabled"} · Using ${this.networkMode}`,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
async setPublicDiscoveryRuntime(enabled: boolean) {
|
|
612
|
+
const profile = await this.updateProfile({ public_enabled: enabled });
|
|
613
|
+
this.socialConfig.public_enabled = enabled;
|
|
614
|
+
await this.writeSocialRuntime();
|
|
615
|
+
return {
|
|
616
|
+
public_enabled: profile.public_enabled,
|
|
617
|
+
note: "Runtime public discovery updated. Existing social.md is unchanged.",
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
async setNetworkModeRuntime(mode: "local" | "lan" | "global-preview") {
|
|
622
|
+
const currentMode = this.networkMode;
|
|
623
|
+
if (mode !== "local" && mode !== "lan" && mode !== "global-preview") {
|
|
624
|
+
throw new Error("invalid_network_mode");
|
|
625
|
+
}
|
|
626
|
+
this.socialConfig.network.mode = mode;
|
|
627
|
+
this.socialConfig.network.adapter = this.adapterForMode(mode);
|
|
628
|
+
this.applyResolvedNetworkConfig();
|
|
629
|
+
this.socialNetworkRequiresRestart = currentMode !== mode || this.adapterMode !== this.socialConfig.network.adapter;
|
|
630
|
+
await this.writeSocialRuntime();
|
|
631
|
+
return {
|
|
632
|
+
mode: this.networkMode,
|
|
633
|
+
adapter: this.socialConfig.network.adapter,
|
|
634
|
+
network_requires_restart: this.socialNetworkRequiresRestart,
|
|
635
|
+
note: "Runtime mode updated. Existing social.md is unchanged.",
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async quickConnectGlobalPreview(options?: { signaling_url?: string; room?: string }) {
|
|
640
|
+
const signalingUrl = String(options?.signaling_url || "").trim();
|
|
641
|
+
const room = String(options?.room || "").trim();
|
|
642
|
+
if (!signalingUrl) {
|
|
643
|
+
throw new Error("missing_signaling_url");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
this.socialConfig.network.mode = "global-preview";
|
|
647
|
+
this.socialConfig.network.adapter = "webrtc-preview";
|
|
648
|
+
this.socialConfig.network.signaling_url = signalingUrl;
|
|
649
|
+
this.socialConfig.network.signaling_urls = [signalingUrl];
|
|
650
|
+
this.socialConfig.network.room = room || "silicaclaw-demo";
|
|
651
|
+
this.applyResolvedNetworkConfig();
|
|
652
|
+
await this.restartNetworkAdapter("quick_connect_global_preview");
|
|
653
|
+
this.socialNetworkRequiresRestart = false;
|
|
654
|
+
await this.writeSocialRuntime();
|
|
655
|
+
await this.log("info", `Quick connect enabled (webrtc-preview, room=${this.webrtcRoom})`);
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
mode: this.networkMode,
|
|
659
|
+
adapter: this.adapterMode,
|
|
660
|
+
signaling_url: this.webrtcSignalingUrls[0] ?? null,
|
|
661
|
+
room: this.webrtcRoom,
|
|
662
|
+
network_requires_restart: false,
|
|
663
|
+
note: "Cross-network preview enabled.",
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async reloadSocialConfig() {
|
|
668
|
+
const before = {
|
|
669
|
+
mode: this.networkMode,
|
|
670
|
+
adapter: this.adapterMode,
|
|
671
|
+
namespace: this.networkNamespace,
|
|
672
|
+
port: this.networkPort,
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
const loaded = loadSocialConfig(this.workspaceRoot);
|
|
676
|
+
this.socialConfig = loaded.config;
|
|
677
|
+
this.socialSourcePath = loaded.meta.source_path;
|
|
678
|
+
this.socialFound = loaded.meta.found;
|
|
679
|
+
this.socialParseError = loaded.meta.parse_error;
|
|
680
|
+
this.socialRawFrontmatter = loaded.raw_frontmatter;
|
|
681
|
+
this.applyResolvedNetworkConfig();
|
|
682
|
+
|
|
683
|
+
await this.applySocialConfigOnCurrentState();
|
|
684
|
+
|
|
685
|
+
const after = {
|
|
686
|
+
mode: this.networkMode,
|
|
687
|
+
adapter: this.socialConfig.network.adapter,
|
|
688
|
+
namespace: this.networkNamespace,
|
|
689
|
+
port: this.networkPort,
|
|
690
|
+
};
|
|
691
|
+
this.socialNetworkRequiresRestart =
|
|
692
|
+
before.mode !== after.mode ||
|
|
693
|
+
before.adapter !== after.adapter ||
|
|
694
|
+
before.namespace !== after.namespace ||
|
|
695
|
+
(before.port ?? null) !== (after.port ?? null);
|
|
696
|
+
|
|
697
|
+
await this.writeSocialRuntime();
|
|
698
|
+
|
|
699
|
+
return this.getSocialConfigView();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
async generateDefaultSocialMd() {
|
|
703
|
+
const result = ensureDefaultSocialMd(this.workspaceRoot, {
|
|
704
|
+
display_name: this.getDefaultDisplayName(),
|
|
705
|
+
bio: "Local AI agent connected to SilicaClaw",
|
|
706
|
+
tags: ["openclaw", "local-first"],
|
|
707
|
+
mode: this.networkMode,
|
|
708
|
+
public_enabled: Boolean(this.profile?.public_enabled),
|
|
709
|
+
});
|
|
710
|
+
await this.reloadSocialConfig();
|
|
711
|
+
return result;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
exportSocialTemplate(): { filename: string; content: string } {
|
|
715
|
+
return {
|
|
716
|
+
filename: "social.md",
|
|
717
|
+
content: generateSocialMdTemplate(this.socialRuntime),
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
getDirectory(): DirectoryState {
|
|
722
|
+
this.compactCacheInMemory();
|
|
723
|
+
return this.directory;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
search(keyword: string): PublicProfileSummary[] {
|
|
727
|
+
this.compactCacheInMemory();
|
|
728
|
+
return searchDirectory(this.directory, keyword, { presenceTTLms: PRESENCE_TTL_MS }).map((profile) => {
|
|
729
|
+
const lastSeenAt = this.directory.presence[profile.agent_id] ?? 0;
|
|
730
|
+
return this.toPublicProfileSummary(profile, { last_seen_at: lastSeenAt });
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
getPublicProfilePreview(): PublicProfileSummary | null {
|
|
735
|
+
if (!this.profile) {
|
|
736
|
+
return null;
|
|
737
|
+
}
|
|
738
|
+
const lastSeenAt = this.directory.presence[this.profile.agent_id] ?? 0;
|
|
739
|
+
return this.toPublicProfileSummary(this.profile, { last_seen_at: lastSeenAt });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
getAgentPublicSummary(agentId: string): PublicProfileSummary | null {
|
|
743
|
+
const profile = this.directory.profiles[agentId];
|
|
744
|
+
if (!profile) {
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
const lastSeenAt = this.directory.presence[agentId] ?? 0;
|
|
748
|
+
return this.toPublicProfileSummary(profile, { last_seen_at: lastSeenAt });
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
getProfile(): PublicProfile | null {
|
|
752
|
+
return this.profile;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
getIdentity(): AgentIdentity | null {
|
|
756
|
+
return this.identity;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
async getLogs() {
|
|
760
|
+
return this.logRepo.get();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
async ensureIdentity(): Promise<AgentIdentity> {
|
|
764
|
+
if (this.identity) {
|
|
765
|
+
return this.identity;
|
|
766
|
+
}
|
|
767
|
+
const identity = createIdentity();
|
|
768
|
+
this.identity = identity;
|
|
769
|
+
await this.identityRepo.set(identity);
|
|
770
|
+
this.initState.identity_auto_created = true;
|
|
771
|
+
|
|
772
|
+
const seededProfile = signProfile(createDefaultProfileInput(identity.agent_id), identity);
|
|
773
|
+
this.profile = seededProfile;
|
|
774
|
+
await this.profileRepo.set(seededProfile);
|
|
775
|
+
this.initState.profile_auto_created = true;
|
|
776
|
+
|
|
777
|
+
await this.log("info", `Identity created automatically: ${identity.agent_id.slice(0, 12)}`);
|
|
778
|
+
return identity;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async updateProfile(input: Partial<ProfileInput>): Promise<PublicProfile> {
|
|
782
|
+
const identity = await this.ensureIdentity();
|
|
783
|
+
const base = this.profile ?? signProfile(createDefaultProfileInput(identity.agent_id), identity);
|
|
784
|
+
|
|
785
|
+
const next = signProfile(
|
|
786
|
+
{
|
|
787
|
+
agent_id: identity.agent_id,
|
|
788
|
+
display_name: input.display_name ?? base.display_name,
|
|
789
|
+
bio: input.bio ?? base.bio,
|
|
790
|
+
tags: input.tags ?? base.tags,
|
|
791
|
+
avatar_url: input.avatar_url ?? base.avatar_url,
|
|
792
|
+
public_enabled: input.public_enabled ?? base.public_enabled,
|
|
793
|
+
},
|
|
794
|
+
identity
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
this.profile = next;
|
|
798
|
+
this.directory = ingestProfileRecord(this.directory, { type: "profile", profile: next });
|
|
799
|
+
await this.profileRepo.set(next);
|
|
800
|
+
await this.persistCache();
|
|
801
|
+
await this.log("info", `Profile updated (public=${next.public_enabled})`);
|
|
802
|
+
|
|
803
|
+
if (next.public_enabled && this.broadcastEnabled) {
|
|
804
|
+
await this.broadcastNow("profile_update");
|
|
805
|
+
}
|
|
806
|
+
await this.writeSocialRuntime();
|
|
807
|
+
|
|
808
|
+
return next;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
async refreshCache() {
|
|
812
|
+
const removed = this.compactCacheInMemory();
|
|
813
|
+
await this.persistCache();
|
|
814
|
+
await this.log("info", `Cache refreshed (expired presence removed=${removed})`);
|
|
815
|
+
return {
|
|
816
|
+
removed_presence: removed,
|
|
817
|
+
profile_count: Object.keys(this.directory.profiles).length,
|
|
818
|
+
index_key_count: Object.keys(this.directory.index).length,
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
async clearDiscoveredCache() {
|
|
823
|
+
const selfAgentId = this.profile?.agent_id || this.identity?.agent_id || "";
|
|
824
|
+
const profileEntries = Object.entries(this.directory.profiles);
|
|
825
|
+
const removedProfiles = profileEntries.filter(([agentId]) => agentId !== selfAgentId).length;
|
|
826
|
+
const removedPresence = Object.entries(this.directory.presence).filter(([agentId]) => agentId !== selfAgentId).length;
|
|
827
|
+
const removedIndexRefs = Object.values(this.directory.index).reduce((acc, agentIds) => {
|
|
828
|
+
const removed = agentIds.filter((agentId) => agentId !== selfAgentId).length;
|
|
829
|
+
return acc + removed;
|
|
830
|
+
}, 0);
|
|
831
|
+
|
|
832
|
+
this.directory = createEmptyDirectoryState();
|
|
833
|
+
if (this.profile) {
|
|
834
|
+
this.directory = ingestProfileRecord(this.directory, {
|
|
835
|
+
type: "profile",
|
|
836
|
+
profile: this.profile,
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
await this.persistCache();
|
|
840
|
+
await this.log("warn", `Discovered cache cleared (profiles=${removedProfiles}, presence=${removedPresence}, index_refs=${removedIndexRefs})`);
|
|
841
|
+
|
|
842
|
+
return {
|
|
843
|
+
removed_profiles: removedProfiles,
|
|
844
|
+
removed_presence: removedPresence,
|
|
845
|
+
removed_index_refs: removedIndexRefs,
|
|
846
|
+
kept_self_profile: Boolean(this.profile),
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async setBroadcastEnabled(enabled: boolean) {
|
|
851
|
+
this.broadcastEnabled = enabled;
|
|
852
|
+
if (enabled) {
|
|
853
|
+
this.startBroadcastLoop();
|
|
854
|
+
await this.log("info", "Broadcast loop enabled");
|
|
855
|
+
if (this.profile?.public_enabled) {
|
|
856
|
+
await this.broadcastNow("manual_start");
|
|
857
|
+
}
|
|
858
|
+
} else {
|
|
859
|
+
if (this.broadcaster) {
|
|
860
|
+
clearInterval(this.broadcaster);
|
|
861
|
+
this.broadcaster = null;
|
|
862
|
+
}
|
|
863
|
+
await this.log("warn", "Broadcast loop paused");
|
|
864
|
+
}
|
|
865
|
+
await this.writeSocialRuntime();
|
|
866
|
+
return { broadcast_enabled: this.broadcastEnabled };
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
async broadcastNow(reason = "manual"): Promise<{ sent: boolean; reason: string }> {
|
|
870
|
+
if (!this.identity || !this.profile) {
|
|
871
|
+
return { sent: false, reason: "missing_identity_or_profile" };
|
|
872
|
+
}
|
|
873
|
+
if (!this.profile.public_enabled) {
|
|
874
|
+
return { sent: false, reason: "public_disabled" };
|
|
875
|
+
}
|
|
876
|
+
if (!this.broadcastEnabled) {
|
|
877
|
+
return { sent: false, reason: "broadcast_paused" };
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
const profileRecord: SignedProfileRecord = {
|
|
881
|
+
type: "profile",
|
|
882
|
+
profile: this.profile,
|
|
883
|
+
};
|
|
884
|
+
const presenceRecord = signPresence(this.identity, Date.now());
|
|
885
|
+
const indexRecords = buildIndexRecords(this.profile);
|
|
886
|
+
|
|
887
|
+
await this.publish("profile", profileRecord);
|
|
888
|
+
await this.publish("presence", presenceRecord);
|
|
889
|
+
for (const record of indexRecords) {
|
|
890
|
+
await this.publish("index", record);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
this.lastBroadcastAt = Date.now();
|
|
894
|
+
this.broadcastCount += 1;
|
|
895
|
+
|
|
896
|
+
this.directory = ingestProfileRecord(this.directory, profileRecord);
|
|
897
|
+
this.directory = ingestPresenceRecord(this.directory, presenceRecord);
|
|
898
|
+
for (const record of indexRecords) {
|
|
899
|
+
this.directory = ingestIndexRecord(this.directory, record);
|
|
900
|
+
}
|
|
901
|
+
this.compactCacheInMemory();
|
|
902
|
+
await this.persistCache();
|
|
903
|
+
|
|
904
|
+
await this.log("info", `Broadcast sent (${indexRecords.length} index refs, reason=${reason})`);
|
|
905
|
+
return { sent: true, reason };
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private async hydrateFromDisk(): Promise<void> {
|
|
909
|
+
this.initState = {
|
|
910
|
+
identity_auto_created: false,
|
|
911
|
+
profile_auto_created: false,
|
|
912
|
+
social_auto_created: this.initState.social_auto_created,
|
|
913
|
+
initialized_at: Date.now(),
|
|
914
|
+
};
|
|
915
|
+
if (this.initState.social_auto_created) {
|
|
916
|
+
await this.log("info", "social.md missing, auto-generated minimal default template");
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const existingIdentity = await this.identityRepo.get();
|
|
920
|
+
const resolvedIdentity = resolveIdentityWithSocial({
|
|
921
|
+
socialConfig: this.socialConfig,
|
|
922
|
+
existingIdentity,
|
|
923
|
+
generatedIdentity: createIdentity(),
|
|
924
|
+
rootDir: this.workspaceRoot,
|
|
925
|
+
});
|
|
926
|
+
this.identity = resolvedIdentity.identity;
|
|
927
|
+
this.resolvedIdentitySource = resolvedIdentity.source;
|
|
928
|
+
this.resolvedOpenClawIdentityPath = resolvedIdentity.openclaw_source_path;
|
|
929
|
+
if (resolvedIdentity.source === "silicaclaw-generated") {
|
|
930
|
+
this.initState.identity_auto_created = true;
|
|
931
|
+
await this.log("info", "identity.json missing, auto-generated SilicaClaw identity");
|
|
932
|
+
}
|
|
933
|
+
if (resolvedIdentity.source === "openclaw-existing" && resolvedIdentity.openclaw_source_path) {
|
|
934
|
+
await this.log("info", `Bound existing OpenClaw identity: ${resolvedIdentity.openclaw_source_path}`);
|
|
935
|
+
}
|
|
936
|
+
await this.identityRepo.set(this.identity);
|
|
937
|
+
|
|
938
|
+
const existingProfile = await this.profileRepo.get();
|
|
939
|
+
const profileInput = resolveProfileInputWithSocial({
|
|
940
|
+
socialConfig: this.socialConfig,
|
|
941
|
+
agentId: this.identity.agent_id,
|
|
942
|
+
existingProfile: existingProfile && existingProfile.agent_id === this.identity.agent_id ? existingProfile : null,
|
|
943
|
+
rootDir: this.workspaceRoot,
|
|
944
|
+
});
|
|
945
|
+
this.profile = signProfile(profileInput, this.identity);
|
|
946
|
+
if (!existingProfile || existingProfile.agent_id !== this.identity.agent_id) {
|
|
947
|
+
this.initState.profile_auto_created = true;
|
|
948
|
+
await this.log("info", "profile.json missing/invalid, initialized from social/default profile");
|
|
949
|
+
}
|
|
950
|
+
await this.profileRepo.set(this.profile);
|
|
951
|
+
|
|
952
|
+
this.directory = dedupeIndex(await this.cacheRepo.get());
|
|
953
|
+
this.directory = ingestProfileRecord(this.directory, { type: "profile", profile: this.profile });
|
|
954
|
+
this.compactCacheInMemory();
|
|
955
|
+
await this.persistCache();
|
|
956
|
+
await this.applySocialConfigOnCurrentState();
|
|
957
|
+
await this.writeSocialRuntime();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
private async applySocialConfigOnCurrentState(): Promise<void> {
|
|
961
|
+
if (!this.identity || !this.profile) {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const nextProfileInput = resolveProfileInputWithSocial({
|
|
966
|
+
socialConfig: this.socialConfig,
|
|
967
|
+
agentId: this.identity.agent_id,
|
|
968
|
+
existingProfile: this.profile,
|
|
969
|
+
rootDir: this.workspaceRoot,
|
|
970
|
+
});
|
|
971
|
+
const nextProfile = signProfile(nextProfileInput, this.identity);
|
|
972
|
+
this.profile = nextProfile;
|
|
973
|
+
await this.profileRepo.set(nextProfile);
|
|
974
|
+
|
|
975
|
+
this.directory = ingestProfileRecord(this.directory, { type: "profile", profile: nextProfile });
|
|
976
|
+
this.compactCacheInMemory();
|
|
977
|
+
await this.persistCache();
|
|
978
|
+
|
|
979
|
+
if (!this.socialConfig.enabled) {
|
|
980
|
+
await this.setBroadcastEnabled(false);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (!this.broadcastEnabled) {
|
|
985
|
+
await this.setBroadcastEnabled(true);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
private async writeSocialRuntime(): Promise<void> {
|
|
990
|
+
const runtime: SocialRuntimeConfig = {
|
|
991
|
+
enabled: this.socialConfig.enabled,
|
|
992
|
+
public_enabled: this.socialConfig.public_enabled,
|
|
993
|
+
source_path: this.socialSourcePath,
|
|
994
|
+
last_loaded_at: Date.now(),
|
|
995
|
+
social_found: this.socialFound,
|
|
996
|
+
parse_error: this.socialParseError,
|
|
997
|
+
resolved_identity: this.identity
|
|
998
|
+
? {
|
|
999
|
+
agent_id: this.identity.agent_id,
|
|
1000
|
+
public_key: this.identity.public_key,
|
|
1001
|
+
created_at: this.identity.created_at,
|
|
1002
|
+
source: this.resolvedIdentitySource,
|
|
1003
|
+
}
|
|
1004
|
+
: null,
|
|
1005
|
+
resolved_profile: this.profile
|
|
1006
|
+
? {
|
|
1007
|
+
display_name: this.profile.display_name,
|
|
1008
|
+
bio: this.profile.bio,
|
|
1009
|
+
avatar_url: this.profile.avatar_url,
|
|
1010
|
+
tags: this.profile.tags,
|
|
1011
|
+
public_enabled: this.profile.public_enabled,
|
|
1012
|
+
}
|
|
1013
|
+
: null,
|
|
1014
|
+
resolved_network: {
|
|
1015
|
+
mode: this.networkMode,
|
|
1016
|
+
adapter: this.adapterMode,
|
|
1017
|
+
namespace: this.networkNamespace,
|
|
1018
|
+
port: this.networkPort,
|
|
1019
|
+
signaling_url: this.webrtcSignalingUrls[0] ?? WEBRTC_SIGNALING_URL,
|
|
1020
|
+
signaling_urls: this.webrtcSignalingUrls,
|
|
1021
|
+
room: this.webrtcRoom,
|
|
1022
|
+
seed_peers: this.webrtcSeedPeers,
|
|
1023
|
+
bootstrap_hints: this.webrtcBootstrapHints,
|
|
1024
|
+
bootstrap_sources: this.webrtcBootstrapSources,
|
|
1025
|
+
},
|
|
1026
|
+
resolved_discovery: this.socialConfig.discovery,
|
|
1027
|
+
visibility: this.socialConfig.visibility,
|
|
1028
|
+
openclaw: this.socialConfig.openclaw,
|
|
1029
|
+
};
|
|
1030
|
+
this.socialRuntime = runtime;
|
|
1031
|
+
await this.socialRuntimeRepo.set(runtime);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
private async onMessage(topic: "profile" | "presence" | "index", data: unknown): Promise<void> {
|
|
1035
|
+
this.receivedCount += 1;
|
|
1036
|
+
this.receivedByTopic[topic] = (this.receivedByTopic[topic] ?? 0) + 1;
|
|
1037
|
+
this.lastMessageAt = Date.now();
|
|
1038
|
+
|
|
1039
|
+
if (topic === "profile") {
|
|
1040
|
+
const record = data as SignedProfileRecord;
|
|
1041
|
+
if (!record?.profile?.agent_id || !record?.profile?.signature) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (record.profile.agent_id === this.identity?.agent_id && this.identity) {
|
|
1046
|
+
if (!verifyProfile(record.profile, this.identity.public_key)) {
|
|
1047
|
+
await this.log("warn", "Rejected self profile with invalid signature");
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
this.directory = ingestProfileRecord(this.directory, record);
|
|
1053
|
+
this.compactCacheInMemory();
|
|
1054
|
+
await this.persistCache();
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (topic === "presence") {
|
|
1059
|
+
const record = data as PresenceRecord;
|
|
1060
|
+
if (!record?.agent_id || !record?.signature || typeof record.timestamp !== "number") {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
if (record.agent_id === this.identity?.agent_id && this.identity) {
|
|
1065
|
+
if (!verifyPresence(record, this.identity.public_key)) {
|
|
1066
|
+
await this.log("warn", "Rejected invalid self presence signature");
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
this.directory = ingestPresenceRecord(this.directory, record);
|
|
1072
|
+
this.compactCacheInMemory();
|
|
1073
|
+
await this.persistCache();
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const record = data as IndexRefRecord;
|
|
1078
|
+
if (!record?.key || !record?.agent_id) {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
this.directory = ingestIndexRecord(this.directory, record);
|
|
1082
|
+
this.directory = dedupeIndex(this.directory);
|
|
1083
|
+
await this.persistCache();
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private startBroadcastLoop(): void {
|
|
1087
|
+
if (this.broadcaster) {
|
|
1088
|
+
clearInterval(this.broadcaster);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (!this.broadcastEnabled) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
this.broadcaster = setInterval(async () => {
|
|
1096
|
+
await this.broadcastNow("interval");
|
|
1097
|
+
}, BROADCAST_INTERVAL_MS);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
private bindNetworkSubscriptions(): void {
|
|
1101
|
+
this.network.subscribe("profile", (data: SignedProfileRecord) => {
|
|
1102
|
+
this.onMessage("profile", data);
|
|
1103
|
+
});
|
|
1104
|
+
this.network.subscribe("presence", (data: PresenceRecord) => {
|
|
1105
|
+
this.onMessage("presence", data);
|
|
1106
|
+
});
|
|
1107
|
+
this.network.subscribe("index", (data: IndexRefRecord) => {
|
|
1108
|
+
this.onMessage("index", data);
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
private buildNetworkAdapter(): {
|
|
1113
|
+
adapter: NetworkAdapter;
|
|
1114
|
+
mode: "mock" | "local-event-bus" | "real-preview" | "webrtc-preview";
|
|
1115
|
+
port: number | null;
|
|
1116
|
+
} {
|
|
1117
|
+
const mode = (process.env.NETWORK_ADAPTER as typeof this.adapterMode | undefined) || this.socialConfig.network.adapter;
|
|
1118
|
+
if (mode === "mock") {
|
|
1119
|
+
return {
|
|
1120
|
+
adapter: new MockNetworkAdapter(),
|
|
1121
|
+
mode: "mock",
|
|
1122
|
+
port: null,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
if (mode === "real-preview") {
|
|
1126
|
+
return {
|
|
1127
|
+
adapter: new RealNetworkAdapterPreview({
|
|
1128
|
+
peerId: NETWORK_PEER_ID,
|
|
1129
|
+
namespace: this.networkNamespace,
|
|
1130
|
+
transport: new UdpLanBroadcastTransport({
|
|
1131
|
+
port: this.networkPort ?? undefined,
|
|
1132
|
+
bindAddress: NETWORK_UDP_BIND_ADDRESS,
|
|
1133
|
+
broadcastAddress: NETWORK_UDP_BROADCAST_ADDRESS,
|
|
1134
|
+
}),
|
|
1135
|
+
peerDiscovery: new HeartbeatPeerDiscovery({
|
|
1136
|
+
heartbeatIntervalMs: NETWORK_HEARTBEAT_INTERVAL_MS,
|
|
1137
|
+
staleAfterMs: NETWORK_PEER_STALE_AFTER_MS,
|
|
1138
|
+
removeAfterMs: NETWORK_PEER_REMOVE_AFTER_MS,
|
|
1139
|
+
}),
|
|
1140
|
+
maxMessageBytes: NETWORK_MAX_MESSAGE_BYTES,
|
|
1141
|
+
dedupeWindowMs: NETWORK_DEDUPE_WINDOW_MS,
|
|
1142
|
+
dedupeMaxEntries: NETWORK_DEDUPE_MAX_ENTRIES,
|
|
1143
|
+
maxFutureDriftMs: NETWORK_MAX_FUTURE_DRIFT_MS,
|
|
1144
|
+
maxPastDriftMs: NETWORK_MAX_PAST_DRIFT_MS,
|
|
1145
|
+
}),
|
|
1146
|
+
mode: "real-preview",
|
|
1147
|
+
port: this.networkPort,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
if (mode === "webrtc-preview") {
|
|
1151
|
+
return {
|
|
1152
|
+
adapter: new WebRTCPreviewAdapter({
|
|
1153
|
+
peerId: NETWORK_PEER_ID,
|
|
1154
|
+
namespace: this.networkNamespace,
|
|
1155
|
+
signalingUrl: this.webrtcSignalingUrls[0] ?? WEBRTC_SIGNALING_URL,
|
|
1156
|
+
signalingUrls: this.webrtcSignalingUrls,
|
|
1157
|
+
room: this.webrtcRoom,
|
|
1158
|
+
seedPeers: this.webrtcSeedPeers,
|
|
1159
|
+
bootstrapHints: this.webrtcBootstrapHints,
|
|
1160
|
+
bootstrapSources: this.webrtcBootstrapSources,
|
|
1161
|
+
maxMessageBytes: NETWORK_MAX_MESSAGE_BYTES,
|
|
1162
|
+
maxFutureDriftMs: NETWORK_MAX_FUTURE_DRIFT_MS,
|
|
1163
|
+
maxPastDriftMs: NETWORK_MAX_PAST_DRIFT_MS,
|
|
1164
|
+
}),
|
|
1165
|
+
mode: "webrtc-preview",
|
|
1166
|
+
port: this.networkPort,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
adapter: new LocalEventBusAdapter(),
|
|
1171
|
+
mode: "local-event-bus",
|
|
1172
|
+
port: null,
|
|
1173
|
+
};
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
private async restartNetworkAdapter(reason: string): Promise<void> {
|
|
1177
|
+
const previous = this.network;
|
|
1178
|
+
try {
|
|
1179
|
+
await previous.stop();
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
await this.log("warn", `Old adapter stop error during restart (${reason}): ${error instanceof Error ? error.message : String(error)}`);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const next = this.buildNetworkAdapter();
|
|
1185
|
+
this.network = next.adapter;
|
|
1186
|
+
this.adapterMode = next.mode;
|
|
1187
|
+
this.networkPort = next.port;
|
|
1188
|
+
|
|
1189
|
+
await this.network.start();
|
|
1190
|
+
this.bindNetworkSubscriptions();
|
|
1191
|
+
this.startBroadcastLoop();
|
|
1192
|
+
|
|
1193
|
+
if (this.broadcastEnabled && this.profile?.public_enabled) {
|
|
1194
|
+
await this.broadcastNow("adapter_restart");
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
private compactCacheInMemory(): number {
|
|
1199
|
+
const cleaned = cleanupExpiredPresence(this.directory, Date.now(), PRESENCE_TTL_MS);
|
|
1200
|
+
this.directory = dedupeIndex(cleaned.state);
|
|
1201
|
+
return cleaned.removed;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
private async publish(topic: string, data: unknown): Promise<void> {
|
|
1205
|
+
await this.network.publish(topic, data);
|
|
1206
|
+
this.publishedByTopic[topic] = (this.publishedByTopic[topic] ?? 0) + 1;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private async persistCache(): Promise<void> {
|
|
1210
|
+
await this.cacheRepo.set(this.directory);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
private async log(level: "info" | "warn" | "error", message: string): Promise<void> {
|
|
1214
|
+
await this.logRepo.append({
|
|
1215
|
+
level,
|
|
1216
|
+
message,
|
|
1217
|
+
timestamp: Date.now(),
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
private getAdapterDiagnostics(): Record<string, any> | null {
|
|
1222
|
+
if (typeof (this.network as any).getDiagnostics !== "function") {
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
return (this.network as any).getDiagnostics();
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
private toPublicProfileSummary(
|
|
1229
|
+
profile: PublicProfile,
|
|
1230
|
+
options?: { last_seen_at?: number }
|
|
1231
|
+
): PublicProfileSummary {
|
|
1232
|
+
const lastSeenAt = options?.last_seen_at ?? this.directory.presence[profile.agent_id] ?? 0;
|
|
1233
|
+
const online = isAgentOnline(lastSeenAt, Date.now(), PRESENCE_TTL_MS);
|
|
1234
|
+
const isSelf = profile.agent_id === this.identity?.agent_id;
|
|
1235
|
+
const visibility = isSelf
|
|
1236
|
+
? {
|
|
1237
|
+
show_tags: this.socialConfig.visibility.show_tags,
|
|
1238
|
+
show_last_seen: this.socialConfig.visibility.show_last_seen,
|
|
1239
|
+
show_capabilities_summary: this.socialConfig.visibility.show_capabilities_summary,
|
|
1240
|
+
}
|
|
1241
|
+
: {
|
|
1242
|
+
show_tags: true,
|
|
1243
|
+
show_last_seen: true,
|
|
1244
|
+
show_capabilities_summary: true,
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
const selfPublicKey = isSelf ? this.identity?.public_key ?? null : null;
|
|
1248
|
+
const verifiedProfile = Boolean(
|
|
1249
|
+
isSelf &&
|
|
1250
|
+
selfPublicKey &&
|
|
1251
|
+
verifyProfile(profile, selfPublicKey)
|
|
1252
|
+
);
|
|
1253
|
+
|
|
1254
|
+
return buildPublicProfileSummary({
|
|
1255
|
+
profile,
|
|
1256
|
+
online,
|
|
1257
|
+
last_seen_at: lastSeenAt || null,
|
|
1258
|
+
network_mode: isSelf ? this.networkMode : "unknown",
|
|
1259
|
+
openclaw_bound: isSelf
|
|
1260
|
+
? this.resolvedIdentitySource === "openclaw-existing"
|
|
1261
|
+
: profile.tags.some((tag) => String(tag).trim().toLowerCase() === "openclaw"),
|
|
1262
|
+
visibility,
|
|
1263
|
+
profile_version: PROFILE_VERSION,
|
|
1264
|
+
public_key_fingerprint: selfPublicKey ? this.fingerprintPublicKey(selfPublicKey) : null,
|
|
1265
|
+
verified_profile: verifiedProfile,
|
|
1266
|
+
now: Date.now(),
|
|
1267
|
+
presence_ttl_ms: PRESENCE_TTL_MS,
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
private fingerprintPublicKey(publicKey: string): string {
|
|
1272
|
+
const digest = createHash("sha256").update(publicKey, "utf8").digest("hex");
|
|
1273
|
+
return `${digest.slice(0, 12)}:${digest.slice(-8)}`;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
private getOnboardingSummary() {
|
|
1277
|
+
const summary = this.getIntegrationSummary();
|
|
1278
|
+
const publicEnabled = Boolean(this.profile?.public_enabled);
|
|
1279
|
+
return {
|
|
1280
|
+
first_run: Boolean(
|
|
1281
|
+
this.initState.social_auto_created ||
|
|
1282
|
+
this.initState.identity_auto_created ||
|
|
1283
|
+
this.initState.profile_auto_created
|
|
1284
|
+
),
|
|
1285
|
+
connected: summary.connected,
|
|
1286
|
+
discoverable: summary.discoverable,
|
|
1287
|
+
mode: this.networkMode,
|
|
1288
|
+
public_enabled: publicEnabled,
|
|
1289
|
+
can_enable_public_discovery: !publicEnabled,
|
|
1290
|
+
next_steps: [
|
|
1291
|
+
"Update display name in Profile page",
|
|
1292
|
+
"Export social.md from Social Config",
|
|
1293
|
+
],
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
private getDefaultDisplayName(): string {
|
|
1298
|
+
const host = hostname().trim().replace(/\s+/g, "-").slice(0, 24);
|
|
1299
|
+
return host ? `OpenClaw @ ${host}` : "OpenClaw Agent";
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
private adapterForMode(mode: "local" | "lan" | "global-preview"): "local-event-bus" | "real-preview" | "webrtc-preview" {
|
|
1303
|
+
if (mode === "local") return "local-event-bus";
|
|
1304
|
+
if (mode === "lan") return "real-preview";
|
|
1305
|
+
return "webrtc-preview";
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
private applyResolvedNetworkConfig(): void {
|
|
1309
|
+
this.networkMode = this.socialConfig.network.mode || "lan";
|
|
1310
|
+
this.networkNamespace = this.socialConfig.network.namespace || process.env.NETWORK_NAMESPACE || "silicaclaw.preview";
|
|
1311
|
+
this.networkPort = Number(this.socialConfig.network.port || process.env.NETWORK_PORT || 44123);
|
|
1312
|
+
|
|
1313
|
+
const builtInGlobalSignalingUrls = ["http://localhost:4510"];
|
|
1314
|
+
const builtInGlobalRoom = "silicaclaw-global-preview";
|
|
1315
|
+
|
|
1316
|
+
const signalingUrlsSocial = dedupeStrings(this.socialConfig.network.signaling_urls || []);
|
|
1317
|
+
const signalingUrlSocial = String(this.socialConfig.network.signaling_url || "").trim();
|
|
1318
|
+
const signalingUrlsEnv = dedupeStrings(parseListEnv(WEBRTC_SIGNALING_URLS));
|
|
1319
|
+
const signalingUrlEnvSingle = String(WEBRTC_SIGNALING_URL || "").trim();
|
|
1320
|
+
|
|
1321
|
+
let signalingUrls: string[] = [];
|
|
1322
|
+
let signalingSource = "";
|
|
1323
|
+
if (signalingUrlsSocial.length > 0) {
|
|
1324
|
+
signalingUrls = signalingUrlsSocial;
|
|
1325
|
+
signalingSource = "social.md:network.signaling_urls";
|
|
1326
|
+
} else if (signalingUrlSocial) {
|
|
1327
|
+
signalingUrls = [signalingUrlSocial];
|
|
1328
|
+
signalingSource = "social.md:network.signaling_url";
|
|
1329
|
+
} else if (this.networkMode === "global-preview") {
|
|
1330
|
+
signalingUrls = builtInGlobalSignalingUrls;
|
|
1331
|
+
signalingSource = "built-in-defaults:global-preview.signaling_urls";
|
|
1332
|
+
} else if (signalingUrlsEnv.length > 0) {
|
|
1333
|
+
signalingUrls = signalingUrlsEnv;
|
|
1334
|
+
signalingSource = "env:WEBRTC_SIGNALING_URLS";
|
|
1335
|
+
} else if (signalingUrlEnvSingle) {
|
|
1336
|
+
signalingUrls = [signalingUrlEnvSingle];
|
|
1337
|
+
signalingSource = "env:WEBRTC_SIGNALING_URL";
|
|
1338
|
+
} else {
|
|
1339
|
+
signalingUrls = ["http://localhost:4510"];
|
|
1340
|
+
signalingSource = "default:http://localhost:4510";
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const roomSocial = String(this.socialConfig.network.room || "").trim();
|
|
1344
|
+
const roomEnv = String(WEBRTC_ROOM || "").trim();
|
|
1345
|
+
const room =
|
|
1346
|
+
roomSocial ||
|
|
1347
|
+
(this.networkMode === "global-preview" ? builtInGlobalRoom : "") ||
|
|
1348
|
+
roomEnv ||
|
|
1349
|
+
"silicaclaw-room";
|
|
1350
|
+
const roomSource = roomSocial
|
|
1351
|
+
? "social.md:network.room"
|
|
1352
|
+
: this.networkMode === "global-preview"
|
|
1353
|
+
? "built-in-defaults:global-preview.room"
|
|
1354
|
+
: roomEnv
|
|
1355
|
+
? "env:WEBRTC_ROOM"
|
|
1356
|
+
: "default:silicaclaw-room";
|
|
1357
|
+
|
|
1358
|
+
const seedPeersSocial = dedupeStrings(this.socialConfig.network.seed_peers || []);
|
|
1359
|
+
const seedPeersEnv = dedupeStrings(parseListEnv(WEBRTC_SEED_PEERS));
|
|
1360
|
+
const seedPeers = seedPeersSocial.length > 0 ? seedPeersSocial : seedPeersEnv;
|
|
1361
|
+
const seedPeersSource =
|
|
1362
|
+
seedPeersSocial.length > 0
|
|
1363
|
+
? "social.md:network.seed_peers"
|
|
1364
|
+
: seedPeersEnv.length > 0
|
|
1365
|
+
? "env:WEBRTC_SEED_PEERS"
|
|
1366
|
+
: "default:none";
|
|
1367
|
+
|
|
1368
|
+
const bootstrapHintsSocial = dedupeStrings(this.socialConfig.network.bootstrap_hints || []);
|
|
1369
|
+
const bootstrapHintsEnv = dedupeStrings(parseListEnv(WEBRTC_BOOTSTRAP_HINTS));
|
|
1370
|
+
const bootstrapHints = bootstrapHintsSocial.length > 0 ? bootstrapHintsSocial : bootstrapHintsEnv;
|
|
1371
|
+
const bootstrapHintsSource =
|
|
1372
|
+
bootstrapHintsSocial.length > 0
|
|
1373
|
+
? "social.md:network.bootstrap_hints"
|
|
1374
|
+
: bootstrapHintsEnv.length > 0
|
|
1375
|
+
? "env:WEBRTC_BOOTSTRAP_HINTS"
|
|
1376
|
+
: "default:none";
|
|
1377
|
+
|
|
1378
|
+
this.webrtcSignalingUrls = signalingUrls;
|
|
1379
|
+
this.webrtcRoom = room;
|
|
1380
|
+
this.webrtcSeedPeers = seedPeers;
|
|
1381
|
+
this.webrtcBootstrapHints = bootstrapHints;
|
|
1382
|
+
this.webrtcBootstrapSources = [signalingSource, roomSource, seedPeersSource, bootstrapHintsSource];
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function sendOk<T>(res: Response, data: T, meta?: Record<string, unknown>) {
|
|
1387
|
+
res.json({ ok: true, data, meta });
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function sendError(
|
|
1391
|
+
res: Response,
|
|
1392
|
+
status: number,
|
|
1393
|
+
code: string,
|
|
1394
|
+
message: string,
|
|
1395
|
+
details?: unknown
|
|
1396
|
+
) {
|
|
1397
|
+
const error: ApiErrorShape = { code, message };
|
|
1398
|
+
if (details !== undefined) {
|
|
1399
|
+
error.details = details;
|
|
1400
|
+
}
|
|
1401
|
+
res.status(status).json({ ok: false, error });
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function asyncRoute(
|
|
1405
|
+
handler: (req: Request, res: Response) => Promise<void> | void
|
|
1406
|
+
): (req: Request, res: Response, next: NextFunction) => void {
|
|
1407
|
+
return (req, res, next) => {
|
|
1408
|
+
Promise.resolve(handler(req, res)).catch(next);
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
function resolveLocalConsoleStaticDir(): string {
|
|
1413
|
+
const candidates = [
|
|
1414
|
+
resolve(process.cwd(), "public"),
|
|
1415
|
+
resolve(process.cwd(), "apps", "local-console", "public"),
|
|
1416
|
+
resolve(__dirname, "..", "public"),
|
|
1417
|
+
resolve(__dirname, "..", "..", "apps", "local-console", "public"),
|
|
1418
|
+
];
|
|
1419
|
+
|
|
1420
|
+
for (const dir of candidates) {
|
|
1421
|
+
if (existsSync(resolve(dir, "index.html"))) {
|
|
1422
|
+
return dir;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
return candidates[0];
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
async function main() {
|
|
1430
|
+
const app = express();
|
|
1431
|
+
const port = Number(process.env.PORT || 4310);
|
|
1432
|
+
const staticDir = resolveLocalConsoleStaticDir();
|
|
1433
|
+
|
|
1434
|
+
const node = new LocalNodeService();
|
|
1435
|
+
await node.start();
|
|
1436
|
+
|
|
1437
|
+
app.use(cors({ origin: true }));
|
|
1438
|
+
app.use(express.json());
|
|
1439
|
+
|
|
1440
|
+
app.get("/api/identity", (_req, res) => {
|
|
1441
|
+
sendOk(res, node.getIdentity());
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
app.post(
|
|
1445
|
+
"/api/identity/create",
|
|
1446
|
+
asyncRoute(async (_req, res) => {
|
|
1447
|
+
const identity = await node.ensureIdentity();
|
|
1448
|
+
sendOk(res, identity, { message: "Identity is ready" });
|
|
1449
|
+
})
|
|
1450
|
+
);
|
|
1451
|
+
|
|
1452
|
+
app.get("/api/profile", (_req, res) => {
|
|
1453
|
+
sendOk(res, node.getProfile());
|
|
1454
|
+
});
|
|
1455
|
+
|
|
1456
|
+
app.get("/api/public-profile/preview", (_req, res) => {
|
|
1457
|
+
sendOk(res, node.getPublicProfilePreview());
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
app.get("/api/runtime/paths", (_req, res) => {
|
|
1461
|
+
sendOk(res, node.getRuntimePaths());
|
|
1462
|
+
});
|
|
1463
|
+
|
|
1464
|
+
app.put(
|
|
1465
|
+
"/api/profile",
|
|
1466
|
+
asyncRoute(async (req, res) => {
|
|
1467
|
+
const body = req.body as Partial<ProfileInput>;
|
|
1468
|
+
const tags = Array.isArray(body.tags)
|
|
1469
|
+
? body.tags.map((tag: unknown) => String(tag).trim()).filter(Boolean)
|
|
1470
|
+
: undefined;
|
|
1471
|
+
|
|
1472
|
+
const profile = await node.updateProfile({
|
|
1473
|
+
...body,
|
|
1474
|
+
tags,
|
|
1475
|
+
display_name: body.display_name?.toString() ?? undefined,
|
|
1476
|
+
bio: body.bio?.toString() ?? undefined,
|
|
1477
|
+
avatar_url: body.avatar_url?.toString() ?? undefined,
|
|
1478
|
+
public_enabled: typeof body.public_enabled === "boolean" ? body.public_enabled : undefined,
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
sendOk(res, profile, { message: "Profile saved" });
|
|
1482
|
+
})
|
|
1483
|
+
);
|
|
1484
|
+
|
|
1485
|
+
app.get("/api/overview", (_req, res) => {
|
|
1486
|
+
sendOk(res, node.getOverview());
|
|
1487
|
+
});
|
|
1488
|
+
|
|
1489
|
+
app.get("/api/integration/status", (_req, res) => {
|
|
1490
|
+
sendOk(res, node.getIntegrationStatus());
|
|
1491
|
+
});
|
|
1492
|
+
|
|
1493
|
+
app.get("/api/network", (_req, res) => {
|
|
1494
|
+
sendOk(res, node.getNetworkSummary());
|
|
1495
|
+
});
|
|
1496
|
+
|
|
1497
|
+
app.get("/api/network/config", (_req, res) => {
|
|
1498
|
+
sendOk(res, node.getNetworkConfig());
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
app.get("/api/network/stats", (_req, res) => {
|
|
1502
|
+
sendOk(res, node.getNetworkStats());
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
app.post(
|
|
1506
|
+
"/api/network/quick-connect-global-preview",
|
|
1507
|
+
asyncRoute(async (req, res) => {
|
|
1508
|
+
const body = (req.body ?? {}) as { signaling_url?: unknown; room?: unknown };
|
|
1509
|
+
const signalingUrl = String(body.signaling_url || "").trim();
|
|
1510
|
+
const room = String(body.room || "").trim();
|
|
1511
|
+
if (!signalingUrl) {
|
|
1512
|
+
sendError(res, 400, "invalid_request", "signaling_url is required");
|
|
1513
|
+
return;
|
|
1514
|
+
}
|
|
1515
|
+
const result = await node.quickConnectGlobalPreview({
|
|
1516
|
+
signaling_url: signalingUrl,
|
|
1517
|
+
room,
|
|
1518
|
+
});
|
|
1519
|
+
sendOk(res, result, { message: "Cross-network preview enabled" });
|
|
1520
|
+
})
|
|
1521
|
+
);
|
|
1522
|
+
|
|
1523
|
+
app.get("/api/peers", (_req, res) => {
|
|
1524
|
+
sendOk(res, node.getPeersSummary());
|
|
1525
|
+
});
|
|
1526
|
+
|
|
1527
|
+
app.get("/api/discovery/events", (_req, res) => {
|
|
1528
|
+
sendOk(res, node.getDiscoveryEvents());
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
registerSocialRoutes(app, {
|
|
1532
|
+
getSocialConfigView: () => node.getSocialConfigView(),
|
|
1533
|
+
getIntegrationSummary: () => node.getIntegrationSummary(),
|
|
1534
|
+
exportSocialTemplate: () => node.exportSocialTemplate(),
|
|
1535
|
+
setNetworkModeRuntime: (mode) => node.setNetworkModeRuntime(mode),
|
|
1536
|
+
reloadSocialConfig: () => node.reloadSocialConfig(),
|
|
1537
|
+
generateDefaultSocialMd: () => node.generateDefaultSocialMd(),
|
|
1538
|
+
});
|
|
1539
|
+
|
|
1540
|
+
app.post(
|
|
1541
|
+
"/api/broadcast/start",
|
|
1542
|
+
asyncRoute(async (_req, res) => {
|
|
1543
|
+
const summary = await node.setBroadcastEnabled(true);
|
|
1544
|
+
sendOk(res, summary, { message: "Broadcast started" });
|
|
1545
|
+
})
|
|
1546
|
+
);
|
|
1547
|
+
|
|
1548
|
+
app.post(
|
|
1549
|
+
"/api/public-discovery/enable",
|
|
1550
|
+
asyncRoute(async (_req, res) => {
|
|
1551
|
+
const result = await node.setPublicDiscoveryRuntime(true);
|
|
1552
|
+
sendOk(res, result, { message: "Public discovery enabled (runtime)" });
|
|
1553
|
+
})
|
|
1554
|
+
);
|
|
1555
|
+
|
|
1556
|
+
app.post(
|
|
1557
|
+
"/api/public-discovery/disable",
|
|
1558
|
+
asyncRoute(async (_req, res) => {
|
|
1559
|
+
const result = await node.setPublicDiscoveryRuntime(false);
|
|
1560
|
+
sendOk(res, result, { message: "Public discovery disabled (runtime)" });
|
|
1561
|
+
})
|
|
1562
|
+
);
|
|
1563
|
+
|
|
1564
|
+
app.post(
|
|
1565
|
+
"/api/broadcast/stop",
|
|
1566
|
+
asyncRoute(async (_req, res) => {
|
|
1567
|
+
const summary = await node.setBroadcastEnabled(false);
|
|
1568
|
+
sendOk(res, summary, { message: "Broadcast stopped" });
|
|
1569
|
+
})
|
|
1570
|
+
);
|
|
1571
|
+
|
|
1572
|
+
app.post(
|
|
1573
|
+
"/api/broadcast/now",
|
|
1574
|
+
asyncRoute(async (_req, res) => {
|
|
1575
|
+
const result = await node.broadcastNow("manual_button");
|
|
1576
|
+
sendOk(res, result, {
|
|
1577
|
+
message: result.sent ? "Broadcast published" : `Broadcast skipped: ${result.reason}`,
|
|
1578
|
+
});
|
|
1579
|
+
})
|
|
1580
|
+
);
|
|
1581
|
+
|
|
1582
|
+
app.post(
|
|
1583
|
+
"/api/cache/refresh",
|
|
1584
|
+
asyncRoute(async (_req, res) => {
|
|
1585
|
+
const result = await node.refreshCache();
|
|
1586
|
+
sendOk(res, result, { message: "Cache refreshed" });
|
|
1587
|
+
})
|
|
1588
|
+
);
|
|
1589
|
+
|
|
1590
|
+
app.post(
|
|
1591
|
+
"/api/cache/clear",
|
|
1592
|
+
asyncRoute(async (_req, res) => {
|
|
1593
|
+
const result = await node.clearDiscoveredCache();
|
|
1594
|
+
sendOk(res, result, { message: "Discovered cache cleared (self profile kept)" });
|
|
1595
|
+
})
|
|
1596
|
+
);
|
|
1597
|
+
|
|
1598
|
+
app.get(
|
|
1599
|
+
"/api/logs",
|
|
1600
|
+
asyncRoute(async (_req, res) => {
|
|
1601
|
+
sendOk(res, await node.getLogs());
|
|
1602
|
+
})
|
|
1603
|
+
);
|
|
1604
|
+
|
|
1605
|
+
app.get("/api/search", (req, res) => {
|
|
1606
|
+
const q = String(req.query.q ?? "");
|
|
1607
|
+
sendOk(res, node.search(q));
|
|
1608
|
+
});
|
|
1609
|
+
|
|
1610
|
+
app.get("/api/agents/:agentId", (req, res) => {
|
|
1611
|
+
const state = node.getDirectory();
|
|
1612
|
+
const agentId = req.params.agentId;
|
|
1613
|
+
const profile = state.profiles[agentId];
|
|
1614
|
+
if (!profile) {
|
|
1615
|
+
sendError(res, 404, "AGENT_NOT_FOUND", "Agent not found", { agent_id: agentId });
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const lastSeenAt = state.presence[agentId] ?? 0;
|
|
1620
|
+
const summary = node.getAgentPublicSummary(agentId);
|
|
1621
|
+
sendOk(res, {
|
|
1622
|
+
profile,
|
|
1623
|
+
summary,
|
|
1624
|
+
last_seen_at: summary?.last_seen_at ?? null,
|
|
1625
|
+
online: isAgentOnline(lastSeenAt, Date.now(), PRESENCE_TTL_MS),
|
|
1626
|
+
presence_ttl_ms: PRESENCE_TTL_MS,
|
|
1627
|
+
});
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
app.get("/api/health", (_req, res) => {
|
|
1631
|
+
sendOk(res, { ok: true });
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
app.use(express.static(staticDir));
|
|
1635
|
+
|
|
1636
|
+
app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
|
1637
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1638
|
+
sendError(res, 500, "INTERNAL_ERROR", message);
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
app.listen(port, () => {
|
|
1642
|
+
// eslint-disable-next-line no-console
|
|
1643
|
+
console.log(`SilicaClaw local-console running: http://localhost:${port}`);
|
|
1644
|
+
});
|
|
1645
|
+
|
|
1646
|
+
process.on("SIGINT", async () => {
|
|
1647
|
+
await node.stop();
|
|
1648
|
+
process.exit(0);
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
main().catch((error) => {
|
|
1653
|
+
// eslint-disable-next-line no-console
|
|
1654
|
+
console.error(error);
|
|
1655
|
+
process.exit(1);
|
|
1656
|
+
});
|