@sym-bot/mesh-channel 0.1.21 → 0.1.22
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/.claude-plugin/marketplace.json +40 -0
- package/.claude-plugin/plugin.json +39 -33
- package/.mcp.json +14 -14
- package/CHANGELOG.md +292 -218
- package/LICENSE +201 -0
- package/README.md +209 -192
- package/SECURITY.md +89 -89
- package/bin/install.js +350 -350
- package/package.json +32 -32
- package/server.js +488 -325
package/bin/install.js
CHANGED
|
@@ -1,350 +1,350 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* sym-mesh-channel install — interactive setup for the MCP server.
|
|
6
|
-
*
|
|
7
|
-
* Run: npx @sym-bot/mesh-channel init
|
|
8
|
-
*
|
|
9
|
-
* What it does:
|
|
10
|
-
* 1. Detects the platform and the host name suggestion (claude-mac /
|
|
11
|
-
* claude-win / claude-linux), or accepts an override.
|
|
12
|
-
* 2. Resolves the absolute path to the installed server.js so Claude
|
|
13
|
-
* Code can spawn it.
|
|
14
|
-
* 3. Reads ~/.claude.json (the Claude Code settings file), backs it
|
|
15
|
-
* up, adds an `mcpServers` entry under the current project for
|
|
16
|
-
* `claude-sym-mesh`, atomically writes the result.
|
|
17
|
-
* 4. Prints the launch command including the Channels dev flag.
|
|
18
|
-
*
|
|
19
|
-
* Safety:
|
|
20
|
-
* - Backs up ~/.claude.json to ~/.claude.json.bak-<timestamp> before
|
|
21
|
-
* any write.
|
|
22
|
-
* - Validates JSON parses round-trip before writing.
|
|
23
|
-
* - Atomic via write-to-tmp + rename.
|
|
24
|
-
* - Refuses to overwrite an existing claude-sym-mesh entry without
|
|
25
|
-
* --force.
|
|
26
|
-
*
|
|
27
|
-
* Copyright (c) 2026 SYM.BOT Ltd. Apache 2.0 License.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
const fs = require('fs');
|
|
31
|
-
const path = require('path');
|
|
32
|
-
const os = require('os');
|
|
33
|
-
|
|
34
|
-
const args = process.argv.slice(2);
|
|
35
|
-
const force = args.includes('--force');
|
|
36
|
-
const isPostinstall = args.includes('--postinstall');
|
|
37
|
-
const isProject = args.includes('--project');
|
|
38
|
-
const cmd = args.find((a) => !a.startsWith('--')) || 'init';
|
|
39
|
-
|
|
40
|
-
if (cmd !== 'init') {
|
|
41
|
-
process.stderr.write(`Unknown command: ${cmd}\nUsage: sym-mesh-channel init [--project] [--force]\n`);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// --postinstall always runs global install (npm postinstall runs from
|
|
46
|
-
// npm's staging directory, not the user's project dir). If both flags
|
|
47
|
-
// are passed, the --project flag is ignored during postinstall.
|
|
48
|
-
const useProjectMode = isProject && !isPostinstall;
|
|
49
|
-
|
|
50
|
-
// ── Detect platform & defaults ────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
// Default: hostname-based identity, unique per machine. Prevents
|
|
53
|
-
// the ghost-peer bug where two machines with the same default name
|
|
54
|
-
// create phantom peers that absorb messages.
|
|
55
|
-
const defaultNodeName = `claude-${os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-')}`;
|
|
56
|
-
|
|
57
|
-
// SYM_NODE_NAME from env wins over default
|
|
58
|
-
const nodeName = process.env.SYM_NODE_NAME || defaultNodeName;
|
|
59
|
-
|
|
60
|
-
// ── Resolve server.js path ────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
// Resolve server.js from the installed package location. require.resolve
|
|
63
|
-
// returns the actual installed path regardless of where postinstall runs
|
|
64
|
-
// from (npm on Windows may run postinstall from a temp staging directory).
|
|
65
|
-
let serverJsPath;
|
|
66
|
-
try {
|
|
67
|
-
serverJsPath = require.resolve('@sym-bot/mesh-channel/server.js');
|
|
68
|
-
} catch {
|
|
69
|
-
// Fallback for local development / cloned repo
|
|
70
|
-
serverJsPath = path.resolve(__dirname, '..', 'server.js');
|
|
71
|
-
}
|
|
72
|
-
if (!fs.existsSync(serverJsPath)) {
|
|
73
|
-
process.stderr.write(`ERROR: cannot find server.js at ${serverJsPath}\n`);
|
|
74
|
-
process.stderr.write('This installer must be run from a published @sym-bot/mesh-channel package.\n');
|
|
75
|
-
process.exit(1);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Shared timestamp for backup filenames
|
|
79
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
80
|
-
|
|
81
|
-
// ── Project-scoped install (--project flag) ───────────────────────
|
|
82
|
-
// Writes <cwd>/.mcp.json + merges <cwd>/.claude/settings.local.json
|
|
83
|
-
// instead of touching ~/.claude.json. Use this when you want multiple
|
|
84
|
-
// Claude Code sessions on one machine to appear as distinct mesh peers
|
|
85
|
-
// (one per project), each with its own SYM_NODE_NAME. Project-level
|
|
86
|
-
// .mcp.json overrides the global ~/.claude.json mcpServers entry when
|
|
87
|
-
// Claude Code is launched from that directory.
|
|
88
|
-
|
|
89
|
-
if (useProjectMode) {
|
|
90
|
-
const projectDir = process.cwd();
|
|
91
|
-
const mcpJsonPath = path.join(projectDir, '.mcp.json');
|
|
92
|
-
const claudeDir = path.join(projectDir, '.claude');
|
|
93
|
-
const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
|
|
94
|
-
|
|
95
|
-
// Read existing .mcp.json (if any)
|
|
96
|
-
let mcpJson = null;
|
|
97
|
-
if (fs.existsSync(mcpJsonPath)) {
|
|
98
|
-
try {
|
|
99
|
-
mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
|
100
|
-
} catch (e) {
|
|
101
|
-
process.stderr.write(`ERROR: ${mcpJsonPath} is not valid JSON: ${e.message}\n`);
|
|
102
|
-
process.stderr.write('Refusing to overwrite a corrupt file. Fix or remove it and retry.\n');
|
|
103
|
-
process.exit(1);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
mcpJson = mcpJson || {};
|
|
107
|
-
if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
|
|
108
|
-
|
|
109
|
-
// Refuse to overwrite an existing claude-sym-mesh entry without --force
|
|
110
|
-
if (mcpJson.mcpServers['claude-sym-mesh'] && !force) {
|
|
111
|
-
process.stderr.write(`'claude-sym-mesh' is already configured in ${mcpJsonPath}.\n`);
|
|
112
|
-
process.stderr.write('Re-run with --force to overwrite, or remove the existing entry first.\n');
|
|
113
|
-
process.exit(2);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Build the MCP entry (identical shape to global mode)
|
|
117
|
-
const projectEntry = {
|
|
118
|
-
command: 'node',
|
|
119
|
-
args: [serverJsPath],
|
|
120
|
-
env: {
|
|
121
|
-
SYM_NODE_NAME: nodeName,
|
|
122
|
-
// Explicitly blank relay env vars — see comment on the global
|
|
123
|
-
// install path below for why.
|
|
124
|
-
SYM_RELAY_URL: '',
|
|
125
|
-
SYM_RELAY_TOKEN: '',
|
|
126
|
-
},
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// Backup existing .mcp.json if present
|
|
130
|
-
let mcpBackupPath = null;
|
|
131
|
-
if (fs.existsSync(mcpJsonPath)) {
|
|
132
|
-
mcpBackupPath = `${mcpJsonPath}.bak-${ts}`;
|
|
133
|
-
fs.copyFileSync(mcpJsonPath, mcpBackupPath);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
mcpJson.mcpServers['claude-sym-mesh'] = projectEntry;
|
|
137
|
-
|
|
138
|
-
// Atomic write .mcp.json
|
|
139
|
-
const mcpSerialized = JSON.stringify(mcpJson, null, 2) + '\n';
|
|
140
|
-
try { JSON.parse(mcpSerialized); } catch (e) {
|
|
141
|
-
process.stderr.write(`ERROR: serialization produced invalid JSON: ${e.message}\n`);
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
const mcpTmpPath = `${mcpJsonPath}.tmp-${process.pid}`;
|
|
145
|
-
fs.writeFileSync(mcpTmpPath, mcpSerialized);
|
|
146
|
-
fs.renameSync(mcpTmpPath, mcpJsonPath);
|
|
147
|
-
|
|
148
|
-
// Merge <projectDir>/.claude/settings.local.json. Claude Code gates
|
|
149
|
-
// loading of project-scoped MCP servers on the enabledMcpjsonServers
|
|
150
|
-
// allowlist in this file — without the merge, the .mcp.json we just
|
|
151
|
-
// wrote would not actually be loaded.
|
|
152
|
-
if (!fs.existsSync(claudeDir)) {
|
|
153
|
-
fs.mkdirSync(claudeDir, { recursive: true });
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let existingSettings = null;
|
|
157
|
-
if (fs.existsSync(settingsLocalPath)) {
|
|
158
|
-
try {
|
|
159
|
-
existingSettings = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8'));
|
|
160
|
-
} catch (e) {
|
|
161
|
-
process.stderr.write(`ERROR: ${settingsLocalPath} is not valid JSON: ${e.message}\n`);
|
|
162
|
-
process.exit(1);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Snapshot serialized form BEFORE mutating so the change-detection
|
|
167
|
-
// below can't be fooled by object aliasing (existingSettings and
|
|
168
|
-
// settings point at the same object after the `|| {}`).
|
|
169
|
-
const beforeSerialized = existingSettings ? JSON.stringify(existingSettings) : null;
|
|
170
|
-
const settings = existingSettings || {};
|
|
171
|
-
|
|
172
|
-
const enabled = new Set(Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : []);
|
|
173
|
-
enabled.add('claude-sym-mesh');
|
|
174
|
-
settings.enabledMcpjsonServers = Array.from(enabled);
|
|
175
|
-
settings.enableAllProjectMcpServers = true;
|
|
176
|
-
|
|
177
|
-
const afterSerialized = JSON.stringify(settings);
|
|
178
|
-
const settingsChanged = beforeSerialized !== afterSerialized;
|
|
179
|
-
|
|
180
|
-
let settingsBackupPath = null;
|
|
181
|
-
if (settingsChanged) {
|
|
182
|
-
if (existingSettings) {
|
|
183
|
-
settingsBackupPath = `${settingsLocalPath}.bak-${ts}`;
|
|
184
|
-
fs.copyFileSync(settingsLocalPath, settingsBackupPath);
|
|
185
|
-
}
|
|
186
|
-
const settingsSerialized = JSON.stringify(settings, null, 2) + '\n';
|
|
187
|
-
const settingsTmpPath = `${settingsLocalPath}.tmp-${process.pid}`;
|
|
188
|
-
fs.writeFileSync(settingsTmpPath, settingsSerialized);
|
|
189
|
-
fs.renameSync(settingsTmpPath, settingsLocalPath);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Print next steps
|
|
193
|
-
const launchCmdProject = `claude --dangerously-load-development-channels server:claude-sym-mesh`;
|
|
194
|
-
const lines = [
|
|
195
|
-
'',
|
|
196
|
-
`✓ sym-mesh-channel configured for project: ${projectDir}`,
|
|
197
|
-
'',
|
|
198
|
-
` Node name: ${nodeName}`,
|
|
199
|
-
` Server path: ${serverJsPath}`,
|
|
200
|
-
` Wrote: ${mcpJsonPath}`,
|
|
201
|
-
];
|
|
202
|
-
if (mcpBackupPath) lines.push(` Backup: ${mcpBackupPath}`);
|
|
203
|
-
if (settingsChanged) {
|
|
204
|
-
lines.push(` Updated: ${settingsLocalPath}`);
|
|
205
|
-
if (settingsBackupPath) lines.push(` Backup: ${settingsBackupPath}`);
|
|
206
|
-
}
|
|
207
|
-
lines.push(
|
|
208
|
-
'',
|
|
209
|
-
'Launch Claude Code from this directory:',
|
|
210
|
-
'',
|
|
211
|
-
` ${launchCmdProject}`,
|
|
212
|
-
'',
|
|
213
|
-
'Project-level .mcp.json overrides the global ~/.claude.json entry',
|
|
214
|
-
'when Claude Code runs from this directory. To give each project its',
|
|
215
|
-
'own mesh identity, run `sym-mesh-channel init --project` from each',
|
|
216
|
-
'project root with a distinct SYM_NODE_NAME.',
|
|
217
|
-
'',
|
|
218
|
-
);
|
|
219
|
-
console.log(lines.join('\n'));
|
|
220
|
-
process.exit(0);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// ── Locate Claude Code settings file ──────────────────────────────
|
|
224
|
-
|
|
225
|
-
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
226
|
-
|
|
227
|
-
if (!fs.existsSync(claudeJsonPath)) {
|
|
228
|
-
if (isPostinstall) {
|
|
229
|
-
// During postinstall, skip silently if Claude Code isn't installed yet
|
|
230
|
-
console.log('sym-mesh-channel: ~/.claude.json not found — run `sym-mesh-channel init` after installing Claude Code.');
|
|
231
|
-
process.exit(0);
|
|
232
|
-
}
|
|
233
|
-
process.stderr.write(`ERROR: ${claudeJsonPath} not found.\n`);
|
|
234
|
-
process.stderr.write('Claude Code does not appear to be installed (or has not been launched yet).\n');
|
|
235
|
-
process.stderr.write('Install Claude Code from https://claude.com/code first, launch it once, then re-run this installer.\n');
|
|
236
|
-
process.exit(1);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// ── Read and back up ──────────────────────────────────────────────
|
|
240
|
-
|
|
241
|
-
let claudeJson;
|
|
242
|
-
try {
|
|
243
|
-
const raw = fs.readFileSync(claudeJsonPath, 'utf8');
|
|
244
|
-
claudeJson = JSON.parse(raw);
|
|
245
|
-
} catch (e) {
|
|
246
|
-
process.stderr.write(`ERROR: ${claudeJsonPath} is not valid JSON: ${e.message}\n`);
|
|
247
|
-
process.stderr.write('Refusing to overwrite a corrupt Claude Code settings file.\n');
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// `ts` was defined above, shared with project-mode install
|
|
252
|
-
const backupPath = `${claudeJsonPath}.bak-${ts}`;
|
|
253
|
-
fs.copyFileSync(claudeJsonPath, backupPath);
|
|
254
|
-
|
|
255
|
-
// ── Find the MCP servers entry to insert into ───────────────────
|
|
256
|
-
// Write to global mcpServers (available in all Claude Code sessions),
|
|
257
|
-
// not project-scoped. A mesh node should be available everywhere.
|
|
258
|
-
|
|
259
|
-
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
260
|
-
|
|
261
|
-
// ── Refuse to overwrite without --force ──────────────────────────
|
|
262
|
-
|
|
263
|
-
if (claudeJson.mcpServers['claude-sym-mesh'] && !force) {
|
|
264
|
-
if (isPostinstall) {
|
|
265
|
-
// During postinstall, silently skip if already configured
|
|
266
|
-
console.log('sym-mesh-channel: already configured in ~/.claude.json (skipping)');
|
|
267
|
-
process.exit(0);
|
|
268
|
-
}
|
|
269
|
-
process.stderr.write(`'claude-sym-mesh' is already configured in ~/.claude.json.\n`);
|
|
270
|
-
process.stderr.write('Re-run with --force to overwrite, or remove the existing entry first.\n');
|
|
271
|
-
process.exit(2);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ── Build the entry ───────────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
const entry = {
|
|
277
|
-
command: 'node',
|
|
278
|
-
args: [serverJsPath],
|
|
279
|
-
env: {
|
|
280
|
-
SYM_NODE_NAME: nodeName,
|
|
281
|
-
// Explicitly blank the relay vars so the MCP doesn't inherit them
|
|
282
|
-
// from the parent shell (e.g. ~/.zshrc exports). Claude Code's env
|
|
283
|
-
// block is ADDITIVE — omitting a key doesn't remove it from the
|
|
284
|
-
// child process. Setting to '' makes process.env.SYM_RELAY_URL
|
|
285
|
-
// falsy in JS, so the SymNode skips the relay and runs LAN-only.
|
|
286
|
-
//
|
|
287
|
-
// To enable cross-network connectivity later, replace these empty
|
|
288
|
-
// values with your relay URL and token (see README).
|
|
289
|
-
SYM_RELAY_URL: '',
|
|
290
|
-
SYM_RELAY_TOKEN: '',
|
|
291
|
-
},
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
claudeJson.mcpServers['claude-sym-mesh'] = entry;
|
|
295
|
-
|
|
296
|
-
// ── Atomic write ──────────────────────────────────────────────────
|
|
297
|
-
|
|
298
|
-
const serialized = JSON.stringify(claudeJson, null, 2);
|
|
299
|
-
|
|
300
|
-
// Validate round-trip parses
|
|
301
|
-
try {
|
|
302
|
-
JSON.parse(serialized);
|
|
303
|
-
} catch (e) {
|
|
304
|
-
process.stderr.write(`ERROR: serialization produced invalid JSON: ${e.message}\n`);
|
|
305
|
-
process.stderr.write(`Backup is at ${backupPath} — your original file is unchanged.\n`);
|
|
306
|
-
process.exit(1);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const tmpPath = `${claudeJsonPath}.tmp-${process.pid}`;
|
|
310
|
-
try {
|
|
311
|
-
fs.writeFileSync(tmpPath, serialized);
|
|
312
|
-
fs.renameSync(tmpPath, claudeJsonPath);
|
|
313
|
-
} catch (e) {
|
|
314
|
-
// EBUSY on Windows when Claude Code has ~/.claude.json locked
|
|
315
|
-
if (e.code === 'EBUSY' || e.code === 'EPERM') {
|
|
316
|
-
try { fs.unlinkSync(tmpPath); } catch {}
|
|
317
|
-
if (isPostinstall) {
|
|
318
|
-
console.log('sym-mesh-channel: ~/.claude.json is locked (Claude Code may be running).');
|
|
319
|
-
console.log('Run `sym-mesh-channel init` after quitting Claude Code.');
|
|
320
|
-
process.exit(0);
|
|
321
|
-
}
|
|
322
|
-
process.stderr.write(`ERROR: ${claudeJsonPath} is locked — Claude Code may be running.\n`);
|
|
323
|
-
process.stderr.write('Quit Claude Code, then re-run: sym-mesh-channel init\n');
|
|
324
|
-
process.stderr.write(`Backup is at ${backupPath}\n`);
|
|
325
|
-
process.exit(1);
|
|
326
|
-
}
|
|
327
|
-
throw e;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// ── Print next steps ──────────────────────────────────────────────
|
|
331
|
-
|
|
332
|
-
const launchCmd = `claude --dangerously-load-development-channels server:claude-sym-mesh`;
|
|
333
|
-
|
|
334
|
-
console.log(`
|
|
335
|
-
✓ sym-mesh-channel configured globally in ~/.claude.json
|
|
336
|
-
|
|
337
|
-
Node name: ${nodeName}
|
|
338
|
-
Server path: ${serverJsPath}
|
|
339
|
-
Backup: ${backupPath}
|
|
340
|
-
|
|
341
|
-
Launch Claude Code with the Channels flag:
|
|
342
|
-
|
|
343
|
-
${launchCmd}
|
|
344
|
-
|
|
345
|
-
Inside Claude Code, verify:
|
|
346
|
-
|
|
347
|
-
sym_status → node id, relay state, peer count
|
|
348
|
-
sym_peers → discovered peers via Bonjour or relay
|
|
349
|
-
sym_send "hello mesh" → broadcast to all peers
|
|
350
|
-
`);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* sym-mesh-channel install — interactive setup for the MCP server.
|
|
6
|
+
*
|
|
7
|
+
* Run: npx @sym-bot/mesh-channel init
|
|
8
|
+
*
|
|
9
|
+
* What it does:
|
|
10
|
+
* 1. Detects the platform and the host name suggestion (claude-mac /
|
|
11
|
+
* claude-win / claude-linux), or accepts an override.
|
|
12
|
+
* 2. Resolves the absolute path to the installed server.js so Claude
|
|
13
|
+
* Code can spawn it.
|
|
14
|
+
* 3. Reads ~/.claude.json (the Claude Code settings file), backs it
|
|
15
|
+
* up, adds an `mcpServers` entry under the current project for
|
|
16
|
+
* `claude-sym-mesh`, atomically writes the result.
|
|
17
|
+
* 4. Prints the launch command including the Channels dev flag.
|
|
18
|
+
*
|
|
19
|
+
* Safety:
|
|
20
|
+
* - Backs up ~/.claude.json to ~/.claude.json.bak-<timestamp> before
|
|
21
|
+
* any write.
|
|
22
|
+
* - Validates JSON parses round-trip before writing.
|
|
23
|
+
* - Atomic via write-to-tmp + rename.
|
|
24
|
+
* - Refuses to overwrite an existing claude-sym-mesh entry without
|
|
25
|
+
* --force.
|
|
26
|
+
*
|
|
27
|
+
* Copyright (c) 2026 SYM.BOT Ltd. Apache 2.0 License.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
const path = require('path');
|
|
32
|
+
const os = require('os');
|
|
33
|
+
|
|
34
|
+
const args = process.argv.slice(2);
|
|
35
|
+
const force = args.includes('--force');
|
|
36
|
+
const isPostinstall = args.includes('--postinstall');
|
|
37
|
+
const isProject = args.includes('--project');
|
|
38
|
+
const cmd = args.find((a) => !a.startsWith('--')) || 'init';
|
|
39
|
+
|
|
40
|
+
if (cmd !== 'init') {
|
|
41
|
+
process.stderr.write(`Unknown command: ${cmd}\nUsage: sym-mesh-channel init [--project] [--force]\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --postinstall always runs global install (npm postinstall runs from
|
|
46
|
+
// npm's staging directory, not the user's project dir). If both flags
|
|
47
|
+
// are passed, the --project flag is ignored during postinstall.
|
|
48
|
+
const useProjectMode = isProject && !isPostinstall;
|
|
49
|
+
|
|
50
|
+
// ── Detect platform & defaults ────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
// Default: hostname-based identity, unique per machine. Prevents
|
|
53
|
+
// the ghost-peer bug where two machines with the same default name
|
|
54
|
+
// create phantom peers that absorb messages.
|
|
55
|
+
const defaultNodeName = `claude-${os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-')}`;
|
|
56
|
+
|
|
57
|
+
// SYM_NODE_NAME from env wins over default
|
|
58
|
+
const nodeName = process.env.SYM_NODE_NAME || defaultNodeName;
|
|
59
|
+
|
|
60
|
+
// ── Resolve server.js path ────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
// Resolve server.js from the installed package location. require.resolve
|
|
63
|
+
// returns the actual installed path regardless of where postinstall runs
|
|
64
|
+
// from (npm on Windows may run postinstall from a temp staging directory).
|
|
65
|
+
let serverJsPath;
|
|
66
|
+
try {
|
|
67
|
+
serverJsPath = require.resolve('@sym-bot/mesh-channel/server.js');
|
|
68
|
+
} catch {
|
|
69
|
+
// Fallback for local development / cloned repo
|
|
70
|
+
serverJsPath = path.resolve(__dirname, '..', 'server.js');
|
|
71
|
+
}
|
|
72
|
+
if (!fs.existsSync(serverJsPath)) {
|
|
73
|
+
process.stderr.write(`ERROR: cannot find server.js at ${serverJsPath}\n`);
|
|
74
|
+
process.stderr.write('This installer must be run from a published @sym-bot/mesh-channel package.\n');
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Shared timestamp for backup filenames
|
|
79
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
80
|
+
|
|
81
|
+
// ── Project-scoped install (--project flag) ───────────────────────
|
|
82
|
+
// Writes <cwd>/.mcp.json + merges <cwd>/.claude/settings.local.json
|
|
83
|
+
// instead of touching ~/.claude.json. Use this when you want multiple
|
|
84
|
+
// Claude Code sessions on one machine to appear as distinct mesh peers
|
|
85
|
+
// (one per project), each with its own SYM_NODE_NAME. Project-level
|
|
86
|
+
// .mcp.json overrides the global ~/.claude.json mcpServers entry when
|
|
87
|
+
// Claude Code is launched from that directory.
|
|
88
|
+
|
|
89
|
+
if (useProjectMode) {
|
|
90
|
+
const projectDir = process.cwd();
|
|
91
|
+
const mcpJsonPath = path.join(projectDir, '.mcp.json');
|
|
92
|
+
const claudeDir = path.join(projectDir, '.claude');
|
|
93
|
+
const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
|
|
94
|
+
|
|
95
|
+
// Read existing .mcp.json (if any)
|
|
96
|
+
let mcpJson = null;
|
|
97
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
98
|
+
try {
|
|
99
|
+
mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
|
100
|
+
} catch (e) {
|
|
101
|
+
process.stderr.write(`ERROR: ${mcpJsonPath} is not valid JSON: ${e.message}\n`);
|
|
102
|
+
process.stderr.write('Refusing to overwrite a corrupt file. Fix or remove it and retry.\n');
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
mcpJson = mcpJson || {};
|
|
107
|
+
if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
|
|
108
|
+
|
|
109
|
+
// Refuse to overwrite an existing claude-sym-mesh entry without --force
|
|
110
|
+
if (mcpJson.mcpServers['claude-sym-mesh'] && !force) {
|
|
111
|
+
process.stderr.write(`'claude-sym-mesh' is already configured in ${mcpJsonPath}.\n`);
|
|
112
|
+
process.stderr.write('Re-run with --force to overwrite, or remove the existing entry first.\n');
|
|
113
|
+
process.exit(2);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Build the MCP entry (identical shape to global mode)
|
|
117
|
+
const projectEntry = {
|
|
118
|
+
command: 'node',
|
|
119
|
+
args: [serverJsPath],
|
|
120
|
+
env: {
|
|
121
|
+
SYM_NODE_NAME: nodeName,
|
|
122
|
+
// Explicitly blank relay env vars — see comment on the global
|
|
123
|
+
// install path below for why.
|
|
124
|
+
SYM_RELAY_URL: '',
|
|
125
|
+
SYM_RELAY_TOKEN: '',
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Backup existing .mcp.json if present
|
|
130
|
+
let mcpBackupPath = null;
|
|
131
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
132
|
+
mcpBackupPath = `${mcpJsonPath}.bak-${ts}`;
|
|
133
|
+
fs.copyFileSync(mcpJsonPath, mcpBackupPath);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
mcpJson.mcpServers['claude-sym-mesh'] = projectEntry;
|
|
137
|
+
|
|
138
|
+
// Atomic write .mcp.json
|
|
139
|
+
const mcpSerialized = JSON.stringify(mcpJson, null, 2) + '\n';
|
|
140
|
+
try { JSON.parse(mcpSerialized); } catch (e) {
|
|
141
|
+
process.stderr.write(`ERROR: serialization produced invalid JSON: ${e.message}\n`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
const mcpTmpPath = `${mcpJsonPath}.tmp-${process.pid}`;
|
|
145
|
+
fs.writeFileSync(mcpTmpPath, mcpSerialized);
|
|
146
|
+
fs.renameSync(mcpTmpPath, mcpJsonPath);
|
|
147
|
+
|
|
148
|
+
// Merge <projectDir>/.claude/settings.local.json. Claude Code gates
|
|
149
|
+
// loading of project-scoped MCP servers on the enabledMcpjsonServers
|
|
150
|
+
// allowlist in this file — without the merge, the .mcp.json we just
|
|
151
|
+
// wrote would not actually be loaded.
|
|
152
|
+
if (!fs.existsSync(claudeDir)) {
|
|
153
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let existingSettings = null;
|
|
157
|
+
if (fs.existsSync(settingsLocalPath)) {
|
|
158
|
+
try {
|
|
159
|
+
existingSettings = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8'));
|
|
160
|
+
} catch (e) {
|
|
161
|
+
process.stderr.write(`ERROR: ${settingsLocalPath} is not valid JSON: ${e.message}\n`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Snapshot serialized form BEFORE mutating so the change-detection
|
|
167
|
+
// below can't be fooled by object aliasing (existingSettings and
|
|
168
|
+
// settings point at the same object after the `|| {}`).
|
|
169
|
+
const beforeSerialized = existingSettings ? JSON.stringify(existingSettings) : null;
|
|
170
|
+
const settings = existingSettings || {};
|
|
171
|
+
|
|
172
|
+
const enabled = new Set(Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : []);
|
|
173
|
+
enabled.add('claude-sym-mesh');
|
|
174
|
+
settings.enabledMcpjsonServers = Array.from(enabled);
|
|
175
|
+
settings.enableAllProjectMcpServers = true;
|
|
176
|
+
|
|
177
|
+
const afterSerialized = JSON.stringify(settings);
|
|
178
|
+
const settingsChanged = beforeSerialized !== afterSerialized;
|
|
179
|
+
|
|
180
|
+
let settingsBackupPath = null;
|
|
181
|
+
if (settingsChanged) {
|
|
182
|
+
if (existingSettings) {
|
|
183
|
+
settingsBackupPath = `${settingsLocalPath}.bak-${ts}`;
|
|
184
|
+
fs.copyFileSync(settingsLocalPath, settingsBackupPath);
|
|
185
|
+
}
|
|
186
|
+
const settingsSerialized = JSON.stringify(settings, null, 2) + '\n';
|
|
187
|
+
const settingsTmpPath = `${settingsLocalPath}.tmp-${process.pid}`;
|
|
188
|
+
fs.writeFileSync(settingsTmpPath, settingsSerialized);
|
|
189
|
+
fs.renameSync(settingsTmpPath, settingsLocalPath);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Print next steps
|
|
193
|
+
const launchCmdProject = `claude --dangerously-load-development-channels server:claude-sym-mesh`;
|
|
194
|
+
const lines = [
|
|
195
|
+
'',
|
|
196
|
+
`✓ sym-mesh-channel configured for project: ${projectDir}`,
|
|
197
|
+
'',
|
|
198
|
+
` Node name: ${nodeName}`,
|
|
199
|
+
` Server path: ${serverJsPath}`,
|
|
200
|
+
` Wrote: ${mcpJsonPath}`,
|
|
201
|
+
];
|
|
202
|
+
if (mcpBackupPath) lines.push(` Backup: ${mcpBackupPath}`);
|
|
203
|
+
if (settingsChanged) {
|
|
204
|
+
lines.push(` Updated: ${settingsLocalPath}`);
|
|
205
|
+
if (settingsBackupPath) lines.push(` Backup: ${settingsBackupPath}`);
|
|
206
|
+
}
|
|
207
|
+
lines.push(
|
|
208
|
+
'',
|
|
209
|
+
'Launch Claude Code from this directory:',
|
|
210
|
+
'',
|
|
211
|
+
` ${launchCmdProject}`,
|
|
212
|
+
'',
|
|
213
|
+
'Project-level .mcp.json overrides the global ~/.claude.json entry',
|
|
214
|
+
'when Claude Code runs from this directory. To give each project its',
|
|
215
|
+
'own mesh identity, run `sym-mesh-channel init --project` from each',
|
|
216
|
+
'project root with a distinct SYM_NODE_NAME.',
|
|
217
|
+
'',
|
|
218
|
+
);
|
|
219
|
+
console.log(lines.join('\n'));
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ── Locate Claude Code settings file ──────────────────────────────
|
|
224
|
+
|
|
225
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
226
|
+
|
|
227
|
+
if (!fs.existsSync(claudeJsonPath)) {
|
|
228
|
+
if (isPostinstall) {
|
|
229
|
+
// During postinstall, skip silently if Claude Code isn't installed yet
|
|
230
|
+
console.log('sym-mesh-channel: ~/.claude.json not found — run `sym-mesh-channel init` after installing Claude Code.');
|
|
231
|
+
process.exit(0);
|
|
232
|
+
}
|
|
233
|
+
process.stderr.write(`ERROR: ${claudeJsonPath} not found.\n`);
|
|
234
|
+
process.stderr.write('Claude Code does not appear to be installed (or has not been launched yet).\n');
|
|
235
|
+
process.stderr.write('Install Claude Code from https://claude.com/code first, launch it once, then re-run this installer.\n');
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ── Read and back up ──────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
let claudeJson;
|
|
242
|
+
try {
|
|
243
|
+
const raw = fs.readFileSync(claudeJsonPath, 'utf8');
|
|
244
|
+
claudeJson = JSON.parse(raw);
|
|
245
|
+
} catch (e) {
|
|
246
|
+
process.stderr.write(`ERROR: ${claudeJsonPath} is not valid JSON: ${e.message}\n`);
|
|
247
|
+
process.stderr.write('Refusing to overwrite a corrupt Claude Code settings file.\n');
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// `ts` was defined above, shared with project-mode install
|
|
252
|
+
const backupPath = `${claudeJsonPath}.bak-${ts}`;
|
|
253
|
+
fs.copyFileSync(claudeJsonPath, backupPath);
|
|
254
|
+
|
|
255
|
+
// ── Find the MCP servers entry to insert into ───────────────────
|
|
256
|
+
// Write to global mcpServers (available in all Claude Code sessions),
|
|
257
|
+
// not project-scoped. A mesh node should be available everywhere.
|
|
258
|
+
|
|
259
|
+
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
260
|
+
|
|
261
|
+
// ── Refuse to overwrite without --force ──────────────────────────
|
|
262
|
+
|
|
263
|
+
if (claudeJson.mcpServers['claude-sym-mesh'] && !force) {
|
|
264
|
+
if (isPostinstall) {
|
|
265
|
+
// During postinstall, silently skip if already configured
|
|
266
|
+
console.log('sym-mesh-channel: already configured in ~/.claude.json (skipping)');
|
|
267
|
+
process.exit(0);
|
|
268
|
+
}
|
|
269
|
+
process.stderr.write(`'claude-sym-mesh' is already configured in ~/.claude.json.\n`);
|
|
270
|
+
process.stderr.write('Re-run with --force to overwrite, or remove the existing entry first.\n');
|
|
271
|
+
process.exit(2);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Build the entry ───────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
const entry = {
|
|
277
|
+
command: 'node',
|
|
278
|
+
args: [serverJsPath],
|
|
279
|
+
env: {
|
|
280
|
+
SYM_NODE_NAME: nodeName,
|
|
281
|
+
// Explicitly blank the relay vars so the MCP doesn't inherit them
|
|
282
|
+
// from the parent shell (e.g. ~/.zshrc exports). Claude Code's env
|
|
283
|
+
// block is ADDITIVE — omitting a key doesn't remove it from the
|
|
284
|
+
// child process. Setting to '' makes process.env.SYM_RELAY_URL
|
|
285
|
+
// falsy in JS, so the SymNode skips the relay and runs LAN-only.
|
|
286
|
+
//
|
|
287
|
+
// To enable cross-network connectivity later, replace these empty
|
|
288
|
+
// values with your relay URL and token (see README).
|
|
289
|
+
SYM_RELAY_URL: '',
|
|
290
|
+
SYM_RELAY_TOKEN: '',
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
claudeJson.mcpServers['claude-sym-mesh'] = entry;
|
|
295
|
+
|
|
296
|
+
// ── Atomic write ──────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
const serialized = JSON.stringify(claudeJson, null, 2);
|
|
299
|
+
|
|
300
|
+
// Validate round-trip parses
|
|
301
|
+
try {
|
|
302
|
+
JSON.parse(serialized);
|
|
303
|
+
} catch (e) {
|
|
304
|
+
process.stderr.write(`ERROR: serialization produced invalid JSON: ${e.message}\n`);
|
|
305
|
+
process.stderr.write(`Backup is at ${backupPath} — your original file is unchanged.\n`);
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const tmpPath = `${claudeJsonPath}.tmp-${process.pid}`;
|
|
310
|
+
try {
|
|
311
|
+
fs.writeFileSync(tmpPath, serialized);
|
|
312
|
+
fs.renameSync(tmpPath, claudeJsonPath);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
// EBUSY on Windows when Claude Code has ~/.claude.json locked
|
|
315
|
+
if (e.code === 'EBUSY' || e.code === 'EPERM') {
|
|
316
|
+
try { fs.unlinkSync(tmpPath); } catch {}
|
|
317
|
+
if (isPostinstall) {
|
|
318
|
+
console.log('sym-mesh-channel: ~/.claude.json is locked (Claude Code may be running).');
|
|
319
|
+
console.log('Run `sym-mesh-channel init` after quitting Claude Code.');
|
|
320
|
+
process.exit(0);
|
|
321
|
+
}
|
|
322
|
+
process.stderr.write(`ERROR: ${claudeJsonPath} is locked — Claude Code may be running.\n`);
|
|
323
|
+
process.stderr.write('Quit Claude Code, then re-run: sym-mesh-channel init\n');
|
|
324
|
+
process.stderr.write(`Backup is at ${backupPath}\n`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
throw e;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ── Print next steps ──────────────────────────────────────────────
|
|
331
|
+
|
|
332
|
+
const launchCmd = `claude --dangerously-load-development-channels server:claude-sym-mesh`;
|
|
333
|
+
|
|
334
|
+
console.log(`
|
|
335
|
+
✓ sym-mesh-channel configured globally in ~/.claude.json
|
|
336
|
+
|
|
337
|
+
Node name: ${nodeName}
|
|
338
|
+
Server path: ${serverJsPath}
|
|
339
|
+
Backup: ${backupPath}
|
|
340
|
+
|
|
341
|
+
Launch Claude Code with the Channels flag:
|
|
342
|
+
|
|
343
|
+
${launchCmd}
|
|
344
|
+
|
|
345
|
+
Inside Claude Code, verify:
|
|
346
|
+
|
|
347
|
+
sym_status → node id, relay state, peer count
|
|
348
|
+
sym_peers → discovered peers via Bonjour or relay
|
|
349
|
+
sym_send "hello mesh" → broadcast to all peers
|
|
350
|
+
`);
|