@sym-bot/sym 0.1.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/lib/node.js ADDED
@@ -0,0 +1,507 @@
1
+ 'use strict';
2
+
3
+ const net = require('net');
4
+ const crypto = require('crypto');
5
+ const path = require('path');
6
+ const { EventEmitter } = require('events');
7
+ const { MeshNode } = require('mesh-cognition');
8
+ const { nodeDir, loadOrCreateIdentity, ensureDir, log: logMsg } = require('./config');
9
+ const { MemoryStore } = require('./memory-store');
10
+ const { FrameParser, sendFrame } = require('./frame-parser');
11
+ const { encode, DIM } = require('./context-encoder');
12
+
13
+ /**
14
+ * SymNode — a sovereign mesh node with cognitive coupling.
15
+ *
16
+ * Each node encodes its memories into a hidden state vector.
17
+ * When peers connect, the coupling engine evaluates drift between
18
+ * their cognitive states and autonomously decides whether to couple.
19
+ *
20
+ * Aligned peers share memories. Divergent peers stay independent.
21
+ * The intelligence is in the decision to share, not in the sharing itself.
22
+ */
23
+ class SymNode extends EventEmitter {
24
+
25
+ constructor(opts = {}) {
26
+ super();
27
+ if (!opts.name) throw new Error('SymNode requires a name');
28
+ this._silent = opts.silent || false;
29
+
30
+ this.name = opts.name;
31
+ this._cognitiveProfile = opts.cognitiveProfile || null;
32
+ this._moodThreshold = opts.moodThreshold ?? 0.8;
33
+ this._identity = loadOrCreateIdentity(this.name);
34
+ this._dir = nodeDir(this.name);
35
+ this._memoriesDir = path.join(this._dir, 'memories');
36
+ this._store = new MemoryStore(this._memoriesDir, this.name);
37
+
38
+ // Coupling engine — evaluates peer cognitive state
39
+ this._meshNode = new MeshNode({ hiddenDim: DIM });
40
+ this._initLocalState();
41
+
42
+ // Peer state
43
+ this._peers = new Map();
44
+ this._server = null;
45
+ this._bonjour = null;
46
+ this._browser = null;
47
+ this._port = 0;
48
+ this._running = false;
49
+
50
+ // Timers
51
+ this._heartbeatInterval = opts.heartbeatInterval || 5000;
52
+ this._heartbeatTimeout = opts.heartbeatTimeout || 15000;
53
+ this._heartbeatTimer = null;
54
+ this._encodeInterval = opts.encodeInterval || 30000;
55
+ this._encodeTimer = null;
56
+ }
57
+
58
+ // ── Context Encoding ───────────────────────────────────────
59
+
60
+ _initLocalState() {
61
+ const context = this._buildContext();
62
+ if (context.length > 5) {
63
+ const { h1, h2 } = encode(context);
64
+ this._meshNode.updateLocalState(h1, h2, 0.8);
65
+ } else {
66
+ const h1 = Array.from({ length: DIM }, () => (Math.random() - 0.5) * 0.1);
67
+ const h2 = Array.from({ length: DIM }, () => (Math.random() - 0.5) * 0.1);
68
+ this._meshNode.updateLocalState(h1, h2, 0.3);
69
+ }
70
+ }
71
+
72
+ _buildContext() {
73
+ const parts = [];
74
+ if (this._cognitiveProfile) parts.push(this._cognitiveProfile);
75
+ const entries = this._store.allEntries().slice(0, 20);
76
+ parts.push(...entries.map(e => e.content || ''));
77
+ return parts.join('\n');
78
+ }
79
+
80
+ _reencodeAndBroadcast() {
81
+ const context = this._buildContext();
82
+ if (context.length < 5) return;
83
+
84
+ const { h1, h2 } = encode(context);
85
+ this._meshNode.updateLocalState(h1, h2, 0.8);
86
+
87
+ // Broadcast cognitive state to all peers for re-evaluation
88
+ this._broadcastToPeers({ type: 'state-sync', h1, h2, confidence: 0.8 });
89
+ }
90
+
91
+ /**
92
+ * Update cognitive state from external context (e.g. Claude Code's memories).
93
+ * Does not store anything — just re-encodes and broadcasts.
94
+ */
95
+ updateContext(text) {
96
+ if (!text || text.length < 5) return;
97
+ const { h1, h2 } = encode(text);
98
+ this._meshNode.updateLocalState(h1, h2, 0.8);
99
+ this._broadcastToPeers({ type: 'state-sync', h1, h2, confidence: 0.8 });
100
+ }
101
+
102
+ /**
103
+ * Share content with cognitively aligned peers without storing locally.
104
+ * Used by ClaudeMemoryBridge — Claude Code's memory dir is the source of truth.
105
+ */
106
+ shareWithPeers(content, opts = {}) {
107
+ const entry = {
108
+ key: opts.key || `memory-${Date.now()}`,
109
+ content,
110
+ source: opts.source || this.name,
111
+ tags: opts.tags || [],
112
+ timestamp: Date.now(),
113
+ };
114
+
115
+ this._meshNode.coupledState();
116
+ const decisions = this._meshNode.couplingDecisions;
117
+
118
+ let shared = 0;
119
+ for (const [peerId, peer] of this._peers) {
120
+ const d = decisions.get(peerId);
121
+ if (d && d.decision === 'rejected') {
122
+ this._log(`Not sharing with ${peer.name} — rejected (drift: ${d.drift.toFixed(3)})`);
123
+ continue;
124
+ }
125
+ sendFrame(peer.socket, { type: 'memory-share', ...entry });
126
+ shared++;
127
+ if (d) {
128
+ this._log(`Shared with ${peer.name} — ${d.decision} (drift: ${d.drift.toFixed(3)})`);
129
+ }
130
+ }
131
+
132
+ this._log(`Shared: "${content.slice(0, 50)}${content.length > 50 ? '...' : ''}" → ${shared}/${this._peers.size} peers`);
133
+ return entry;
134
+ }
135
+
136
+ // ── Lifecycle ──────────────────────────────────────────────
137
+
138
+ async start() {
139
+ if (this._running) return;
140
+ this._running = true;
141
+
142
+ await this._startServer();
143
+ this._startDiscovery();
144
+
145
+ this._heartbeatTimer = setInterval(() => this._checkHeartbeats(), this._heartbeatInterval);
146
+ this._encodeTimer = setInterval(() => this._reencodeAndBroadcast(), this._encodeInterval);
147
+
148
+ this._log(`Started (port: ${this._port}, id: ${this._identity.nodeId.slice(0, 8)})`);
149
+ }
150
+
151
+ async stop() {
152
+ if (!this._running) return;
153
+ this._running = false;
154
+
155
+ if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
156
+ if (this._encodeTimer) clearInterval(this._encodeTimer);
157
+
158
+ for (const [, peer] of this._peers) {
159
+ try { peer.socket.destroy(); } catch {}
160
+ }
161
+ this._peers.clear();
162
+
163
+ if (this._bonjour) {
164
+ try { this._bonjour.destroy(); } catch {}
165
+ this._bonjour = null;
166
+ }
167
+
168
+ if (this._server) {
169
+ this._server.close();
170
+ this._server = null;
171
+ }
172
+
173
+ this._log('Stopped');
174
+ }
175
+
176
+ // ── Memory (with cognitive coupling) ───────────────────────
177
+
178
+ remember(content, opts = {}) {
179
+ const entry = this._store.write(content, opts);
180
+
181
+ // Re-encode context with new memory
182
+ const context = this._buildContext();
183
+ const { h1, h2 } = encode(context);
184
+ this._meshNode.updateLocalState(h1, h2, 0.8);
185
+
186
+ // Evaluate coupling with each peer — only share with aligned/guarded
187
+ // Trigger coupling evaluation
188
+ this._meshNode.coupledState();
189
+ const decisions = this._meshNode.couplingDecisions;
190
+
191
+ let shared = 0;
192
+ for (const [peerId, peer] of this._peers) {
193
+ const d = decisions.get(peerId);
194
+
195
+ if (d && d.decision === 'rejected') {
196
+ this._log(`Not sharing with ${peer.name} — rejected (drift: ${d.drift.toFixed(3)})`);
197
+ continue;
198
+ }
199
+
200
+ sendFrame(peer.socket, { type: 'memory-share', ...entry });
201
+ shared++;
202
+
203
+ if (d) {
204
+ this._log(`Shared with ${peer.name} — ${d.decision} (drift: ${d.drift.toFixed(3)})`);
205
+ }
206
+ }
207
+
208
+ this._log(`Remembered: "${content.slice(0, 50)}${content.length > 50 ? '...' : ''}" → ${shared}/${this._peers.size} peers`);
209
+ return entry;
210
+ }
211
+
212
+ recall(query) {
213
+ return this._store.search(query);
214
+ }
215
+
216
+ // ── Mood (with cognitive evaluation) ───────────────────────
217
+
218
+ /**
219
+ * Broadcast mood to the mesh. Receiving agents evaluate this
220
+ * against their own cognitive state and autonomously decide
221
+ * whether to act. No routing. No registration. The coupling
222
+ * engine decides.
223
+ */
224
+ broadcastMood(mood, opts = {}) {
225
+ const frame = {
226
+ type: 'mood',
227
+ from: this._identity.nodeId,
228
+ fromName: this.name,
229
+ mood,
230
+ context: opts.context || null,
231
+ timestamp: Date.now(),
232
+ };
233
+ this._broadcastToPeers(frame);
234
+ this._log(`Mood broadcast: "${mood.slice(0, 50)}"`);
235
+ }
236
+
237
+ // ── Communication ──────────────────────────────────────────
238
+
239
+ send(message, opts = {}) {
240
+ const frame = {
241
+ type: 'message',
242
+ from: this._identity.nodeId,
243
+ fromName: this.name,
244
+ content: message,
245
+ timestamp: Date.now(),
246
+ };
247
+ if (opts.to) {
248
+ const peer = this._peers.get(opts.to);
249
+ if (peer) sendFrame(peer.socket, frame);
250
+ } else {
251
+ this._broadcastToPeers(frame);
252
+ }
253
+ }
254
+
255
+ // ── Monitoring ─────────────────────────────────────────────
256
+
257
+ peers() {
258
+ const result = [];
259
+ const decisions = this._meshNode.couplingDecisions;
260
+ for (const [id, peer] of this._peers) {
261
+ const d = decisions.get(id);
262
+ result.push({
263
+ id: id.slice(0, 8),
264
+ name: peer.name || 'unknown',
265
+ connected: true,
266
+ lastSeen: peer.lastSeen,
267
+ coupling: d ? d.decision : 'pending',
268
+ drift: d ? parseFloat(d.drift.toFixed(3)) : null,
269
+ });
270
+ }
271
+ return result;
272
+ }
273
+
274
+ memories() {
275
+ return this._store.count();
276
+ }
277
+
278
+ coherence() {
279
+ return this._meshNode.coherence;
280
+ }
281
+
282
+ status() {
283
+ return {
284
+ name: this.name,
285
+ nodeId: this._identity.nodeId.slice(0, 8),
286
+ running: this._running,
287
+ port: this._port,
288
+ peers: this.peers(),
289
+ peerCount: this._peers.size,
290
+ memoryCount: this.memories(),
291
+ coherence: this.coherence(),
292
+ };
293
+ }
294
+
295
+ // ── TCP Server ─────────────────────────────────────────────
296
+
297
+ _startServer() {
298
+ return new Promise((resolve, reject) => {
299
+ this._server = net.createServer((socket) => {
300
+ this._handleInboundConnection(socket);
301
+ });
302
+ this._server.on('error', (err) => {
303
+ this._log(`Server error: ${err.message}`);
304
+ reject(err);
305
+ });
306
+ this._server.listen(0, '0.0.0.0', () => {
307
+ this._port = this._server.address().port;
308
+ resolve();
309
+ });
310
+ });
311
+ }
312
+
313
+ _handleInboundConnection(socket) {
314
+ const parser = new FrameParser();
315
+ let identified = false;
316
+ const timeout = setTimeout(() => { if (!identified) socket.destroy(); }, 10000);
317
+
318
+ socket.on('data', (chunk) => { if (!identified) parser.feed(chunk); });
319
+
320
+ parser.on('message', (msg) => {
321
+ if (identified) return;
322
+ if (msg.type !== 'handshake') { socket.destroy(); return; }
323
+ identified = true;
324
+ clearTimeout(timeout);
325
+ if (this._peers.has(msg.nodeId)) { socket.destroy(); return; }
326
+ const peer = this._createPeer(socket, msg.nodeId, msg.name, false);
327
+ if (parser.buffer.length > 0) peer.parser.feed(parser.buffer);
328
+ this._addPeer(peer);
329
+ });
330
+
331
+ socket.on('error', () => clearTimeout(timeout));
332
+ }
333
+
334
+ // ── Bonjour Discovery ──────────────────────────────────────
335
+
336
+ _startDiscovery() {
337
+ const { Bonjour } = require('bonjour-service');
338
+ this._bonjour = new Bonjour();
339
+
340
+ this._bonjour.publish({
341
+ name: this._identity.nodeId,
342
+ type: 'sym',
343
+ port: this._port,
344
+ txt: { 'node-id': this._identity.nodeId, 'node-name': this.name, 'hostname': this._identity.hostname },
345
+ });
346
+
347
+ this._browser = this._bonjour.find({ type: 'sym' });
348
+
349
+ this._browser.on('up', (service) => {
350
+ const peerId = service.txt?.['node-id'];
351
+ if (!peerId || peerId === this._identity.nodeId) return;
352
+ const peerName = service.txt?.['node-name'] || 'unknown';
353
+ const address = service.referer?.address || service.addresses?.[0];
354
+ const port = service.port;
355
+ if (!address || !port) return;
356
+ if (this._identity.nodeId < peerId && !this._peers.has(peerId)) {
357
+ this._connectToPeer(address, port, peerId, peerName);
358
+ }
359
+ });
360
+ }
361
+
362
+ _connectToPeer(address, port, peerId, peerName) {
363
+ if (this._peers.has(peerId)) return;
364
+ const socket = net.createConnection({ host: address, port }, () => {
365
+ const peer = this._createPeer(socket, peerId, peerName, true);
366
+ this._addPeer(peer);
367
+ });
368
+ socket.on('error', (err) => this._log(`Connect failed to ${peerName}: ${err.message}`));
369
+ socket.setTimeout(10000, () => socket.destroy());
370
+ }
371
+
372
+ // ── Peer Management ────────────────────────────────────────
373
+
374
+ _createPeer(socket, peerId, peerName, isOutbound) {
375
+ const parser = new FrameParser();
376
+ socket.on('data', (chunk) => parser.feed(chunk));
377
+ parser.on('message', (msg) => {
378
+ const peer = this._peers.get(peerId);
379
+ if (peer) peer.lastSeen = Date.now();
380
+ this._handlePeerMessage(peerId, peerName, msg);
381
+ });
382
+ parser.on('error', (err) => this._log(`Frame error from ${peerName}: ${err.message}`));
383
+ socket.on('close', () => {
384
+ this._peers.delete(peerId);
385
+ this._meshNode.removePeer(peerId);
386
+ this._log(`Peer disconnected: ${peerName}`);
387
+ this.emit('peer-left', { id: peerId, name: peerName });
388
+ });
389
+ socket.on('error', () => {});
390
+ return { socket, parser, peerId, name: peerName, isOutbound, lastSeen: Date.now() };
391
+ }
392
+
393
+ _addPeer(peer) {
394
+ this._peers.set(peer.peerId, peer);
395
+
396
+ // Handshake
397
+ sendFrame(peer.socket, { type: 'handshake', nodeId: this._identity.nodeId, name: this.name });
398
+
399
+ // Send cognitive state for coupling evaluation
400
+ const [h1, h2] = this._meshNode.coupledState();
401
+ sendFrame(peer.socket, { type: 'state-sync', h1, h2, confidence: 0.8 });
402
+
403
+ this._log(`Peer connected: ${peer.name} (${peer.isOutbound ? 'outbound' : 'inbound'})`);
404
+ this.emit('peer-joined', { id: peer.peerId, name: peer.name });
405
+ }
406
+
407
+ _handlePeerMessage(peerId, peerName, msg) {
408
+ switch (msg.type) {
409
+ case 'handshake':
410
+ break;
411
+
412
+ case 'state-sync':
413
+ // Peer sent their cognitive state — evaluate coupling
414
+ if (msg.h1?.length === DIM && msg.h2?.length === DIM) {
415
+ this._meshNode.addPeer(peerId, msg.h1, msg.h2, msg.confidence || 0.5);
416
+ this._meshNode.coupledState(); // trigger evaluation
417
+ const d = this._meshNode.couplingDecisions.get(peerId);
418
+ if (d) {
419
+ this._log(`Coupling with ${peerName}: ${d.decision} (drift: ${d.drift.toFixed(3)})`);
420
+ this.emit('coupling-decision', { peer: peerName, decision: d.decision, drift: d.drift });
421
+ }
422
+ }
423
+ break;
424
+
425
+ case 'memory-share':
426
+ if (msg.content) {
427
+ // Check coupling before accepting
428
+ const d = this._meshNode.couplingDecisions.get(peerId);
429
+ if (d && d.decision === 'rejected') {
430
+ this._log(`Rejected memory from ${peerName} (drift: ${d.drift.toFixed(3)})`);
431
+ break;
432
+ }
433
+ this._store.receiveFromPeer(peerId, msg);
434
+ this._log(`Memory from ${peerName}: "${(msg.content || '').slice(0, 40)}..." [${d?.decision || 'accepted'}]`);
435
+ this.emit('memory-received', { from: peerName, entry: msg, decision: d?.decision });
436
+ }
437
+ break;
438
+
439
+ case 'mood':
440
+ // Peer broadcast their mood. Use the SDK's coupling engine to evaluate
441
+ // whether this mood is relevant to our cognitive state.
442
+ if (msg.mood) {
443
+ const { h1: moodH1, h2: moodH2 } = encode(msg.mood);
444
+ const moodPeerId = `mood-${peerId}`;
445
+
446
+ // Evaluate mood using the SDK's coupling engine
447
+ this._meshNode.addPeer(moodPeerId, moodH1, moodH2, 0.8);
448
+ this._meshNode.coupledState();
449
+ const d = this._meshNode.couplingDecisions.get(moodPeerId);
450
+ this._meshNode.removePeer(moodPeerId);
451
+
452
+ const from = msg.fromName || peerName;
453
+ const drift = d ? d.drift : 1;
454
+
455
+ // Mood uses the agent's moodThreshold (default 0.8) — more permissive
456
+ // than memory sharing (0.5). User wellbeing crosses domain boundaries.
457
+ if (drift <= this._moodThreshold) {
458
+ this._log(`Mood from ${from}: "${msg.mood.slice(0, 50)}" → ACCEPTED (drift: ${drift.toFixed(3)}, threshold: ${this._moodThreshold})`);
459
+ this.emit('mood-accepted', { from, mood: msg.mood, drift, context: msg.context });
460
+ } else {
461
+ this._log(`Mood from ${from}: "${msg.mood.slice(0, 50)}" → IGNORED (drift: ${drift.toFixed(3)}, threshold: ${this._moodThreshold})`);
462
+ this.emit('mood-rejected', { from, mood: msg.mood, drift });
463
+ }
464
+ }
465
+ break;
466
+
467
+ case 'message':
468
+ this._log(`Message from ${msg.fromName || peerName}: ${(msg.content || '').slice(0, 60)}`);
469
+ this.emit('message', msg.fromName || peerName, msg.content, msg);
470
+ break;
471
+
472
+ case 'ping':
473
+ const peer = this._peers.get(peerId);
474
+ if (peer) sendFrame(peer.socket, { type: 'pong' });
475
+ break;
476
+
477
+ case 'pong':
478
+ break;
479
+ }
480
+ }
481
+
482
+ _broadcastToPeers(frame) {
483
+ for (const [, peer] of this._peers) {
484
+ sendFrame(peer.socket, frame);
485
+ }
486
+ }
487
+
488
+ _checkHeartbeats() {
489
+ const now = Date.now();
490
+ for (const [id, peer] of this._peers) {
491
+ if (now - peer.lastSeen > this._heartbeatTimeout) {
492
+ this._log(`Heartbeat timeout: ${peer.name}`);
493
+ peer.socket.destroy();
494
+ this._peers.delete(id);
495
+ this._meshNode.removePeer(id);
496
+ } else if (now - peer.lastSeen > this._heartbeatInterval) {
497
+ sendFrame(peer.socket, { type: 'ping' });
498
+ }
499
+ }
500
+ }
501
+
502
+ _log(msg) {
503
+ if (!this._silent) logMsg(this.name, msg);
504
+ }
505
+ }
506
+
507
+ module.exports = { SymNode };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@sym-bot/sym",
3
+ "version": "0.1.0",
4
+ "description": "Local AI mesh — every agent is a sovereign node, the mesh is the agents",
5
+ "main": "lib/node.js",
6
+ "bin": {
7
+ "sym-mcp": "./integrations/claude-code/mcp-server.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test tests/*.test.js"
11
+ },
12
+ "keywords": [
13
+ "mesh",
14
+ "p2p",
15
+ "collective-intelligence",
16
+ "ai-agents",
17
+ "mesh-cognition",
18
+ "mmp",
19
+ "peer-to-peer",
20
+ "local-ai",
21
+ "privacy"
22
+ ],
23
+ "author": "SYM.BOT Ltd <info@sym.bot> (https://sym.bot)",
24
+ "license": "Apache-2.0",
25
+ "homepage": "https://github.com/sym-bot/sym",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/sym-bot/sym.git"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.27.1",
35
+ "bonjour-service": "^1.3.0",
36
+ "mesh-cognition": "^1.1.0"
37
+ }
38
+ }