@sym-bot/mesh-channel 0.1.5
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/CHANGELOG.md +70 -0
- package/README.md +70 -0
- package/package.json +24 -0
- package/server.js +290 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Bumped `@sym-bot/sym` dep `^0.3.68` → `^0.3.69` (0.3.68 deprecated;
|
|
8
|
+
same code in 0.3.69 with a cleaner published tarball).
|
|
9
|
+
- Added `files` whitelist to `package.json` and `.npmignore` for
|
|
10
|
+
`*.bak`, `*.swp`, `.DS_Store` so future publishes can't accidentally
|
|
11
|
+
ship local backup files. First NPM publish of this package.
|
|
12
|
+
|
|
13
|
+
## 0.1.4
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Bumped `@sym-bot/sym` dep `^0.3.43` → `^0.3.68` to pick up
|
|
18
|
+
duplicate-identity refusal (close code 4004) and the new
|
|
19
|
+
`identity-collision` event.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Wired `node.on('identity-collision', ...)` to `process.exit(2)` so
|
|
24
|
+
the MCP dies cleanly when the relay reports a duplicate-identity
|
|
25
|
+
race. Together with v0.1.3's clean shutdown, this fully resolves
|
|
26
|
+
the host-side half of the duplicate-identity bug.
|
|
27
|
+
|
|
28
|
+
## 0.1.3
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Clean shutdown handlers (SIGTERM/SIGINT/SIGHUP) that call
|
|
33
|
+
`node.stop()` before exiting, so the SymNode disconnects from the
|
|
34
|
+
relay before the process dies. Without this, restarts left zombie
|
|
35
|
+
registrations on the relay until the next heartbeat tick (up to
|
|
36
|
+
30s), creating a duplicate-identity race window for the next MCP
|
|
37
|
+
spawn. Idempotent re-entry guard.
|
|
38
|
+
|
|
39
|
+
## 0.1.2
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Suppressed `peer-joined` / `peer-left` events from being pushed to
|
|
44
|
+
Claude's context as `<channel>` notifications. Presence is high-
|
|
45
|
+
frequency and low-signal — a relay reconnect could fire one event
|
|
46
|
+
per peer per cycle, flooding the context window. CMBs and direct
|
|
47
|
+
messages still flow through.
|
|
48
|
+
|
|
49
|
+
## 0.1.1
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- Replaced hardcoded `claude-code` / `claude-code-mac` literals with
|
|
54
|
+
a single `NODE_NAME` constant sourced from `process.env.SYM_NODE_NAME`
|
|
55
|
+
(default `claude-code-mac`). Enables platform-scoped naming per
|
|
56
|
+
MMP §3.1.2 without source edits. Fixed stale display strings in
|
|
57
|
+
the MCP instructions, `sym_send` perspective, `sym_status` header,
|
|
58
|
+
and the self-echo dedup filter.
|
|
59
|
+
|
|
60
|
+
## 0.1.0
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- Initial release. MCP server that runs a `SymNode` peer node inside
|
|
65
|
+
a Claude Code session — own identity, own relay connection, own
|
|
66
|
+
SVAF evaluation. Tools: `sym_send`, `sym_observe`, `sym_recall`,
|
|
67
|
+
`sym_peers`, `sym_status`. Mesh events arrive as `<channel>`
|
|
68
|
+
notifications when launched with
|
|
69
|
+
`claude --dangerously-load-development-channels server:claude-sym-mesh`
|
|
70
|
+
(allowlisted server name required by Claude Code Channels).
|
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# sym-mesh-channel
|
|
2
|
+
|
|
3
|
+
MCP server that makes Claude Code a peer node on the [SYM mesh](https://sym.bot).
|
|
4
|
+
|
|
5
|
+
This is a **peer node**, not a client. It has its own identity, its own relay connection, and its own SVAF evaluation with domain-specific field weights.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
### 1. Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
git clone https://github.com/sym-bot/sym-mesh-channel.git
|
|
13
|
+
cd sym-mesh-channel
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 2. Configure Claude Code
|
|
18
|
+
|
|
19
|
+
Add to `~/.claude/mcp.json` (macOS/Linux) or `%USERPROFILE%\.claude\mcp.json` (Windows):
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"sym-mesh-channel": {
|
|
25
|
+
"command": "node",
|
|
26
|
+
"args": ["/absolute/path/to/sym-mesh-channel/server.js"],
|
|
27
|
+
"env": {
|
|
28
|
+
"SYM_RELAY_URL": "wss://your-relay-url",
|
|
29
|
+
"SYM_RELAY_TOKEN": "your-token",
|
|
30
|
+
"SYM_NODE_NAME": "claude-code-mac"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`SYM_NODE_NAME` sets the node's identity on the mesh. Use platform-scoped names (`claude-code-mac`, `claude-code-win`) when running the same role on multiple devices, per MMP §3.1.2. Defaults to `claude-code-mac` if unset.
|
|
38
|
+
|
|
39
|
+
### 3. Restart Claude Code
|
|
40
|
+
|
|
41
|
+
The MCP server loads on startup. Run `/mcp` to verify connection.
|
|
42
|
+
|
|
43
|
+
## Tools
|
|
44
|
+
|
|
45
|
+
| Tool | Description |
|
|
46
|
+
|------|-------------|
|
|
47
|
+
| `sym_send` | Broadcast a message to all mesh peers |
|
|
48
|
+
| `sym_observe` | Share a structured CAT7 observation |
|
|
49
|
+
| `sym_recall` | Search mesh memory |
|
|
50
|
+
| `sym_peers` | List connected peers |
|
|
51
|
+
| `sym_status` | Node status — relay, peers, memory count |
|
|
52
|
+
|
|
53
|
+
## Architecture
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
Claude Code ←stdio→ sym-mesh-channel (SymNode) ←wss→ relay ←wss→ other peers
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- **Outbound**: Claude Code calls MCP tools → SymNode sends to mesh
|
|
60
|
+
- **Inbound**: Mesh events → SymNode → MCP channel notifications → Claude Code
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
- Node.js >= 18
|
|
65
|
+
- Claude Code with MCP support
|
|
66
|
+
- A SYM relay server (see [sym-relay](https://github.com/sym-bot/sym-relay))
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
Apache 2.0 — SYM.BOT Ltd
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sym-bot/mesh-channel",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "MCP server — Claude Code as a peer node on the SYM mesh",
|
|
5
|
+
"main": "server.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"sym-mesh-channel": "server.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"server.js",
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@sym-bot/sym": "^0.3.69",
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"author": "SYM.BOT Ltd <info@sym.bot> (https://sym.bot)",
|
|
23
|
+
"license": "Apache-2.0"
|
|
24
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* sym-mesh-channel — MCP server that makes Claude Code a peer node on the SYM mesh.
|
|
6
|
+
*
|
|
7
|
+
* Architecture (MMP Section 13.9: Local Event Interface):
|
|
8
|
+
* SymNode (own identity, own SVAF field weights) → relay → mesh
|
|
9
|
+
* MCP channel notifications → Claude Code (real-time push)
|
|
10
|
+
* MCP tools → SymNode methods (send, observe, recall)
|
|
11
|
+
*
|
|
12
|
+
* This is a PEER NODE, not a client of the daemon. It has its own identity,
|
|
13
|
+
* its own relay connection, and its own SVAF evaluation with engineering-domain
|
|
14
|
+
* field weights. Per MMP Section 3: every participant is a peer.
|
|
15
|
+
*
|
|
16
|
+
* Copyright (c) 2026 SYM.BOT Ltd. Apache 2.0 License.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
|
20
|
+
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
|
21
|
+
const {
|
|
22
|
+
CallToolRequestSchema,
|
|
23
|
+
ListToolsRequestSchema,
|
|
24
|
+
} = require('@modelcontextprotocol/sdk/types.js');
|
|
25
|
+
const { SymNode } = require('@sym-bot/sym');
|
|
26
|
+
|
|
27
|
+
// ── Engineering-domain field weights (SVAF α_f) ──────────────
|
|
28
|
+
|
|
29
|
+
const FIELD_WEIGHTS = {
|
|
30
|
+
focus: 2.0, // code, architecture, technical decisions
|
|
31
|
+
issue: 2.0, // bugs, blockers, technical debt
|
|
32
|
+
intent: 1.5, // what needs building
|
|
33
|
+
motivation: 1.0, // why it matters
|
|
34
|
+
commitment: 1.5, // deadlines, dependencies
|
|
35
|
+
perspective: 0.5, // viewpoint — low for engineering
|
|
36
|
+
mood: 0.8, // user fatigue affects code quality
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── SymNode — full peer on the mesh ──────────────────────────
|
|
40
|
+
|
|
41
|
+
const NODE_NAME = process.env.SYM_NODE_NAME || 'claude-code-mac';
|
|
42
|
+
|
|
43
|
+
const node = new SymNode({
|
|
44
|
+
name: NODE_NAME,
|
|
45
|
+
cognitiveProfile: 'Engineering node. Code, architecture, debugging, technical decisions.',
|
|
46
|
+
svafFieldWeights: FIELD_WEIGHTS,
|
|
47
|
+
svafFreshnessSeconds: 7200, // 2hr — session-length context
|
|
48
|
+
relay: process.env.SYM_RELAY_URL || null,
|
|
49
|
+
relayToken: process.env.SYM_RELAY_TOKEN || null,
|
|
50
|
+
silent: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Identity collision (added in @sym-bot/sym 0.3.68): the relay told us
|
|
54
|
+
// another process is holding our nodeId. Don't try to reconnect — that
|
|
55
|
+
// caused the peer-flap loop documented in v0.1.2/v0.1.3 commit messages.
|
|
56
|
+
// Exit so Claude Code can decide whether to respawn (with the freshness
|
|
57
|
+
// window now elapsed) or surface the failure to the user.
|
|
58
|
+
node.on('identity-collision', (info) => {
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
`sym-mesh-channel: identity collision on relay — another process is holding ` +
|
|
61
|
+
`nodeId=${info.nodeId} name=${info.name}. Exiting.\n`
|
|
62
|
+
);
|
|
63
|
+
process.exit(2);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ── MCP Server ───────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
const mcp = new Server(
|
|
69
|
+
{ name: 'sym-mesh', version: '0.1.0' },
|
|
70
|
+
{
|
|
71
|
+
capabilities: {
|
|
72
|
+
tools: {},
|
|
73
|
+
experimental: { 'claude/channel': {} },
|
|
74
|
+
},
|
|
75
|
+
instructions:
|
|
76
|
+
`You are a peer node on the SYM mesh (identity: ${NODE_NAME}). ` +
|
|
77
|
+
'Mesh events arrive as <channel> notifications in real-time. ' +
|
|
78
|
+
'When you see a message or CMB from another node, respond via the sym_send tool if actionable. ' +
|
|
79
|
+
'Share observations about the user\'s state via sym_observe. ' +
|
|
80
|
+
'Search mesh memory via sym_recall.',
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// ── Tools ────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
87
|
+
tools: [
|
|
88
|
+
{
|
|
89
|
+
name: 'sym_send',
|
|
90
|
+
description: 'Send a message to all mesh peers. Stored as a persistent CMB and broadcast via relay.',
|
|
91
|
+
inputSchema: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: { message: { type: 'string', description: 'Message to broadcast' } },
|
|
94
|
+
required: ['message'],
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'sym_observe',
|
|
99
|
+
description: 'Share a structured CAT7 observation with the mesh. Extract fields from what you observe.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
focus: { type: 'string' },
|
|
104
|
+
issue: { type: 'string' },
|
|
105
|
+
intent: { type: 'string' },
|
|
106
|
+
motivation: { type: 'string' },
|
|
107
|
+
commitment: { type: 'string' },
|
|
108
|
+
perspective: { type: 'string' },
|
|
109
|
+
mood: {
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
text: { type: 'string' },
|
|
113
|
+
valence: { type: 'number' },
|
|
114
|
+
arousal: { type: 'number' },
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ['focus'],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'sym_recall',
|
|
123
|
+
description: 'Search mesh memory for relevant CMBs.',
|
|
124
|
+
inputSchema: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
properties: { query: { type: 'string', description: 'Search query (empty for all)' } },
|
|
127
|
+
required: ['query'],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'sym_peers',
|
|
132
|
+
description: 'List connected mesh peers.',
|
|
133
|
+
inputSchema: { type: 'object', properties: {} },
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'sym_status',
|
|
137
|
+
description: 'Get mesh node status — relay connection, peer count, memory count.',
|
|
138
|
+
inputSchema: { type: 'object', properties: {} },
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
144
|
+
const { name, arguments: args } = request.params;
|
|
145
|
+
|
|
146
|
+
switch (name) {
|
|
147
|
+
case 'sym_send': {
|
|
148
|
+
const msg = args.message;
|
|
149
|
+
node.send(msg);
|
|
150
|
+
node.remember({
|
|
151
|
+
focus: msg,
|
|
152
|
+
issue: 'none',
|
|
153
|
+
intent: 'inter-node message',
|
|
154
|
+
motivation: 'mesh communication',
|
|
155
|
+
commitment: msg.slice(0, 120),
|
|
156
|
+
perspective: `${NODE_NAME}, direct message`,
|
|
157
|
+
mood: { text: 'neutral', valence: 0, arousal: 0 },
|
|
158
|
+
});
|
|
159
|
+
const peers = node.peers();
|
|
160
|
+
return { content: [{ type: 'text', text: `Message sent to ${peers.length} peer(s).` }] };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
case 'sym_observe': {
|
|
164
|
+
const fields = {
|
|
165
|
+
focus: args.focus || 'observation',
|
|
166
|
+
issue: args.issue || 'none',
|
|
167
|
+
intent: args.intent || 'observation',
|
|
168
|
+
motivation: args.motivation || '',
|
|
169
|
+
commitment: args.commitment || '',
|
|
170
|
+
perspective: args.perspective || NODE_NAME,
|
|
171
|
+
mood: args.mood || { text: 'neutral', valence: 0, arousal: 0 },
|
|
172
|
+
};
|
|
173
|
+
const entry = node.remember(fields);
|
|
174
|
+
return { content: [{ type: 'text', text: entry ? `Observed: ${entry.key}` : 'Duplicate — already in memory.' }] };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
case 'sym_recall': {
|
|
178
|
+
const results = node.recall(args.query || '');
|
|
179
|
+
if (results.length === 0) {
|
|
180
|
+
return { content: [{ type: 'text', text: 'No memories found.' }] };
|
|
181
|
+
}
|
|
182
|
+
const lines = results.slice(0, 10).map(r => {
|
|
183
|
+
const focus = r.cmb?.fields?.focus?.text || r.content || '';
|
|
184
|
+
const source = r.source || r.cmb?.createdBy || 'unknown';
|
|
185
|
+
const time = r.timestamp ? new Date(r.timestamp).toLocaleString() : '';
|
|
186
|
+
return `[${source}] ${time}\n ${focus.slice(0, 150)}`;
|
|
187
|
+
});
|
|
188
|
+
return { content: [{ type: 'text', text: lines.join('\n\n') }] };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
case 'sym_peers': {
|
|
192
|
+
const peers = node.peers();
|
|
193
|
+
if (peers.length === 0) {
|
|
194
|
+
return { content: [{ type: 'text', text: 'No peers connected.' }] };
|
|
195
|
+
}
|
|
196
|
+
const lines = peers.map(p => `${p.name} via ${p.source || 'unknown'}`);
|
|
197
|
+
return { content: [{ type: 'text', text: `${peers.length} peer(s):\n${lines.join('\n')}` }] };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case 'sym_status': {
|
|
201
|
+
const s = node.status();
|
|
202
|
+
return {
|
|
203
|
+
content: [{
|
|
204
|
+
type: 'text',
|
|
205
|
+
text: `Node: ${NODE_NAME} (${node.nodeId?.slice(0, 8) || '?'})\n` +
|
|
206
|
+
`Relay: ${s.relayConnected ? 'connected' : 'disconnected'}\n` +
|
|
207
|
+
`Peers: ${s.peerCount || 0}\n` +
|
|
208
|
+
`Memories: ${s.memoryCount || 0}`,
|
|
209
|
+
}],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
default:
|
|
214
|
+
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }] };
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// ── Mesh Events → Channel Notifications ──────────────────────
|
|
219
|
+
|
|
220
|
+
function pushChannel(eventType, data) {
|
|
221
|
+
try {
|
|
222
|
+
mcp.notification({
|
|
223
|
+
method: 'notifications/claude/channel',
|
|
224
|
+
params: {
|
|
225
|
+
content: typeof data === 'string' ? data : JSON.stringify(data),
|
|
226
|
+
meta: { event_type: eventType, source: 'sym-mesh' },
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
node.on('cmb-accepted', (entry) => {
|
|
233
|
+
// Don't echo back our own CMBs
|
|
234
|
+
if (entry.source === NODE_NAME || entry.cmb?.createdBy === NODE_NAME) return;
|
|
235
|
+
|
|
236
|
+
const source = entry.source || entry.cmb?.createdBy || 'unknown';
|
|
237
|
+
const focus = entry.cmb?.fields?.focus?.text || entry.content || '';
|
|
238
|
+
const mood = entry.cmb?.fields?.mood?.text || '';
|
|
239
|
+
pushChannel('cmb', `[${source}] ${focus}${mood && mood !== 'neutral' ? ` (mood: ${mood})` : ''}`);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
node.on('message', (from, content) => {
|
|
243
|
+
pushChannel('message', `[message from ${from}] ${content}`);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Peer presence events are intentionally NOT pushed to Claude's context.
|
|
247
|
+
// They're high-frequency, low-signal (peers flap on relay reconnects, daemon
|
|
248
|
+
// restarts, NAT keepalive blips), and a flood will eat the context window.
|
|
249
|
+
// Use sym_peers / sym_status on demand instead. Only CMBs and direct messages
|
|
250
|
+
// are surfaced as channel notifications — those carry actual cognitive payload.
|
|
251
|
+
|
|
252
|
+
// ── Start ────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
// Clean shutdown — disconnect from the relay before exiting so other peers
|
|
255
|
+
// see us leave immediately, and so a fast restart of this MCP doesn't race
|
|
256
|
+
// our own zombie connection on the relay (which would trigger the relay's
|
|
257
|
+
// duplicate-nodeId replacement path and cause peer flap loops).
|
|
258
|
+
//
|
|
259
|
+
// Idempotent: Claude Code may send SIGTERM and then SIGKILL; we want the
|
|
260
|
+
// first signal to get us cleanly off the relay even if the second one
|
|
261
|
+
// arrives before stop() resolves.
|
|
262
|
+
let shuttingDown = false;
|
|
263
|
+
async function shutdown(signal) {
|
|
264
|
+
if (shuttingDown) return;
|
|
265
|
+
shuttingDown = true;
|
|
266
|
+
try {
|
|
267
|
+
await node.stop();
|
|
268
|
+
} catch {
|
|
269
|
+
// Best effort — we're exiting anyway. Don't block on cleanup errors.
|
|
270
|
+
}
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
275
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
276
|
+
process.on('SIGHUP', () => shutdown('SIGHUP'));
|
|
277
|
+
|
|
278
|
+
async function main() {
|
|
279
|
+
// Start SymNode — connects to relay as a peer
|
|
280
|
+
await node.start();
|
|
281
|
+
|
|
282
|
+
// Start MCP server — communicates with Claude Code via stdio
|
|
283
|
+
const transport = new StdioServerTransport();
|
|
284
|
+
await mcp.connect(transport);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
main().catch((err) => {
|
|
288
|
+
process.stderr.write(`sym-mesh-channel failed: ${err.message}\n`);
|
|
289
|
+
process.exit(1);
|
|
290
|
+
});
|