@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,90 @@
1
+ import { Express } from "express";
2
+
3
+ type SocialRoutesDeps = {
4
+ getSocialConfigView: () => unknown;
5
+ getIntegrationSummary: () => unknown;
6
+ exportSocialTemplate: () => { filename: string; content: string };
7
+ setNetworkModeRuntime: (mode: "local" | "lan" | "global-preview") => Promise<unknown>;
8
+ reloadSocialConfig: () => Promise<unknown>;
9
+ generateDefaultSocialMd: () => Promise<unknown>;
10
+ };
11
+
12
+ function sendOk(res: any, data: unknown, meta?: Record<string, unknown>) {
13
+ res.json({ ok: true, data, meta });
14
+ }
15
+
16
+ function sendError(res: any, status: number, code: string, message: string, details?: unknown) {
17
+ res.status(status).json({
18
+ ok: false,
19
+ error: {
20
+ code,
21
+ message,
22
+ details,
23
+ },
24
+ });
25
+ }
26
+
27
+ export function registerSocialRoutes(app: Express, deps: SocialRoutesDeps): void {
28
+ app.get("/api/social/config", (_req, res) => {
29
+ sendOk(res, deps.getSocialConfigView());
30
+ });
31
+
32
+ app.get("/api/social/integration-summary", (_req, res) => {
33
+ sendOk(res, deps.getIntegrationSummary());
34
+ });
35
+
36
+ app.get("/api/social/export-template", (_req, res) => {
37
+ sendOk(res, deps.exportSocialTemplate());
38
+ });
39
+
40
+ app.post("/api/social/runtime-mode", async (req, res) => {
41
+ try {
42
+ const mode = String(req.body?.mode || "");
43
+ if (mode !== "local" && mode !== "lan" && mode !== "global-preview") {
44
+ sendError(res, 400, "INVALID_MODE", "mode must be local | lan | global-preview");
45
+ return;
46
+ }
47
+ const result = await deps.setNetworkModeRuntime(mode);
48
+ sendOk(res, result, { message: "Runtime network mode updated (social.md unchanged)" });
49
+ } catch (error) {
50
+ sendError(
51
+ res,
52
+ 500,
53
+ "SOCIAL_RUNTIME_MODE_FAILED",
54
+ error instanceof Error ? error.message : "Runtime mode update failed"
55
+ );
56
+ }
57
+ });
58
+
59
+ app.post("/api/social/reload", async (_req, res) => {
60
+ try {
61
+ const result = await deps.reloadSocialConfig();
62
+ sendOk(res, result, { message: "Social config reloaded" });
63
+ } catch (error) {
64
+ sendError(
65
+ res,
66
+ 500,
67
+ "SOCIAL_RELOAD_FAILED",
68
+ error instanceof Error ? error.message : "Social reload failed"
69
+ );
70
+ }
71
+ });
72
+
73
+ app.post("/api/social/generate-default", async (_req, res) => {
74
+ try {
75
+ const result = await deps.generateDefaultSocialMd();
76
+ sendOk(
77
+ res,
78
+ result,
79
+ { message: (result as { created?: boolean }).created ? "Default social.md generated" : "social.md already exists" }
80
+ );
81
+ } catch (error) {
82
+ sendError(
83
+ res,
84
+ 500,
85
+ "SOCIAL_GENERATE_FAILED",
86
+ error instanceof Error ? error.message : "social.md generation failed"
87
+ );
88
+ }
89
+ });
90
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist"
5
+ },
6
+ "include": ["src"]
7
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@silicaclaw/public-explorer",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "tsx src/server.ts",
7
+ "build": "tsc -p tsconfig.json",
8
+ "check": "tsc -p tsconfig.json --noEmit"
9
+ },
10
+ "dependencies": {
11
+ "cors": "^2.8.5",
12
+ "express": "^4.21.2"
13
+ },
14
+ "devDependencies": {
15
+ "@types/cors": "^2.8.17",
16
+ "@types/express": "^4.17.21",
17
+ "tsx": "^4.19.3",
18
+ "typescript": "^5.8.2"
19
+ }
20
+ }
@@ -0,0 +1,5 @@
1
+ Place the official SilicaClaw crab logo image at:
2
+
3
+ - apps/public-explorer/public/assets/silicaclaw-logo.png
4
+
5
+ This file is referenced by the Public Explorer header brand block.
@@ -0,0 +1,483 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>SilicaClaw Public Explorer</title>
7
+ <meta name="description" content="SilicaClaw serverless public directory explorer for agents." />
8
+ <meta property="og:type" content="website" />
9
+ <meta property="og:title" content="SilicaClaw Public Explorer" />
10
+ <meta property="og:description" content="Search and browse public SilicaClaw agents in a local-first network." />
11
+ <meta property="og:image" content="/assets/silicaclaw-logo.png" />
12
+ <meta name="twitter:card" content="summary_large_image" />
13
+ <meta name="twitter:title" content="SilicaClaw Public Explorer" />
14
+ <meta name="twitter:description" content="Search and browse public SilicaClaw agents in a local-first network." />
15
+ <meta name="twitter:image" content="/assets/silicaclaw-logo.png" />
16
+ <link rel="icon" type="image/png" href="/assets/silicaclaw-logo.png" />
17
+ <link rel="apple-touch-icon" href="/assets/silicaclaw-logo.png" />
18
+ <style>
19
+ :root {
20
+ --bg: #0e1015;
21
+ --panel: #161920;
22
+ --line: #1e2028;
23
+ --line-strong: #2e3040;
24
+ --text: #d4d4d8;
25
+ --text-strong: #f4f4f5;
26
+ --muted: #636370;
27
+ --brand: #ff5c5c;
28
+ --brand-hover: #ff7070;
29
+ --ok: #22c55e;
30
+ --warn: #f59e0b;
31
+ }
32
+ :root[data-theme-mode="light"] {
33
+ --bg: #f8f9fa;
34
+ --panel: #ffffff;
35
+ --line: #e5e5ea;
36
+ --line-strong: #d1d1d6;
37
+ --text: #3c3c43;
38
+ --text-strong: #1a1a1e;
39
+ --muted: #8e8e93;
40
+ --brand: #dc2626;
41
+ --brand-hover: #ef4444;
42
+ --ok: #16a34a;
43
+ --warn: #d97706;
44
+ }
45
+ * { box-sizing: border-box; }
46
+ body {
47
+ margin: 0;
48
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", sans-serif;
49
+ color: var(--text);
50
+ background:
51
+ radial-gradient(900px 420px at 8% -12%, rgba(255, 92, 92, 0.18), transparent 60%),
52
+ linear-gradient(180deg, #0e1015 0%, #0e1015 62%, #0f1219 100%);
53
+ transition: background .2s ease, color .2s ease;
54
+ }
55
+ :root[data-theme-mode="light"] body {
56
+ background:
57
+ radial-gradient(900px 420px at 8% -12%, rgba(220, 38, 38, 0.1), transparent 60%),
58
+ linear-gradient(180deg, #f8f9fa 0%, #f8f9fa 62%, #eef1f6 100%);
59
+ }
60
+ .container { max-width: 1100px; margin: 24px auto; padding: 0 14px 20px; }
61
+ .header {
62
+ border: 1px solid var(--line);
63
+ border-radius: 14px;
64
+ background: var(--panel);
65
+ padding: 14px;
66
+ }
67
+ h1 { margin: 0; }
68
+ .header-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
69
+ .brand-wrap { display: flex; align-items: center; gap: 10px; min-width: 0; }
70
+ .brand-logo {
71
+ width: 36px;
72
+ height: 36px;
73
+ border-radius: 10px;
74
+ object-fit: cover;
75
+ display: block;
76
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.24);
77
+ border: 1px solid color-mix(in srgb, var(--line) 75%, transparent);
78
+ }
79
+ .brand-fallback {
80
+ width: 36px;
81
+ height: 36px;
82
+ border-radius: 10px;
83
+ background: linear-gradient(135deg, var(--brand), var(--brand-hover));
84
+ color: #fff;
85
+ font-weight: 900;
86
+ display: grid;
87
+ place-items: center;
88
+ }
89
+ .brand-title { min-width: 0; }
90
+ .brand-title h1 { margin: 0; }
91
+ .muted { color: var(--muted); }
92
+ .theme-switch {
93
+ display: inline-flex;
94
+ gap: 4px;
95
+ padding: 3px;
96
+ border-radius: 999px;
97
+ border: 1px solid var(--line);
98
+ background: color-mix(in srgb, var(--panel) 80%, transparent);
99
+ }
100
+ .theme-switch button {
101
+ border: 1px solid transparent;
102
+ background: transparent;
103
+ color: var(--muted);
104
+ border-radius: 999px;
105
+ padding: 4px 10px;
106
+ font-size: 12px;
107
+ font-weight: 600;
108
+ }
109
+ .theme-switch button.active {
110
+ color: var(--text-strong);
111
+ background: color-mix(in srgb, var(--brand) 16%, transparent);
112
+ border-color: color-mix(in srgb, var(--brand) 22%, transparent);
113
+ }
114
+ .search {
115
+ display: grid;
116
+ grid-template-columns: 1fr auto;
117
+ gap: 8px;
118
+ margin-top: 10px;
119
+ }
120
+ input {
121
+ border: 1px solid var(--line);
122
+ border-radius: 10px;
123
+ background: #0f131b;
124
+ color: var(--text);
125
+ font: inherit;
126
+ padding: 10px;
127
+ }
128
+ button {
129
+ border: 0;
130
+ border-radius: 10px;
131
+ background: var(--brand);
132
+ color: #fff;
133
+ padding: 10px 14px;
134
+ font-weight: 700;
135
+ cursor: pointer;
136
+ }
137
+ button.secondary {
138
+ background: #1a202d;
139
+ color: var(--text);
140
+ border: 1px solid var(--line);
141
+ }
142
+ button:hover { background: var(--brand-hover); }
143
+ button.secondary:hover { border-color: var(--line-strong); background: #202736; }
144
+ .state {
145
+ margin-top: 10px;
146
+ border: 1px dashed var(--line);
147
+ border-radius: 12px;
148
+ text-align: center;
149
+ color: var(--muted);
150
+ padding: 20px;
151
+ background: #11151d;
152
+ }
153
+ .cards {
154
+ margin-top: 12px;
155
+ display: grid;
156
+ gap: 10px;
157
+ grid-template-columns: repeat(2, minmax(0, 1fr));
158
+ }
159
+ .card {
160
+ border: 1px solid var(--line);
161
+ border-radius: 14px;
162
+ background: var(--panel);
163
+ padding: 12px;
164
+ cursor: pointer;
165
+ }
166
+ .card:hover { border-color: var(--line-strong); }
167
+ .badge {
168
+ display: inline-block;
169
+ border: 1px solid var(--line-strong);
170
+ border-radius: 999px;
171
+ padding: 2px 8px;
172
+ font-size: 11px;
173
+ color: var(--text);
174
+ background: color-mix(in srgb, var(--panel) 75%, transparent);
175
+ }
176
+ .badge.ok { color: var(--ok); border-color: rgba(34, 197, 94, 0.45); }
177
+ .badge.warn { color: var(--warn); border-color: rgba(245, 158, 11, 0.45); }
178
+ .badge.err { color: #ef4444; border-color: rgba(239, 68, 68, 0.45); }
179
+ .chips { margin-top: 8px; }
180
+ .chip {
181
+ display: inline-block;
182
+ border: 1px solid var(--line-strong);
183
+ background: #1f2330;
184
+ border-radius: 999px;
185
+ padding: 3px 8px;
186
+ font-size: 12px;
187
+ margin-right: 6px;
188
+ }
189
+ .meta {
190
+ margin-top: 10px;
191
+ display: flex;
192
+ justify-content: space-between;
193
+ }
194
+ .online { color: var(--ok); font-weight: 700; }
195
+ .offline { color: #ef4444; font-weight: 700; }
196
+ .mono { font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
197
+ .detail {
198
+ margin-top: 12px;
199
+ border: 1px solid var(--line);
200
+ border-radius: 14px;
201
+ background: var(--panel);
202
+ padding: 14px;
203
+ }
204
+ .detail-hero {
205
+ display: flex;
206
+ justify-content: space-between;
207
+ gap: 12px;
208
+ align-items: flex-start;
209
+ }
210
+ .detail-grid {
211
+ margin-top: 10px;
212
+ display: grid;
213
+ gap: 8px;
214
+ grid-template-columns: repeat(2, minmax(0, 1fr));
215
+ }
216
+ .detail-item {
217
+ border: 1px solid var(--line);
218
+ border-radius: 10px;
219
+ padding: 8px;
220
+ }
221
+ .hidden { display: none; }
222
+ .toast {
223
+ position: fixed;
224
+ right: 20px;
225
+ bottom: 20px;
226
+ border: 1px solid var(--line-strong);
227
+ border-radius: 10px;
228
+ background: color-mix(in srgb, var(--panel) 92%, #000 8%);
229
+ color: var(--text-strong);
230
+ padding: 10px 12px;
231
+ font-size: 13px;
232
+ opacity: 0;
233
+ transform: translateY(8px);
234
+ transition: opacity .2s ease, transform .2s ease;
235
+ pointer-events: none;
236
+ }
237
+ .toast.show {
238
+ opacity: 1;
239
+ transform: translateY(0);
240
+ }
241
+ @media (max-width: 900px) { .cards { grid-template-columns: 1fr; } }
242
+ </style>
243
+ </head>
244
+ <body>
245
+ <div class="container">
246
+ <header class="header">
247
+ <div class="header-top">
248
+ <div class="brand-wrap">
249
+ <img id="brandLogo" class="brand-logo" src="/assets/silicaclaw-logo.png" alt="SilicaClaw logo" />
250
+ <div id="brandFallback" class="brand-fallback hidden">SC</div>
251
+ <div class="brand-title">
252
+ <h1>SilicaClaw Public Explorer</h1>
253
+ </div>
254
+ </div>
255
+ <div class="theme-switch">
256
+ <button id="themeDarkBtn" type="button">Dark</button>
257
+ <button id="themeLightBtn" type="button">Light</button>
258
+ </div>
259
+ </div>
260
+ <div class="muted">OpenClaw-inspired P2P directory browsing UI</div>
261
+ <div class="search">
262
+ <input id="q" placeholder="Search tag or name prefix" />
263
+ <button id="searchBtn">Search</button>
264
+ </div>
265
+ </header>
266
+
267
+ <div id="state"></div>
268
+ <div id="cards" class="cards"></div>
269
+ <section id="detail" class="detail hidden"></section>
270
+ </div>
271
+ <div id="toast" class="toast"></div>
272
+
273
+ <script>
274
+ const API_BASE = localStorage.getItem('silicaclaw_api_base') || 'http://localhost:4310';
275
+ const state = document.getElementById('state');
276
+ const cards = document.getElementById('cards');
277
+ const detail = document.getElementById('detail');
278
+
279
+ function shortId(id) { return id ? `${id.slice(0, 10)}...${id.slice(-6)}` : '-'; }
280
+ function toPrettyJson(obj) {
281
+ try {
282
+ return JSON.stringify(obj, null, 2);
283
+ } catch {
284
+ return String(obj);
285
+ }
286
+ }
287
+ function toast(msg) {
288
+ const t = document.getElementById('toast');
289
+ t.textContent = msg;
290
+ t.classList.add('show');
291
+ setTimeout(() => t.classList.remove('show'), 1800);
292
+ }
293
+ async function copyText(text, btn, successText = 'Copied') {
294
+ try {
295
+ await navigator.clipboard.writeText(text);
296
+ toast(successText);
297
+ if (!btn) return;
298
+ const old = btn.textContent || '';
299
+ btn.disabled = true;
300
+ btn.textContent = 'Copied';
301
+ setTimeout(() => {
302
+ btn.textContent = old;
303
+ btn.disabled = false;
304
+ }, 900);
305
+ } catch (err) {
306
+ toast(err instanceof Error ? err.message : 'Copy failed');
307
+ }
308
+ }
309
+ function applyTheme(mode) {
310
+ const next = mode === 'light' ? 'light' : 'dark';
311
+ document.documentElement.setAttribute('data-theme-mode', next);
312
+ localStorage.setItem('silicaclaw_theme_mode', next);
313
+ document.getElementById('themeDarkBtn').classList.toggle('active', next === 'dark');
314
+ document.getElementById('themeLightBtn').classList.toggle('active', next === 'light');
315
+ }
316
+
317
+ async function api(path) {
318
+ const res = await fetch(`${API_BASE}${path}`);
319
+ const json = await res.json().catch(() => null);
320
+ if (!res.ok || !json || !json.ok) throw new Error(json?.error?.message || `Request failed (${res.status})`);
321
+ return json;
322
+ }
323
+
324
+ function renderState(text) { state.innerHTML = `<div class="state">${text}</div>`; }
325
+ function clearState() { state.innerHTML = ''; }
326
+
327
+ async function search() {
328
+ try {
329
+ renderState('Searching directory...');
330
+ const q = document.getElementById('q').value.trim();
331
+ const profiles = (await api(`/api/search?q=${encodeURIComponent(q)}`)).data || [];
332
+ if (!profiles.length) {
333
+ cards.innerHTML = '';
334
+ renderState(q ? `No result for "${q}".` : 'No discovered public agent yet.');
335
+ return;
336
+ }
337
+ clearState();
338
+ cards.innerHTML = profiles.map((p) => `
339
+ <article class="card" data-id="${p.agent_id}">
340
+ <div style="display:flex; justify-content:space-between; gap:8px; align-items:center;">
341
+ <h3 style="margin:0;">${p.display_name || '(unnamed agent)'}</h3>
342
+ ${p.openclaw_bound ? '<span class="badge">OpenClaw</span>' : ''}
343
+ </div>
344
+ <div class="muted" style="margin-top:6px;">${p.bio || 'No bio yet.'}</div>
345
+ <div class="chips">${(p.tags || []).map((t) => `<span class="chip">${t}</span>`).join('') || '<span class="muted">No tags</span>'}</div>
346
+ <div class="chips">${(p.capabilities_summary || []).map((t) => `<span class="chip">${t}</span>`).join('') || '<span class="muted">No capabilities</span>'}</div>
347
+ <div class="chips">
348
+ <span class="badge ${p.verification_status === 'verified' ? 'ok' : p.verification_status === 'stale' ? 'warn' : 'err'}">${p.verification_status || 'unverified'}</span>
349
+ <span class="badge ${p.freshness_status === 'live' ? 'ok' : p.freshness_status === 'recently_seen' ? 'warn' : 'err'}">${p.freshness_status || 'stale'}</span>
350
+ </div>
351
+ <div class="meta">
352
+ <span class="mono">${shortId(p.agent_id)} · mode:${p.network_mode || 'unknown'}</span>
353
+ <span class="${p.online ? 'online' : 'offline'}">${p.online ? 'online' : 'offline'}</span>
354
+ </div>
355
+ </article>
356
+ `).join('');
357
+ cards.querySelectorAll('.card').forEach((el) => el.addEventListener('click', () => {
358
+ location.hash = `#agent/${el.dataset.id}`;
359
+ }));
360
+ } catch (e) {
361
+ cards.innerHTML = '';
362
+ renderState(`Search failed: ${e instanceof Error ? e.message : 'unknown error'}`);
363
+ }
364
+ }
365
+
366
+ async function showDetail(agentId) {
367
+ cards.classList.add('hidden');
368
+ state.classList.add('hidden');
369
+ detail.classList.remove('hidden');
370
+ try {
371
+ const d = (await api(`/api/agents/${agentId}`)).data;
372
+ const p = d.profile;
373
+ const s = d.summary || {};
374
+ detail.innerHTML = `
375
+ <button id="backBtn">Back</button>
376
+ <div class="detail-hero">
377
+ <div>
378
+ <h2 style="margin:0;">${p.display_name || '(unnamed agent)'}</h2>
379
+ <div class="muted" style="margin-top:6px;">${p.bio || 'No bio provided.'}</div>
380
+ </div>
381
+ <div>
382
+ ${s.openclaw_bound ? '<span class="badge">OpenClaw Agent</span>' : ''}
383
+ </div>
384
+ </div>
385
+ <h3>Identity</h3>
386
+ <div class="detail-grid">
387
+ <div class="detail-item"><b>Display Name:</b> ${p.display_name || '(unnamed agent)'}</div>
388
+ <div class="detail-item"><b>Agent ID:</b> <span class="mono">${p.agent_id}</span></div>
389
+ <div class="detail-item"><b>Public Key Fingerprint:</b> <span class="mono">${s.public_key_fingerprint || 'unavailable'}</span></div>
390
+ <div class="detail-item"><b>Profile Version:</b> ${s.profile_version || 'v1'}</div>
391
+ </div>
392
+ <h3>Verified Claims</h3>
393
+ <div class="muted mono">source: signed_claims</div>
394
+ <p class="chips">${(s.capabilities_summary || []).map((t) => `<span class="chip">${t}</span>`).join('') || '<span class="muted">No capabilities summary</span>'}</p>
395
+ <p class="chips">${(s.tags || p.tags || []).map((t) => `<span class="chip">${t}</span>`).join('') || '<span class="muted">No tags</span>'}</p>
396
+ <div class="detail-grid">
397
+ <div class="detail-item"><b>verification_status:</b> <span class="badge ${s.verification_status === 'verified' ? 'ok' : s.verification_status === 'stale' ? 'warn' : 'err'}">${s.verification_status || 'unverified'}</span></div>
398
+ <div class="detail-item"><b>verified_profile:</b> ${s.verified_profile ? 'yes' : 'no'}</div>
399
+ <div class="detail-item"><b>profile_updated_at:</b> ${s.profile_updated_at ? new Date(s.profile_updated_at).toLocaleString() : '-'}</div>
400
+ <div class="detail-item"><b>public_enabled:</b> ${s.signed_claims?.public_enabled ? 'true' : 'false'}</div>
401
+ </div>
402
+ <h3>Observed Presence</h3>
403
+ <div class="muted mono">source: observed_state</div>
404
+ <div class="detail-grid">
405
+ <div class="detail-item"><b>online:</b> <span class="${d.online ? 'online' : 'offline'}">${d.online ? 'online' : 'offline'}</span></div>
406
+ <div class="detail-item"><b>freshness:</b> <span class="badge ${s.freshness_status === 'live' ? 'ok' : s.freshness_status === 'recently_seen' ? 'warn' : 'err'}">${s.freshness_status || 'stale'}</span></div>
407
+ <div class="detail-item"><b>verified_presence_recent:</b> ${s.verified_presence_recent ? 'yes' : 'no'}</div>
408
+ <div class="detail-item"><b>presence_seen_at:</b> ${
409
+ s.visibility && s.visibility.show_last_seen === false
410
+ ? 'Hidden by visibility'
411
+ : (s.presence_seen_at ? new Date(s.presence_seen_at).toLocaleString() : '-')
412
+ }</div>
413
+ </div>
414
+ <h3>Integration</h3>
415
+ <div class="muted mono">source: integration_metadata</div>
416
+ <div class="detail-grid">
417
+ <div class="detail-item"><b>network_mode:</b> ${s.network_mode || 'unknown'}</div>
418
+ <div class="detail-item"><b>openclaw_bound:</b> ${s.openclaw_bound ? 'yes' : 'no'}</div>
419
+ </div>
420
+ <h3>Public Visibility</h3>
421
+ <div class="detail-grid">
422
+ <div class="detail-item"><b>visible:</b> ${(s.public_visibility?.visible_fields || []).join(', ') || '-'}</div>
423
+ <div class="detail-item"><b>hidden:</b> ${(s.public_visibility?.hidden_fields || []).join(', ') || '-'}</div>
424
+ </div>
425
+ <p><b>Agent ID:</b> <span class="mono">${p.agent_id}</span> <button class="secondary" id="copyAgentIdBtn">Copy</button></p>
426
+ <p><b>Public Key Fingerprint:</b> <span class="mono">${s.public_key_fingerprint || 'unavailable'}</span> <button class="secondary" id="copyFingerprintBtn">Copy</button></p>
427
+ <p><button class="secondary" id="copyPublicSummaryBtn">Copy public profile summary</button> <button class="secondary" id="copyIdentitySummaryBtn">Copy identity summary</button></p>
428
+ `;
429
+ document.getElementById('backBtn').addEventListener('click', () => { location.hash = ''; });
430
+ document.getElementById('copyAgentIdBtn').addEventListener('click', async (event) => copyText(p.agent_id, event.currentTarget, 'Agent ID copied'));
431
+ document.getElementById('copyFingerprintBtn').addEventListener('click', async (event) => copyText(s.public_key_fingerprint || 'unavailable', event.currentTarget, 'Fingerprint copied'));
432
+ document.getElementById('copyPublicSummaryBtn').addEventListener('click', async (event) => copyText(toPrettyJson(s), event.currentTarget, 'Public profile summary copied'));
433
+ document.getElementById('copyIdentitySummaryBtn').addEventListener('click', async () => {
434
+ const identitySummary = {
435
+ agent_id: p.agent_id,
436
+ display_name: p.display_name || "",
437
+ public_key_fingerprint: s.public_key_fingerprint || null,
438
+ profile_version: s.profile_version || "v1",
439
+ };
440
+ await copyText(toPrettyJson(identitySummary), document.getElementById('copyIdentitySummaryBtn'), 'Identity summary copied');
441
+ });
442
+ } catch (e) {
443
+ detail.innerHTML = `<div class="state">Load failed: ${e instanceof Error ? e.message : 'unknown error'}</div>`;
444
+ }
445
+ }
446
+
447
+ function route() {
448
+ if (location.hash.startsWith('#agent/')) {
449
+ showDetail(location.hash.slice(7));
450
+ } else {
451
+ detail.classList.add('hidden');
452
+ cards.classList.remove('hidden');
453
+ state.classList.remove('hidden');
454
+ search();
455
+ }
456
+ }
457
+
458
+ document.getElementById('searchBtn').addEventListener('click', search);
459
+ document.getElementById('q').addEventListener('keydown', (e) => { if (e.key === 'Enter') search(); });
460
+ document.getElementById('themeDarkBtn').addEventListener('click', () => applyTheme('dark'));
461
+ document.getElementById('themeLightBtn').addEventListener('click', () => applyTheme('light'));
462
+ window.addEventListener('hashchange', route);
463
+
464
+ (() => {
465
+ const logo = document.getElementById('brandLogo');
466
+ const fallback = document.getElementById('brandFallback');
467
+ if (!logo || !fallback) return;
468
+ logo.addEventListener('error', () => {
469
+ logo.style.display = 'none';
470
+ fallback.classList.remove('hidden');
471
+ });
472
+ logo.addEventListener('load', () => {
473
+ logo.style.display = 'block';
474
+ fallback.classList.add('hidden');
475
+ });
476
+ })();
477
+
478
+ applyTheme(localStorage.getItem('silicaclaw_theme_mode') || 'dark');
479
+ route();
480
+ setInterval(() => { if (!location.hash) search(); }, 5000);
481
+ </script>
482
+ </body>
483
+ </html>
@@ -0,0 +1,32 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import { resolve } from "path";
4
+ import { existsSync } from "fs";
5
+
6
+ const app = express();
7
+ const port = Number(process.env.PORT || 4311);
8
+
9
+ function resolvePublicExplorerStaticDir(): string {
10
+ const candidates = [
11
+ resolve(process.cwd(), "public"),
12
+ resolve(process.cwd(), "apps", "public-explorer", "public"),
13
+ resolve(__dirname, "..", "public"),
14
+ resolve(__dirname, "..", "..", "apps", "public-explorer", "public"),
15
+ ];
16
+
17
+ for (const dir of candidates) {
18
+ if (existsSync(resolve(dir, "index.html"))) {
19
+ return dir;
20
+ }
21
+ }
22
+
23
+ return candidates[0];
24
+ }
25
+
26
+ app.use(cors({ origin: true }));
27
+ app.use(express.static(resolvePublicExplorerStaticDir()));
28
+
29
+ app.listen(port, () => {
30
+ // eslint-disable-next-line no-console
31
+ console.log(`SilicaClaw public-explorer running: http://localhost:${port}`);
32
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist"
5
+ },
6
+ "include": ["src"]
7
+ }