@sym-bot/mesh-channel 0.3.2 → 0.3.4
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 -40
- package/.claude-plugin/plugin.json +39 -39
- package/.mcp.json +14 -14
- package/CHANGELOG.md +546 -463
- package/LICENSE +201 -201
- package/README.md +361 -310
- package/SECURITY.md +89 -89
- package/bin/install.js +603 -487
- package/package.json +49 -49
- package/server.js +886 -878
package/bin/install.js
CHANGED
|
@@ -1,487 +1,603 @@
|
|
|
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 a LIVE claude-sym-mesh entry without --force.
|
|
25
|
-
* An entry whose args[0] server.js path no longer exists on disk is
|
|
26
|
-
* treated as STALE and rewritten in place — a stale entry guarantees
|
|
27
|
-
* a broken MCP transport, so "preserving" it is never what the user
|
|
28
|
-
* wants. SYM_NODE_NAME from the stale entry is preserved so the
|
|
29
|
-
* mesh identity doesn't drift to the hostname-based default.
|
|
30
|
-
* - Also scans every project-scoped mcpServers entry and rewrites any
|
|
31
|
-
* project entry whose claude-sym-mesh.args[0] path has gone stale,
|
|
32
|
-
* again preserving each project's SYM_NODE_NAME. This prevents the
|
|
33
|
-
* "ghost project" failure mode where user-global was fixed but
|
|
34
|
-
* project-scoped entries silently continue to point at the old path.
|
|
35
|
-
*
|
|
36
|
-
* Copyright (c) 2026 SYM.BOT. Apache 2.0 License.
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
const fs = require('fs');
|
|
40
|
-
const path = require('path');
|
|
41
|
-
const os = require('os');
|
|
42
|
-
|
|
43
|
-
const args = process.argv.slice(2);
|
|
44
|
-
const force = args.includes('--force');
|
|
45
|
-
const isPostinstall = args.includes('--postinstall');
|
|
46
|
-
const isProject = args.includes('--project');
|
|
47
|
-
const cmd = args.find((a) => !a.startsWith('--')) || 'init';
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
process.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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 a LIVE claude-sym-mesh entry without --force.
|
|
25
|
+
* An entry whose args[0] server.js path no longer exists on disk is
|
|
26
|
+
* treated as STALE and rewritten in place — a stale entry guarantees
|
|
27
|
+
* a broken MCP transport, so "preserving" it is never what the user
|
|
28
|
+
* wants. SYM_NODE_NAME from the stale entry is preserved so the
|
|
29
|
+
* mesh identity doesn't drift to the hostname-based default.
|
|
30
|
+
* - Also scans every project-scoped mcpServers entry and rewrites any
|
|
31
|
+
* project entry whose claude-sym-mesh.args[0] path has gone stale,
|
|
32
|
+
* again preserving each project's SYM_NODE_NAME. This prevents the
|
|
33
|
+
* "ghost project" failure mode where user-global was fixed but
|
|
34
|
+
* project-scoped entries silently continue to point at the old path.
|
|
35
|
+
*
|
|
36
|
+
* Copyright (c) 2026 SYM.BOT. Apache 2.0 License.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
const fs = require('fs');
|
|
40
|
+
const path = require('path');
|
|
41
|
+
const os = require('os');
|
|
42
|
+
|
|
43
|
+
const args = process.argv.slice(2);
|
|
44
|
+
const force = args.includes('--force');
|
|
45
|
+
const isPostinstall = args.includes('--postinstall');
|
|
46
|
+
const isProject = args.includes('--project');
|
|
47
|
+
const cmd = args.find((a) => !a.startsWith('--')) || 'init';
|
|
48
|
+
|
|
49
|
+
// --group <name>: persist a SYM_GROUP env entry into the written .mcp.json /
|
|
50
|
+
// ~/.claude.json so the node joins that group on every Claude Code launch.
|
|
51
|
+
// Without this flag, the env block omits SYM_GROUP and the node falls back
|
|
52
|
+
// to the default _sym._tcp mesh on startup. Runtime sym_join_group hot-swaps
|
|
53
|
+
// only last for the current session — without persistence, peers in named
|
|
54
|
+
// groups silently revert to default and become invisible to teammates.
|
|
55
|
+
const groupArgIdx = args.indexOf('--group');
|
|
56
|
+
const groupArg = groupArgIdx !== -1 ? args[groupArgIdx + 1] : null;
|
|
57
|
+
|
|
58
|
+
if (cmd !== 'init' && cmd !== 'doctor') {
|
|
59
|
+
process.stderr.write(`Unknown command: ${cmd}\nUsage: sym-mesh-channel init [--project] [--force] [--group <name>]\n sym-mesh-channel doctor\n`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const KEBAB_CASE_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
64
|
+
function validateGroupValue(value, source) {
|
|
65
|
+
if (!value) return;
|
|
66
|
+
if (value === 'default') return;
|
|
67
|
+
if (!KEBAB_CASE_RE.test(value)) {
|
|
68
|
+
process.stderr.write(`ERROR: ${source} "${value}" must be kebab-case (e.g. backend-team) or "default".\n`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
validateGroupValue(groupArg, '--group');
|
|
73
|
+
// Apply the same gate to the env-var path. Pre-0.3.4-followup, a malformed
|
|
74
|
+
// SYM_GROUP=' ' or SYM_GROUP=Backend_Team value flowed through unvalidated
|
|
75
|
+
// and got written into the .mcp.json env block as-is, producing an mDNS
|
|
76
|
+
// service type the SymNode would silently fail to register on. Now both
|
|
77
|
+
// inputs share the validator with the same error message shape.
|
|
78
|
+
validateGroupValue(process.env.SYM_GROUP, 'SYM_GROUP');
|
|
79
|
+
|
|
80
|
+
// ── isStaleEntry: a claude-sym-mesh entry whose server.js path is gone ──
|
|
81
|
+
// Returns true when the entry exists but its args[0] path does not resolve
|
|
82
|
+
// to a file on disk. Such an entry can never spawn the MCP server — every
|
|
83
|
+
// launch yields "Failed to reconnect" in /mcp. Treating it as rewritable
|
|
84
|
+
// on postinstall means users who move or uninstall an old copy of the repo
|
|
85
|
+
// get healed automatically on the next `npm install -g @sym-bot/mesh-channel`
|
|
86
|
+
// without needing to know about --force.
|
|
87
|
+
function isStaleEntry(entry) {
|
|
88
|
+
if (!entry || !Array.isArray(entry.args) || entry.args.length === 0) return false;
|
|
89
|
+
const p = entry.args[0];
|
|
90
|
+
if (typeof p !== 'string' || !p) return false;
|
|
91
|
+
try { return !fs.existsSync(p); } catch { return true; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// preserveNodeName: return the SYM_NODE_NAME from an existing entry's env
|
|
95
|
+
// so rewrites keep the mesh identity. Falls back to nothing if absent; the
|
|
96
|
+
// caller then uses the computed default.
|
|
97
|
+
function preserveNodeName(entry) {
|
|
98
|
+
if (!entry || !entry.env || typeof entry.env.SYM_NODE_NAME !== 'string') return null;
|
|
99
|
+
const n = entry.env.SYM_NODE_NAME.trim();
|
|
100
|
+
return n || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// preserveGroup: return the SYM_GROUP from an existing entry's env so
|
|
104
|
+
// rewrites keep the mesh group. Same shape as preserveNodeName — without
|
|
105
|
+
// this, healing a stale entry would drop a previously-persisted group
|
|
106
|
+
// and silently downgrade the node to the default _sym._tcp mesh,
|
|
107
|
+
// stranding teammates who stay in the named group.
|
|
108
|
+
function preserveGroup(entry) {
|
|
109
|
+
if (!entry || !entry.env || typeof entry.env.SYM_GROUP !== 'string') return null;
|
|
110
|
+
const g = entry.env.SYM_GROUP.trim();
|
|
111
|
+
return g || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --postinstall always runs global install (npm postinstall runs from
|
|
115
|
+
// npm's staging directory, not the user's project dir). If both flags
|
|
116
|
+
// are passed, the --project flag is ignored during postinstall.
|
|
117
|
+
const useProjectMode = isProject && !isPostinstall;
|
|
118
|
+
|
|
119
|
+
// ── Detect platform & defaults ────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
// Default: hostname-based identity, unique per machine. Prevents
|
|
122
|
+
// the ghost-peer bug where two machines with the same default name
|
|
123
|
+
// create phantom peers that absorb messages.
|
|
124
|
+
const defaultNodeName = `claude-${os.hostname().toLowerCase().replace(/[^a-z0-9-]/g, '-')}`;
|
|
125
|
+
|
|
126
|
+
// SYM_NODE_NAME from env wins over default
|
|
127
|
+
const nodeName = process.env.SYM_NODE_NAME || defaultNodeName;
|
|
128
|
+
|
|
129
|
+
// Capture the user's *explicit* group intent for this install, distinct
|
|
130
|
+
// from "user didn't ask, use existing or default":
|
|
131
|
+
// null → user didn't pass --group or SYM_GROUP
|
|
132
|
+
// 'default' → user explicitly wants the global _sym._tcp mesh
|
|
133
|
+
// (escape hatch: revert from a named group, with --force)
|
|
134
|
+
// '<kebab-name>' → user explicitly wants this named group
|
|
135
|
+
const explicitGroup = groupArg !== null ? groupArg
|
|
136
|
+
: (process.env.SYM_GROUP || null);
|
|
137
|
+
|
|
138
|
+
// resolveGroup: per-scope group resolution that respects both the user's
|
|
139
|
+
// explicit intent AND the existing entry's persisted state.
|
|
140
|
+
//
|
|
141
|
+
// With --force AND an explicit value: flag/env wins. The user is
|
|
142
|
+
// deliberately overriding state. `--force --group new-team` switches
|
|
143
|
+
// groups; `--force --group default` reverts to the global mesh.
|
|
144
|
+
//
|
|
145
|
+
// Without --force, OR with --force but no explicit value: preserve
|
|
146
|
+
// from the existing entry (heal-path job is to NOT lose user state).
|
|
147
|
+
// Falls back to the explicit value, then to none.
|
|
148
|
+
//
|
|
149
|
+
// Returns the SYM_GROUP value to write, or null to omit the key entirely
|
|
150
|
+
// (which the caller maps to "leave SYM_GROUP out of the env block, node
|
|
151
|
+
// uses default _sym._tcp on launch").
|
|
152
|
+
function resolveGroup(existingEntry) {
|
|
153
|
+
const preserved = preserveGroup(existingEntry);
|
|
154
|
+
if (force && explicitGroup !== null) {
|
|
155
|
+
return explicitGroup === 'default' ? null : explicitGroup;
|
|
156
|
+
}
|
|
157
|
+
if (preserved) return preserved;
|
|
158
|
+
if (explicitGroup && explicitGroup !== 'default') return explicitGroup;
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Resolve server.js path ────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
// Resolve server.js from the installed package location. require.resolve
|
|
165
|
+
// returns the actual installed path regardless of where postinstall runs
|
|
166
|
+
// from (npm on Windows may run postinstall from a temp staging directory).
|
|
167
|
+
let serverJsPath;
|
|
168
|
+
try {
|
|
169
|
+
serverJsPath = require.resolve('@sym-bot/mesh-channel/server.js');
|
|
170
|
+
} catch {
|
|
171
|
+
// Fallback for local development / cloned repo
|
|
172
|
+
serverJsPath = path.resolve(__dirname, '..', 'server.js');
|
|
173
|
+
}
|
|
174
|
+
if (!fs.existsSync(serverJsPath)) {
|
|
175
|
+
process.stderr.write(`ERROR: cannot find server.js at ${serverJsPath}\n`);
|
|
176
|
+
process.stderr.write('This installer must be run from a published @sym-bot/mesh-channel package.\n');
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Shared timestamp for backup filenames
|
|
181
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
182
|
+
|
|
183
|
+
// ── Project-scoped install (--project flag) ───────────────────────
|
|
184
|
+
// Writes <cwd>/.mcp.json + merges <cwd>/.claude/settings.local.json
|
|
185
|
+
// instead of touching ~/.claude.json. Use this when you want multiple
|
|
186
|
+
// Claude Code sessions on one machine to appear as distinct mesh peers
|
|
187
|
+
// (one per project), each with its own SYM_NODE_NAME. Project-level
|
|
188
|
+
// .mcp.json overrides the global ~/.claude.json mcpServers entry when
|
|
189
|
+
// Claude Code is launched from that directory.
|
|
190
|
+
|
|
191
|
+
if (useProjectMode) {
|
|
192
|
+
const projectDir = process.cwd();
|
|
193
|
+
const mcpJsonPath = path.join(projectDir, '.mcp.json');
|
|
194
|
+
const claudeDir = path.join(projectDir, '.claude');
|
|
195
|
+
const settingsLocalPath = path.join(claudeDir, 'settings.local.json');
|
|
196
|
+
|
|
197
|
+
// Read existing .mcp.json (if any)
|
|
198
|
+
let mcpJson = null;
|
|
199
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
200
|
+
try {
|
|
201
|
+
mcpJson = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
|
|
202
|
+
} catch (e) {
|
|
203
|
+
process.stderr.write(`ERROR: ${mcpJsonPath} is not valid JSON: ${e.message}\n`);
|
|
204
|
+
process.stderr.write('Refusing to overwrite a corrupt file. Fix or remove it and retry.\n');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
mcpJson = mcpJson || {};
|
|
209
|
+
if (!mcpJson.mcpServers) mcpJson.mcpServers = {};
|
|
210
|
+
|
|
211
|
+
// Refuse to overwrite a LIVE claude-sym-mesh entry without --force.
|
|
212
|
+
// Stale entries (args[0] missing on disk) are always rewritable —
|
|
213
|
+
// see isStaleEntry comment above.
|
|
214
|
+
const existingProjectEntry = mcpJson.mcpServers['claude-sym-mesh'];
|
|
215
|
+
const projectEntryIsStale = isStaleEntry(existingProjectEntry);
|
|
216
|
+
if (existingProjectEntry && !force && !projectEntryIsStale) {
|
|
217
|
+
process.stderr.write(`'claude-sym-mesh' is already configured in ${mcpJsonPath}.\n`);
|
|
218
|
+
process.stderr.write('Re-run with --force to overwrite, or remove the existing entry first.\n');
|
|
219
|
+
process.exit(2);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Preserve the prior node name on rewrite so mesh identity doesn't drift
|
|
223
|
+
// back to the hostname default on every reinstall.
|
|
224
|
+
const projectNodeName = preserveNodeName(existingProjectEntry) || nodeName;
|
|
225
|
+
|
|
226
|
+
// Group resolution priority — see resolveGroup() at top of file.
|
|
227
|
+
// Summary: --force + explicit flag/env wins; otherwise preserve, then
|
|
228
|
+
// explicit, then omit. `--group default` with --force = revert to mesh.
|
|
229
|
+
const projectGroup = resolveGroup(existingProjectEntry);
|
|
230
|
+
|
|
231
|
+
// Build the MCP entry (identical shape to global mode)
|
|
232
|
+
const projectEntry = {
|
|
233
|
+
command: 'node',
|
|
234
|
+
args: [serverJsPath],
|
|
235
|
+
env: {
|
|
236
|
+
SYM_NODE_NAME: projectNodeName,
|
|
237
|
+
// Explicitly blank relay env vars — see comment on the global
|
|
238
|
+
// install path below for why.
|
|
239
|
+
SYM_RELAY_URL: '',
|
|
240
|
+
SYM_RELAY_TOKEN: '',
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
// SYM_GROUP is only written when explicitly set. Omitting it (rather than
|
|
244
|
+
// writing an empty string) keeps the JSON file minimal for the common
|
|
245
|
+
// single-team case AND avoids the "default group accidentally pinned"
|
|
246
|
+
// failure mode where a blank value masks the server.js fallback.
|
|
247
|
+
if (projectGroup) projectEntry.env.SYM_GROUP = projectGroup;
|
|
248
|
+
|
|
249
|
+
// Backup existing .mcp.json if present
|
|
250
|
+
let mcpBackupPath = null;
|
|
251
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
252
|
+
mcpBackupPath = `${mcpJsonPath}.bak-${ts}`;
|
|
253
|
+
fs.copyFileSync(mcpJsonPath, mcpBackupPath);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
mcpJson.mcpServers['claude-sym-mesh'] = projectEntry;
|
|
257
|
+
|
|
258
|
+
// Atomic write .mcp.json
|
|
259
|
+
const mcpSerialized = JSON.stringify(mcpJson, null, 2) + '\n';
|
|
260
|
+
try { JSON.parse(mcpSerialized); } catch (e) {
|
|
261
|
+
process.stderr.write(`ERROR: serialization produced invalid JSON: ${e.message}\n`);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
}
|
|
264
|
+
const mcpTmpPath = `${mcpJsonPath}.tmp-${process.pid}`;
|
|
265
|
+
fs.writeFileSync(mcpTmpPath, mcpSerialized);
|
|
266
|
+
fs.renameSync(mcpTmpPath, mcpJsonPath);
|
|
267
|
+
|
|
268
|
+
// Merge <projectDir>/.claude/settings.local.json. Claude Code gates
|
|
269
|
+
// loading of project-scoped MCP servers on the enabledMcpjsonServers
|
|
270
|
+
// allowlist in this file — without the merge, the .mcp.json we just
|
|
271
|
+
// wrote would not actually be loaded.
|
|
272
|
+
if (!fs.existsSync(claudeDir)) {
|
|
273
|
+
fs.mkdirSync(claudeDir, { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let existingSettings = null;
|
|
277
|
+
if (fs.existsSync(settingsLocalPath)) {
|
|
278
|
+
try {
|
|
279
|
+
existingSettings = JSON.parse(fs.readFileSync(settingsLocalPath, 'utf8'));
|
|
280
|
+
} catch (e) {
|
|
281
|
+
process.stderr.write(`ERROR: ${settingsLocalPath} is not valid JSON: ${e.message}\n`);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Snapshot serialized form BEFORE mutating so the change-detection
|
|
287
|
+
// below can't be fooled by object aliasing (existingSettings and
|
|
288
|
+
// settings point at the same object after the `|| {}`).
|
|
289
|
+
const beforeSerialized = existingSettings ? JSON.stringify(existingSettings) : null;
|
|
290
|
+
const settings = existingSettings || {};
|
|
291
|
+
|
|
292
|
+
const enabled = new Set(Array.isArray(settings.enabledMcpjsonServers) ? settings.enabledMcpjsonServers : []);
|
|
293
|
+
enabled.add('claude-sym-mesh');
|
|
294
|
+
settings.enabledMcpjsonServers = Array.from(enabled);
|
|
295
|
+
settings.enableAllProjectMcpServers = true;
|
|
296
|
+
|
|
297
|
+
const afterSerialized = JSON.stringify(settings);
|
|
298
|
+
const settingsChanged = beforeSerialized !== afterSerialized;
|
|
299
|
+
|
|
300
|
+
let settingsBackupPath = null;
|
|
301
|
+
if (settingsChanged) {
|
|
302
|
+
if (existingSettings) {
|
|
303
|
+
settingsBackupPath = `${settingsLocalPath}.bak-${ts}`;
|
|
304
|
+
fs.copyFileSync(settingsLocalPath, settingsBackupPath);
|
|
305
|
+
}
|
|
306
|
+
const settingsSerialized = JSON.stringify(settings, null, 2) + '\n';
|
|
307
|
+
const settingsTmpPath = `${settingsLocalPath}.tmp-${process.pid}`;
|
|
308
|
+
fs.writeFileSync(settingsTmpPath, settingsSerialized);
|
|
309
|
+
fs.renameSync(settingsTmpPath, settingsLocalPath);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Print next steps
|
|
313
|
+
const launchCmdProject = `claude --dangerously-load-development-channels server:claude-sym-mesh`;
|
|
314
|
+
const lines = [
|
|
315
|
+
'',
|
|
316
|
+
`✓ sym-mesh-channel configured for project: ${projectDir}`,
|
|
317
|
+
'',
|
|
318
|
+
` Node name: ${projectNodeName}${projectEntryIsStale ? ' (preserved from stale entry)' : ''}`,
|
|
319
|
+
` Mesh group: ${projectGroup || 'default (global _sym._tcp mesh)'}`,
|
|
320
|
+
` Server path: ${serverJsPath}`,
|
|
321
|
+
` Wrote: ${mcpJsonPath}`,
|
|
322
|
+
];
|
|
323
|
+
if (mcpBackupPath) lines.push(` Backup: ${mcpBackupPath}`);
|
|
324
|
+
if (settingsChanged) {
|
|
325
|
+
lines.push(` Updated: ${settingsLocalPath}`);
|
|
326
|
+
if (settingsBackupPath) lines.push(` Backup: ${settingsBackupPath}`);
|
|
327
|
+
}
|
|
328
|
+
lines.push(
|
|
329
|
+
'',
|
|
330
|
+
'Launch Claude Code from this directory:',
|
|
331
|
+
'',
|
|
332
|
+
` ${launchCmdProject}`,
|
|
333
|
+
'',
|
|
334
|
+
'Project-level .mcp.json overrides the global ~/.claude.json entry',
|
|
335
|
+
'when Claude Code runs from this directory. To give each project its',
|
|
336
|
+
'own mesh identity, run `sym-mesh-channel init --project` from each',
|
|
337
|
+
'project root with a distinct SYM_NODE_NAME.',
|
|
338
|
+
'',
|
|
339
|
+
);
|
|
340
|
+
console.log(lines.join('\n'));
|
|
341
|
+
process.exit(0);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Locate Claude Code settings file ──────────────────────────────
|
|
345
|
+
|
|
346
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
347
|
+
|
|
348
|
+
if (!fs.existsSync(claudeJsonPath)) {
|
|
349
|
+
if (isPostinstall) {
|
|
350
|
+
// During postinstall, skip silently if Claude Code isn't installed yet
|
|
351
|
+
console.log('sym-mesh-channel: ~/.claude.json not found — run `sym-mesh-channel init` after installing Claude Code.');
|
|
352
|
+
process.exit(0);
|
|
353
|
+
}
|
|
354
|
+
process.stderr.write(`ERROR: ${claudeJsonPath} not found.\n`);
|
|
355
|
+
process.stderr.write('Claude Code does not appear to be installed (or has not been launched yet).\n');
|
|
356
|
+
process.stderr.write('Install Claude Code from https://claude.com/code first, launch it once, then re-run this installer.\n');
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Read and back up ──────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
let claudeJson;
|
|
363
|
+
try {
|
|
364
|
+
const raw = fs.readFileSync(claudeJsonPath, 'utf8');
|
|
365
|
+
claudeJson = JSON.parse(raw);
|
|
366
|
+
} catch (e) {
|
|
367
|
+
process.stderr.write(`ERROR: ${claudeJsonPath} is not valid JSON: ${e.message}\n`);
|
|
368
|
+
process.stderr.write('Refusing to overwrite a corrupt Claude Code settings file.\n');
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// `ts` was defined above, shared with project-mode install
|
|
373
|
+
const backupPath = `${claudeJsonPath}.bak-${ts}`;
|
|
374
|
+
fs.copyFileSync(claudeJsonPath, backupPath);
|
|
375
|
+
|
|
376
|
+
// ── Find the MCP servers entry to insert into ───────────────────
|
|
377
|
+
// Write to global mcpServers (available in all Claude Code sessions),
|
|
378
|
+
// not project-scoped. A mesh node should be available everywhere.
|
|
379
|
+
|
|
380
|
+
if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
|
|
381
|
+
|
|
382
|
+
// ── doctor: report-only scan, no writes ──────────────────────────
|
|
383
|
+
// Surface every claude-sym-mesh entry (user-global + every project-scope)
|
|
384
|
+
// with whether its server.js is reachable and what node name it uses.
|
|
385
|
+
// Useful when /mcp reports "Failed to reconnect" and the user wants to
|
|
386
|
+
// inspect scope conflicts without mutating state.
|
|
387
|
+
|
|
388
|
+
if (cmd === 'doctor') {
|
|
389
|
+
const rows = [];
|
|
390
|
+
const topEntry = claudeJson.mcpServers['claude-sym-mesh'];
|
|
391
|
+
if (topEntry) {
|
|
392
|
+
rows.push({
|
|
393
|
+
scope: 'user-global',
|
|
394
|
+
path: (topEntry.args || [])[0] || '(no path)',
|
|
395
|
+
node: preserveNodeName(topEntry) || '(no SYM_NODE_NAME)',
|
|
396
|
+
group: preserveGroup(topEntry) || 'default',
|
|
397
|
+
live: !isStaleEntry(topEntry),
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
const projects = claudeJson.projects && typeof claudeJson.projects === 'object' ? claudeJson.projects : {};
|
|
401
|
+
for (const [projPath, proj] of Object.entries(projects)) {
|
|
402
|
+
const e = proj && proj.mcpServers && proj.mcpServers['claude-sym-mesh'];
|
|
403
|
+
if (!e) continue;
|
|
404
|
+
rows.push({
|
|
405
|
+
scope: `project ${projPath}`,
|
|
406
|
+
path: (e.args || [])[0] || '(no path)',
|
|
407
|
+
node: preserveNodeName(e) || '(no SYM_NODE_NAME)',
|
|
408
|
+
group: preserveGroup(e) || 'default',
|
|
409
|
+
live: !isStaleEntry(e),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
if (rows.length === 0) {
|
|
413
|
+
console.log('No claude-sym-mesh entries found in ~/.claude.json.');
|
|
414
|
+
console.log('Run `sym-mesh-channel init` to configure.');
|
|
415
|
+
process.exit(0);
|
|
416
|
+
}
|
|
417
|
+
console.log('');
|
|
418
|
+
console.log('claude-sym-mesh entries in ~/.claude.json:');
|
|
419
|
+
console.log('');
|
|
420
|
+
for (const r of rows) {
|
|
421
|
+
console.log(` [${r.live ? 'live ' : 'STALE'}] ${r.scope}`);
|
|
422
|
+
console.log(` node: ${r.node}`);
|
|
423
|
+
console.log(` group: ${r.group}`);
|
|
424
|
+
console.log(` path: ${r.path}`);
|
|
425
|
+
}
|
|
426
|
+
const staleCount = rows.filter((r) => !r.live).length;
|
|
427
|
+
|
|
428
|
+
// Heuristic: if multiple entries reference the same Claude identity
|
|
429
|
+
// (same machine) but disagree on group, peers will see each other as
|
|
430
|
+
// disconnected — same incident pattern that cost ~24h of duplex outage
|
|
431
|
+
// at SYM.BOT (CMO=default vs COO=sym-bot-team, 2026-05-02). Surface as
|
|
432
|
+
// a warning so users can spot the mismatch before reaching for the
|
|
433
|
+
// troubleshooting section.
|
|
434
|
+
const groups = new Set(rows.map((r) => r.group));
|
|
435
|
+
const groupMismatch = rows.length > 1 && groups.size > 1;
|
|
436
|
+
|
|
437
|
+
console.log('');
|
|
438
|
+
if (staleCount > 0) {
|
|
439
|
+
console.log(`${staleCount} stale entr${staleCount === 1 ? 'y' : 'ies'} — run \`sym-mesh-channel init\` to heal.`);
|
|
440
|
+
} else {
|
|
441
|
+
console.log('All entries are live.');
|
|
442
|
+
}
|
|
443
|
+
if (groupMismatch) {
|
|
444
|
+
console.log('');
|
|
445
|
+
console.log(`⚠ Group mismatch across entries: ${Array.from(groups).join(', ')}.`);
|
|
446
|
+
console.log(' Nodes in different groups cannot discover each other on Bonjour.');
|
|
447
|
+
console.log(' If teammates expect to see each other, align the SYM_GROUP env var.');
|
|
448
|
+
console.log(' See README "Team mesh groups → Persisting your group across restarts".');
|
|
449
|
+
}
|
|
450
|
+
process.exit(0);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// ── Classify the top-level entry ─────────────────────────────────
|
|
454
|
+
|
|
455
|
+
const existingTopEntry = claudeJson.mcpServers['claude-sym-mesh'];
|
|
456
|
+
const topEntryIsStale = isStaleEntry(existingTopEntry);
|
|
457
|
+
|
|
458
|
+
// Refuse to overwrite a LIVE entry without --force. A stale entry is
|
|
459
|
+
// always rewritable — see isStaleEntry comment at top of file.
|
|
460
|
+
if (existingTopEntry && !force && !topEntryIsStale) {
|
|
461
|
+
if (isPostinstall) {
|
|
462
|
+
// During postinstall, silently skip if already configured and live
|
|
463
|
+
console.log('sym-mesh-channel: already configured in ~/.claude.json (skipping)');
|
|
464
|
+
process.exit(0);
|
|
465
|
+
}
|
|
466
|
+
process.stderr.write(`'claude-sym-mesh' is already configured in ~/.claude.json.\n`);
|
|
467
|
+
process.stderr.write('Re-run with --force to overwrite, or remove the existing entry first.\n');
|
|
468
|
+
process.exit(2);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Preserve the prior node name on rewrite so mesh identity doesn't drift.
|
|
472
|
+
const topNodeName = preserveNodeName(existingTopEntry) || nodeName;
|
|
473
|
+
|
|
474
|
+
// Resolve SYM_GROUP for the global entry — see resolveGroup() at top.
|
|
475
|
+
// Heal-path default preserves; --force lets the user explicitly switch
|
|
476
|
+
// groups (or back to default mesh) in one command.
|
|
477
|
+
const topGroup = resolveGroup(existingTopEntry);
|
|
478
|
+
|
|
479
|
+
// ── Build the entry ───────────────────────────────────────────────
|
|
480
|
+
|
|
481
|
+
const entry = {
|
|
482
|
+
command: 'node',
|
|
483
|
+
args: [serverJsPath],
|
|
484
|
+
env: {
|
|
485
|
+
SYM_NODE_NAME: topNodeName,
|
|
486
|
+
// Explicitly blank the relay vars so the MCP doesn't inherit them
|
|
487
|
+
// from the parent shell (e.g. ~/.zshrc exports). Claude Code's env
|
|
488
|
+
// block is ADDITIVE — omitting a key doesn't remove it from the
|
|
489
|
+
// child process. Setting to '' makes process.env.SYM_RELAY_URL
|
|
490
|
+
// falsy in JS, so the SymNode skips the relay and runs LAN-only.
|
|
491
|
+
//
|
|
492
|
+
// To enable cross-network connectivity later, replace these empty
|
|
493
|
+
// values with your relay URL and token (see README).
|
|
494
|
+
SYM_RELAY_URL: '',
|
|
495
|
+
SYM_RELAY_TOKEN: '',
|
|
496
|
+
},
|
|
497
|
+
};
|
|
498
|
+
// SYM_GROUP only emitted when explicitly chosen — see project-mode comment
|
|
499
|
+
// for the rationale. Omitted = node uses the global _sym._tcp default.
|
|
500
|
+
if (topGroup) entry.env.SYM_GROUP = topGroup;
|
|
501
|
+
|
|
502
|
+
claudeJson.mcpServers['claude-sym-mesh'] = entry;
|
|
503
|
+
|
|
504
|
+
// ── Heal stale project-scoped entries ─────────────────────────────
|
|
505
|
+
// ~/.claude.json can contain per-project mcpServers overrides under
|
|
506
|
+
// claudeJson.projects[<path>].mcpServers. Claude Code prefers project-scoped
|
|
507
|
+
// over user-global when launched from that directory, so a stale project
|
|
508
|
+
// entry silently shadows a fresh user-global heal. Scan every project,
|
|
509
|
+
// rewrite any claude-sym-mesh entry whose args[0] is missing on disk,
|
|
510
|
+
// preserving the project's SYM_NODE_NAME.
|
|
511
|
+
|
|
512
|
+
const healedProjects = [];
|
|
513
|
+
const projects = claudeJson.projects && typeof claudeJson.projects === 'object' ? claudeJson.projects : {};
|
|
514
|
+
for (const [projPath, proj] of Object.entries(projects)) {
|
|
515
|
+
const projEntry = proj && proj.mcpServers && proj.mcpServers['claude-sym-mesh'];
|
|
516
|
+
if (!projEntry) continue;
|
|
517
|
+
if (!isStaleEntry(projEntry)) continue;
|
|
518
|
+
const projNodeName = preserveNodeName(projEntry) || nodeName;
|
|
519
|
+
// Preserve SYM_GROUP on stale-heal — same reason as preserveNodeName.
|
|
520
|
+
// The user explicitly chose this group at some prior install; healing a
|
|
521
|
+
// path issue must not silently revert their group membership.
|
|
522
|
+
const projGroupName = preserveGroup(projEntry);
|
|
523
|
+
const healedEntry = {
|
|
524
|
+
command: 'node',
|
|
525
|
+
args: [serverJsPath],
|
|
526
|
+
env: {
|
|
527
|
+
SYM_NODE_NAME: projNodeName,
|
|
528
|
+
SYM_RELAY_URL: projEntry.env && typeof projEntry.env.SYM_RELAY_URL === 'string' ? projEntry.env.SYM_RELAY_URL : '',
|
|
529
|
+
SYM_RELAY_TOKEN: projEntry.env && typeof projEntry.env.SYM_RELAY_TOKEN === 'string' ? projEntry.env.SYM_RELAY_TOKEN : '',
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
if (projGroupName) healedEntry.env.SYM_GROUP = projGroupName;
|
|
533
|
+
proj.mcpServers['claude-sym-mesh'] = healedEntry;
|
|
534
|
+
healedProjects.push({ path: projPath, node: projNodeName, group: projGroupName });
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ── Atomic write ──────────────────────────────────────────────────
|
|
538
|
+
|
|
539
|
+
const serialized = JSON.stringify(claudeJson, null, 2);
|
|
540
|
+
|
|
541
|
+
// Validate round-trip parses
|
|
542
|
+
try {
|
|
543
|
+
JSON.parse(serialized);
|
|
544
|
+
} catch (e) {
|
|
545
|
+
process.stderr.write(`ERROR: serialization produced invalid JSON: ${e.message}\n`);
|
|
546
|
+
process.stderr.write(`Backup is at ${backupPath} — your original file is unchanged.\n`);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const tmpPath = `${claudeJsonPath}.tmp-${process.pid}`;
|
|
551
|
+
try {
|
|
552
|
+
fs.writeFileSync(tmpPath, serialized);
|
|
553
|
+
fs.renameSync(tmpPath, claudeJsonPath);
|
|
554
|
+
} catch (e) {
|
|
555
|
+
// EBUSY on Windows when Claude Code has ~/.claude.json locked
|
|
556
|
+
if (e.code === 'EBUSY' || e.code === 'EPERM') {
|
|
557
|
+
try { fs.unlinkSync(tmpPath); } catch {}
|
|
558
|
+
if (isPostinstall) {
|
|
559
|
+
console.log('sym-mesh-channel: ~/.claude.json is locked (Claude Code may be running).');
|
|
560
|
+
console.log('Run `sym-mesh-channel init` after quitting Claude Code.');
|
|
561
|
+
process.exit(0);
|
|
562
|
+
}
|
|
563
|
+
process.stderr.write(`ERROR: ${claudeJsonPath} is locked — Claude Code may be running.\n`);
|
|
564
|
+
process.stderr.write('Quit Claude Code, then re-run: sym-mesh-channel init\n');
|
|
565
|
+
process.stderr.write(`Backup is at ${backupPath}\n`);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
throw e;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ── Print next steps ──────────────────────────────────────────────
|
|
572
|
+
|
|
573
|
+
const launchCmd = `claude --dangerously-load-development-channels server:claude-sym-mesh`;
|
|
574
|
+
|
|
575
|
+
const healedLines = healedProjects.length
|
|
576
|
+
? '\n Healed stale project-scoped entries (now pointing at fresh server.js):\n' +
|
|
577
|
+
healedProjects.map((p) => ` • ${p.path} (node: ${p.node}${p.group ? `, group: ${p.group}` : ''})`).join('\n') + '\n'
|
|
578
|
+
: '';
|
|
579
|
+
|
|
580
|
+
const nodeNameSuffix = topEntryIsStale ? ' (preserved from stale entry)' : '';
|
|
581
|
+
|
|
582
|
+
console.log(`
|
|
583
|
+
✓ sym-mesh-channel configured globally in ~/.claude.json
|
|
584
|
+
|
|
585
|
+
Node name: ${topNodeName}${nodeNameSuffix}
|
|
586
|
+
Mesh group: ${topGroup || 'default (global _sym._tcp mesh)'}
|
|
587
|
+
Server path: ${serverJsPath}
|
|
588
|
+
Backup: ${backupPath}
|
|
589
|
+
${healedLines}
|
|
590
|
+
Launch Claude Code with the Channels flag:
|
|
591
|
+
|
|
592
|
+
${launchCmd}
|
|
593
|
+
|
|
594
|
+
Inside Claude Code, verify:
|
|
595
|
+
|
|
596
|
+
sym_status → node id, relay state, peer count
|
|
597
|
+
sym_peers → discovered peers via Bonjour or relay
|
|
598
|
+
sym_send "hello mesh" → broadcast to all peers
|
|
599
|
+
|
|
600
|
+
Troubleshoot a broken install with:
|
|
601
|
+
|
|
602
|
+
sym-mesh-channel doctor
|
|
603
|
+
`);
|