@dolusoft/claude-collab 1.10.3 → 1.11.0
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/README.md +146 -56
- package/dist/cli.js +313 -477
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +312 -476
- package/dist/mcp-main.js.map +1 -1
- package/package.json +80 -81
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
5
|
-
import dgram from 'dgram';
|
|
6
5
|
import os, { tmpdir } from 'os';
|
|
6
|
+
import { spawn, execFile } from 'child_process';
|
|
7
7
|
import { EventEmitter } from 'events';
|
|
8
|
-
import { execFile, spawn } from 'child_process';
|
|
9
8
|
import { unlinkSync } from 'fs';
|
|
10
9
|
import { join } from 'path';
|
|
11
10
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
@@ -19,131 +18,33 @@ function serialize(msg) {
|
|
|
19
18
|
function parse(data) {
|
|
20
19
|
return JSON.parse(data);
|
|
21
20
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
peers = /* @__PURE__ */ new Map();
|
|
31
|
-
myName = "";
|
|
32
|
-
myWsPort = 0;
|
|
33
|
-
start(name, wsPort) {
|
|
34
|
-
this.myName = name;
|
|
35
|
-
this.myWsPort = wsPort;
|
|
36
|
-
const socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
|
37
|
-
this.socket = socket;
|
|
38
|
-
socket.on("error", (err) => {
|
|
39
|
-
console.error("[multicast] socket error:", err.message);
|
|
40
|
-
});
|
|
41
|
-
socket.on("message", (buf, rinfo) => {
|
|
42
|
-
try {
|
|
43
|
-
const msg = JSON.parse(buf.toString());
|
|
44
|
-
this.handleMessage(msg, rinfo.address);
|
|
45
|
-
} catch {
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
socket.bind(MULTICAST_PORT, () => {
|
|
49
|
-
try {
|
|
50
|
-
socket.addMembership(MULTICAST_ADDR);
|
|
51
|
-
socket.setMulticastTTL(1);
|
|
52
|
-
socket.setMulticastLoopback(false);
|
|
53
|
-
} catch (err) {
|
|
54
|
-
console.error("[multicast] membership error:", err);
|
|
55
|
-
}
|
|
56
|
-
this.announce();
|
|
57
|
-
this.heartbeatTimer = setInterval(() => this.announce(), HEARTBEAT_INTERVAL_MS);
|
|
58
|
-
this.timeoutTimer = setInterval(() => this.checkTimeouts(), 5e3);
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
stop() {
|
|
62
|
-
if (this.heartbeatTimer) {
|
|
63
|
-
clearInterval(this.heartbeatTimer);
|
|
64
|
-
this.heartbeatTimer = null;
|
|
65
|
-
}
|
|
66
|
-
if (this.timeoutTimer) {
|
|
67
|
-
clearInterval(this.timeoutTimer);
|
|
68
|
-
this.timeoutTimer = null;
|
|
69
|
-
}
|
|
70
|
-
if (this.socket) {
|
|
71
|
-
this.sendMessage({ type: "LEAVE", name: this.myName });
|
|
72
|
-
try {
|
|
73
|
-
this.socket.dropMembership(MULTICAST_ADDR);
|
|
74
|
-
this.socket.close();
|
|
75
|
-
} catch {
|
|
76
|
-
}
|
|
77
|
-
this.socket = null;
|
|
78
|
-
}
|
|
79
|
-
this.peers.clear();
|
|
80
|
-
}
|
|
81
|
-
resolveLocalIp() {
|
|
82
|
-
const interfaces = os.networkInterfaces();
|
|
83
|
-
for (const iface of Object.values(interfaces)) {
|
|
84
|
-
if (!iface) continue;
|
|
85
|
-
for (const addr of iface) {
|
|
86
|
-
if (addr.family === "IPv4" && !addr.internal) return addr.address;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return "127.0.0.1";
|
|
90
|
-
}
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
// Private
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
announce() {
|
|
95
|
-
this.sendMessage({ type: "ANNOUNCE", name: this.myName, wsPort: this.myWsPort });
|
|
96
|
-
}
|
|
97
|
-
sendMessage(msg) {
|
|
98
|
-
if (!this.socket) return;
|
|
99
|
-
const buf = Buffer.from(JSON.stringify(msg));
|
|
100
|
-
this.socket.send(buf, MULTICAST_PORT, MULTICAST_ADDR, (err) => {
|
|
101
|
-
if (err) console.error("[multicast] send error:", err.message);
|
|
21
|
+
function runNetshElevated(argArray) {
|
|
22
|
+
const argList = argArray.map((a) => `"${a}"`).join(",");
|
|
23
|
+
const psCommand = `Start-Process -FilePath "netsh" -ArgumentList @(${argList}) -Verb RunAs -Wait`;
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const ps = spawn("powershell", ["-NoProfile", "-Command", psCommand]);
|
|
26
|
+
ps.on("close", (code) => {
|
|
27
|
+
if (code === 0) resolve();
|
|
28
|
+
else reject(new Error(`Firewall UAC prompt was cancelled or denied (exit code ${code}).`));
|
|
102
29
|
});
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!this.socket) return;
|
|
106
|
-
const buf = Buffer.from(JSON.stringify({ type: "ANNOUNCE", name: this.myName, wsPort: this.myWsPort, unicast: true }));
|
|
107
|
-
this.socket.send(buf, MULTICAST_PORT, toIp, (err) => {
|
|
108
|
-
if (err) console.error(`[multicast] unicast reply to ${toIp} error:`, err.message);
|
|
30
|
+
ps.on("error", (err) => {
|
|
31
|
+
reject(new Error(`Failed to launch PowerShell: ${err.message}`));
|
|
109
32
|
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
this.sendUnicast(fromIp);
|
|
126
|
-
}
|
|
127
|
-
this.emit("peer-found", { name: msg.name, ip: fromIp, wsPort: msg.wsPort });
|
|
128
|
-
} else if (msg.type === "LEAVE") {
|
|
129
|
-
if (this.peers.has(msg.name)) {
|
|
130
|
-
this.peers.delete(msg.name);
|
|
131
|
-
this.emit("peer-lost", msg.name);
|
|
132
|
-
console.error(`[multicast] peer left: ${msg.name}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
checkTimeouts() {
|
|
137
|
-
const now = Date.now();
|
|
138
|
-
for (const [name, peer] of this.peers) {
|
|
139
|
-
if (now - peer.lastSeen > PEER_TIMEOUT_MS) {
|
|
140
|
-
this.peers.delete(name);
|
|
141
|
-
this.emit("peer-lost", name);
|
|
142
|
-
console.error(`[multicast] peer timed out: ${name}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function addFirewallRule(port) {
|
|
36
|
+
await runNetshElevated([
|
|
37
|
+
"advfirewall",
|
|
38
|
+
"firewall",
|
|
39
|
+
"add",
|
|
40
|
+
"rule",
|
|
41
|
+
`name=claude-collab-${port}`,
|
|
42
|
+
"protocol=TCP",
|
|
43
|
+
"dir=in",
|
|
44
|
+
`localport=${port}`,
|
|
45
|
+
"action=allow"
|
|
46
|
+
]);
|
|
47
|
+
}
|
|
147
48
|
var CS_CONINJECT = `
|
|
148
49
|
using System;
|
|
149
50
|
using System.Collections.Generic;
|
|
@@ -363,17 +264,18 @@ var InjectionQueue = class extends EventEmitter {
|
|
|
363
264
|
var injectionQueue = new InjectionQueue();
|
|
364
265
|
|
|
365
266
|
// src/infrastructure/p2p/p2p-node.ts
|
|
267
|
+
var FIXED_PORT = 12345;
|
|
366
268
|
var P2PNode = class {
|
|
367
|
-
constructor(portRange = [1e4, 19999]) {
|
|
368
|
-
this.portRange = portRange;
|
|
369
|
-
}
|
|
370
269
|
server = null;
|
|
371
270
|
myName = "";
|
|
372
271
|
running = false;
|
|
272
|
+
firewallOpened = false;
|
|
373
273
|
// One connection per peer (inbound or outbound — whichever was established first)
|
|
374
274
|
peerConnections = /* @__PURE__ */ new Map();
|
|
375
275
|
// Reverse map: ws → peer name (only for registered connections)
|
|
376
276
|
wsToName = /* @__PURE__ */ new Map();
|
|
277
|
+
// IP of each known peer: name → ip
|
|
278
|
+
peerIPs = /* @__PURE__ */ new Map();
|
|
377
279
|
// Prevent duplicate outbound connect attempts
|
|
378
280
|
connectingPeers = /* @__PURE__ */ new Set();
|
|
379
281
|
incomingQuestions = /* @__PURE__ */ new Map();
|
|
@@ -385,8 +287,6 @@ var P2PNode = class {
|
|
|
385
287
|
answerWaiters = /* @__PURE__ */ new Map();
|
|
386
288
|
// Answers queued for offline peers: peerName → AnswerMsg (delivered on reconnect)
|
|
387
289
|
pendingOutboundAnswers = /* @__PURE__ */ new Map();
|
|
388
|
-
discovery = null;
|
|
389
|
-
boundPort = 0;
|
|
390
290
|
// ---------------------------------------------------------------------------
|
|
391
291
|
// ICollabClient implementation
|
|
392
292
|
// ---------------------------------------------------------------------------
|
|
@@ -394,7 +294,7 @@ var P2PNode = class {
|
|
|
394
294
|
return this.running;
|
|
395
295
|
}
|
|
396
296
|
get port() {
|
|
397
|
-
return
|
|
297
|
+
return FIXED_PORT;
|
|
398
298
|
}
|
|
399
299
|
get currentTeamId() {
|
|
400
300
|
return this.myName || void 0;
|
|
@@ -402,16 +302,67 @@ var P2PNode = class {
|
|
|
402
302
|
async join(name, displayName) {
|
|
403
303
|
this.myName = name;
|
|
404
304
|
await this.startServer();
|
|
405
|
-
this.startDiscovery();
|
|
406
305
|
return {
|
|
407
306
|
memberId: v4(),
|
|
408
307
|
teamId: name,
|
|
409
308
|
teamName: name,
|
|
410
309
|
displayName,
|
|
411
310
|
status: "ONLINE",
|
|
412
|
-
port:
|
|
311
|
+
port: FIXED_PORT
|
|
413
312
|
};
|
|
414
313
|
}
|
|
314
|
+
async connectByIp(ip) {
|
|
315
|
+
if (!this.firewallOpened) {
|
|
316
|
+
await addFirewallRule(FIXED_PORT);
|
|
317
|
+
this.firewallOpened = true;
|
|
318
|
+
}
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
const url = `ws://${ip}:${FIXED_PORT}`;
|
|
321
|
+
const ws = new WebSocket(url);
|
|
322
|
+
const timeout = setTimeout(() => {
|
|
323
|
+
ws.terminate();
|
|
324
|
+
reject(new Error(`Connection to ${ip}:${FIXED_PORT} timed out`));
|
|
325
|
+
}, 1e4);
|
|
326
|
+
ws.on("open", () => {
|
|
327
|
+
this.sendToWs(ws, { type: "HELLO", name: this.myName });
|
|
328
|
+
});
|
|
329
|
+
ws.on("message", (data) => {
|
|
330
|
+
try {
|
|
331
|
+
const msg = parse(data.toString());
|
|
332
|
+
if (msg.type === "HELLO_ACK") {
|
|
333
|
+
clearTimeout(timeout);
|
|
334
|
+
if (this.peerConnections.has(msg.name)) {
|
|
335
|
+
ws.terminate();
|
|
336
|
+
resolve(msg.name);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
this.registerPeer(msg.name, ip, ws);
|
|
340
|
+
this.afterHandshake(msg.name, ws);
|
|
341
|
+
resolve(msg.name);
|
|
342
|
+
}
|
|
343
|
+
this.handleMessage(ws, msg);
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
ws.on("close", () => {
|
|
348
|
+
clearTimeout(timeout);
|
|
349
|
+
this.connectingPeers.delete(ip);
|
|
350
|
+
const name = this.wsToName.get(ws);
|
|
351
|
+
if (name) {
|
|
352
|
+
this.wsToName.delete(ws);
|
|
353
|
+
if (this.peerConnections.get(name) === ws) {
|
|
354
|
+
this.peerConnections.delete(name);
|
|
355
|
+
console.error(`[p2p] disconnected from peer: ${name}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
ws.on("error", (err) => {
|
|
360
|
+
clearTimeout(timeout);
|
|
361
|
+
this.connectingPeers.delete(ip);
|
|
362
|
+
reject(new Error(`Failed to connect to ${ip}:${FIXED_PORT} \u2014 ${err.message}`));
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
}
|
|
415
366
|
async ask(toPeer, content, format) {
|
|
416
367
|
const ws = this.peerConnections.get(toPeer);
|
|
417
368
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
@@ -429,9 +380,7 @@ var P2PNode = class {
|
|
|
429
380
|
}
|
|
430
381
|
waitForAnswer(questionId, timeoutMs) {
|
|
431
382
|
const cached = this.receivedAnswers.get(questionId);
|
|
432
|
-
if (cached)
|
|
433
|
-
return Promise.resolve(this.formatAnswer(questionId, cached));
|
|
434
|
-
}
|
|
383
|
+
if (cached) return Promise.resolve(this.formatAnswer(questionId, cached));
|
|
435
384
|
return new Promise((resolve) => {
|
|
436
385
|
const timeout = setTimeout(() => {
|
|
437
386
|
this.answerWaiters.delete(questionId);
|
|
@@ -498,10 +447,22 @@ var P2PNode = class {
|
|
|
498
447
|
getInfo() {
|
|
499
448
|
return {
|
|
500
449
|
teamName: this.myName,
|
|
501
|
-
port:
|
|
502
|
-
connectedPeers: [...this.peerConnections.keys()]
|
|
450
|
+
port: FIXED_PORT,
|
|
451
|
+
connectedPeers: [...this.peerConnections.keys()],
|
|
452
|
+
peerIPs: Object.fromEntries(this.peerIPs)
|
|
503
453
|
};
|
|
504
454
|
}
|
|
455
|
+
getLocalIps() {
|
|
456
|
+
const result = [];
|
|
457
|
+
const interfaces = os.networkInterfaces();
|
|
458
|
+
for (const iface of Object.values(interfaces)) {
|
|
459
|
+
if (!iface) continue;
|
|
460
|
+
for (const addr of iface) {
|
|
461
|
+
if (addr.family === "IPv4" && !addr.internal) result.push(addr.address);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
505
466
|
getHistory() {
|
|
506
467
|
const entries = [];
|
|
507
468
|
for (const [questionId, sent] of this.sentQuestions) {
|
|
@@ -528,11 +489,10 @@ var P2PNode = class {
|
|
|
528
489
|
return entries.sort((a, b) => a.askedAt.localeCompare(b.askedAt));
|
|
529
490
|
}
|
|
530
491
|
async disconnect() {
|
|
531
|
-
this.discovery?.stop();
|
|
532
|
-
this.discovery = null;
|
|
533
492
|
for (const ws of this.peerConnections.values()) ws.close();
|
|
534
493
|
this.peerConnections.clear();
|
|
535
494
|
this.wsToName.clear();
|
|
495
|
+
this.peerIPs.clear();
|
|
536
496
|
this.server?.close();
|
|
537
497
|
this.server = null;
|
|
538
498
|
this.running = false;
|
|
@@ -541,40 +501,28 @@ var P2PNode = class {
|
|
|
541
501
|
// Private: server startup
|
|
542
502
|
// ---------------------------------------------------------------------------
|
|
543
503
|
startServer() {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const wss = new WebSocketServer({ port });
|
|
553
|
-
wss.once("listening", () => {
|
|
554
|
-
this.server = wss;
|
|
555
|
-
this.boundPort = port;
|
|
556
|
-
this.running = true;
|
|
557
|
-
console.error(`[p2p] listening on port ${port} as "${this.myName}"`);
|
|
558
|
-
this.attachServerHandlers(wss);
|
|
559
|
-
resolve();
|
|
560
|
-
});
|
|
561
|
-
wss.once("error", (err) => {
|
|
562
|
-
wss.close();
|
|
563
|
-
if (err.code === "EADDRINUSE") {
|
|
564
|
-
tryBind(attemptsLeft - 1).then(resolve, reject);
|
|
565
|
-
} else {
|
|
566
|
-
reject(err);
|
|
567
|
-
}
|
|
568
|
-
});
|
|
504
|
+
return new Promise((resolve, reject) => {
|
|
505
|
+
const wss = new WebSocketServer({ port: FIXED_PORT });
|
|
506
|
+
wss.once("listening", () => {
|
|
507
|
+
this.server = wss;
|
|
508
|
+
this.running = true;
|
|
509
|
+
console.error(`[p2p] listening on port ${FIXED_PORT} as "${this.myName}"`);
|
|
510
|
+
this.attachServerHandlers(wss);
|
|
511
|
+
resolve();
|
|
569
512
|
});
|
|
570
|
-
|
|
571
|
-
|
|
513
|
+
wss.once("error", (err) => {
|
|
514
|
+
wss.close();
|
|
515
|
+
reject(err);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
572
518
|
}
|
|
573
519
|
attachServerHandlers(wss) {
|
|
574
|
-
wss.on("connection", (ws) => {
|
|
520
|
+
wss.on("connection", (ws, request) => {
|
|
521
|
+
const rawIp = request.socket.remoteAddress ?? "";
|
|
522
|
+
const remoteIp = rawIp.replace(/^::ffff:/, "");
|
|
575
523
|
ws.on("message", (data) => {
|
|
576
524
|
try {
|
|
577
|
-
this.
|
|
525
|
+
this.handleInboundMessage(ws, remoteIp, parse(data.toString()));
|
|
578
526
|
} catch {
|
|
579
527
|
}
|
|
580
528
|
});
|
|
@@ -597,21 +545,35 @@ var P2PNode = class {
|
|
|
597
545
|
});
|
|
598
546
|
}
|
|
599
547
|
// ---------------------------------------------------------------------------
|
|
600
|
-
// Private:
|
|
548
|
+
// Private: mesh helpers
|
|
601
549
|
// ---------------------------------------------------------------------------
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
this.
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
550
|
+
/** Called once handshake completes — exchange peer lists and announce to existing peers. */
|
|
551
|
+
afterHandshake(newPeerName, ws) {
|
|
552
|
+
const listForNew = [...this.peerConnections.keys()].filter((n) => n !== newPeerName).map((n) => ({ name: n, ip: this.peerIPs.get(n) ?? "" })).filter((p) => p.ip !== "");
|
|
553
|
+
this.sendToWs(ws, { type: "PEER_LIST", peers: listForNew });
|
|
554
|
+
const newIp = this.peerIPs.get(newPeerName);
|
|
555
|
+
if (newIp) {
|
|
556
|
+
for (const [name, existingWs] of this.peerConnections) {
|
|
557
|
+
if (name !== newPeerName) {
|
|
558
|
+
this.sendToWs(existingWs, { type: "PEER_ANNOUNCE", name: newPeerName, ip: newIp });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
611
562
|
}
|
|
612
|
-
|
|
613
|
-
this.
|
|
614
|
-
|
|
563
|
+
registerPeer(name, ip, ws) {
|
|
564
|
+
this.peerConnections.set(name, ws);
|
|
565
|
+
this.wsToName.set(ws, name);
|
|
566
|
+
this.peerIPs.set(name, ip);
|
|
567
|
+
this.connectingPeers.delete(name);
|
|
568
|
+
console.error(`[p2p] peer registered: ${name} @ ${ip}`);
|
|
569
|
+
this.deliverPendingAnswer(name, ws);
|
|
570
|
+
}
|
|
571
|
+
/** Connect outbound to a peer discovered via PEER_LIST or PEER_ANNOUNCE. */
|
|
572
|
+
connectMeshPeer(name, ip) {
|
|
573
|
+
if (this.peerConnections.has(name)) return;
|
|
574
|
+
if (this.connectingPeers.has(name)) return;
|
|
575
|
+
this.connectingPeers.add(name);
|
|
576
|
+
const ws = new WebSocket(`ws://${ip}:${FIXED_PORT}`);
|
|
615
577
|
ws.on("open", () => {
|
|
616
578
|
this.sendToWs(ws, { type: "HELLO", name: this.myName });
|
|
617
579
|
});
|
|
@@ -622,52 +584,70 @@ var P2PNode = class {
|
|
|
622
584
|
}
|
|
623
585
|
});
|
|
624
586
|
ws.on("close", () => {
|
|
625
|
-
this.connectingPeers.delete(
|
|
626
|
-
const
|
|
627
|
-
if (
|
|
587
|
+
this.connectingPeers.delete(name);
|
|
588
|
+
const peerName = this.wsToName.get(ws);
|
|
589
|
+
if (peerName) {
|
|
628
590
|
this.wsToName.delete(ws);
|
|
629
|
-
if (this.peerConnections.get(
|
|
630
|
-
this.peerConnections.delete(
|
|
631
|
-
console.error(`[p2p] disconnected from peer: ${
|
|
591
|
+
if (this.peerConnections.get(peerName) === ws) {
|
|
592
|
+
this.peerConnections.delete(peerName);
|
|
593
|
+
console.error(`[p2p] disconnected from mesh peer: ${peerName}`);
|
|
632
594
|
}
|
|
633
595
|
}
|
|
634
596
|
});
|
|
635
597
|
ws.on("error", (err) => {
|
|
636
|
-
console.error(`[p2p] connect to "${
|
|
637
|
-
this.connectingPeers.delete(
|
|
598
|
+
console.error(`[p2p] mesh connect to "${name}" @ ${ip} failed: ${err.message}`);
|
|
599
|
+
this.connectingPeers.delete(name);
|
|
638
600
|
});
|
|
639
601
|
}
|
|
640
602
|
// ---------------------------------------------------------------------------
|
|
641
603
|
// Private: message handling
|
|
642
604
|
// ---------------------------------------------------------------------------
|
|
605
|
+
/** Handles messages on inbound connections (server side — we know the remote IP). */
|
|
606
|
+
handleInboundMessage(ws, remoteIp, msg) {
|
|
607
|
+
for (const handler of this.pendingHandlers) handler(msg);
|
|
608
|
+
if (msg.type === "HELLO") {
|
|
609
|
+
if (this.peerConnections.has(msg.name)) {
|
|
610
|
+
ws.terminate();
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
this.registerPeer(msg.name, remoteIp, ws);
|
|
614
|
+
this.sendToWs(ws, { type: "HELLO_ACK", name: this.myName });
|
|
615
|
+
console.error(`[p2p] peer joined (inbound): ${msg.name}`);
|
|
616
|
+
this.afterHandshake(msg.name, ws);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
this.handleMessage(ws, msg);
|
|
620
|
+
}
|
|
621
|
+
/** Handles all other messages (both inbound and outbound connections). */
|
|
643
622
|
handleMessage(ws, msg) {
|
|
644
623
|
for (const handler of this.pendingHandlers) handler(msg);
|
|
645
624
|
switch (msg.type) {
|
|
646
|
-
case "
|
|
647
|
-
if (this.peerConnections.has(msg.name)) {
|
|
648
|
-
|
|
649
|
-
|
|
625
|
+
case "HELLO_ACK":
|
|
626
|
+
if (!this.peerConnections.has(msg.name)) {
|
|
627
|
+
this.peerConnections.set(msg.name, ws);
|
|
628
|
+
this.wsToName.set(ws, msg.name);
|
|
629
|
+
this.connectingPeers.delete(msg.name);
|
|
630
|
+
console.error(`[p2p] connected to mesh peer: ${msg.name}`);
|
|
631
|
+
this.deliverPendingAnswer(msg.name, ws);
|
|
632
|
+
this.afterHandshake(msg.name, ws);
|
|
650
633
|
}
|
|
651
|
-
this.peerConnections.set(msg.name, ws);
|
|
652
|
-
this.wsToName.set(ws, msg.name);
|
|
653
|
-
this.connectingPeers.delete(msg.name);
|
|
654
|
-
this.sendToWs(ws, { type: "HELLO_ACK", name: this.myName });
|
|
655
|
-
console.error(`[p2p] peer joined (inbound): ${msg.name}`);
|
|
656
|
-
this.deliverPendingAnswer(msg.name, ws);
|
|
657
634
|
break;
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
635
|
+
case "PEER_LIST":
|
|
636
|
+
for (const peer of msg.peers) {
|
|
637
|
+
if (peer.name !== this.myName && !this.peerConnections.has(peer.name)) {
|
|
638
|
+
console.error(`[p2p] mesh: connecting to ${peer.name} @ ${peer.ip} via PEER_LIST`);
|
|
639
|
+
this.peerIPs.set(peer.name, peer.ip);
|
|
640
|
+
this.connectMeshPeer(peer.name, peer.ip);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
break;
|
|
644
|
+
case "PEER_ANNOUNCE":
|
|
645
|
+
if (msg.name !== this.myName && !this.peerConnections.has(msg.name)) {
|
|
646
|
+
console.error(`[p2p] mesh: connecting to ${msg.name} @ ${msg.ip} via PEER_ANNOUNCE`);
|
|
647
|
+
this.peerIPs.set(msg.name, msg.ip);
|
|
648
|
+
this.connectMeshPeer(msg.name, msg.ip);
|
|
663
649
|
}
|
|
664
|
-
this.peerConnections.set(msg.name, ws);
|
|
665
|
-
this.wsToName.set(ws, msg.name);
|
|
666
|
-
this.connectingPeers.delete(msg.name);
|
|
667
|
-
console.error(`[p2p] connected to peer: ${msg.name}`);
|
|
668
|
-
this.deliverPendingAnswer(msg.name, ws);
|
|
669
650
|
break;
|
|
670
|
-
}
|
|
671
651
|
case "ASK":
|
|
672
652
|
this.handleIncomingAsk(ws, msg);
|
|
673
653
|
break;
|
|
@@ -740,6 +720,72 @@ var P2PNode = class {
|
|
|
740
720
|
});
|
|
741
721
|
}
|
|
742
722
|
};
|
|
723
|
+
var CONNECT_DESCRIPTION = `Connect to another Claude instance by their IP address.
|
|
724
|
+
|
|
725
|
+
WHEN TO USE:
|
|
726
|
+
- First time connecting to a teammate
|
|
727
|
+
- A new peer wants to join an existing session
|
|
728
|
+
|
|
729
|
+
WHAT HAPPENS:
|
|
730
|
+
1. First call only: opens Windows Firewall port 12345 via UAC popup (stays open)
|
|
731
|
+
2. Connects directly to the peer at ws://IP:12345
|
|
732
|
+
3. Both sides exchange their peer lists \u2014 everyone connects to everyone (full mesh)
|
|
733
|
+
|
|
734
|
+
FINDING YOUR IP:
|
|
735
|
+
- The other person calls status() to see their LAN IP, then tells you
|
|
736
|
+
|
|
737
|
+
AFTER CONNECTING:
|
|
738
|
+
- Use peers() to confirm connection
|
|
739
|
+
- Use ask() to send questions to connected peers`;
|
|
740
|
+
function registerConnectTool(server, client) {
|
|
741
|
+
server.tool(
|
|
742
|
+
"connect",
|
|
743
|
+
CONNECT_DESCRIPTION,
|
|
744
|
+
{
|
|
745
|
+
ip: z.string().describe("The peer's LAN IP address (e.g. 192.168.1.5)")
|
|
746
|
+
},
|
|
747
|
+
async ({ ip }) => {
|
|
748
|
+
if (!client.isConnected) {
|
|
749
|
+
return {
|
|
750
|
+
content: [{ type: "text", text: "Node is not ready yet. Wait a moment and try again." }],
|
|
751
|
+
isError: true
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
try {
|
|
755
|
+
const peerName = await client.connectByIp(ip);
|
|
756
|
+
const info = client.getInfo();
|
|
757
|
+
const allPeers = info.connectedPeers;
|
|
758
|
+
const others = allPeers.filter((n) => n !== peerName);
|
|
759
|
+
const lines = [
|
|
760
|
+
`Connected to "${peerName}" (${ip}).`
|
|
761
|
+
];
|
|
762
|
+
if (others.length > 0) {
|
|
763
|
+
lines.push(``, `Mesh peers also connecting: ${others.map((n) => `"${n}"`).join(", ")}`);
|
|
764
|
+
}
|
|
765
|
+
lines.push(``, `Use peers() to see all connected peers.`);
|
|
766
|
+
return {
|
|
767
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
768
|
+
};
|
|
769
|
+
} catch (err) {
|
|
770
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
771
|
+
return {
|
|
772
|
+
content: [{
|
|
773
|
+
type: "text",
|
|
774
|
+
text: [
|
|
775
|
+
`Failed to connect to ${ip}: ${msg}`,
|
|
776
|
+
``,
|
|
777
|
+
`Make sure:`,
|
|
778
|
+
` \u2022 The peer is running claude-collab (--name <name>)`,
|
|
779
|
+
` \u2022 The IP address is correct (they can check with status())`,
|
|
780
|
+
` \u2022 Port 12345 is not blocked on their machine`
|
|
781
|
+
].join("\n")
|
|
782
|
+
}],
|
|
783
|
+
isError: true
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
);
|
|
788
|
+
}
|
|
743
789
|
var ASK_DESCRIPTION = `Send a question to another Claude instance on the LAN and wait for their answer.
|
|
744
790
|
|
|
745
791
|
WHEN TO USE:
|
|
@@ -885,22 +931,20 @@ function registerReplyTool(server, client) {
|
|
|
885
931
|
}
|
|
886
932
|
|
|
887
933
|
// src/presentation/mcp/tools/peers.tool.ts
|
|
888
|
-
var PEERS_DESCRIPTION = `
|
|
934
|
+
var PEERS_DESCRIPTION = `List all currently active peer connections.
|
|
889
935
|
|
|
890
936
|
WHEN TO USE:
|
|
891
937
|
- Before calling ask() \u2014 to confirm the target peer is online and get their exact name
|
|
892
|
-
- After
|
|
938
|
+
- After connect() \u2014 to verify the connection succeeded and the mesh formed
|
|
893
939
|
- When a peer seems unreachable \u2014 to check if they are still connected
|
|
894
940
|
|
|
895
941
|
WHAT IT SHOWS:
|
|
896
|
-
- Your own name and
|
|
897
|
-
- All peers
|
|
942
|
+
- Your own name and port
|
|
943
|
+
- All peers with an active direct connection
|
|
898
944
|
|
|
899
945
|
IF NO PEERS ARE LISTED:
|
|
900
|
-
-
|
|
901
|
-
-
|
|
902
|
-
- Call firewall_open() so peers on other machines can connect inbound to you
|
|
903
|
-
- If still no peers, verify all machines are on the same LAN/subnet`;
|
|
946
|
+
- Use connect(ip) to connect to a peer
|
|
947
|
+
- Ask your teammate to run status() to get their IP`;
|
|
904
948
|
function registerPeersTool(server, client) {
|
|
905
949
|
server.tool("peers", PEERS_DESCRIPTION, {}, async () => {
|
|
906
950
|
const info = client.getInfo();
|
|
@@ -927,13 +971,17 @@ function registerPeersTool(server, client) {
|
|
|
927
971
|
`You are "${myName}" (listening on port ${myPort}).`,
|
|
928
972
|
`No peers connected yet.`,
|
|
929
973
|
``,
|
|
930
|
-
`
|
|
931
|
-
`
|
|
974
|
+
`Use connect(ip) to connect to a peer.`,
|
|
975
|
+
`Ask your teammate to run status() to get their IP.`
|
|
932
976
|
].join("\n")
|
|
933
977
|
}]
|
|
934
978
|
};
|
|
935
979
|
}
|
|
936
|
-
const
|
|
980
|
+
const peerIPs = info.peerIPs ?? {};
|
|
981
|
+
const list = connected.map((name) => {
|
|
982
|
+
const ip = peerIPs[name] ? ` (${peerIPs[name]})` : "";
|
|
983
|
+
return ` \u2022 ${name}${ip}`;
|
|
984
|
+
}).join("\n");
|
|
937
985
|
return {
|
|
938
986
|
content: [{
|
|
939
987
|
type: "text",
|
|
@@ -986,263 +1034,51 @@ ${answerLine}`;
|
|
|
986
1034
|
};
|
|
987
1035
|
});
|
|
988
1036
|
}
|
|
989
|
-
function runNetshElevated(argArray) {
|
|
990
|
-
const argList = argArray.map((a) => `"${a}"`).join(",");
|
|
991
|
-
const psCommand = `Start-Process -FilePath "netsh" -ArgumentList @(${argList}) -Verb RunAs -Wait`;
|
|
992
|
-
return new Promise((resolve, reject) => {
|
|
993
|
-
const ps = spawn("powershell", ["-NoProfile", "-Command", psCommand]);
|
|
994
|
-
ps.on("close", (code) => {
|
|
995
|
-
if (code === 0) resolve();
|
|
996
|
-
else reject(new Error(`Firewall UAC prompt was cancelled or denied (exit code ${code}).`));
|
|
997
|
-
});
|
|
998
|
-
ps.on("error", (err) => {
|
|
999
|
-
reject(new Error(`Failed to launch PowerShell: ${err.message}`));
|
|
1000
|
-
});
|
|
1001
|
-
});
|
|
1002
|
-
}
|
|
1003
|
-
async function runNetsh(argArray) {
|
|
1004
|
-
await runNetshElevated(argArray);
|
|
1005
|
-
}
|
|
1006
|
-
async function addFirewallRule(port) {
|
|
1007
|
-
await runNetsh([
|
|
1008
|
-
"advfirewall",
|
|
1009
|
-
"firewall",
|
|
1010
|
-
"add",
|
|
1011
|
-
"rule",
|
|
1012
|
-
`name=claude-collab-${port}`,
|
|
1013
|
-
"protocol=TCP",
|
|
1014
|
-
"dir=in",
|
|
1015
|
-
`localport=${port}`,
|
|
1016
|
-
"action=allow"
|
|
1017
|
-
]);
|
|
1018
|
-
try {
|
|
1019
|
-
await runNetsh([
|
|
1020
|
-
"advfirewall",
|
|
1021
|
-
"firewall",
|
|
1022
|
-
"add",
|
|
1023
|
-
"rule",
|
|
1024
|
-
"name=claude-collab-discovery",
|
|
1025
|
-
"protocol=UDP",
|
|
1026
|
-
"dir=in",
|
|
1027
|
-
"localport=11776",
|
|
1028
|
-
"action=allow"
|
|
1029
|
-
]);
|
|
1030
|
-
} catch {
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
async function removeFirewallRule(port) {
|
|
1034
|
-
await runNetsh([
|
|
1035
|
-
"advfirewall",
|
|
1036
|
-
"firewall",
|
|
1037
|
-
"delete",
|
|
1038
|
-
"rule",
|
|
1039
|
-
`name=claude-collab-${port}`
|
|
1040
|
-
]);
|
|
1041
|
-
try {
|
|
1042
|
-
await runNetsh(["advfirewall", "firewall", "delete", "rule", "name=claude-collab-discovery"]);
|
|
1043
|
-
} catch {
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
1037
|
|
|
1047
|
-
// src/presentation/mcp/tools/
|
|
1048
|
-
var
|
|
1049
|
-
|
|
1050
|
-
WHEN YOU NEED THIS:
|
|
1051
|
-
- Other peers cannot see you in their peers() list even though you are on the same LAN
|
|
1052
|
-
- You just started and want to make sure all peers can reach you
|
|
1053
|
-
- A peer explicitly says they cannot connect to you
|
|
1054
|
-
|
|
1055
|
-
WHEN YOU DO NOT NEED THIS:
|
|
1056
|
-
- You can already see peers in peers() \u2014 the connection is working
|
|
1057
|
-
- You are connecting outbound to others (outbound connections do not require firewall rules)
|
|
1058
|
-
|
|
1059
|
-
WHAT HAPPENS:
|
|
1060
|
-
- On most systems: the rule is applied immediately (terminal is already elevated)
|
|
1061
|
-
- On standard Windows: a UAC popup appears \u2014 the user must click Yes
|
|
1062
|
-
- On VMs or headless setups: applied directly if running as Administrator
|
|
1063
|
-
|
|
1064
|
-
The rule is named "claude-collab-{port}" and allows inbound TCP on your current listen port.
|
|
1065
|
-
Call firewall_close() when you are done to clean up the rule.`;
|
|
1066
|
-
function registerFirewallOpenTool(server, client) {
|
|
1067
|
-
server.tool(
|
|
1068
|
-
"firewall_open",
|
|
1069
|
-
FIREWALL_OPEN_DESCRIPTION,
|
|
1070
|
-
{
|
|
1071
|
-
port: z.number().min(1024).max(65535).optional().describe(
|
|
1072
|
-
"Port to open. Defaults to your current listen port \u2014 omit this unless you have a specific reason to override."
|
|
1073
|
-
)
|
|
1074
|
-
},
|
|
1075
|
-
async ({ port }) => {
|
|
1076
|
-
const targetPort = port ?? client.getInfo().port;
|
|
1077
|
-
if (!targetPort) {
|
|
1078
|
-
return {
|
|
1079
|
-
content: [{ type: "text", text: "P2P node is not running yet \u2014 port unknown. Try again in a moment." }],
|
|
1080
|
-
isError: true
|
|
1081
|
-
};
|
|
1082
|
-
}
|
|
1083
|
-
try {
|
|
1084
|
-
await addFirewallRule(targetPort);
|
|
1085
|
-
return {
|
|
1086
|
-
content: [{
|
|
1087
|
-
type: "text",
|
|
1088
|
-
text: [
|
|
1089
|
-
`Firewall rule opened for port ${targetPort} (claude-collab-${targetPort}).`,
|
|
1090
|
-
`Peers on the LAN can now connect to you inbound.`,
|
|
1091
|
-
`Call firewall_close() when you are done with this session.`
|
|
1092
|
-
].join("\n")
|
|
1093
|
-
}]
|
|
1094
|
-
};
|
|
1095
|
-
} catch (err) {
|
|
1096
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1097
|
-
return {
|
|
1098
|
-
content: [{
|
|
1099
|
-
type: "text",
|
|
1100
|
-
text: [
|
|
1101
|
-
`Failed to open firewall: ${msg}`,
|
|
1102
|
-
``,
|
|
1103
|
-
`Try running your terminal as Administrator and call firewall_open() again.`
|
|
1104
|
-
].join("\n")
|
|
1105
|
-
}],
|
|
1106
|
-
isError: true
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
var FIREWALL_CLOSE_DESCRIPTION = `Remove the Windows Firewall inbound rule that was opened by firewall_open().
|
|
1038
|
+
// src/presentation/mcp/tools/status.tool.ts
|
|
1039
|
+
var STATUS_DESCRIPTION = `Show your identity and network address on the collaboration network.
|
|
1113
1040
|
|
|
1114
1041
|
WHEN TO USE:
|
|
1115
|
-
-
|
|
1116
|
-
-
|
|
1117
|
-
-
|
|
1118
|
-
|
|
1119
|
-
WHAT HAPPENS:
|
|
1120
|
-
- The inbound TCP rule "claude-collab-{port}" is deleted from Windows Firewall
|
|
1121
|
-
- Existing active connections are not dropped \u2014 only new inbound connections are blocked
|
|
1122
|
-
- On standard Windows: a UAC popup appears \u2014 the user must click Yes
|
|
1123
|
-
- On VMs or admin terminals: applied directly without a popup
|
|
1042
|
+
- To find your LAN IP so teammates can connect to you via connect(ip)
|
|
1043
|
+
- To verify your node started correctly
|
|
1044
|
+
- To see how many peers are currently connected
|
|
1124
1045
|
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
function
|
|
1128
|
-
server.tool(
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
"Port whose rule to remove. Defaults to your current listen port. Pass a specific port to clean up a rule from a previous session."
|
|
1134
|
-
)
|
|
1135
|
-
},
|
|
1136
|
-
async ({ port }) => {
|
|
1137
|
-
const targetPort = port ?? client.getInfo().port;
|
|
1138
|
-
if (!targetPort) {
|
|
1139
|
-
return {
|
|
1140
|
-
content: [{ type: "text", text: "P2P node is not running yet \u2014 port unknown. Try again in a moment." }],
|
|
1141
|
-
isError: true
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
try {
|
|
1145
|
-
await removeFirewallRule(targetPort);
|
|
1146
|
-
return {
|
|
1147
|
-
content: [{
|
|
1148
|
-
type: "text",
|
|
1149
|
-
text: `Firewall rule removed for port ${targetPort} (claude-collab-${targetPort}).`
|
|
1150
|
-
}]
|
|
1151
|
-
};
|
|
1152
|
-
} catch (err) {
|
|
1153
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1154
|
-
return {
|
|
1155
|
-
content: [{
|
|
1156
|
-
type: "text",
|
|
1157
|
-
text: [
|
|
1158
|
-
`Failed to remove firewall rule: ${msg}`,
|
|
1159
|
-
``,
|
|
1160
|
-
`The rule may not exist (if firewall_open was never called this session), or try running as Administrator.`
|
|
1161
|
-
].join("\n")
|
|
1162
|
-
}],
|
|
1163
|
-
isError: true
|
|
1164
|
-
};
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
);
|
|
1168
|
-
}
|
|
1169
|
-
var PEER_FIND_DESCRIPTION = `Discover and connect to peers on the LAN automatically.
|
|
1170
|
-
|
|
1171
|
-
WHAT IT DOES:
|
|
1172
|
-
1. Opens your firewall so peers can connect inbound to you (UAC popup)
|
|
1173
|
-
2. Waits 30 seconds while multicast discovery finds peers
|
|
1174
|
-
3. Closes the firewall (UAC popup) \u2014 established connections persist
|
|
1175
|
-
|
|
1176
|
-
WHEN TO USE:
|
|
1177
|
-
- First time setup: everyone on the team calls peer_find
|
|
1178
|
-
- Adding a new peer to an existing session: only the NEW peer calls peer_find
|
|
1179
|
-
(existing peers will connect to them automatically \u2014 no action needed from others)
|
|
1180
|
-
- After a disconnect/restart: the reconnecting peer calls peer_find
|
|
1181
|
-
|
|
1182
|
-
HOW NEW PEERS JOIN AN EXISTING SESSION:
|
|
1183
|
-
Existing peers always listen for multicast announcements in the background.
|
|
1184
|
-
When you call peer_find, they hear your announcement and connect OUTBOUND to you.
|
|
1185
|
-
Outbound connections do not require a firewall rule on their side.
|
|
1186
|
-
You only need your own firewall open to accept those inbound connections.
|
|
1187
|
-
|
|
1188
|
-
NOTE: Two UAC popups will appear \u2014 one to open, one to close after the wait.`;
|
|
1189
|
-
function registerPeerFindTool(server, client) {
|
|
1190
|
-
server.tool(
|
|
1191
|
-
"peer_find",
|
|
1192
|
-
PEER_FIND_DESCRIPTION,
|
|
1193
|
-
{
|
|
1194
|
-
wait_seconds: z.number().min(10).max(120).optional().describe("How long to wait for peers in seconds (default: 30)")
|
|
1195
|
-
},
|
|
1196
|
-
async ({ wait_seconds = 30 }) => {
|
|
1197
|
-
const port = client.getInfo().port;
|
|
1198
|
-
if (!port) {
|
|
1199
|
-
return {
|
|
1200
|
-
content: [{ type: "text", text: "P2P node is not running yet. Try again in a moment." }],
|
|
1201
|
-
isError: true
|
|
1202
|
-
};
|
|
1203
|
-
}
|
|
1204
|
-
try {
|
|
1205
|
-
await addFirewallRule(port);
|
|
1206
|
-
} catch (err) {
|
|
1207
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1208
|
-
return {
|
|
1209
|
-
content: [{ type: "text", text: `Failed to open firewall: ${msg}` }],
|
|
1210
|
-
isError: true
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
const peersAtStart = new Set(client.getInfo().connectedPeers);
|
|
1214
|
-
await new Promise((resolve) => setTimeout(resolve, wait_seconds * 1e3));
|
|
1215
|
-
try {
|
|
1216
|
-
await removeFirewallRule(port);
|
|
1217
|
-
} catch {
|
|
1218
|
-
}
|
|
1219
|
-
const allPeers = client.getInfo().connectedPeers;
|
|
1220
|
-
const newPeers = allPeers.filter((p) => !peersAtStart.has(p));
|
|
1221
|
-
if (allPeers.length === 0) {
|
|
1222
|
-
return {
|
|
1223
|
-
content: [{
|
|
1224
|
-
type: "text",
|
|
1225
|
-
text: [
|
|
1226
|
-
`No peers found after ${wait_seconds}s.`,
|
|
1227
|
-
``,
|
|
1228
|
-
`Make sure other peers are also running peer_find at the same time,`,
|
|
1229
|
-
`and that all machines are on the same LAN.`
|
|
1230
|
-
].join("\n")
|
|
1231
|
-
}]
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
const lines = [
|
|
1235
|
-
`Firewall closed. Connected peers (${allPeers.length}):`,
|
|
1236
|
-
...allPeers.map((p) => ` \u2022 ${p}${newPeers.includes(p) ? " (new)" : ""}`),
|
|
1237
|
-
``,
|
|
1238
|
-
`Connections will persist until a peer disconnects or restarts.`,
|
|
1239
|
-
`If a peer disconnects, they call peer_find again \u2014 no action needed from you.`
|
|
1240
|
-
];
|
|
1046
|
+
SHARE YOUR IP:
|
|
1047
|
+
- Tell your teammate the IP shown here, they call connect("<your IP>")`;
|
|
1048
|
+
function registerStatusTool(server, client) {
|
|
1049
|
+
server.tool("status", STATUS_DESCRIPTION, {}, async () => {
|
|
1050
|
+
const info = client.getInfo();
|
|
1051
|
+
const myName = info.teamName ?? "(starting...)";
|
|
1052
|
+
const port = info.port ?? "?";
|
|
1053
|
+
if (!client.isConnected) {
|
|
1241
1054
|
return {
|
|
1242
|
-
content: [{
|
|
1055
|
+
content: [{
|
|
1056
|
+
type: "text",
|
|
1057
|
+
text: `Node is not running yet. Port ${port} may be in use \u2014 check MCP process logs.`
|
|
1058
|
+
}],
|
|
1059
|
+
isError: true
|
|
1243
1060
|
};
|
|
1244
1061
|
}
|
|
1245
|
-
|
|
1062
|
+
let ips = [];
|
|
1063
|
+
if (client instanceof P2PNode) {
|
|
1064
|
+
ips = client.getLocalIps();
|
|
1065
|
+
}
|
|
1066
|
+
const ipLine = ips.length > 0 ? ips.join(", ") : "(could not detect \u2014 check your network interface)";
|
|
1067
|
+
const peerCount = info.connectedPeers.length;
|
|
1068
|
+
const peerLine = peerCount === 0 ? "No peers connected yet. Share your IP so others can connect(ip)." : `Connected to ${peerCount} peer(s): ${info.connectedPeers.map((n) => `"${n}"`).join(", ")}`;
|
|
1069
|
+
return {
|
|
1070
|
+
content: [{
|
|
1071
|
+
type: "text",
|
|
1072
|
+
text: [
|
|
1073
|
+
`Name: ${myName}`,
|
|
1074
|
+
`IP: ${ipLine}`,
|
|
1075
|
+
`Port: ${port}`,
|
|
1076
|
+
``,
|
|
1077
|
+
peerLine
|
|
1078
|
+
].join("\n")
|
|
1079
|
+
}]
|
|
1080
|
+
};
|
|
1081
|
+
});
|
|
1246
1082
|
}
|
|
1247
1083
|
|
|
1248
1084
|
// src/presentation/mcp/server.ts
|
|
@@ -1252,13 +1088,12 @@ function createMcpServer(options) {
|
|
|
1252
1088
|
name: "claude-collab",
|
|
1253
1089
|
version: "0.1.0"
|
|
1254
1090
|
});
|
|
1091
|
+
registerConnectTool(server, client);
|
|
1092
|
+
registerStatusTool(server, client);
|
|
1255
1093
|
registerAskTool(server, client);
|
|
1256
1094
|
registerReplyTool(server, client);
|
|
1257
1095
|
registerPeersTool(server, client);
|
|
1258
1096
|
registerHistoryTool(server, client);
|
|
1259
|
-
registerFirewallOpenTool(server, client);
|
|
1260
|
-
registerFirewallCloseTool(server, client);
|
|
1261
|
-
registerPeerFindTool(server, client);
|
|
1262
1097
|
return server;
|
|
1263
1098
|
}
|
|
1264
1099
|
async function startMcpServer(options) {
|
|
@@ -1269,11 +1104,12 @@ async function startMcpServer(options) {
|
|
|
1269
1104
|
|
|
1270
1105
|
// src/cli.ts
|
|
1271
1106
|
var program = new Command();
|
|
1272
|
-
program.name("claude-collab").description("
|
|
1107
|
+
program.name("claude-collab").description("Collaboration between Claude Code terminals via MCP").version("0.1.0").requiredOption("--name <name>", 'Your name on the network (e.g. "alice")').action(async (options) => {
|
|
1273
1108
|
const node = new P2PNode();
|
|
1274
1109
|
const mcpReady = startMcpServer({ client: node });
|
|
1275
1110
|
node.join(options.name, options.name).catch((err) => {
|
|
1276
|
-
console.error(`[cli]
|
|
1111
|
+
console.error(`[cli] Failed to start on port 12345: ${err.message}`);
|
|
1112
|
+
console.error(`[cli] Make sure port 12345 is not already in use.`);
|
|
1277
1113
|
});
|
|
1278
1114
|
await mcpReady;
|
|
1279
1115
|
});
|