@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,249 @@
|
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
import { randomUUID, createHash } from 'crypto';
|
|
4
|
+
|
|
5
|
+
const port = Number(process.env.PORT || process.env.WEBRTC_SIGNALING_PORT || 4510);
|
|
6
|
+
const PEER_STALE_MS = Number(process.env.WEBRTC_SIGNALING_PEER_STALE_MS || 120000);
|
|
7
|
+
const SIGNAL_DEDUPE_WINDOW_MS = Number(process.env.WEBRTC_SIGNALING_DEDUPE_WINDOW_MS || 60000);
|
|
8
|
+
|
|
9
|
+
/** @type {Map<string, {peers: Map<string, {last_seen_at:number}>, queues: Map<string, any[]>, signal_fingerprints: Map<string, number>}>} */
|
|
10
|
+
const rooms = new Map();
|
|
11
|
+
|
|
12
|
+
const counters = {
|
|
13
|
+
join_total: 0,
|
|
14
|
+
leave_total: 0,
|
|
15
|
+
signal_total: 0,
|
|
16
|
+
invalid_payload_total: 0,
|
|
17
|
+
duplicate_signal_total: 0,
|
|
18
|
+
stale_peers_cleaned_total: 0,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function getRoom(roomId) {
|
|
22
|
+
const id = String(roomId || '').trim() || 'silicaclaw-room';
|
|
23
|
+
if (!rooms.has(id)) {
|
|
24
|
+
rooms.set(id, {
|
|
25
|
+
peers: new Map(),
|
|
26
|
+
queues: new Map(),
|
|
27
|
+
signal_fingerprints: new Map(),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return rooms.get(id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function now() {
|
|
34
|
+
return Date.now();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function json(res, status, payload) {
|
|
38
|
+
const body = JSON.stringify(payload);
|
|
39
|
+
res.writeHead(status, {
|
|
40
|
+
'content-type': 'application/json; charset=utf-8',
|
|
41
|
+
'access-control-allow-origin': '*',
|
|
42
|
+
'access-control-allow-methods': 'GET,POST,OPTIONS',
|
|
43
|
+
'access-control-allow-headers': 'content-type',
|
|
44
|
+
});
|
|
45
|
+
res.end(body);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseBody(req) {
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
let raw = '';
|
|
51
|
+
req.on('data', (chunk) => {
|
|
52
|
+
raw += chunk;
|
|
53
|
+
if (raw.length > 2 * 1024 * 1024) {
|
|
54
|
+
raw = '';
|
|
55
|
+
req.destroy();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
req.on('end', () => {
|
|
59
|
+
try {
|
|
60
|
+
resolve(raw ? JSON.parse(raw) : {});
|
|
61
|
+
} catch {
|
|
62
|
+
resolve({});
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cleanupRoom(roomId) {
|
|
69
|
+
const room = rooms.get(roomId);
|
|
70
|
+
if (!room) return;
|
|
71
|
+
const threshold = now() - PEER_STALE_MS;
|
|
72
|
+
for (const [peerId, peer] of room.peers.entries()) {
|
|
73
|
+
if (peer.last_seen_at < threshold) {
|
|
74
|
+
room.peers.delete(peerId);
|
|
75
|
+
room.queues.delete(peerId);
|
|
76
|
+
counters.stale_peers_cleaned_total += 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const dedupeThreshold = now() - SIGNAL_DEDUPE_WINDOW_MS;
|
|
80
|
+
for (const [key, ts] of room.signal_fingerprints.entries()) {
|
|
81
|
+
if (ts < dedupeThreshold) {
|
|
82
|
+
room.signal_fingerprints.delete(key);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (room.peers.size === 0) {
|
|
86
|
+
rooms.delete(roomId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function touchPeer(room, peerId) {
|
|
91
|
+
if (!room.peers.has(peerId)) {
|
|
92
|
+
room.peers.set(peerId, { last_seen_at: now() });
|
|
93
|
+
} else {
|
|
94
|
+
room.peers.get(peerId).last_seen_at = now();
|
|
95
|
+
}
|
|
96
|
+
if (!room.queues.has(peerId)) {
|
|
97
|
+
room.queues.set(peerId, []);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isValidSignalPayload(body) {
|
|
102
|
+
const from = String(body?.from_peer_id || '');
|
|
103
|
+
const to = String(body?.to_peer_id || '');
|
|
104
|
+
const type = String(body?.type || '');
|
|
105
|
+
const payload = body?.payload;
|
|
106
|
+
if (!from || !to || !type) return false;
|
|
107
|
+
if (type !== 'offer' && type !== 'answer' && type !== 'candidate') return false;
|
|
108
|
+
if (payload === undefined || payload === null) return false;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function signalFingerprint(roomId, body) {
|
|
113
|
+
const digest = createHash('sha256')
|
|
114
|
+
.update(JSON.stringify({
|
|
115
|
+
room: roomId,
|
|
116
|
+
from: body.from_peer_id,
|
|
117
|
+
to: body.to_peer_id,
|
|
118
|
+
type: body.type,
|
|
119
|
+
payload: body.payload,
|
|
120
|
+
}))
|
|
121
|
+
.digest('hex');
|
|
122
|
+
return `${roomId}:${digest}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
setInterval(() => {
|
|
126
|
+
for (const roomId of Array.from(rooms.keys())) {
|
|
127
|
+
cleanupRoom(roomId);
|
|
128
|
+
}
|
|
129
|
+
}, Math.max(5000, Math.floor(PEER_STALE_MS / 4))).unref();
|
|
130
|
+
|
|
131
|
+
const server = http.createServer(async (req, res) => {
|
|
132
|
+
if (!req.url) return json(res, 400, { ok: false, error: 'missing_url' });
|
|
133
|
+
if (req.method === 'OPTIONS') return json(res, 200, { ok: true });
|
|
134
|
+
|
|
135
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
136
|
+
|
|
137
|
+
if (req.method === 'GET' && url.pathname === '/health') {
|
|
138
|
+
return json(res, 200, {
|
|
139
|
+
ok: true,
|
|
140
|
+
rooms: rooms.size,
|
|
141
|
+
counters,
|
|
142
|
+
peer_stale_ms: PEER_STALE_MS,
|
|
143
|
+
signal_dedupe_window_ms: SIGNAL_DEDUPE_WINDOW_MS,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (req.method === 'GET' && url.pathname === '/peers') {
|
|
148
|
+
const roomId = String(url.searchParams.get('room') || 'silicaclaw-room');
|
|
149
|
+
const room = getRoom(roomId);
|
|
150
|
+
cleanupRoom(roomId);
|
|
151
|
+
return json(res, 200, { ok: true, peers: Array.from(room.peers.keys()) });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (req.method === 'GET' && url.pathname === '/poll') {
|
|
155
|
+
const roomId = String(url.searchParams.get('room') || 'silicaclaw-room');
|
|
156
|
+
const peerId = String(url.searchParams.get('peer_id') || '');
|
|
157
|
+
if (!peerId) {
|
|
158
|
+
counters.invalid_payload_total += 1;
|
|
159
|
+
return json(res, 400, { ok: false, error: 'missing_peer_id' });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const room = getRoom(roomId);
|
|
163
|
+
touchPeer(room, peerId);
|
|
164
|
+
cleanupRoom(roomId);
|
|
165
|
+
|
|
166
|
+
const queue = room.queues.get(peerId) || [];
|
|
167
|
+
room.queues.set(peerId, []);
|
|
168
|
+
return json(res, 200, { ok: true, messages: queue });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (req.method === 'POST' && url.pathname === '/join') {
|
|
172
|
+
const body = await parseBody(req);
|
|
173
|
+
const roomId = String(body.room || 'silicaclaw-room');
|
|
174
|
+
const peerId = String(body.peer_id || '');
|
|
175
|
+
if (!peerId) {
|
|
176
|
+
counters.invalid_payload_total += 1;
|
|
177
|
+
return json(res, 400, { ok: false, error: 'missing_peer_id' });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const room = getRoom(roomId);
|
|
181
|
+
touchPeer(room, peerId);
|
|
182
|
+
counters.join_total += 1;
|
|
183
|
+
|
|
184
|
+
return json(res, 200, { ok: true, peers: Array.from(room.peers.keys()) });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (req.method === 'POST' && url.pathname === '/leave') {
|
|
188
|
+
const body = await parseBody(req);
|
|
189
|
+
const roomId = String(body.room || 'silicaclaw-room');
|
|
190
|
+
const peerId = String(body.peer_id || '');
|
|
191
|
+
const room = getRoom(roomId);
|
|
192
|
+
|
|
193
|
+
if (peerId) {
|
|
194
|
+
room.peers.delete(peerId);
|
|
195
|
+
room.queues.delete(peerId);
|
|
196
|
+
counters.leave_total += 1;
|
|
197
|
+
} else {
|
|
198
|
+
counters.invalid_payload_total += 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
cleanupRoom(roomId);
|
|
202
|
+
return json(res, 200, { ok: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (req.method === 'POST' && url.pathname === '/signal') {
|
|
206
|
+
const body = await parseBody(req);
|
|
207
|
+
const roomId = String(body.room || 'silicaclaw-room');
|
|
208
|
+
if (!isValidSignalPayload(body)) {
|
|
209
|
+
counters.invalid_payload_total += 1;
|
|
210
|
+
return json(res, 400, { ok: false, error: 'invalid_signal_payload' });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const room = getRoom(roomId);
|
|
214
|
+
const fromPeerId = String(body.from_peer_id);
|
|
215
|
+
const toPeerId = String(body.to_peer_id);
|
|
216
|
+
|
|
217
|
+
touchPeer(room, fromPeerId);
|
|
218
|
+
touchPeer(room, toPeerId);
|
|
219
|
+
|
|
220
|
+
const fp = signalFingerprint(roomId, body);
|
|
221
|
+
const existingTs = room.signal_fingerprints.get(fp);
|
|
222
|
+
if (existingTs && now() - existingTs <= SIGNAL_DEDUPE_WINDOW_MS) {
|
|
223
|
+
counters.duplicate_signal_total += 1;
|
|
224
|
+
return json(res, 200, { ok: true, duplicate: true });
|
|
225
|
+
}
|
|
226
|
+
room.signal_fingerprints.set(fp, now());
|
|
227
|
+
|
|
228
|
+
if (!room.queues.has(toPeerId)) room.queues.set(toPeerId, []);
|
|
229
|
+
room.queues.get(toPeerId).push({
|
|
230
|
+
id: String(body.id || randomUUID()),
|
|
231
|
+
room: roomId,
|
|
232
|
+
from_peer_id: fromPeerId,
|
|
233
|
+
to_peer_id: toPeerId,
|
|
234
|
+
type: String(body.type),
|
|
235
|
+
payload: body.payload,
|
|
236
|
+
at: now(),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
counters.signal_total += 1;
|
|
240
|
+
return json(res, 200, { ok: true });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return json(res, 404, { ok: false, error: 'not_found' });
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
server.listen(port, () => {
|
|
247
|
+
// eslint-disable-next-line no-console
|
|
248
|
+
console.log(`WebRTC signaling preview server running on http://localhost:${port}`);
|
|
249
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
enabled: true
|
|
3
|
+
public_enabled: false
|
|
4
|
+
|
|
5
|
+
identity:
|
|
6
|
+
display_name: "My OpenClaw"
|
|
7
|
+
bio: "Local OpenClaw agent connected to SilicaClaw"
|
|
8
|
+
tags:
|
|
9
|
+
- openclaw
|
|
10
|
+
- research
|
|
11
|
+
|
|
12
|
+
network:
|
|
13
|
+
mode: "lan"
|
|
14
|
+
|
|
15
|
+
openclaw:
|
|
16
|
+
bind_existing_identity: true
|
|
17
|
+
use_openclaw_profile_if_available: true
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Social
|
|
21
|
+
|
|
22
|
+
This file connects an existing OpenClaw instance to SilicaClaw.
|
|
23
|
+
|
|
24
|
+
- `enabled`: whether SilicaClaw integration is active
|
|
25
|
+
- `public_enabled`: whether this agent appears in public directory
|
|
26
|
+
- `identity`: public profile fields
|
|
27
|
+
- `network`: SilicaClaw network settings
|
|
28
|
+
- `discovery`: broadcast/discovery settings
|
|
29
|
+
- `visibility`: controls public field visibility
|
|
30
|
+
- `openclaw`: reuse OpenClaw identity/profile when available
|