@dolusoft/claude-collab 1.9.5 → 1.9.7
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 +119 -98
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +119 -98
- 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);
|
|
@@ -934,17 +975,7 @@ ${answerLine}`;
|
|
|
934
975
|
};
|
|
935
976
|
});
|
|
936
977
|
}
|
|
937
|
-
function
|
|
938
|
-
return new Promise((resolve, reject) => {
|
|
939
|
-
const proc = spawn("netsh", argArray, { stdio: "ignore" });
|
|
940
|
-
proc.on("close", (code) => {
|
|
941
|
-
if (code === 0) resolve();
|
|
942
|
-
else reject(new Error(`netsh exited with code ${code}`));
|
|
943
|
-
});
|
|
944
|
-
proc.on("error", (err) => reject(new Error(`netsh not found: ${err.message}`)));
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
function runElevated(argArray) {
|
|
978
|
+
function runNetshElevated(argArray) {
|
|
948
979
|
const argList = argArray.map((a) => `"${a}"`).join(",");
|
|
949
980
|
const psCommand = `Start-Process -FilePath "netsh" -ArgumentList @(${argList}) -Verb RunAs -Wait`;
|
|
950
981
|
return new Promise((resolve, reject) => {
|
|
@@ -959,16 +990,10 @@ function runElevated(argArray) {
|
|
|
959
990
|
});
|
|
960
991
|
}
|
|
961
992
|
async function runNetsh(argArray) {
|
|
962
|
-
|
|
963
|
-
await runDirect(argArray);
|
|
964
|
-
return { method: "direct" };
|
|
965
|
-
} catch {
|
|
966
|
-
await runElevated(argArray);
|
|
967
|
-
return { method: "elevated" };
|
|
968
|
-
}
|
|
993
|
+
await runNetshElevated(argArray);
|
|
969
994
|
}
|
|
970
995
|
async function addFirewallRule(port) {
|
|
971
|
-
|
|
996
|
+
await runNetsh([
|
|
972
997
|
"advfirewall",
|
|
973
998
|
"firewall",
|
|
974
999
|
"add",
|
|
@@ -988,15 +1013,14 @@ async function addFirewallRule(port) {
|
|
|
988
1013
|
"name=claude-collab-discovery",
|
|
989
1014
|
"protocol=UDP",
|
|
990
1015
|
"dir=in",
|
|
991
|
-
"localport=
|
|
1016
|
+
"localport=11776",
|
|
992
1017
|
"action=allow"
|
|
993
1018
|
]);
|
|
994
1019
|
} catch {
|
|
995
1020
|
}
|
|
996
|
-
return result;
|
|
997
1021
|
}
|
|
998
1022
|
async function removeFirewallRule(port) {
|
|
999
|
-
|
|
1023
|
+
await runNetsh([
|
|
1000
1024
|
"advfirewall",
|
|
1001
1025
|
"firewall",
|
|
1002
1026
|
"delete",
|
|
@@ -1007,7 +1031,6 @@ async function removeFirewallRule(port) {
|
|
|
1007
1031
|
await runNetsh(["advfirewall", "firewall", "delete", "rule", "name=claude-collab-discovery"]);
|
|
1008
1032
|
} catch {
|
|
1009
1033
|
}
|
|
1010
|
-
return result;
|
|
1011
1034
|
}
|
|
1012
1035
|
|
|
1013
1036
|
// src/presentation/mcp/tools/firewall-open.tool.ts
|
|
@@ -1047,13 +1070,12 @@ function registerFirewallOpenTool(server, client) {
|
|
|
1047
1070
|
};
|
|
1048
1071
|
}
|
|
1049
1072
|
try {
|
|
1050
|
-
|
|
1051
|
-
const how = method === "direct" ? "applied directly (already elevated)" : "applied via UAC popup";
|
|
1073
|
+
await addFirewallRule(targetPort);
|
|
1052
1074
|
return {
|
|
1053
1075
|
content: [{
|
|
1054
1076
|
type: "text",
|
|
1055
1077
|
text: [
|
|
1056
|
-
`Firewall rule opened for port ${targetPort} (claude-collab-${targetPort})
|
|
1078
|
+
`Firewall rule opened for port ${targetPort} (claude-collab-${targetPort}).`,
|
|
1057
1079
|
`Peers on the LAN can now connect to you inbound.`,
|
|
1058
1080
|
`Call firewall_close() when you are done with this session.`
|
|
1059
1081
|
].join("\n")
|
|
@@ -1109,12 +1131,11 @@ function registerFirewallCloseTool(server, client) {
|
|
|
1109
1131
|
};
|
|
1110
1132
|
}
|
|
1111
1133
|
try {
|
|
1112
|
-
|
|
1113
|
-
const how = method === "direct" ? "applied directly (already elevated)" : "applied via UAC popup";
|
|
1134
|
+
await removeFirewallRule(targetPort);
|
|
1114
1135
|
return {
|
|
1115
1136
|
content: [{
|
|
1116
1137
|
type: "text",
|
|
1117
|
-
text: `Firewall rule removed for port ${targetPort} (claude-collab-${targetPort})
|
|
1138
|
+
text: `Firewall rule removed for port ${targetPort} (claude-collab-${targetPort}).`
|
|
1118
1139
|
}]
|
|
1119
1140
|
};
|
|
1120
1141
|
} catch (err) {
|