@sym-bot/sym 0.2.0 → 0.2.2

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.
@@ -0,0 +1,227 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * IPC client for connecting to sym-daemon as a virtual node.
5
+ *
6
+ * Communicates via Unix socket with newline-delimited JSON.
7
+ * Provides the same API surface as SymNode so consumers
8
+ * (MCP server, etc.) can use either transparently.
9
+ *
10
+ * Copyright (c) 2026 SYM.BOT Ltd. Apache 2.0 License.
11
+ */
12
+
13
+ const net = require('net');
14
+ const { EventEmitter } = require('events');
15
+
16
+ const DEFAULT_SOCKET = '/tmp/sym.sock';
17
+
18
+ class SymDaemonClient extends EventEmitter {
19
+
20
+ constructor(opts = {}) {
21
+ super();
22
+ this._socketPath = opts.socketPath || DEFAULT_SOCKET;
23
+ this._name = opts.name || 'virtual-node';
24
+ this._cognitiveProfile = opts.cognitiveProfile || null;
25
+ this._socket = null;
26
+ this._buffer = '';
27
+ this._connected = false;
28
+ this._requestId = 0;
29
+ this._pendingRequests = new Map(); // id → { resolve, reject, timeout }
30
+ }
31
+
32
+ /** Whether the client is connected to the daemon. */
33
+ get connected() { return this._connected; }
34
+
35
+ /** Connect to the daemon and register as a virtual node. */
36
+ async connect() {
37
+ return new Promise((resolve, reject) => {
38
+ const socket = net.createConnection(this._socketPath, () => {
39
+ this._socket = socket;
40
+ this._connected = true;
41
+
42
+ // Register as virtual node
43
+ this._send({
44
+ type: 'register',
45
+ name: this._name,
46
+ cognitiveProfile: this._cognitiveProfile,
47
+ });
48
+
49
+ // Wait for registered response
50
+ const onFirstMessage = (msg) => {
51
+ if (msg.type === 'registered') {
52
+ this._nodeId = msg.nodeId;
53
+ this._nodeName = msg.name;
54
+ resolve({ nodeId: msg.nodeId, name: msg.name, relay: msg.relay });
55
+ }
56
+ };
57
+ this.once('_raw', onFirstMessage);
58
+
59
+ // Timeout registration
60
+ setTimeout(() => {
61
+ this.removeListener('_raw', onFirstMessage);
62
+ reject(new Error('Daemon registration timeout'));
63
+ }, 5000);
64
+ });
65
+
66
+ socket.on('data', (data) => this._onData(data));
67
+
68
+ socket.on('close', () => {
69
+ this._connected = false;
70
+ this._socket = null;
71
+ this.emit('disconnected');
72
+ });
73
+
74
+ socket.on('error', (err) => {
75
+ this._connected = false;
76
+ if (err.code === 'ENOENT' || err.code === 'ECONNREFUSED') {
77
+ reject(new Error('sym-daemon not running'));
78
+ } else {
79
+ reject(err);
80
+ }
81
+ });
82
+ });
83
+ }
84
+
85
+ /** Disconnect from the daemon. */
86
+ disconnect() {
87
+ if (this._socket) {
88
+ this._socket.end();
89
+ this._socket = null;
90
+ }
91
+ this._connected = false;
92
+ }
93
+
94
+ // ── SymNode-compatible API ─────────────────────────────────
95
+
96
+ /** Broadcast mood to the mesh via daemon. */
97
+ broadcastMood(mood, opts = {}) {
98
+ this._send({ type: 'mood', mood, context: opts.context });
99
+ }
100
+
101
+ /** Store a memory in the mesh via daemon. */
102
+ remember(content, opts = {}) {
103
+ this._send({ type: 'remember', content, tags: opts.tags });
104
+ return { key: `pending-${Date.now()}` };
105
+ }
106
+
107
+ /** Search memories via daemon. Returns a promise. */
108
+ async recall(query) {
109
+ const result = await this._request({ type: 'recall', query });
110
+ return result.results || [];
111
+ }
112
+
113
+ /** Send a message to all peers via daemon. */
114
+ send(message) {
115
+ this._send({ type: 'send', message });
116
+ }
117
+
118
+ /** Get connected peers via daemon. Returns a promise. */
119
+ async peers() {
120
+ const result = await this._request({ type: 'peers' });
121
+ return result.peers || [];
122
+ }
123
+
124
+ /** Get full status via daemon. Returns a promise. */
125
+ async status() {
126
+ const result = await this._request({ type: 'status' });
127
+ return result.status || {};
128
+ }
129
+
130
+ /** No-op for SymNode compatibility. */
131
+ start() {}
132
+
133
+ /** Disconnect. */
134
+ stop() { this.disconnect(); }
135
+
136
+ // ── Internal ───────────────────────────────────────────────
137
+
138
+ _send(msg) {
139
+ if (!this._socket || !this._connected) return;
140
+ try {
141
+ this._socket.write(JSON.stringify(msg) + '\n');
142
+ } catch {}
143
+ }
144
+
145
+ /** Send a request and wait for matching result. */
146
+ _request(msg) {
147
+ return new Promise((resolve, reject) => {
148
+ const action = msg.type;
149
+ this._send(msg);
150
+
151
+ // Listen for next result with matching action
152
+ const handler = (raw) => {
153
+ if (raw.type === 'result' && raw.action === action) {
154
+ this.removeListener('_raw', handler);
155
+ clearTimeout(timer);
156
+ resolve(raw);
157
+ }
158
+ };
159
+ this.on('_raw', handler);
160
+
161
+ const timer = setTimeout(() => {
162
+ this.removeListener('_raw', handler);
163
+ reject(new Error(`Daemon request timeout: ${action}`));
164
+ }, 10000);
165
+ });
166
+ }
167
+
168
+ _onData(data) {
169
+ this._buffer += data.toString();
170
+ let idx;
171
+ while ((idx = this._buffer.indexOf('\n')) !== -1) {
172
+ const line = this._buffer.slice(0, idx);
173
+ this._buffer = this._buffer.slice(idx + 1);
174
+ if (!line.trim()) continue;
175
+
176
+ try {
177
+ const msg = JSON.parse(line);
178
+ this.emit('_raw', msg);
179
+
180
+ // Forward mesh events as SymNode-compatible events
181
+ if (msg.type === 'event') {
182
+ switch (msg.event) {
183
+ case 'mood-accepted':
184
+ this.emit('mood-accepted', msg.data);
185
+ break;
186
+ case 'mood-rejected':
187
+ this.emit('mood-rejected', msg.data);
188
+ break;
189
+ case 'message':
190
+ this.emit('message', msg.data.from, msg.data.content, msg.data);
191
+ break;
192
+ case 'peer-joined':
193
+ this.emit('peer-joined', msg.data);
194
+ break;
195
+ case 'peer-left':
196
+ this.emit('peer-left', msg.data);
197
+ break;
198
+ case 'coupling-decision':
199
+ this.emit('coupling-decision', msg.data);
200
+ break;
201
+ case 'memory-received':
202
+ this.emit('memory-received', msg.data);
203
+ break;
204
+ }
205
+ }
206
+ } catch {}
207
+ }
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Connect to sym-daemon. Throws if daemon is not running.
213
+ * The daemon MUST be running — there is no standalone fallback.
214
+ * Install daemon: node bin/sym-daemon.js --install
215
+ */
216
+ async function connectToDaemon(opts = {}) {
217
+ const client = new SymDaemonClient({
218
+ socketPath: opts.socketPath || DEFAULT_SOCKET,
219
+ name: opts.name || 'claude-code',
220
+ cognitiveProfile: opts.cognitiveProfile,
221
+ });
222
+
223
+ await client.connect();
224
+ return client;
225
+ }
226
+
227
+ module.exports = { SymDaemonClient, connectToDaemon };
package/lib/node.js CHANGED
@@ -323,6 +323,7 @@ class SymNode extends EventEmitter {
323
323
  if (peer) peer.transport.send(frame);
324
324
  } else {
325
325
  this._broadcastToPeers(frame);
326
+ this._wakeSleepingPeers('message', frame);
326
327
  }
327
328
  }
328
329
 
@@ -806,7 +807,8 @@ class SymNode extends EventEmitter {
806
807
  const knownPeers = [];
807
808
  for (const [id, wc] of this._peerWakeChannels) {
808
809
  if (id !== peer.peerId) {
809
- knownPeers.push({ nodeId: id, wakeChannel: wc, lastSeen: Date.now() });
810
+ const peerEntry = this._peers.get(id);
811
+ knownPeers.push({ nodeId: id, name: peerEntry?.name || 'unknown', wakeChannel: wc, lastSeen: Date.now() });
810
812
  }
811
813
  }
812
814
  if (knownPeers.length > 0) {
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@sym-bot/sym",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Local AI mesh — every agent is a sovereign node, the mesh is the agents",
5
5
  "main": "lib/node.js",
6
6
  "bin": {
7
- "sym-mcp": "./integrations/claude-code/mcp-server.js"
7
+ "sym-mcp": "./integrations/claude-code/mcp-server.js",
8
+ "sym-daemon": "./bin/sym-daemon.js"
8
9
  },
9
10
  "scripts": {
10
11
  "test": "node --test tests/*.test.js"