@sym-bot/sym 0.1.0 → 0.2.1

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,88 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+ const { FrameParser, sendFrame } = require('./frame-parser');
5
+
6
+ /**
7
+ * Transport abstraction for SYM peer connections.
8
+ *
9
+ * All transports emit:
10
+ * 'message' (msg) — parsed JSON frame from peer
11
+ * 'close' () — connection closed
12
+ * 'error' (err) — connection error
13
+ *
14
+ * All transports implement:
15
+ * send(frame) — send a JSON frame to the peer
16
+ * close() — close the connection
17
+ */
18
+
19
+ /**
20
+ * TcpTransport — wraps a raw TCP socket with length-prefixed framing.
21
+ */
22
+ class TcpTransport extends EventEmitter {
23
+
24
+ constructor(socket) {
25
+ super();
26
+ this._socket = socket;
27
+ this._parser = new FrameParser();
28
+ this._closed = false;
29
+
30
+ socket.on('data', (chunk) => this._parser.feed(chunk));
31
+ this._parser.on('message', (msg) => this.emit('message', msg));
32
+ this._parser.on('error', (err) => this.emit('error', err));
33
+ socket.on('close', () => { this._closed = true; this.emit('close'); });
34
+ socket.on('error', (err) => this.emit('error', err));
35
+ }
36
+
37
+ send(frame) {
38
+ if (this._closed) return;
39
+ sendFrame(this._socket, frame);
40
+ }
41
+
42
+ close() {
43
+ if (this._closed) return;
44
+ this._closed = true;
45
+ try { this._socket.destroy(); } catch {}
46
+ }
47
+
48
+ /** Drain any buffered bytes into a new transport (for handshake hand-off). */
49
+ get pendingBuffer() {
50
+ return this._parser.buffer || Buffer.alloc(0);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * RelayPeerTransport — a virtual transport for a specific peer over a shared
56
+ * WebSocket relay connection.
57
+ *
58
+ * Multiple peers share one WebSocket to the relay. Each RelayPeerTransport
59
+ * targets a specific peer nodeId via the envelope's `to` field.
60
+ */
61
+ class RelayPeerTransport extends EventEmitter {
62
+
63
+ constructor(relayWs, targetNodeId) {
64
+ super();
65
+ this._ws = relayWs;
66
+ this._targetNodeId = targetNodeId;
67
+ this._closed = false;
68
+ }
69
+
70
+ send(frame) {
71
+ if (this._closed || !this._ws || this._ws.readyState !== 1) return;
72
+ this._ws.send(JSON.stringify({ to: this._targetNodeId, payload: frame }));
73
+ }
74
+
75
+ close() {
76
+ if (this._closed) return;
77
+ this._closed = true;
78
+ this.emit('close');
79
+ }
80
+
81
+ /** Called by the relay client when the shared WebSocket closes. */
82
+ destroy() {
83
+ this._closed = true;
84
+ this.emit('close');
85
+ }
86
+ }
87
+
88
+ module.exports = { TcpTransport, RelayPeerTransport };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@sym-bot/sym",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
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"
@@ -33,6 +34,7 @@
33
34
  "dependencies": {
34
35
  "@modelcontextprotocol/sdk": "^1.27.1",
35
36
  "bonjour-service": "^1.3.0",
36
- "mesh-cognition": "^1.1.0"
37
+ "mesh-cognition": "^1.1.0",
38
+ "ws": "^8.20.0"
37
39
  }
38
40
  }
@@ -0,0 +1,7 @@
1
+ FROM node:18-alpine
2
+ WORKDIR /app
3
+ COPY package*.json ./
4
+ RUN npm ci --production
5
+ COPY . .
6
+ EXPOSE 8080
7
+ CMD ["node", "server.js"]
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ const LEVELS = { error: 0, warn: 1, info: 2, debug: 3 };
4
+
5
+ class Logger {
6
+
7
+ constructor(level = 'info') {
8
+ this._level = LEVELS[level] ?? LEVELS.info;
9
+ }
10
+
11
+ error(msg, ctx) { this._write('error', msg, ctx); }
12
+ warn(msg, ctx) { this._write('warn', msg, ctx); }
13
+ info(msg, ctx) { this._write('info', msg, ctx); }
14
+ debug(msg, ctx) { this._write('debug', msg, ctx); }
15
+
16
+ _write(level, msg, ctx) {
17
+ if (LEVELS[level] > this._level) return;
18
+ const ts = new Date().toISOString();
19
+ const prefix = `${ts} [${level.toUpperCase()}]`;
20
+ if (ctx) {
21
+ process.stdout.write(`${prefix} ${msg} ${JSON.stringify(ctx)}\n`);
22
+ } else {
23
+ process.stdout.write(`${prefix} ${msg}\n`);
24
+ }
25
+ }
26
+ }
27
+
28
+ module.exports = { Logger };
@@ -0,0 +1,388 @@
1
+ 'use strict';
2
+
3
+ const { Logger } = require('./logger');
4
+
5
+ /**
6
+ * SymRelay — a peer node in the MMP mesh.
7
+ *
8
+ * The relay is a full mesh participant with its own identity. It forwards
9
+ * frames between authenticated nodes without inspecting payloads (dumb
10
+ * transport), but participates in peer gossip — storing wake channels
11
+ * and peer metadata so that newly-connecting nodes can learn about
12
+ * sleeping peers they've never met.
13
+ *
14
+ * All coupling decisions remain on-device. The relay never evaluates
15
+ * cognitive state.
16
+ *
17
+ * MMP v0.2.0 Protocol:
18
+ * 1. Client connects via WebSocket
19
+ * 2. Client sends: { type: 'relay-auth', nodeId, name, token?, wakeChannel? }
20
+ * 3. Relay validates, registers, updates peer directory
21
+ * 4. Relay sends: { type: 'relay-peers', peers: [{ nodeId, name, wakeChannel?, offline? }] }
22
+ * — includes disconnected peers with wake channels (gossip)
23
+ * 5. Relay notifies others: { type: 'relay-peer-joined', nodeId, name }
24
+ * 6. Client sends: { to?: nodeId, payload: <MMP frame> }
25
+ * 7. Relay forwards: { from: nodeId, fromName: name, payload: <MMP frame> }
26
+ * 8. Relay intercepts peer-info and wake-channel payloads to update directory
27
+ * 9. On disconnect: { type: 'relay-peer-left', nodeId, name }
28
+ */
29
+ class SymRelay {
30
+
31
+ constructor(opts = {}) {
32
+ this._token = opts.token || null;
33
+ this._log = opts.logger || new Logger(opts.logLevel || 'info');
34
+ this._pingInterval = opts.pingInterval || 30000;
35
+ this._pingTimeout = opts.pingTimeout || 10000;
36
+
37
+ /** Active WebSocket connections. nodeId → ConnectionState */
38
+ this._connections = new Map();
39
+
40
+ /**
41
+ * Peer directory — retained across disconnects for gossip.
42
+ * nodeId → { name: string, wakeChannel?: WakeChannel, lastSeen: number }
43
+ *
44
+ * This is what makes the relay a gossip hub: when a new node connects,
45
+ * the relay shares what it knows about peers the new node has never met.
46
+ */
47
+ this._peerDirectory = new Map();
48
+ this._peerDirectoryTTL = opts.peerDirectoryTTL || 7 * 24 * 60 * 60 * 1000; // 7 days
49
+
50
+ this._pingTimer = null;
51
+ }
52
+
53
+ // ── Public API ──────────────────────────────────────────────
54
+
55
+ get connectionCount() {
56
+ return this._connections.size;
57
+ }
58
+
59
+ get directorySize() {
60
+ return this._peerDirectory.size;
61
+ }
62
+
63
+ start() {
64
+ this._pingTimer = setInterval(() => this._heartbeat(), this._pingInterval);
65
+ }
66
+
67
+ stop() {
68
+ if (this._pingTimer) clearInterval(this._pingTimer);
69
+ for (const [, conn] of this._connections) {
70
+ conn.ws.close(1001, 'Server shutting down');
71
+ }
72
+ this._connections.clear();
73
+ }
74
+
75
+ /**
76
+ * Handle a new WebSocket connection. Called by the server on 'connection'.
77
+ */
78
+ handleConnection(ws) {
79
+ let authenticated = false;
80
+ const authTimeout = setTimeout(() => {
81
+ if (!authenticated) {
82
+ this._log.warn('Auth timeout — closing connection');
83
+ ws.close(4001, 'Auth timeout');
84
+ }
85
+ }, 10000);
86
+
87
+ ws.on('message', (data) => {
88
+ let msg;
89
+ try { msg = JSON.parse(data.toString()); } catch {
90
+ this._log.warn('Invalid JSON received');
91
+ return;
92
+ }
93
+
94
+ if (!authenticated) {
95
+ authenticated = this._authenticate(ws, msg, authTimeout);
96
+ return;
97
+ }
98
+
99
+ this._onMessage(msg, ws);
100
+ });
101
+
102
+ ws.on('close', () => {
103
+ clearTimeout(authTimeout);
104
+ this._removeBySocket(ws);
105
+ });
106
+
107
+ ws.on('error', (err) => {
108
+ this._log.warn(`WebSocket error: ${err.message}`);
109
+ });
110
+ }
111
+
112
+ // ── Authentication ─────────────────────────────────────────
113
+
114
+ _authenticate(ws, msg, authTimeout) {
115
+ if (msg.type !== 'relay-auth' || !msg.nodeId || !msg.name) {
116
+ this._log.warn('Invalid auth message', { type: msg.type });
117
+ ws.close(4002, 'Invalid auth');
118
+ clearTimeout(authTimeout);
119
+ return false;
120
+ }
121
+
122
+ if (this._token && msg.token !== this._token) {
123
+ this._log.warn('Auth rejected — invalid token', { name: msg.name });
124
+ ws.send(JSON.stringify({ type: 'relay-error', message: 'Invalid token' }));
125
+ ws.close(4003, 'Invalid token');
126
+ clearTimeout(authTimeout);
127
+ return false;
128
+ }
129
+
130
+ // Replace duplicate nodeId connection
131
+ if (this._connections.has(msg.nodeId)) {
132
+ const existing = this._connections.get(msg.nodeId);
133
+ this._log.info('Duplicate nodeId — closing old connection', { name: msg.name });
134
+ existing.ws.close(4004, 'Replaced by new connection');
135
+ this._removeConnection(msg.nodeId);
136
+ }
137
+
138
+ clearTimeout(authTimeout);
139
+
140
+ // Register active connection
141
+ this._connections.set(msg.nodeId, {
142
+ ws,
143
+ nodeId: msg.nodeId,
144
+ name: msg.name,
145
+ connectedAt: Date.now(),
146
+ lastSeen: Date.now(),
147
+ alive: true,
148
+ });
149
+
150
+ // Update peer directory
151
+ this._updateDirectory(msg.nodeId, msg.name, msg.wakeChannel);
152
+
153
+ this._log.info(`Peer authenticated: ${msg.name} (${msg.nodeId.slice(0, 8)})`, {
154
+ connections: this._connections.size,
155
+ directory: this._peerDirectory.size,
156
+ });
157
+
158
+ // Send peer list with gossip (connected + offline peers with wake channels)
159
+ ws.send(JSON.stringify({
160
+ type: 'relay-peers',
161
+ peers: this._buildPeerList(msg.nodeId),
162
+ }));
163
+
164
+ // Notify existing peers
165
+ this._broadcast(msg.nodeId, {
166
+ type: 'relay-peer-joined',
167
+ nodeId: msg.nodeId,
168
+ name: msg.name,
169
+ });
170
+
171
+ return true;
172
+ }
173
+
174
+ // ── Peer Directory ──────────────────────────────────────────
175
+
176
+ /**
177
+ * Update the peer directory with the latest information about a node.
178
+ * The directory survives disconnects — this is the gossip state.
179
+ */
180
+ _updateDirectory(nodeId, name, wakeChannel) {
181
+ const existing = this._peerDirectory.get(nodeId) || {};
182
+ const entry = {
183
+ ...existing,
184
+ name: name || existing.name,
185
+ lastSeen: Date.now(),
186
+ };
187
+ if (wakeChannel && this._isValidWakeChannel(wakeChannel)) {
188
+ entry.wakeChannel = wakeChannel;
189
+ }
190
+ this._peerDirectory.set(nodeId, entry);
191
+ }
192
+
193
+ /**
194
+ * Validate wake channel structure.
195
+ */
196
+ _isValidWakeChannel(wc) {
197
+ return wc
198
+ && typeof wc.platform === 'string'
199
+ && wc.platform !== 'none'
200
+ && typeof wc.token === 'string'
201
+ && wc.token.length > 0;
202
+ }
203
+
204
+ /**
205
+ * Build the peer list for a newly-connected node.
206
+ * Includes connected peers AND offline peers with wake channels (gossip).
207
+ */
208
+ _buildPeerList(excludeNodeId) {
209
+ const peers = [];
210
+ const seen = new Set();
211
+
212
+ // Connected peers (with wake channels from directory if available)
213
+ for (const [id, conn] of this._connections) {
214
+ if (id === excludeNodeId) continue;
215
+ seen.add(id);
216
+ const dir = this._peerDirectory.get(id);
217
+ const entry = { nodeId: conn.nodeId, name: conn.name };
218
+ if (dir?.wakeChannel) entry.wakeChannel = dir.wakeChannel;
219
+ peers.push(entry);
220
+ }
221
+
222
+ // Offline peers with wake channels (the gossip value-add)
223
+ for (const [id, dir] of this._peerDirectory) {
224
+ if (id === excludeNodeId || seen.has(id)) continue;
225
+ if (!dir.wakeChannel) continue;
226
+ peers.push({
227
+ nodeId: id,
228
+ name: dir.name,
229
+ wakeChannel: dir.wakeChannel,
230
+ offline: true,
231
+ });
232
+ }
233
+
234
+ return peers;
235
+ }
236
+
237
+ // ── Message Routing ────────────────────────────────────────
238
+
239
+ _onMessage(msg, ws) {
240
+ const sender = this._findBySocket(ws);
241
+ if (!sender) return;
242
+
243
+ sender.lastSeen = Date.now();
244
+
245
+ // Relay control messages
246
+ if (msg.type === 'relay-pong') {
247
+ sender.alive = true;
248
+ return;
249
+ }
250
+
251
+ // Intercept peer metadata from payloads (relay participates in gossip)
252
+ this._interceptPeerMetadata(sender, msg);
253
+
254
+ // Forward to specific peer or broadcast
255
+ if (msg.to) {
256
+ this._sendTo(msg.to, {
257
+ from: sender.nodeId,
258
+ fromName: sender.name,
259
+ payload: msg.payload,
260
+ });
261
+ } else if (msg.payload) {
262
+ this._broadcast(sender.nodeId, {
263
+ from: sender.nodeId,
264
+ fromName: sender.name,
265
+ payload: msg.payload,
266
+ });
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Intercept MMP frames that carry peer metadata to update the directory.
272
+ * The relay doesn't inspect cognitive payloads — only peer discovery metadata.
273
+ */
274
+ _interceptPeerMetadata(sender, msg) {
275
+ const payload = msg.payload;
276
+ if (!payload || typeof payload !== 'object') return;
277
+
278
+ // peer-info gossip frame
279
+ if (payload.type === 'peer-info' && Array.isArray(payload.peers)) {
280
+ for (const p of payload.peers) {
281
+ if (!p.nodeId || p.nodeId === sender.nodeId) continue;
282
+ if (p.wakeChannel && this._isValidWakeChannel(p.wakeChannel)) {
283
+ this._updateDirectory(p.nodeId, p.name, p.wakeChannel);
284
+ }
285
+ }
286
+ }
287
+
288
+ // wake-channel frame (direct declaration from sender)
289
+ if (payload.type === 'wake-channel' && this._isValidWakeChannel(payload)) {
290
+ this._updateDirectory(sender.nodeId, sender.name, {
291
+ platform: payload.platform,
292
+ token: payload.token,
293
+ environment: payload.environment,
294
+ });
295
+ this._log.info(`Wake channel stored: ${sender.name} (${payload.platform})`);
296
+ }
297
+ }
298
+
299
+ // ── Transport ──────────────────────────────────────────────
300
+
301
+ _sendTo(targetNodeId, envelope) {
302
+ const conn = this._connections.get(targetNodeId);
303
+ if (!conn) return;
304
+ try {
305
+ conn.ws.send(JSON.stringify(envelope));
306
+ } catch (err) {
307
+ this._log.warn(`Send failed to ${conn.name}: ${err.message}`);
308
+ }
309
+ }
310
+
311
+ _broadcast(excludeNodeId, envelope) {
312
+ const data = JSON.stringify(envelope);
313
+ for (const [id, conn] of this._connections) {
314
+ if (id === excludeNodeId) continue;
315
+ try {
316
+ conn.ws.send(data);
317
+ } catch (err) {
318
+ this._log.warn(`Broadcast failed to ${conn.name}: ${err.message}`);
319
+ }
320
+ }
321
+ }
322
+
323
+ // ── Connection Management ──────────────────────────────────
324
+
325
+ _findBySocket(ws) {
326
+ for (const [, conn] of this._connections) {
327
+ if (conn.ws === ws) return conn;
328
+ }
329
+ return null;
330
+ }
331
+
332
+ _removeBySocket(ws) {
333
+ const conn = this._findBySocket(ws);
334
+ if (conn) this._removeConnection(conn.nodeId);
335
+ }
336
+
337
+ _removeConnection(nodeId) {
338
+ const conn = this._connections.get(nodeId);
339
+ if (!conn) return;
340
+
341
+ this._connections.delete(nodeId);
342
+
343
+ // Update directory lastSeen on disconnect (keeps the entry alive for gossip)
344
+ const dir = this._peerDirectory.get(nodeId);
345
+ if (dir) dir.lastSeen = Date.now();
346
+
347
+ this._log.info(`Peer disconnected: ${conn.name} (${nodeId.slice(0, 8)})`, {
348
+ connections: this._connections.size,
349
+ });
350
+
351
+ this._broadcast(nodeId, {
352
+ type: 'relay-peer-left',
353
+ nodeId: conn.nodeId,
354
+ name: conn.name,
355
+ });
356
+ }
357
+
358
+ // ── Heartbeat ──────────────────────────────────────────────
359
+
360
+ _heartbeat() {
361
+ // Expire stale directory entries (disconnected peers beyond TTL)
362
+ const now = Date.now();
363
+ for (const [nodeId, dir] of this._peerDirectory) {
364
+ if (!this._connections.has(nodeId) && now - dir.lastSeen > this._peerDirectoryTTL) {
365
+ this._peerDirectory.delete(nodeId);
366
+ this._log.info(`Directory expired: ${dir.name} (${nodeId.slice(0, 8)})`);
367
+ }
368
+ }
369
+
370
+ // Ping active connections
371
+ for (const [nodeId, conn] of this._connections) {
372
+ if (!conn.alive) {
373
+ this._log.info(`Heartbeat timeout: ${conn.name}`);
374
+ conn.ws.close(4005, 'Heartbeat timeout');
375
+ this._removeConnection(nodeId);
376
+ continue;
377
+ }
378
+ conn.alive = false;
379
+ try {
380
+ conn.ws.send(JSON.stringify({ type: 'relay-ping' }));
381
+ } catch {
382
+ this._removeConnection(nodeId);
383
+ }
384
+ }
385
+ }
386
+ }
387
+
388
+ module.exports = { SymRelay };
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@sym-bot/relay",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@sym-bot/relay",
9
+ "version": "0.1.0",
10
+ "license": "Apache-2.0",
11
+ "dependencies": {
12
+ "ws": "^8.18.0"
13
+ },
14
+ "engines": {
15
+ "node": ">=18"
16
+ }
17
+ },
18
+ "node_modules/ws": {
19
+ "version": "8.20.0",
20
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
21
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=10.0.0"
25
+ },
26
+ "peerDependencies": {
27
+ "bufferutil": "^4.0.1",
28
+ "utf-8-validate": ">=5.0.2"
29
+ },
30
+ "peerDependenciesMeta": {
31
+ "bufferutil": {
32
+ "optional": true
33
+ },
34
+ "utf-8-validate": {
35
+ "optional": true
36
+ }
37
+ }
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@sym-bot/relay",
3
+ "version": "0.1.0",
4
+ "description": "SYM mesh relay — WebSocket transport for internet-scale cognitive coupling",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js"
8
+ },
9
+ "keywords": ["sym", "mesh", "relay", "websocket", "p2p"],
10
+ "author": "SYM.BOT Ltd <info@sym.bot> (https://sym.bot)",
11
+ "license": "Apache-2.0",
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "dependencies": {
16
+ "ws": "^8.18.0"
17
+ }
18
+ }
@@ -0,0 +1,14 @@
1
+ services:
2
+ - type: web
3
+ name: sym-relay
4
+ runtime: node
5
+ plan: free
6
+ buildCommand: cd sym-relay && npm ci --production
7
+ startCommand: cd sym-relay && node server.js
8
+ envVars:
9
+ - key: PORT
10
+ value: 8080
11
+ - key: SYM_RELAY_TOKEN
12
+ sync: false
13
+ - key: LOG_LEVEL
14
+ value: info
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const http = require('http');
5
+ const { WebSocketServer } = require('ws');
6
+ const { SymRelay } = require('./lib/relay');
7
+ const { Logger } = require('./lib/logger');
8
+
9
+ // ── Configuration ──────────────────────────────────────────────
10
+
11
+ const PORT = parseInt(process.env.PORT, 10) || 8080;
12
+ const TOKEN = process.env.SYM_RELAY_TOKEN || null;
13
+ const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
14
+
15
+ const log = new Logger(LOG_LEVEL);
16
+
17
+ // ── HTTP Server ────────────────────────────────────────────────
18
+
19
+ const httpServer = http.createServer((req, res) => {
20
+ if (req.method === 'GET' && req.url === '/health') {
21
+ res.writeHead(200, { 'Content-Type': 'application/json' });
22
+ res.end(JSON.stringify({
23
+ status: 'ok',
24
+ connections: relay.connectionCount,
25
+ uptime: Math.floor(process.uptime()),
26
+ }));
27
+ return;
28
+ }
29
+ res.writeHead(404);
30
+ res.end();
31
+ });
32
+
33
+ // ── WebSocket Server ───────────────────────────────────────────
34
+
35
+ const wss = new WebSocketServer({ server: httpServer });
36
+ const relay = new SymRelay({ token: TOKEN, logger: log, logLevel: LOG_LEVEL });
37
+
38
+ wss.on('connection', (ws) => {
39
+ relay.handleConnection(ws);
40
+ });
41
+
42
+ relay.start();
43
+
44
+ // ── Start ──────────────────────────────────────────────────────
45
+
46
+ httpServer.listen(PORT, () => {
47
+ log.info(`SYM relay listening on port ${PORT}`, {
48
+ auth: TOKEN ? 'token required' : 'open',
49
+ });
50
+ });
51
+
52
+ // ── Graceful Shutdown ──────────────────────────────────────────
53
+
54
+ function shutdown(signal) {
55
+ log.info(`${signal} received — shutting down`);
56
+ relay.stop();
57
+ wss.close(() => {
58
+ httpServer.close(() => {
59
+ process.exit(0);
60
+ });
61
+ });
62
+ // Force exit after 5s if graceful shutdown stalls
63
+ setTimeout(() => process.exit(1), 5000);
64
+ }
65
+
66
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
67
+ process.on('SIGINT', () => shutdown('SIGINT'));
package/.mcp.json DELETED
@@ -1,12 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "sym": {
4
- "type": "stdio",
5
- "command": "node",
6
- "args": [
7
- "/Users/hongwei/Documents/dev/sym/integrations/claude-code/mcp-server.js"
8
- ],
9
- "env": {}
10
- }
11
- }
12
- }