@dolusoft/claude-collab 1.9.6 → 1.10.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/dist/cli.js +190 -70
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +190 -70
- package/dist/mcp-main.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp-main.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
3
3
|
import { v4 } from 'uuid';
|
|
4
|
-
import
|
|
5
|
-
import { tmpdir
|
|
4
|
+
import dgram from 'dgram';
|
|
5
|
+
import os, { tmpdir } from 'os';
|
|
6
6
|
import { EventEmitter } from 'events';
|
|
7
7
|
import { execFile, spawn } from 'child_process';
|
|
8
8
|
import { unlinkSync } from 'fs';
|
|
@@ -18,79 +18,121 @@ function serialize(msg) {
|
|
|
18
18
|
function parse(data) {
|
|
19
19
|
return JSON.parse(data);
|
|
20
20
|
}
|
|
21
|
-
var
|
|
22
|
-
var
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
for (const iface of ifaces ?? []) {
|
|
27
|
-
if (iface.family !== "IPv4" || iface.internal) continue;
|
|
28
|
-
const ip = iface.address.split(".").map(Number);
|
|
29
|
-
const mask = iface.netmask.split(".").map(Number);
|
|
30
|
-
const broadcast = ip.map((b, i) => b | ~mask[i] & 255).join(".");
|
|
31
|
-
addrs.add(broadcast);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return [...addrs];
|
|
35
|
-
}
|
|
36
|
-
var PeerBroadcaster = class {
|
|
21
|
+
var MULTICAST_ADDR = "239.255.42.42";
|
|
22
|
+
var MULTICAST_PORT = 11776;
|
|
23
|
+
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
24
|
+
var PEER_TIMEOUT_MS = 2e4;
|
|
25
|
+
var MulticastDiscovery = class extends EventEmitter {
|
|
37
26
|
socket = null;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
27
|
+
heartbeatTimer = null;
|
|
28
|
+
timeoutTimer = null;
|
|
29
|
+
peers = /* @__PURE__ */ new Map();
|
|
30
|
+
myName = "";
|
|
31
|
+
myWsPort = 0;
|
|
32
|
+
start(name, wsPort) {
|
|
33
|
+
this.myName = name;
|
|
34
|
+
this.myWsPort = wsPort;
|
|
35
|
+
const socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
|
42
36
|
this.socket = socket;
|
|
43
37
|
socket.on("error", (err) => {
|
|
44
|
-
console.error("[
|
|
38
|
+
console.error("[multicast] socket error:", err.message);
|
|
45
39
|
});
|
|
46
|
-
socket.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
40
|
+
socket.on("message", (buf, rinfo) => {
|
|
41
|
+
try {
|
|
42
|
+
const msg = JSON.parse(buf.toString());
|
|
43
|
+
this.handleMessage(msg, rinfo.address);
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
socket.bind(MULTICAST_PORT, () => {
|
|
48
|
+
try {
|
|
49
|
+
socket.addMembership(MULTICAST_ADDR);
|
|
50
|
+
socket.setMulticastTTL(1);
|
|
51
|
+
socket.setMulticastLoopback(false);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error("[multicast] membership error:", err);
|
|
54
|
+
}
|
|
55
|
+
this.announce();
|
|
56
|
+
this.heartbeatTimer = setInterval(() => this.announce(), HEARTBEAT_INTERVAL_MS);
|
|
57
|
+
this.timeoutTimer = setInterval(() => this.checkTimeouts(), 5e3);
|
|
59
58
|
});
|
|
60
59
|
}
|
|
61
60
|
stop() {
|
|
62
|
-
if (this.
|
|
63
|
-
clearInterval(this.
|
|
64
|
-
this.
|
|
61
|
+
if (this.heartbeatTimer) {
|
|
62
|
+
clearInterval(this.heartbeatTimer);
|
|
63
|
+
this.heartbeatTimer = null;
|
|
64
|
+
}
|
|
65
|
+
if (this.timeoutTimer) {
|
|
66
|
+
clearInterval(this.timeoutTimer);
|
|
67
|
+
this.timeoutTimer = null;
|
|
65
68
|
}
|
|
66
69
|
if (this.socket) {
|
|
67
|
-
this.
|
|
70
|
+
this.sendMessage({ type: "LEAVE", name: this.myName });
|
|
71
|
+
try {
|
|
72
|
+
this.socket.dropMembership(MULTICAST_ADDR);
|
|
73
|
+
this.socket.close();
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
68
76
|
this.socket = null;
|
|
69
77
|
}
|
|
78
|
+
this.peers.clear();
|
|
70
79
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
socket.on("message", (msg, rinfo) => {
|
|
78
|
-
try {
|
|
79
|
-
const data = JSON.parse(msg.toString());
|
|
80
|
-
if (data.type === "claude-collab-peer" && typeof data.name === "string" && typeof data.port === "number") {
|
|
81
|
-
onFound({ name: data.name, host: rinfo.address, port: data.port });
|
|
80
|
+
resolveLocalIp() {
|
|
81
|
+
const interfaces = os.networkInterfaces();
|
|
82
|
+
for (const iface of Object.values(interfaces)) {
|
|
83
|
+
if (!iface) continue;
|
|
84
|
+
for (const addr of iface) {
|
|
85
|
+
if (addr.family === "IPv4" && !addr.internal) return addr.address;
|
|
82
86
|
}
|
|
83
|
-
} catch {
|
|
84
87
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
return "127.0.0.1";
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Private
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
announce() {
|
|
94
|
+
this.sendMessage({ type: "ANNOUNCE", name: this.myName, wsPort: this.myWsPort });
|
|
95
|
+
}
|
|
96
|
+
sendMessage(msg) {
|
|
97
|
+
if (!this.socket) return;
|
|
98
|
+
const buf = Buffer.from(JSON.stringify(msg));
|
|
99
|
+
this.socket.send(buf, MULTICAST_PORT, MULTICAST_ADDR, (err) => {
|
|
100
|
+
if (err) console.error("[multicast] send error:", err.message);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
handleMessage(msg, fromIp) {
|
|
104
|
+
if (msg.type === "ANNOUNCE") {
|
|
105
|
+
if (msg.name === this.myName) return;
|
|
106
|
+
const existing = this.peers.get(msg.name);
|
|
107
|
+
if (!existing) {
|
|
108
|
+
const peer = { name: msg.name, ip: fromIp, wsPort: msg.wsPort, lastSeen: Date.now() };
|
|
109
|
+
this.peers.set(msg.name, peer);
|
|
110
|
+
this.emit("peer-found", { name: peer.name, ip: peer.ip, wsPort: peer.wsPort });
|
|
111
|
+
console.error(`[multicast] discovered peer: ${msg.name} @ ${fromIp}:${msg.wsPort}`);
|
|
112
|
+
} else {
|
|
113
|
+
existing.lastSeen = Date.now();
|
|
114
|
+
existing.ip = fromIp;
|
|
115
|
+
existing.wsPort = msg.wsPort;
|
|
116
|
+
}
|
|
117
|
+
} else if (msg.type === "LEAVE") {
|
|
118
|
+
if (this.peers.has(msg.name)) {
|
|
119
|
+
this.peers.delete(msg.name);
|
|
120
|
+
this.emit("peer-lost", msg.name);
|
|
121
|
+
console.error(`[multicast] peer left: ${msg.name}`);
|
|
122
|
+
}
|
|
91
123
|
}
|
|
92
|
-
}
|
|
93
|
-
|
|
124
|
+
}
|
|
125
|
+
checkTimeouts() {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
for (const [name, peer] of this.peers) {
|
|
128
|
+
if (now - peer.lastSeen > PEER_TIMEOUT_MS) {
|
|
129
|
+
this.peers.delete(name);
|
|
130
|
+
this.emit("peer-lost", name);
|
|
131
|
+
console.error(`[multicast] peer timed out: ${name}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
94
136
|
var CS_CONINJECT = `
|
|
95
137
|
using System;
|
|
96
138
|
using System.Collections.Generic;
|
|
@@ -332,8 +374,7 @@ var P2PNode = class {
|
|
|
332
374
|
answerWaiters = /* @__PURE__ */ new Map();
|
|
333
375
|
// Answers queued for offline peers: peerName → AnswerMsg (delivered on reconnect)
|
|
334
376
|
pendingOutboundAnswers = /* @__PURE__ */ new Map();
|
|
335
|
-
|
|
336
|
-
stopPeerWatcher = null;
|
|
377
|
+
discovery = null;
|
|
337
378
|
boundPort = 0;
|
|
338
379
|
// ---------------------------------------------------------------------------
|
|
339
380
|
// ICollabClient implementation
|
|
@@ -476,8 +517,8 @@ var P2PNode = class {
|
|
|
476
517
|
return entries.sort((a, b) => a.askedAt.localeCompare(b.askedAt));
|
|
477
518
|
}
|
|
478
519
|
async disconnect() {
|
|
479
|
-
this.
|
|
480
|
-
this.
|
|
520
|
+
this.discovery?.stop();
|
|
521
|
+
this.discovery = null;
|
|
481
522
|
for (const ws of this.peerConnections.values()) ws.close();
|
|
482
523
|
this.peerConnections.clear();
|
|
483
524
|
this.wsToName.clear();
|
|
@@ -548,14 +589,14 @@ var P2PNode = class {
|
|
|
548
589
|
// Private: discovery + outbound connections
|
|
549
590
|
// ---------------------------------------------------------------------------
|
|
550
591
|
startDiscovery() {
|
|
551
|
-
|
|
552
|
-
this.
|
|
553
|
-
|
|
554
|
-
if (peer.name === this.myName) return;
|
|
592
|
+
const discovery = new MulticastDiscovery();
|
|
593
|
+
this.discovery = discovery;
|
|
594
|
+
discovery.on("peer-found", (peer) => {
|
|
555
595
|
if (this.peerConnections.has(peer.name)) return;
|
|
556
596
|
if (this.connectingPeers.has(peer.name)) return;
|
|
557
|
-
this.connectToPeer(peer.name, peer.
|
|
597
|
+
this.connectToPeer(peer.name, peer.ip, peer.wsPort);
|
|
558
598
|
});
|
|
599
|
+
discovery.start(this.myName, this.boundPort);
|
|
559
600
|
}
|
|
560
601
|
connectToPeer(peerName, host, port) {
|
|
561
602
|
this.connectingPeers.add(peerName);
|
|
@@ -972,7 +1013,7 @@ async function addFirewallRule(port) {
|
|
|
972
1013
|
"name=claude-collab-discovery",
|
|
973
1014
|
"protocol=UDP",
|
|
974
1015
|
"dir=in",
|
|
975
|
-
"localport=
|
|
1016
|
+
"localport=11776",
|
|
976
1017
|
"action=allow"
|
|
977
1018
|
]);
|
|
978
1019
|
} catch {
|
|
@@ -1114,6 +1155,84 @@ function registerFirewallCloseTool(server, client) {
|
|
|
1114
1155
|
}
|
|
1115
1156
|
);
|
|
1116
1157
|
}
|
|
1158
|
+
var PEER_FIND_DESCRIPTION = `Discover and connect to peers on the LAN automatically.
|
|
1159
|
+
|
|
1160
|
+
WHAT IT DOES:
|
|
1161
|
+
1. Opens your firewall so peers can connect inbound to you (UAC popup)
|
|
1162
|
+
2. Waits 30 seconds while multicast discovery finds peers
|
|
1163
|
+
3. Closes the firewall (UAC popup) \u2014 established connections persist
|
|
1164
|
+
|
|
1165
|
+
WHEN TO USE:
|
|
1166
|
+
- First time setup: everyone on the team calls peer_find
|
|
1167
|
+
- Adding a new peer to an existing session: only the NEW peer calls peer_find
|
|
1168
|
+
(existing peers will connect to them automatically \u2014 no action needed from others)
|
|
1169
|
+
- After a disconnect/restart: the reconnecting peer calls peer_find
|
|
1170
|
+
|
|
1171
|
+
HOW NEW PEERS JOIN AN EXISTING SESSION:
|
|
1172
|
+
Existing peers always listen for multicast announcements in the background.
|
|
1173
|
+
When you call peer_find, they hear your announcement and connect OUTBOUND to you.
|
|
1174
|
+
Outbound connections do not require a firewall rule on their side.
|
|
1175
|
+
You only need your own firewall open to accept those inbound connections.
|
|
1176
|
+
|
|
1177
|
+
NOTE: Two UAC popups will appear \u2014 one to open, one to close after the wait.`;
|
|
1178
|
+
function registerPeerFindTool(server, client) {
|
|
1179
|
+
server.tool(
|
|
1180
|
+
"peer_find",
|
|
1181
|
+
PEER_FIND_DESCRIPTION,
|
|
1182
|
+
{
|
|
1183
|
+
wait_seconds: z.number().min(10).max(120).optional().describe("How long to wait for peers in seconds (default: 30)")
|
|
1184
|
+
},
|
|
1185
|
+
async ({ wait_seconds = 30 }) => {
|
|
1186
|
+
const port = client.getInfo().port;
|
|
1187
|
+
if (!port) {
|
|
1188
|
+
return {
|
|
1189
|
+
content: [{ type: "text", text: "P2P node is not running yet. Try again in a moment." }],
|
|
1190
|
+
isError: true
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
await addFirewallRule(port);
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1197
|
+
return {
|
|
1198
|
+
content: [{ type: "text", text: `Failed to open firewall: ${msg}` }],
|
|
1199
|
+
isError: true
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
const peersAtStart = new Set(client.getInfo().connectedPeers);
|
|
1203
|
+
await new Promise((resolve) => setTimeout(resolve, wait_seconds * 1e3));
|
|
1204
|
+
try {
|
|
1205
|
+
await removeFirewallRule(port);
|
|
1206
|
+
} catch {
|
|
1207
|
+
}
|
|
1208
|
+
const allPeers = client.getInfo().connectedPeers;
|
|
1209
|
+
const newPeers = allPeers.filter((p) => !peersAtStart.has(p));
|
|
1210
|
+
if (allPeers.length === 0) {
|
|
1211
|
+
return {
|
|
1212
|
+
content: [{
|
|
1213
|
+
type: "text",
|
|
1214
|
+
text: [
|
|
1215
|
+
`No peers found after ${wait_seconds}s.`,
|
|
1216
|
+
``,
|
|
1217
|
+
`Make sure other peers are also running peer_find at the same time,`,
|
|
1218
|
+
`and that all machines are on the same LAN.`
|
|
1219
|
+
].join("\n")
|
|
1220
|
+
}]
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
const lines = [
|
|
1224
|
+
`Firewall closed. Connected peers (${allPeers.length}):`,
|
|
1225
|
+
...allPeers.map((p) => ` \u2022 ${p}${newPeers.includes(p) ? " (new)" : ""}`),
|
|
1226
|
+
``,
|
|
1227
|
+
`Connections will persist until a peer disconnects or restarts.`,
|
|
1228
|
+
`If a peer disconnects, they call peer_find again \u2014 no action needed from you.`
|
|
1229
|
+
];
|
|
1230
|
+
return {
|
|
1231
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1117
1236
|
|
|
1118
1237
|
// src/presentation/mcp/server.ts
|
|
1119
1238
|
function createMcpServer(options) {
|
|
@@ -1128,6 +1247,7 @@ function createMcpServer(options) {
|
|
|
1128
1247
|
registerHistoryTool(server, client);
|
|
1129
1248
|
registerFirewallOpenTool(server, client);
|
|
1130
1249
|
registerFirewallCloseTool(server, client);
|
|
1250
|
+
registerPeerFindTool(server, client);
|
|
1131
1251
|
return server;
|
|
1132
1252
|
}
|
|
1133
1253
|
async function startMcpServer(options) {
|