@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/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
5
|
-
import
|
|
6
|
-
import { tmpdir
|
|
5
|
+
import dgram from 'dgram';
|
|
6
|
+
import os, { tmpdir } from 'os';
|
|
7
7
|
import { EventEmitter } from 'events';
|
|
8
8
|
import { execFile, spawn } from 'child_process';
|
|
9
9
|
import { unlinkSync } from 'fs';
|
|
@@ -19,79 +19,121 @@ function serialize(msg) {
|
|
|
19
19
|
function parse(data) {
|
|
20
20
|
return JSON.parse(data);
|
|
21
21
|
}
|
|
22
|
-
var
|
|
23
|
-
var
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
for (const iface of ifaces ?? []) {
|
|
28
|
-
if (iface.family !== "IPv4" || iface.internal) continue;
|
|
29
|
-
const ip = iface.address.split(".").map(Number);
|
|
30
|
-
const mask = iface.netmask.split(".").map(Number);
|
|
31
|
-
const broadcast = ip.map((b, i) => b | ~mask[i] & 255).join(".");
|
|
32
|
-
addrs.add(broadcast);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return [...addrs];
|
|
36
|
-
}
|
|
37
|
-
var PeerBroadcaster = class {
|
|
22
|
+
var MULTICAST_ADDR = "239.255.42.42";
|
|
23
|
+
var MULTICAST_PORT = 11776;
|
|
24
|
+
var HEARTBEAT_INTERVAL_MS = 5e3;
|
|
25
|
+
var PEER_TIMEOUT_MS = 2e4;
|
|
26
|
+
var MulticastDiscovery = class extends EventEmitter {
|
|
38
27
|
socket = null;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
28
|
+
heartbeatTimer = null;
|
|
29
|
+
timeoutTimer = null;
|
|
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 });
|
|
43
37
|
this.socket = socket;
|
|
44
38
|
socket.on("error", (err) => {
|
|
45
|
-
console.error("[
|
|
39
|
+
console.error("[multicast] socket error:", err.message);
|
|
46
40
|
});
|
|
47
|
-
socket.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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);
|
|
60
59
|
});
|
|
61
60
|
}
|
|
62
61
|
stop() {
|
|
63
|
-
if (this.
|
|
64
|
-
clearInterval(this.
|
|
65
|
-
this.
|
|
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;
|
|
66
69
|
}
|
|
67
70
|
if (this.socket) {
|
|
68
|
-
this.
|
|
71
|
+
this.sendMessage({ type: "LEAVE", name: this.myName });
|
|
72
|
+
try {
|
|
73
|
+
this.socket.dropMembership(MULTICAST_ADDR);
|
|
74
|
+
this.socket.close();
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
69
77
|
this.socket = null;
|
|
70
78
|
}
|
|
79
|
+
this.peers.clear();
|
|
71
80
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
socket.on("message", (msg, rinfo) => {
|
|
79
|
-
try {
|
|
80
|
-
const data = JSON.parse(msg.toString());
|
|
81
|
-
if (data.type === "claude-collab-peer" && typeof data.name === "string" && typeof data.port === "number") {
|
|
82
|
-
onFound({ name: data.name, host: rinfo.address, port: data.port });
|
|
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;
|
|
83
87
|
}
|
|
84
|
-
} catch {
|
|
85
88
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
handleMessage(msg, fromIp) {
|
|
105
|
+
if (msg.type === "ANNOUNCE") {
|
|
106
|
+
if (msg.name === this.myName) return;
|
|
107
|
+
const existing = this.peers.get(msg.name);
|
|
108
|
+
if (!existing) {
|
|
109
|
+
const peer = { name: msg.name, ip: fromIp, wsPort: msg.wsPort, lastSeen: Date.now() };
|
|
110
|
+
this.peers.set(msg.name, peer);
|
|
111
|
+
this.emit("peer-found", { name: peer.name, ip: peer.ip, wsPort: peer.wsPort });
|
|
112
|
+
console.error(`[multicast] discovered peer: ${msg.name} @ ${fromIp}:${msg.wsPort}`);
|
|
113
|
+
} else {
|
|
114
|
+
existing.lastSeen = Date.now();
|
|
115
|
+
existing.ip = fromIp;
|
|
116
|
+
existing.wsPort = msg.wsPort;
|
|
117
|
+
}
|
|
118
|
+
} else if (msg.type === "LEAVE") {
|
|
119
|
+
if (this.peers.has(msg.name)) {
|
|
120
|
+
this.peers.delete(msg.name);
|
|
121
|
+
this.emit("peer-lost", msg.name);
|
|
122
|
+
console.error(`[multicast] peer left: ${msg.name}`);
|
|
123
|
+
}
|
|
92
124
|
}
|
|
93
|
-
}
|
|
94
|
-
|
|
125
|
+
}
|
|
126
|
+
checkTimeouts() {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
for (const [name, peer] of this.peers) {
|
|
129
|
+
if (now - peer.lastSeen > PEER_TIMEOUT_MS) {
|
|
130
|
+
this.peers.delete(name);
|
|
131
|
+
this.emit("peer-lost", name);
|
|
132
|
+
console.error(`[multicast] peer timed out: ${name}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
95
137
|
var CS_CONINJECT = `
|
|
96
138
|
using System;
|
|
97
139
|
using System.Collections.Generic;
|
|
@@ -333,8 +375,7 @@ var P2PNode = class {
|
|
|
333
375
|
answerWaiters = /* @__PURE__ */ new Map();
|
|
334
376
|
// Answers queued for offline peers: peerName → AnswerMsg (delivered on reconnect)
|
|
335
377
|
pendingOutboundAnswers = /* @__PURE__ */ new Map();
|
|
336
|
-
|
|
337
|
-
stopPeerWatcher = null;
|
|
378
|
+
discovery = null;
|
|
338
379
|
boundPort = 0;
|
|
339
380
|
// ---------------------------------------------------------------------------
|
|
340
381
|
// ICollabClient implementation
|
|
@@ -477,8 +518,8 @@ var P2PNode = class {
|
|
|
477
518
|
return entries.sort((a, b) => a.askedAt.localeCompare(b.askedAt));
|
|
478
519
|
}
|
|
479
520
|
async disconnect() {
|
|
480
|
-
this.
|
|
481
|
-
this.
|
|
521
|
+
this.discovery?.stop();
|
|
522
|
+
this.discovery = null;
|
|
482
523
|
for (const ws of this.peerConnections.values()) ws.close();
|
|
483
524
|
this.peerConnections.clear();
|
|
484
525
|
this.wsToName.clear();
|
|
@@ -549,14 +590,14 @@ var P2PNode = class {
|
|
|
549
590
|
// Private: discovery + outbound connections
|
|
550
591
|
// ---------------------------------------------------------------------------
|
|
551
592
|
startDiscovery() {
|
|
552
|
-
|
|
553
|
-
this.
|
|
554
|
-
|
|
555
|
-
if (peer.name === this.myName) return;
|
|
593
|
+
const discovery = new MulticastDiscovery();
|
|
594
|
+
this.discovery = discovery;
|
|
595
|
+
discovery.on("peer-found", (peer) => {
|
|
556
596
|
if (this.peerConnections.has(peer.name)) return;
|
|
557
597
|
if (this.connectingPeers.has(peer.name)) return;
|
|
558
|
-
this.connectToPeer(peer.name, peer.
|
|
598
|
+
this.connectToPeer(peer.name, peer.ip, peer.wsPort);
|
|
559
599
|
});
|
|
600
|
+
discovery.start(this.myName, this.boundPort);
|
|
560
601
|
}
|
|
561
602
|
connectToPeer(peerName, host, port) {
|
|
562
603
|
this.connectingPeers.add(peerName);
|
|
@@ -935,17 +976,7 @@ ${answerLine}`;
|
|
|
935
976
|
};
|
|
936
977
|
});
|
|
937
978
|
}
|
|
938
|
-
function
|
|
939
|
-
return new Promise((resolve, reject) => {
|
|
940
|
-
const proc = spawn("netsh", argArray, { stdio: "ignore" });
|
|
941
|
-
proc.on("close", (code) => {
|
|
942
|
-
if (code === 0) resolve();
|
|
943
|
-
else reject(new Error(`netsh exited with code ${code}`));
|
|
944
|
-
});
|
|
945
|
-
proc.on("error", (err) => reject(new Error(`netsh not found: ${err.message}`)));
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
function runElevated(argArray) {
|
|
979
|
+
function runNetshElevated(argArray) {
|
|
949
980
|
const argList = argArray.map((a) => `"${a}"`).join(",");
|
|
950
981
|
const psCommand = `Start-Process -FilePath "netsh" -ArgumentList @(${argList}) -Verb RunAs -Wait`;
|
|
951
982
|
return new Promise((resolve, reject) => {
|
|
@@ -960,16 +991,10 @@ function runElevated(argArray) {
|
|
|
960
991
|
});
|
|
961
992
|
}
|
|
962
993
|
async function runNetsh(argArray) {
|
|
963
|
-
|
|
964
|
-
await runDirect(argArray);
|
|
965
|
-
return { method: "direct" };
|
|
966
|
-
} catch {
|
|
967
|
-
await runElevated(argArray);
|
|
968
|
-
return { method: "elevated" };
|
|
969
|
-
}
|
|
994
|
+
await runNetshElevated(argArray);
|
|
970
995
|
}
|
|
971
996
|
async function addFirewallRule(port) {
|
|
972
|
-
|
|
997
|
+
await runNetsh([
|
|
973
998
|
"advfirewall",
|
|
974
999
|
"firewall",
|
|
975
1000
|
"add",
|
|
@@ -989,15 +1014,14 @@ async function addFirewallRule(port) {
|
|
|
989
1014
|
"name=claude-collab-discovery",
|
|
990
1015
|
"protocol=UDP",
|
|
991
1016
|
"dir=in",
|
|
992
|
-
"localport=
|
|
1017
|
+
"localport=11776",
|
|
993
1018
|
"action=allow"
|
|
994
1019
|
]);
|
|
995
1020
|
} catch {
|
|
996
1021
|
}
|
|
997
|
-
return result;
|
|
998
1022
|
}
|
|
999
1023
|
async function removeFirewallRule(port) {
|
|
1000
|
-
|
|
1024
|
+
await runNetsh([
|
|
1001
1025
|
"advfirewall",
|
|
1002
1026
|
"firewall",
|
|
1003
1027
|
"delete",
|
|
@@ -1008,7 +1032,6 @@ async function removeFirewallRule(port) {
|
|
|
1008
1032
|
await runNetsh(["advfirewall", "firewall", "delete", "rule", "name=claude-collab-discovery"]);
|
|
1009
1033
|
} catch {
|
|
1010
1034
|
}
|
|
1011
|
-
return result;
|
|
1012
1035
|
}
|
|
1013
1036
|
|
|
1014
1037
|
// src/presentation/mcp/tools/firewall-open.tool.ts
|
|
@@ -1048,13 +1071,12 @@ function registerFirewallOpenTool(server, client) {
|
|
|
1048
1071
|
};
|
|
1049
1072
|
}
|
|
1050
1073
|
try {
|
|
1051
|
-
|
|
1052
|
-
const how = method === "direct" ? "applied directly (already elevated)" : "applied via UAC popup";
|
|
1074
|
+
await addFirewallRule(targetPort);
|
|
1053
1075
|
return {
|
|
1054
1076
|
content: [{
|
|
1055
1077
|
type: "text",
|
|
1056
1078
|
text: [
|
|
1057
|
-
`Firewall rule opened for port ${targetPort} (claude-collab-${targetPort})
|
|
1079
|
+
`Firewall rule opened for port ${targetPort} (claude-collab-${targetPort}).`,
|
|
1058
1080
|
`Peers on the LAN can now connect to you inbound.`,
|
|
1059
1081
|
`Call firewall_close() when you are done with this session.`
|
|
1060
1082
|
].join("\n")
|
|
@@ -1110,12 +1132,11 @@ function registerFirewallCloseTool(server, client) {
|
|
|
1110
1132
|
};
|
|
1111
1133
|
}
|
|
1112
1134
|
try {
|
|
1113
|
-
|
|
1114
|
-
const how = method === "direct" ? "applied directly (already elevated)" : "applied via UAC popup";
|
|
1135
|
+
await removeFirewallRule(targetPort);
|
|
1115
1136
|
return {
|
|
1116
1137
|
content: [{
|
|
1117
1138
|
type: "text",
|
|
1118
|
-
text: `Firewall rule removed for port ${targetPort} (claude-collab-${targetPort})
|
|
1139
|
+
text: `Firewall rule removed for port ${targetPort} (claude-collab-${targetPort}).`
|
|
1119
1140
|
}]
|
|
1120
1141
|
};
|
|
1121
1142
|
} catch (err) {
|