@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.
Files changed (77) hide show
  1. package/ARCHITECTURE.md +137 -0
  2. package/CHANGELOG.md +411 -0
  3. package/DEMO_GUIDE.md +89 -0
  4. package/INSTALL.md +156 -0
  5. package/README.md +244 -0
  6. package/RELEASE_NOTES_v1.0.md +65 -0
  7. package/ROADMAP.md +48 -0
  8. package/SOCIAL_MD_SPEC.md +122 -0
  9. package/VERSION +1 -0
  10. package/apps/local-console/package.json +23 -0
  11. package/apps/local-console/public/assets/README.md +5 -0
  12. package/apps/local-console/public/assets/silicaclaw-logo.png +0 -0
  13. package/apps/local-console/public/index.html +1602 -0
  14. package/apps/local-console/src/server.ts +1656 -0
  15. package/apps/local-console/src/socialRoutes.ts +90 -0
  16. package/apps/local-console/tsconfig.json +7 -0
  17. package/apps/public-explorer/package.json +20 -0
  18. package/apps/public-explorer/public/assets/README.md +5 -0
  19. package/apps/public-explorer/public/assets/silicaclaw-logo.png +0 -0
  20. package/apps/public-explorer/public/index.html +483 -0
  21. package/apps/public-explorer/src/server.ts +32 -0
  22. package/apps/public-explorer/tsconfig.json +7 -0
  23. package/docs/QUICK_START.md +48 -0
  24. package/docs/assets/README.md +8 -0
  25. package/docs/assets/banner.svg +25 -0
  26. package/docs/assets/silicaclaw-logo.png +0 -0
  27. package/docs/assets/silicaclaw-og.png +0 -0
  28. package/docs/release/GITHUB_RELEASE_v1.0-beta.md +143 -0
  29. package/docs/screenshots/README.md +8 -0
  30. package/docs/screenshots/v0.3.1-explorer-search.svg +9 -0
  31. package/docs/screenshots/v0.3.1-machine-a-network.svg +9 -0
  32. package/docs/screenshots/v0.3.1-machine-b-peers.svg +9 -0
  33. package/docs/screenshots/v0.3.1-stale-transition.svg +9 -0
  34. package/openclaw.social.md.example +28 -0
  35. package/package.json +64 -0
  36. package/packages/core/package.json +13 -0
  37. package/packages/core/src/crypto.ts +55 -0
  38. package/packages/core/src/directory.ts +171 -0
  39. package/packages/core/src/identity.ts +14 -0
  40. package/packages/core/src/index.ts +11 -0
  41. package/packages/core/src/indexing.ts +42 -0
  42. package/packages/core/src/presence.ts +24 -0
  43. package/packages/core/src/profile.ts +39 -0
  44. package/packages/core/src/publicProfileSummary.ts +180 -0
  45. package/packages/core/src/socialConfig.ts +440 -0
  46. package/packages/core/src/socialResolver.ts +281 -0
  47. package/packages/core/src/socialTemplate.ts +97 -0
  48. package/packages/core/src/types.ts +43 -0
  49. package/packages/core/tsconfig.json +7 -0
  50. package/packages/network/package.json +10 -0
  51. package/packages/network/src/abstractions/messageEnvelope.ts +80 -0
  52. package/packages/network/src/abstractions/peerDiscovery.ts +49 -0
  53. package/packages/network/src/abstractions/topicCodec.ts +4 -0
  54. package/packages/network/src/abstractions/transport.ts +40 -0
  55. package/packages/network/src/codec/jsonMessageEnvelopeCodec.ts +22 -0
  56. package/packages/network/src/codec/jsonTopicCodec.ts +11 -0
  57. package/packages/network/src/discovery/heartbeatPeerDiscovery.ts +173 -0
  58. package/packages/network/src/index.ts +16 -0
  59. package/packages/network/src/localEventBus.ts +61 -0
  60. package/packages/network/src/mock.ts +27 -0
  61. package/packages/network/src/realPreview.ts +436 -0
  62. package/packages/network/src/transport/udpLanBroadcastTransport.ts +173 -0
  63. package/packages/network/src/types.ts +6 -0
  64. package/packages/network/src/webrtcPreview.ts +1052 -0
  65. package/packages/network/tsconfig.json +7 -0
  66. package/packages/storage/package.json +13 -0
  67. package/packages/storage/src/index.ts +3 -0
  68. package/packages/storage/src/jsonRepo.ts +25 -0
  69. package/packages/storage/src/repos.ts +46 -0
  70. package/packages/storage/src/socialRuntimeRepo.ts +51 -0
  71. package/packages/storage/tsconfig.json +7 -0
  72. package/scripts/functional-check.mjs +165 -0
  73. package/scripts/install-logo.sh +53 -0
  74. package/scripts/quickstart.sh +144 -0
  75. package/scripts/silicaclaw-cli.mjs +88 -0
  76. package/scripts/webrtc-signaling-server.mjs +249 -0
  77. package/social.md.example +30 -0
@@ -0,0 +1,440 @@
1
+ export type SocialIdentityConfig = {
2
+ display_name: string;
3
+ bio: string;
4
+ avatar_url: string;
5
+ tags: string[];
6
+ };
7
+
8
+ export type SocialNetworkAdapter = "mock" | "local-event-bus" | "real-preview" | "webrtc-preview";
9
+ export type SocialNetworkMode = "local" | "lan" | "global-preview";
10
+
11
+ export type SocialNetworkConfig = {
12
+ mode: SocialNetworkMode;
13
+ namespace: string;
14
+ adapter: SocialNetworkAdapter;
15
+ port: number;
16
+ signaling_url: string;
17
+ signaling_urls: string[];
18
+ room: string;
19
+ seed_peers: string[];
20
+ bootstrap_hints: string[];
21
+ };
22
+
23
+ export type SocialDiscoveryConfig = {
24
+ discoverable: boolean;
25
+ allow_profile_broadcast: boolean;
26
+ allow_presence_broadcast: boolean;
27
+ };
28
+
29
+ export type SocialVisibilityConfig = {
30
+ show_display_name: boolean;
31
+ show_bio: boolean;
32
+ show_tags: boolean;
33
+ show_agent_id: boolean;
34
+ show_last_seen: boolean;
35
+ show_capabilities_summary: boolean;
36
+ };
37
+
38
+ export type SocialOpenClawConfig = {
39
+ bind_existing_identity: boolean;
40
+ use_openclaw_profile_if_available: boolean;
41
+ };
42
+
43
+ export type SocialConfig = {
44
+ enabled: boolean;
45
+ public_enabled: boolean;
46
+ identity: SocialIdentityConfig;
47
+ network: SocialNetworkConfig;
48
+ discovery: SocialDiscoveryConfig;
49
+ visibility: SocialVisibilityConfig;
50
+ openclaw: SocialOpenClawConfig;
51
+ };
52
+
53
+ export type SocialLoadMeta = {
54
+ found: boolean;
55
+ source_path: string | null;
56
+ parse_error: string | null;
57
+ loaded_at: number;
58
+ };
59
+
60
+ export type SocialRuntimeConfig = {
61
+ enabled: boolean;
62
+ public_enabled: boolean;
63
+ source_path: string | null;
64
+ last_loaded_at: number;
65
+ social_found: boolean;
66
+ parse_error: string | null;
67
+ resolved_identity: {
68
+ agent_id: string;
69
+ public_key: string;
70
+ created_at: number;
71
+ source: "silicaclaw-existing" | "openclaw-existing" | "silicaclaw-generated";
72
+ } | null;
73
+ resolved_profile: {
74
+ display_name: string;
75
+ bio: string;
76
+ avatar_url?: string;
77
+ tags: string[];
78
+ public_enabled: boolean;
79
+ } | null;
80
+ resolved_network: {
81
+ mode: SocialNetworkMode;
82
+ adapter: SocialNetworkAdapter;
83
+ namespace: string;
84
+ port: number | null;
85
+ signaling_url: string;
86
+ signaling_urls: string[];
87
+ room: string;
88
+ seed_peers: string[];
89
+ bootstrap_hints: string[];
90
+ bootstrap_sources: string[];
91
+ };
92
+ resolved_discovery: SocialDiscoveryConfig;
93
+ visibility: SocialVisibilityConfig;
94
+ openclaw: SocialOpenClawConfig;
95
+ };
96
+
97
+ const DEFAULT_SOCIAL_CONFIG: SocialConfig = {
98
+ enabled: true,
99
+ public_enabled: false,
100
+ identity: {
101
+ display_name: "",
102
+ bio: "",
103
+ avatar_url: "",
104
+ tags: [],
105
+ },
106
+ network: {
107
+ mode: "lan",
108
+ namespace: "silicaclaw.preview",
109
+ adapter: "real-preview",
110
+ port: 44123,
111
+ signaling_url: "http://localhost:4510",
112
+ signaling_urls: [],
113
+ room: "silicaclaw-room",
114
+ seed_peers: [],
115
+ bootstrap_hints: [],
116
+ },
117
+ discovery: {
118
+ discoverable: true,
119
+ allow_profile_broadcast: true,
120
+ allow_presence_broadcast: true,
121
+ },
122
+ visibility: {
123
+ show_display_name: true,
124
+ show_bio: true,
125
+ show_tags: true,
126
+ show_agent_id: true,
127
+ show_last_seen: true,
128
+ show_capabilities_summary: true,
129
+ },
130
+ openclaw: {
131
+ bind_existing_identity: true,
132
+ use_openclaw_profile_if_available: true,
133
+ },
134
+ };
135
+
136
+ function parseScalar(value: string): unknown {
137
+ const trimmed = value.trim();
138
+ if (trimmed === "[]") return [];
139
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
140
+ const inner = trimmed.slice(1, -1).trim();
141
+ if (!inner) return [];
142
+ return inner
143
+ .split(",")
144
+ .map((item) => item.trim())
145
+ .map((item) =>
146
+ (item.startsWith('"') && item.endsWith('"')) || (item.startsWith("'") && item.endsWith("'"))
147
+ ? item.slice(1, -1)
148
+ : item
149
+ )
150
+ .filter(Boolean);
151
+ }
152
+ if (trimmed === "true") return true;
153
+ if (trimmed === "false") return false;
154
+ if (trimmed === "null") return null;
155
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) return Number(trimmed);
156
+ if (
157
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
158
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
159
+ ) {
160
+ return trimmed.slice(1, -1);
161
+ }
162
+ return trimmed;
163
+ }
164
+
165
+ function countIndent(line: string): number {
166
+ let count = 0;
167
+ while (count < line.length && line[count] === " ") count += 1;
168
+ return count;
169
+ }
170
+
171
+ function findNextSignificantLine(
172
+ lines: string[],
173
+ start: number
174
+ ): { line: string; indent: number } | null {
175
+ for (let i = start; i < lines.length; i += 1) {
176
+ const raw = lines[i];
177
+ const trimmed = raw.trim();
178
+ if (!trimmed || trimmed.startsWith("#")) continue;
179
+ return { line: trimmed, indent: countIndent(raw) };
180
+ }
181
+ return null;
182
+ }
183
+
184
+ type StackItem = {
185
+ indent: number;
186
+ value: Record<string, unknown> | unknown[];
187
+ kind: "object" | "array";
188
+ };
189
+
190
+ export function parseFrontmatterObject(frontmatter: string): Record<string, unknown> {
191
+ const root: Record<string, unknown> = {};
192
+ const lines = frontmatter.replace(/\r\n/g, "\n").split("\n");
193
+ const stack: StackItem[] = [{ indent: -1, value: root, kind: "object" }];
194
+
195
+ for (let i = 0; i < lines.length; i += 1) {
196
+ const raw = lines[i];
197
+ const trimmed = raw.trim();
198
+ if (!trimmed || trimmed.startsWith("#")) continue;
199
+
200
+ const indent = countIndent(raw);
201
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
202
+ stack.pop();
203
+ }
204
+
205
+ const current = stack[stack.length - 1];
206
+
207
+ if (trimmed.startsWith("- ")) {
208
+ if (current.kind !== "array") {
209
+ continue;
210
+ }
211
+ const itemRaw = trimmed.slice(2).trim();
212
+ if (!itemRaw) {
213
+ const child: Record<string, unknown> = {};
214
+ (current.value as unknown[]).push(child);
215
+ stack.push({ indent, value: child, kind: "object" });
216
+ continue;
217
+ }
218
+ const keyValue = itemRaw.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
219
+ if (keyValue) {
220
+ const child: Record<string, unknown> = {};
221
+ const key = keyValue[1];
222
+ const valueRaw = keyValue[2];
223
+ child[key] = valueRaw ? parseScalar(valueRaw) : "";
224
+ (current.value as unknown[]).push(child);
225
+ continue;
226
+ }
227
+ (current.value as unknown[]).push(parseScalar(itemRaw));
228
+ continue;
229
+ }
230
+
231
+ const match = trimmed.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
232
+ if (!match || current.kind !== "object") {
233
+ continue;
234
+ }
235
+ const key = match[1];
236
+ const valueRaw = match[2];
237
+ const obj = current.value as Record<string, unknown>;
238
+
239
+ if (valueRaw) {
240
+ obj[key] = parseScalar(valueRaw);
241
+ continue;
242
+ }
243
+
244
+ const next = findNextSignificantLine(lines, i + 1);
245
+ const nextIsArray = Boolean(next && next.indent > indent && next.line.startsWith("- "));
246
+ if (nextIsArray) {
247
+ const arr: unknown[] = [];
248
+ obj[key] = arr;
249
+ stack.push({ indent, value: arr, kind: "array" });
250
+ continue;
251
+ }
252
+ const childObj: Record<string, unknown> = {};
253
+ obj[key] = childObj;
254
+ stack.push({ indent, value: childObj, kind: "object" });
255
+ }
256
+
257
+ return root;
258
+ }
259
+
260
+ export function extractFrontmatter(content: string): string | null {
261
+ const normalized = content.replace(/\r\n/g, "\n");
262
+ if (!normalized.startsWith("---\n")) {
263
+ return null;
264
+ }
265
+ const end = normalized.indexOf("\n---", 4);
266
+ if (end < 0) {
267
+ return null;
268
+ }
269
+ return normalized.slice(4, end).trim();
270
+ }
271
+
272
+ function asObject(value: unknown): Record<string, unknown> {
273
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
274
+ return value as Record<string, unknown>;
275
+ }
276
+ return {};
277
+ }
278
+
279
+ function asBool(value: unknown, fallback: boolean): boolean {
280
+ return typeof value === "boolean" ? value : fallback;
281
+ }
282
+
283
+ function asString(value: unknown, fallback: string): string {
284
+ return typeof value === "string" ? value : fallback;
285
+ }
286
+
287
+ function asNumber(value: unknown, fallback: number): number {
288
+ return Number.isFinite(value) ? Number(value) : fallback;
289
+ }
290
+
291
+ function asStringArray(value: unknown, fallback: string[]): string[] {
292
+ if (!Array.isArray(value)) return fallback;
293
+ return value.map((item) => String(item).trim()).filter(Boolean);
294
+ }
295
+
296
+ function asAdapter(value: unknown, fallback: SocialNetworkAdapter): SocialNetworkAdapter {
297
+ if (
298
+ value === "mock" ||
299
+ value === "local-event-bus" ||
300
+ value === "real-preview" ||
301
+ value === "webrtc-preview"
302
+ ) {
303
+ return value;
304
+ }
305
+ return fallback;
306
+ }
307
+
308
+ function asMode(value: unknown, fallback: SocialNetworkMode): SocialNetworkMode {
309
+ if (value === "local" || value === "lan" || value === "global-preview") {
310
+ return value;
311
+ }
312
+ return fallback;
313
+ }
314
+
315
+ function adapterForMode(mode: SocialNetworkMode): SocialNetworkAdapter {
316
+ if (mode === "local") return "local-event-bus";
317
+ if (mode === "lan") return "real-preview";
318
+ return "webrtc-preview";
319
+ }
320
+
321
+ export function normalizeSocialConfig(input: unknown): SocialConfig {
322
+ const root = asObject(input);
323
+ const identity = asObject(root.identity);
324
+ const network = asObject(root.network);
325
+ const discovery = asObject(root.discovery);
326
+ const visibility = asObject(root.visibility);
327
+ const openclaw = asObject(root.openclaw);
328
+
329
+ const signalingUrl = asString(network.signaling_url, DEFAULT_SOCIAL_CONFIG.network.signaling_url);
330
+ const signalingUrls = asStringArray(
331
+ network.signaling_urls,
332
+ DEFAULT_SOCIAL_CONFIG.network.signaling_urls
333
+ );
334
+ const mode = asMode(network.mode, DEFAULT_SOCIAL_CONFIG.network.mode);
335
+
336
+ return {
337
+ enabled: asBool(root.enabled, DEFAULT_SOCIAL_CONFIG.enabled),
338
+ public_enabled: asBool(root.public_enabled, DEFAULT_SOCIAL_CONFIG.public_enabled),
339
+ identity: {
340
+ display_name: asString(identity.display_name, DEFAULT_SOCIAL_CONFIG.identity.display_name),
341
+ bio: asString(identity.bio, DEFAULT_SOCIAL_CONFIG.identity.bio),
342
+ avatar_url: asString(identity.avatar_url, DEFAULT_SOCIAL_CONFIG.identity.avatar_url),
343
+ tags: asStringArray(identity.tags, DEFAULT_SOCIAL_CONFIG.identity.tags),
344
+ },
345
+ network: {
346
+ mode,
347
+ namespace: asString(network.namespace, DEFAULT_SOCIAL_CONFIG.network.namespace),
348
+ adapter: asAdapter(network.adapter, adapterForMode(mode)),
349
+ port: asNumber(network.port, DEFAULT_SOCIAL_CONFIG.network.port),
350
+ signaling_url: signalingUrl,
351
+ signaling_urls: signalingUrls.length > 0 ? signalingUrls : signalingUrl ? [signalingUrl] : [],
352
+ room: asString(network.room, DEFAULT_SOCIAL_CONFIG.network.room),
353
+ seed_peers: asStringArray(network.seed_peers, DEFAULT_SOCIAL_CONFIG.network.seed_peers),
354
+ bootstrap_hints: asStringArray(
355
+ network.bootstrap_hints,
356
+ DEFAULT_SOCIAL_CONFIG.network.bootstrap_hints
357
+ ),
358
+ },
359
+ discovery: {
360
+ discoverable: asBool(discovery.discoverable, DEFAULT_SOCIAL_CONFIG.discovery.discoverable),
361
+ allow_profile_broadcast: asBool(
362
+ discovery.allow_profile_broadcast,
363
+ DEFAULT_SOCIAL_CONFIG.discovery.allow_profile_broadcast
364
+ ),
365
+ allow_presence_broadcast: asBool(
366
+ discovery.allow_presence_broadcast,
367
+ DEFAULT_SOCIAL_CONFIG.discovery.allow_presence_broadcast
368
+ ),
369
+ },
370
+ visibility: {
371
+ show_display_name: asBool(
372
+ visibility.show_display_name,
373
+ DEFAULT_SOCIAL_CONFIG.visibility.show_display_name
374
+ ),
375
+ show_bio: asBool(visibility.show_bio, DEFAULT_SOCIAL_CONFIG.visibility.show_bio),
376
+ show_tags: asBool(visibility.show_tags, DEFAULT_SOCIAL_CONFIG.visibility.show_tags),
377
+ show_agent_id: asBool(visibility.show_agent_id, DEFAULT_SOCIAL_CONFIG.visibility.show_agent_id),
378
+ show_last_seen: asBool(
379
+ visibility.show_last_seen,
380
+ DEFAULT_SOCIAL_CONFIG.visibility.show_last_seen
381
+ ),
382
+ show_capabilities_summary: asBool(
383
+ visibility.show_capabilities_summary,
384
+ DEFAULT_SOCIAL_CONFIG.visibility.show_capabilities_summary
385
+ ),
386
+ },
387
+ openclaw: {
388
+ bind_existing_identity: asBool(
389
+ openclaw.bind_existing_identity,
390
+ DEFAULT_SOCIAL_CONFIG.openclaw.bind_existing_identity
391
+ ),
392
+ use_openclaw_profile_if_available: asBool(
393
+ openclaw.use_openclaw_profile_if_available,
394
+ DEFAULT_SOCIAL_CONFIG.openclaw.use_openclaw_profile_if_available
395
+ ),
396
+ },
397
+ };
398
+ }
399
+
400
+ export function getDefaultSocialConfig(): SocialConfig {
401
+ return JSON.parse(JSON.stringify(DEFAULT_SOCIAL_CONFIG)) as SocialConfig;
402
+ }
403
+
404
+ export type DefaultSocialTemplateOptions = {
405
+ display_name?: string;
406
+ bio?: string;
407
+ tags?: string[];
408
+ mode?: SocialNetworkMode;
409
+ public_enabled?: boolean;
410
+ };
411
+
412
+ export function generateDefaultSocialMdTemplate(options: DefaultSocialTemplateOptions = {}): string {
413
+ const displayName = options.display_name?.trim() || "My OpenClaw Agent";
414
+ const bio = options.bio?.trim() || "Local AI agent running on this machine";
415
+ const tags = Array.isArray(options.tags) && options.tags.length > 0 ? options.tags : ["openclaw", "local-first"];
416
+ const mode = options.mode ?? "lan";
417
+ const publicEnabled = typeof options.public_enabled === "boolean" ? options.public_enabled : false;
418
+ return `---
419
+ enabled: true
420
+ public_enabled: ${publicEnabled}
421
+
422
+ identity:
423
+ display_name: ${JSON.stringify(displayName)}
424
+ bio: ${JSON.stringify(bio)}
425
+ tags:
426
+ ${tags.map((tag) => ` - ${tag}`).join("\n")}
427
+
428
+ network:
429
+ mode: ${JSON.stringify(mode)}
430
+
431
+ openclaw:
432
+ bind_existing_identity: true
433
+ use_openclaw_profile_if_available: true
434
+ ---
435
+
436
+ # social.md
437
+
438
+ This file configures how OpenClaw integrates with SilicaClaw.
439
+ `;
440
+ }
@@ -0,0 +1,281 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { resolve } from "path";
4
+ import { AgentIdentity, ProfileInput, PublicProfile } from "./types";
5
+ import { fromBase64, hashPublicKey } from "./crypto";
6
+ import {
7
+ DefaultSocialTemplateOptions,
8
+ SocialConfig,
9
+ SocialLoadMeta,
10
+ extractFrontmatter,
11
+ generateDefaultSocialMdTemplate,
12
+ normalizeSocialConfig,
13
+ parseFrontmatterObject,
14
+ } from "./socialConfig";
15
+
16
+ export type SocialFileLookup = {
17
+ found: boolean;
18
+ source_path: string | null;
19
+ content: string | null;
20
+ };
21
+
22
+ export type OpenClawIdentityLookup = {
23
+ identity: AgentIdentity | null;
24
+ source_path: string | null;
25
+ };
26
+
27
+ export type OpenClawProfileLookup = {
28
+ profile: Partial<PublicProfile> | null;
29
+ source_path: string | null;
30
+ };
31
+
32
+ export type LoadedSocialConfig = {
33
+ config: SocialConfig;
34
+ meta: SocialLoadMeta;
35
+ raw_frontmatter: Record<string, unknown> | null;
36
+ };
37
+
38
+ export type ResolvedIdentityResult = {
39
+ identity: AgentIdentity;
40
+ source: "silicaclaw-existing" | "openclaw-existing" | "silicaclaw-generated";
41
+ openclaw_source_path: string | null;
42
+ };
43
+
44
+ export function getSocialConfigSearchPaths(rootDir = process.cwd(), homeDir = homedir()): string[] {
45
+ return [
46
+ resolve(rootDir, "social.md"),
47
+ resolve(rootDir, ".openclaw", "social.md"),
48
+ resolve(homeDir, ".openclaw", "social.md"),
49
+ ];
50
+ }
51
+
52
+ export function findSocialMd(rootDir = process.cwd()): SocialFileLookup {
53
+ const candidates = getSocialConfigSearchPaths(rootDir);
54
+ for (const path of candidates) {
55
+ if (!existsSync(path)) continue;
56
+ return {
57
+ found: true,
58
+ source_path: path,
59
+ content: readFileSync(path, "utf8"),
60
+ };
61
+ }
62
+ return {
63
+ found: false,
64
+ source_path: null,
65
+ content: null,
66
+ };
67
+ }
68
+
69
+ export function loadSocialConfig(rootDir = process.cwd()): LoadedSocialConfig {
70
+ const lookup = findSocialMd(rootDir);
71
+ if (!lookup.found || !lookup.content || !lookup.source_path) {
72
+ return {
73
+ config: normalizeSocialConfig({}),
74
+ meta: {
75
+ found: false,
76
+ source_path: null,
77
+ parse_error: null,
78
+ loaded_at: Date.now(),
79
+ },
80
+ raw_frontmatter: null,
81
+ };
82
+ }
83
+
84
+ try {
85
+ const frontmatter = extractFrontmatter(lookup.content);
86
+ if (!frontmatter) {
87
+ return {
88
+ config: normalizeSocialConfig({}),
89
+ meta: {
90
+ found: true,
91
+ source_path: lookup.source_path,
92
+ parse_error: "frontmatter_not_found",
93
+ loaded_at: Date.now(),
94
+ },
95
+ raw_frontmatter: null,
96
+ };
97
+ }
98
+ const parsed = parseFrontmatterObject(frontmatter);
99
+ return {
100
+ config: normalizeSocialConfig(parsed),
101
+ meta: {
102
+ found: true,
103
+ source_path: lookup.source_path,
104
+ parse_error: null,
105
+ loaded_at: Date.now(),
106
+ },
107
+ raw_frontmatter: parsed,
108
+ };
109
+ } catch (error) {
110
+ return {
111
+ config: normalizeSocialConfig({}),
112
+ meta: {
113
+ found: true,
114
+ source_path: lookup.source_path,
115
+ parse_error: error instanceof Error ? error.message : "social_parse_failed",
116
+ loaded_at: Date.now(),
117
+ },
118
+ raw_frontmatter: null,
119
+ };
120
+ }
121
+ }
122
+
123
+ function readJson(path: string): unknown | null {
124
+ if (!existsSync(path)) return null;
125
+ try {
126
+ return JSON.parse(readFileSync(path, "utf8")) as unknown;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function normalizeIdentity(input: unknown): AgentIdentity | null {
133
+ if (typeof input !== "object" || input === null) {
134
+ return null;
135
+ }
136
+ const value = input as Record<string, unknown>;
137
+ if (typeof value.public_key !== "string" || typeof value.private_key !== "string") {
138
+ return null;
139
+ }
140
+
141
+ let agentId = typeof value.agent_id === "string" ? value.agent_id : "";
142
+ if (!agentId) {
143
+ try {
144
+ agentId = hashPublicKey(fromBase64(value.public_key));
145
+ } catch {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ return {
151
+ agent_id: agentId,
152
+ public_key: value.public_key,
153
+ private_key: value.private_key,
154
+ created_at: Number.isFinite(value.created_at) ? Number(value.created_at) : Date.now(),
155
+ };
156
+ }
157
+
158
+ export function findOpenClawIdentity(rootDir = process.cwd(), homeDir = homedir()): OpenClawIdentityLookup {
159
+ const candidates = [
160
+ resolve(rootDir, ".openclaw", "identity.json"),
161
+ resolve(rootDir, "identity.json"),
162
+ resolve(homeDir, ".openclaw", "identity.json"),
163
+ ];
164
+
165
+ for (const path of candidates) {
166
+ const parsed = readJson(path);
167
+ const identity = normalizeIdentity(parsed);
168
+ if (identity) {
169
+ return {
170
+ identity,
171
+ source_path: path,
172
+ };
173
+ }
174
+ }
175
+ return {
176
+ identity: null,
177
+ source_path: null,
178
+ };
179
+ }
180
+
181
+ export function findOpenClawProfile(
182
+ rootDir = process.cwd(),
183
+ homeDir = homedir()
184
+ ): OpenClawProfileLookup {
185
+ const candidates = [
186
+ resolve(rootDir, ".openclaw", "profile.json"),
187
+ resolve(rootDir, "profile.json"),
188
+ resolve(homeDir, ".openclaw", "profile.json"),
189
+ ];
190
+ for (const path of candidates) {
191
+ const parsed = readJson(path);
192
+ if (typeof parsed !== "object" || parsed === null) continue;
193
+ const profile = parsed as Record<string, unknown>;
194
+ return {
195
+ source_path: path,
196
+ profile: {
197
+ display_name: typeof profile.display_name === "string" ? profile.display_name : "",
198
+ bio: typeof profile.bio === "string" ? profile.bio : "",
199
+ avatar_url: typeof profile.avatar_url === "string" ? profile.avatar_url : "",
200
+ tags: Array.isArray(profile.tags)
201
+ ? profile.tags.map((tag) => String(tag).trim()).filter(Boolean)
202
+ : [],
203
+ },
204
+ };
205
+ }
206
+ return {
207
+ profile: null,
208
+ source_path: null,
209
+ };
210
+ }
211
+
212
+ export function resolveIdentityWithSocial(args: {
213
+ socialConfig: SocialConfig;
214
+ existingIdentity: AgentIdentity | null;
215
+ generatedIdentity: AgentIdentity;
216
+ rootDir?: string;
217
+ }): ResolvedIdentityResult {
218
+ const { socialConfig, existingIdentity, generatedIdentity } = args;
219
+ if (socialConfig.openclaw.bind_existing_identity) {
220
+ const openclaw = findOpenClawIdentity(args.rootDir ?? process.cwd());
221
+ if (openclaw.identity) {
222
+ return {
223
+ identity: openclaw.identity,
224
+ source: "openclaw-existing",
225
+ openclaw_source_path: openclaw.source_path,
226
+ };
227
+ }
228
+ }
229
+ if (existingIdentity) {
230
+ return {
231
+ identity: existingIdentity,
232
+ source: "silicaclaw-existing",
233
+ openclaw_source_path: null,
234
+ };
235
+ }
236
+ return {
237
+ identity: generatedIdentity,
238
+ source: "silicaclaw-generated",
239
+ openclaw_source_path: null,
240
+ };
241
+ }
242
+
243
+ export function resolveProfileInputWithSocial(args: {
244
+ socialConfig: SocialConfig;
245
+ agentId: string;
246
+ existingProfile: PublicProfile | null;
247
+ rootDir?: string;
248
+ }): ProfileInput {
249
+ const { socialConfig, agentId, existingProfile } = args;
250
+ const openclawProfile =
251
+ socialConfig.openclaw.use_openclaw_profile_if_available
252
+ ? findOpenClawProfile(args.rootDir ?? process.cwd()).profile
253
+ : null;
254
+
255
+ const baseDisplayName = existingProfile?.display_name || "";
256
+ const baseBio = existingProfile?.bio || "";
257
+ const baseAvatarUrl = existingProfile?.avatar_url || "";
258
+ const baseTags = existingProfile?.tags || [];
259
+
260
+ return {
261
+ agent_id: agentId,
262
+ display_name: socialConfig.identity.display_name || openclawProfile?.display_name || baseDisplayName,
263
+ bio: socialConfig.identity.bio || openclawProfile?.bio || baseBio,
264
+ avatar_url: socialConfig.identity.avatar_url || openclawProfile?.avatar_url || baseAvatarUrl,
265
+ tags: socialConfig.identity.tags.length > 0 ? socialConfig.identity.tags : openclawProfile?.tags || baseTags,
266
+ public_enabled: socialConfig.public_enabled,
267
+ };
268
+ }
269
+
270
+ export function ensureDefaultSocialMd(
271
+ rootDir = process.cwd(),
272
+ options: DefaultSocialTemplateOptions = {}
273
+ ): { path: string; created: boolean } {
274
+ const targetPath = resolve(rootDir, "social.md");
275
+ if (existsSync(targetPath)) {
276
+ return { path: targetPath, created: false };
277
+ }
278
+ mkdirSync(rootDir, { recursive: true });
279
+ writeFileSync(targetPath, generateDefaultSocialMdTemplate(options), "utf8");
280
+ return { path: targetPath, created: true };
281
+ }