@dolusoft/claude-collab 1.3.1 → 1.4.2
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 +161 -292
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +171 -328
- package/dist/mcp-main.js.map +1 -1
- package/package.json +1 -3
package/dist/mcp-main.js
CHANGED
|
@@ -1,169 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
|
-
import { createServer } from 'net';
|
|
4
3
|
import { v4 } from 'uuid';
|
|
5
|
-
import multicastDns from 'multicast-dns';
|
|
6
|
-
import { tmpdir, networkInterfaces } from 'os';
|
|
7
4
|
import { EventEmitter } from 'events';
|
|
8
5
|
import { execFile } from 'child_process';
|
|
9
6
|
import { unlinkSync } from 'fs';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
10
8
|
import { join } from 'path';
|
|
11
9
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
11
|
import { z } from 'zod';
|
|
14
12
|
|
|
15
|
-
function getLocalIp() {
|
|
16
|
-
const nets = networkInterfaces();
|
|
17
|
-
for (const name of Object.keys(nets)) {
|
|
18
|
-
for (const net of nets[name] ?? []) {
|
|
19
|
-
if (net.family === "IPv4" && !net.internal) {
|
|
20
|
-
return net.address;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
return "127.0.0.1";
|
|
25
|
-
}
|
|
26
|
-
var SERVICE_TYPE = "_claude-collab._tcp.local";
|
|
27
|
-
var MdnsDiscovery = class {
|
|
28
|
-
mdns;
|
|
29
|
-
announced = false;
|
|
30
|
-
port = 0;
|
|
31
|
-
teamName = "";
|
|
32
|
-
memberId = "";
|
|
33
|
-
peersByTeam = /* @__PURE__ */ new Map();
|
|
34
|
-
peersByMemberId = /* @__PURE__ */ new Map();
|
|
35
|
-
onPeerFoundCb;
|
|
36
|
-
onPeerLostCb;
|
|
37
|
-
constructor() {
|
|
38
|
-
this.mdns = multicastDns();
|
|
39
|
-
this.setupHandlers();
|
|
40
|
-
}
|
|
41
|
-
get serviceName() {
|
|
42
|
-
return `${this.memberId}.${SERVICE_TYPE}`;
|
|
43
|
-
}
|
|
44
|
-
buildAnswers() {
|
|
45
|
-
return [
|
|
46
|
-
{
|
|
47
|
-
name: SERVICE_TYPE,
|
|
48
|
-
type: "PTR",
|
|
49
|
-
ttl: 300,
|
|
50
|
-
data: this.serviceName
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: this.serviceName,
|
|
54
|
-
type: "SRV",
|
|
55
|
-
ttl: 300,
|
|
56
|
-
data: {
|
|
57
|
-
priority: 0,
|
|
58
|
-
weight: 0,
|
|
59
|
-
port: this.port,
|
|
60
|
-
target: getLocalIp()
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
name: this.serviceName,
|
|
65
|
-
type: "TXT",
|
|
66
|
-
ttl: 300,
|
|
67
|
-
data: [
|
|
68
|
-
Buffer.from(`team=${this.teamName}`),
|
|
69
|
-
Buffer.from(`memberId=${this.memberId}`),
|
|
70
|
-
Buffer.from("ver=1")
|
|
71
|
-
]
|
|
72
|
-
}
|
|
73
|
-
];
|
|
74
|
-
}
|
|
75
|
-
setupHandlers() {
|
|
76
|
-
this.mdns.on("query", (query) => {
|
|
77
|
-
if (!this.announced) return;
|
|
78
|
-
const questions = query.questions ?? [];
|
|
79
|
-
const ptrQuery = questions.find(
|
|
80
|
-
(q) => q.type === "PTR" && q.name === SERVICE_TYPE
|
|
81
|
-
);
|
|
82
|
-
if (!ptrQuery) return;
|
|
83
|
-
this.mdns.respond({ answers: this.buildAnswers() });
|
|
84
|
-
});
|
|
85
|
-
this.mdns.on("response", (response) => {
|
|
86
|
-
this.parseResponse(response);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
parseResponse(response) {
|
|
90
|
-
const allRecords = [
|
|
91
|
-
...response.answers ?? [],
|
|
92
|
-
...response.additionals ?? []
|
|
93
|
-
];
|
|
94
|
-
const ptrRecords = allRecords.filter(
|
|
95
|
-
(r) => r.type === "PTR" && r.name === SERVICE_TYPE
|
|
96
|
-
);
|
|
97
|
-
for (const ptr of ptrRecords) {
|
|
98
|
-
const instanceName = ptr.data;
|
|
99
|
-
const srv = allRecords.find(
|
|
100
|
-
(r) => r.type === "SRV" && r.name === instanceName
|
|
101
|
-
);
|
|
102
|
-
const txt = allRecords.find(
|
|
103
|
-
(r) => r.type === "TXT" && r.name === instanceName
|
|
104
|
-
);
|
|
105
|
-
if (!srv) continue;
|
|
106
|
-
const port = srv.data.port;
|
|
107
|
-
const host = srv.data.target || "127.0.0.1";
|
|
108
|
-
let teamName = "";
|
|
109
|
-
let memberId = "";
|
|
110
|
-
if (txt) {
|
|
111
|
-
const txtData = txt.data ?? [];
|
|
112
|
-
for (const entry of txtData) {
|
|
113
|
-
const str = Buffer.isBuffer(entry) ? entry.toString() : String(entry);
|
|
114
|
-
if (str.startsWith("team=")) teamName = str.slice(5);
|
|
115
|
-
if (str.startsWith("memberId=")) memberId = str.slice(9);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (!teamName || !memberId) continue;
|
|
119
|
-
if (memberId === this.memberId) continue;
|
|
120
|
-
const ptrTtl = ptr.ttl ?? 300;
|
|
121
|
-
const srvTtl = srv.ttl ?? 300;
|
|
122
|
-
if (ptrTtl === 0 || srvTtl === 0) {
|
|
123
|
-
this.peersByTeam.delete(teamName);
|
|
124
|
-
this.peersByMemberId.delete(memberId);
|
|
125
|
-
this.onPeerLostCb?.(memberId);
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
const peer = { host, port, teamName, memberId };
|
|
129
|
-
this.peersByTeam.set(teamName, peer);
|
|
130
|
-
this.peersByMemberId.set(memberId, peer);
|
|
131
|
-
this.onPeerFoundCb?.(peer);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Announce this node's service via mDNS.
|
|
136
|
-
* Sends an unsolicited response so existing peers notice immediately.
|
|
137
|
-
*/
|
|
138
|
-
announce(port, teamName, memberId) {
|
|
139
|
-
this.port = port;
|
|
140
|
-
this.teamName = teamName;
|
|
141
|
-
this.memberId = memberId;
|
|
142
|
-
this.announced = true;
|
|
143
|
-
this.mdns.respond({ answers: this.buildAnswers() });
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Send a PTR query to discover existing peers.
|
|
147
|
-
*/
|
|
148
|
-
discover() {
|
|
149
|
-
this.mdns.query({
|
|
150
|
-
questions: [{ name: SERVICE_TYPE, type: "PTR" }]
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
getPeerByTeam(teamName) {
|
|
154
|
-
return this.peersByTeam.get(teamName);
|
|
155
|
-
}
|
|
156
|
-
onPeerFound(cb) {
|
|
157
|
-
this.onPeerFoundCb = cb;
|
|
158
|
-
}
|
|
159
|
-
onPeerLost(cb) {
|
|
160
|
-
this.onPeerLostCb = cb;
|
|
161
|
-
}
|
|
162
|
-
destroy() {
|
|
163
|
-
this.mdns.destroy();
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
|
|
167
13
|
// src/infrastructure/p2p/p2p-message-protocol.ts
|
|
168
14
|
function serializeP2PMsg(msg) {
|
|
169
15
|
return JSON.stringify(msg);
|
|
@@ -179,8 +25,6 @@ using System.Runtime.InteropServices;
|
|
|
179
25
|
public class ConInject {
|
|
180
26
|
[DllImport("kernel32.dll")] public static extern bool FreeConsole();
|
|
181
27
|
[DllImport("kernel32.dll")] public static extern bool AttachConsole(uint pid);
|
|
182
|
-
[DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
|
|
183
|
-
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hwnd);
|
|
184
28
|
[DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
|
|
185
29
|
public static extern IntPtr CreateFile(
|
|
186
30
|
string lpFileName, uint dwDesiredAccess, uint dwShareMode,
|
|
@@ -209,7 +53,7 @@ public class ConInject {
|
|
|
209
53
|
return CreateFile("CONIN$", 0xC0000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
|
|
210
54
|
}
|
|
211
55
|
|
|
212
|
-
// Inject
|
|
56
|
+
// Inject plain text characters into console input buffer
|
|
213
57
|
public static int InjectText(uint pid, string text) {
|
|
214
58
|
IntPtr hIn = OpenConin(pid);
|
|
215
59
|
if (hIn == new IntPtr(-1)) return -1;
|
|
@@ -227,13 +71,56 @@ public class ConInject {
|
|
|
227
71
|
return ok ? (int)written : -2;
|
|
228
72
|
}
|
|
229
73
|
|
|
230
|
-
//
|
|
231
|
-
public static
|
|
232
|
-
|
|
233
|
-
if (
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
74
|
+
// Inject Enter (VK_RETURN = 0x0D)
|
|
75
|
+
public static int InjectEnter(uint pid) {
|
|
76
|
+
IntPtr hIn = OpenConin(pid);
|
|
77
|
+
if (hIn == new IntPtr(-1)) return -1;
|
|
78
|
+
|
|
79
|
+
var records = new INPUT_RECORD[] {
|
|
80
|
+
new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, wVirtualKeyCode=0x0D, UnicodeChar=0x0D },
|
|
81
|
+
new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, wVirtualKeyCode=0x0D, UnicodeChar=0x0D }
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
uint written;
|
|
85
|
+
bool ok = WriteConsoleInput(hIn, records, (uint)records.Length, out written);
|
|
86
|
+
CloseHandle(hIn);
|
|
87
|
+
return ok ? (int)written : -2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Inject Ctrl+U (VK_U = 0x55, char = 0x15)
|
|
91
|
+
public static int InjectCtrlU(uint pid) {
|
|
92
|
+
IntPtr hIn = OpenConin(pid);
|
|
93
|
+
if (hIn == new IntPtr(-1)) return -1;
|
|
94
|
+
|
|
95
|
+
var records = new INPUT_RECORD[] {
|
|
96
|
+
new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, wVirtualKeyCode=0xA2, dwControlKeyState=LEFT_CTRL },
|
|
97
|
+
new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, wVirtualKeyCode=0x55, UnicodeChar=0x15, dwControlKeyState=LEFT_CTRL },
|
|
98
|
+
new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, wVirtualKeyCode=0x55, UnicodeChar=0x15, dwControlKeyState=LEFT_CTRL },
|
|
99
|
+
new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, wVirtualKeyCode=0xA2, dwControlKeyState=0 }
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
uint written;
|
|
103
|
+
bool ok = WriteConsoleInput(hIn, records, (uint)records.Length, out written);
|
|
104
|
+
CloseHandle(hIn);
|
|
105
|
+
return ok ? (int)written : -2;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Inject Ctrl+Y (VK_Y = 0x59, char = 0x19)
|
|
109
|
+
public static int InjectCtrlY(uint pid) {
|
|
110
|
+
IntPtr hIn = OpenConin(pid);
|
|
111
|
+
if (hIn == new IntPtr(-1)) return -1;
|
|
112
|
+
|
|
113
|
+
var records = new INPUT_RECORD[] {
|
|
114
|
+
new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, wVirtualKeyCode=0xA2, dwControlKeyState=LEFT_CTRL },
|
|
115
|
+
new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, wVirtualKeyCode=0x59, UnicodeChar=0x19, dwControlKeyState=LEFT_CTRL },
|
|
116
|
+
new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, wVirtualKeyCode=0x59, UnicodeChar=0x19, dwControlKeyState=LEFT_CTRL },
|
|
117
|
+
new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, wVirtualKeyCode=0xA2, dwControlKeyState=0 }
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
uint written;
|
|
121
|
+
bool ok = WriteConsoleInput(hIn, records, (uint)records.Length, out written);
|
|
122
|
+
CloseHandle(hIn);
|
|
123
|
+
return ok ? (int)written : -2;
|
|
237
124
|
}
|
|
238
125
|
}
|
|
239
126
|
`;
|
|
@@ -271,33 +158,24 @@ async function windowsInject(text) {
|
|
|
271
158
|
const script = buildScript(claudePid, `
|
|
272
159
|
$textBytes = [System.Convert]::FromBase64String('${textB64}')
|
|
273
160
|
$text = [System.Text.Encoding]::Unicode.GetString($textBytes)
|
|
274
|
-
$wsh = New-Object -ComObject WScript.Shell
|
|
275
161
|
|
|
276
|
-
# 1.
|
|
277
|
-
|
|
278
|
-
Start-Sleep -Milliseconds
|
|
279
|
-
$wsh.SendKeys('^u')
|
|
280
|
-
Start-Sleep -Milliseconds 150
|
|
162
|
+
# 1. Ctrl+U to save user's current text to kill ring
|
|
163
|
+
[ConInject]::InjectCtrlU([uint32]$claudePid) | Out-Null
|
|
164
|
+
Start-Sleep -Milliseconds 100
|
|
281
165
|
|
|
282
166
|
# 2. Write question text into console input buffer
|
|
283
167
|
[ConInject]::InjectText([uint32]$claudePid, $text) | Out-Null
|
|
168
|
+
Start-Sleep -Milliseconds 50
|
|
284
169
|
|
|
285
|
-
# 3.
|
|
286
|
-
[ConInject]::
|
|
287
|
-
Start-Sleep -Milliseconds 150
|
|
288
|
-
|
|
289
|
-
# 4. Send Enter
|
|
290
|
-
$wsh.SendKeys('~')
|
|
170
|
+
# 3. Send Enter
|
|
171
|
+
[ConInject]::InjectEnter([uint32]$claudePid) | Out-Null
|
|
291
172
|
`);
|
|
292
173
|
await run(script);
|
|
293
174
|
}
|
|
294
175
|
async function windowsInjectCtrlY() {
|
|
295
176
|
const claudePid = process.ppid;
|
|
296
177
|
const script = buildScript(claudePid, `
|
|
297
|
-
$
|
|
298
|
-
$hwnd = [ConInject]::FocusConsole([uint32]$claudePid)
|
|
299
|
-
Start-Sleep -Milliseconds 150
|
|
300
|
-
$wsh.SendKeys('^y')
|
|
178
|
+
[ConInject]::InjectCtrlY([uint32]$claudePid) | Out-Null
|
|
301
179
|
`);
|
|
302
180
|
await run(script);
|
|
303
181
|
}
|
|
@@ -364,35 +242,18 @@ var config = {
|
|
|
364
242
|
*/
|
|
365
243
|
p2p: {
|
|
366
244
|
/**
|
|
367
|
-
*
|
|
245
|
+
* Fixed port for the WS server. Override with CLAUDE_COLLAB_PORT env var.
|
|
368
246
|
*/
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Maximum port for the random WS server port range
|
|
372
|
-
*/
|
|
373
|
-
portRangeMax: 19999
|
|
247
|
+
port: Number(process.env["CLAUDE_COLLAB_PORT"] ?? 11777)
|
|
374
248
|
}};
|
|
375
249
|
|
|
376
250
|
// src/infrastructure/p2p/p2p-node.ts
|
|
377
|
-
function getRandomPort(min, max) {
|
|
378
|
-
return new Promise((resolve) => {
|
|
379
|
-
const port = Math.floor(Math.random() * (max - min + 1)) + min;
|
|
380
|
-
const server = createServer();
|
|
381
|
-
server.listen(port, () => {
|
|
382
|
-
server.close(() => resolve(port));
|
|
383
|
-
});
|
|
384
|
-
server.on("error", () => {
|
|
385
|
-
resolve(getRandomPort(min, max));
|
|
386
|
-
});
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
251
|
var P2PNode = class {
|
|
390
252
|
wss = null;
|
|
391
|
-
mdnsDiscovery = null;
|
|
392
253
|
port = 0;
|
|
393
254
|
// Connections indexed by remote team name
|
|
394
255
|
peerConns = /* @__PURE__ */ new Map();
|
|
395
|
-
// Reverse lookup: ws → teamName (for cleanup
|
|
256
|
+
// Reverse lookup: ws → teamName (for cleanup)
|
|
396
257
|
wsToTeam = /* @__PURE__ */ new Map();
|
|
397
258
|
// Questions we received from remote peers (our inbox)
|
|
398
259
|
incomingQuestions = /* @__PURE__ */ new Map();
|
|
@@ -411,14 +272,13 @@ var P2PNode = class {
|
|
|
411
272
|
return this.localMember?.teamName;
|
|
412
273
|
}
|
|
413
274
|
/**
|
|
414
|
-
* Starts the WS server on
|
|
275
|
+
* Starts the WS server on the configured fixed port.
|
|
415
276
|
* Called automatically from join() if not yet started.
|
|
416
277
|
*/
|
|
417
278
|
async start() {
|
|
418
|
-
this.port =
|
|
279
|
+
this.port = config.p2p.port;
|
|
419
280
|
this.wss = new WebSocketServer({ port: this.port });
|
|
420
281
|
this.setupWssHandlers();
|
|
421
|
-
this.mdnsDiscovery = new MdnsDiscovery();
|
|
422
282
|
this._isStarted = true;
|
|
423
283
|
console.error(`P2P node started on port ${this.port}`);
|
|
424
284
|
}
|
|
@@ -428,17 +288,60 @@ var P2PNode = class {
|
|
|
428
288
|
}
|
|
429
289
|
const memberId = v4();
|
|
430
290
|
this.localMember = { memberId, teamName, displayName };
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
291
|
+
return { memberId, teamId: teamName, teamName, displayName, status: "ONLINE" };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Connects to a peer at the given IP and port.
|
|
295
|
+
* Performs a bidirectional HELLO handshake and returns the peer's team name.
|
|
296
|
+
*/
|
|
297
|
+
async connectPeer(ip, port) {
|
|
298
|
+
if (!this.localMember) {
|
|
299
|
+
throw new Error("Must call join() before connectPeer()");
|
|
300
|
+
}
|
|
301
|
+
const targetPort = port ?? config.p2p.port;
|
|
302
|
+
const ws = new WebSocket(`ws://${ip}:${targetPort}`);
|
|
303
|
+
ws.on("message", (data) => {
|
|
304
|
+
try {
|
|
305
|
+
const msg = parseP2PMsg(data.toString());
|
|
306
|
+
this.handleMessage(ws, msg);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error("Failed to parse P2P message:", err);
|
|
437
309
|
}
|
|
438
310
|
});
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
311
|
+
ws.on("close", () => {
|
|
312
|
+
const team = this.wsToTeam.get(ws);
|
|
313
|
+
if (team) {
|
|
314
|
+
if (this.peerConns.get(team) === ws) {
|
|
315
|
+
this.peerConns.delete(team);
|
|
316
|
+
}
|
|
317
|
+
this.wsToTeam.delete(ws);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
await new Promise((resolve, reject) => {
|
|
321
|
+
const timeout = setTimeout(
|
|
322
|
+
() => reject(new Error(`Connection timeout to ${ip}:${targetPort}`)),
|
|
323
|
+
5e3
|
|
324
|
+
);
|
|
325
|
+
ws.on("open", () => {
|
|
326
|
+
clearTimeout(timeout);
|
|
327
|
+
const hello = {
|
|
328
|
+
type: "P2P_HELLO",
|
|
329
|
+
fromTeam: this.localMember.teamName,
|
|
330
|
+
fromMemberId: this.localMember.memberId
|
|
331
|
+
};
|
|
332
|
+
ws.send(serializeP2PMsg(hello));
|
|
333
|
+
resolve();
|
|
334
|
+
});
|
|
335
|
+
ws.on("error", (err) => {
|
|
336
|
+
clearTimeout(timeout);
|
|
337
|
+
reject(err);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
const helloMsg = await this.waitForResponse(
|
|
341
|
+
(m) => m.type === "P2P_HELLO",
|
|
342
|
+
1e4
|
|
343
|
+
);
|
|
344
|
+
return helloMsg.fromTeam;
|
|
442
345
|
}
|
|
443
346
|
async ask(toTeam, content, format) {
|
|
444
347
|
const ws = await this.getPeerConnection(toTeam);
|
|
@@ -544,7 +447,6 @@ var P2PNode = class {
|
|
|
544
447
|
};
|
|
545
448
|
}
|
|
546
449
|
async disconnect() {
|
|
547
|
-
this.mdnsDiscovery?.destroy();
|
|
548
450
|
for (const ws of this.peerConns.values()) {
|
|
549
451
|
ws.close();
|
|
550
452
|
}
|
|
@@ -566,6 +468,14 @@ var P2PNode = class {
|
|
|
566
468
|
ws.on("message", (data) => {
|
|
567
469
|
try {
|
|
568
470
|
const msg = parseP2PMsg(data.toString());
|
|
471
|
+
if (msg.type === "P2P_HELLO" && this.localMember) {
|
|
472
|
+
const hello = {
|
|
473
|
+
type: "P2P_HELLO",
|
|
474
|
+
fromTeam: this.localMember.teamName,
|
|
475
|
+
fromMemberId: this.localMember.memberId
|
|
476
|
+
};
|
|
477
|
+
ws.send(serializeP2PMsg(hello));
|
|
478
|
+
}
|
|
569
479
|
this.handleMessage(ws, msg);
|
|
570
480
|
} catch (err) {
|
|
571
481
|
console.error("Failed to parse incoming P2P message:", err);
|
|
@@ -583,7 +493,7 @@ var P2PNode = class {
|
|
|
583
493
|
});
|
|
584
494
|
}
|
|
585
495
|
// ---------------------------------------------------------------------------
|
|
586
|
-
// Private: unified message handler
|
|
496
|
+
// Private: unified message handler
|
|
587
497
|
// ---------------------------------------------------------------------------
|
|
588
498
|
handleMessage(ws, msg) {
|
|
589
499
|
for (const handler of this.pendingHandlers) {
|
|
@@ -678,78 +588,9 @@ var P2PNode = class {
|
|
|
678
588
|
if (existing && existing.readyState === WebSocket.OPEN) {
|
|
679
589
|
return existing;
|
|
680
590
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
await this.waitForMdnsPeer(teamName, 1e4);
|
|
685
|
-
peer = this.mdnsDiscovery?.getPeerByTeam(teamName);
|
|
686
|
-
}
|
|
687
|
-
if (!peer) {
|
|
688
|
-
throw new Error(
|
|
689
|
-
`Peer for team '${teamName}' not found via mDNS. Make sure the other terminal has joined with that team name.`
|
|
690
|
-
);
|
|
691
|
-
}
|
|
692
|
-
return this.connectToPeer(teamName, peer.host, peer.port);
|
|
693
|
-
}
|
|
694
|
-
async connectToPeer(teamName, host, port) {
|
|
695
|
-
const existing = this.peerConns.get(teamName);
|
|
696
|
-
if (existing && existing.readyState === WebSocket.OPEN) {
|
|
697
|
-
return existing;
|
|
698
|
-
}
|
|
699
|
-
const ws = new WebSocket(`ws://${host}:${port}`);
|
|
700
|
-
await new Promise((resolve, reject) => {
|
|
701
|
-
const timeout = setTimeout(
|
|
702
|
-
() => reject(new Error(`Connection timeout to team '${teamName}'`)),
|
|
703
|
-
5e3
|
|
704
|
-
);
|
|
705
|
-
ws.on("open", () => {
|
|
706
|
-
clearTimeout(timeout);
|
|
707
|
-
const hello = {
|
|
708
|
-
type: "P2P_HELLO",
|
|
709
|
-
fromTeam: this.localMember?.teamName ?? "unknown",
|
|
710
|
-
fromMemberId: this.localMember?.memberId ?? "unknown"
|
|
711
|
-
};
|
|
712
|
-
ws.send(serializeP2PMsg(hello));
|
|
713
|
-
resolve();
|
|
714
|
-
});
|
|
715
|
-
ws.on("error", (err) => {
|
|
716
|
-
clearTimeout(timeout);
|
|
717
|
-
reject(err);
|
|
718
|
-
});
|
|
719
|
-
});
|
|
720
|
-
ws.on("message", (data) => {
|
|
721
|
-
try {
|
|
722
|
-
const msg = parseP2PMsg(data.toString());
|
|
723
|
-
this.handleMessage(ws, msg);
|
|
724
|
-
} catch (err) {
|
|
725
|
-
console.error("Failed to parse P2P message:", err);
|
|
726
|
-
}
|
|
727
|
-
});
|
|
728
|
-
ws.on("close", () => {
|
|
729
|
-
if (this.peerConns.get(teamName) === ws) {
|
|
730
|
-
this.peerConns.delete(teamName);
|
|
731
|
-
}
|
|
732
|
-
});
|
|
733
|
-
this.peerConns.set(teamName, ws);
|
|
734
|
-
return ws;
|
|
735
|
-
}
|
|
736
|
-
waitForMdnsPeer(teamName, timeoutMs) {
|
|
737
|
-
return new Promise((resolve, reject) => {
|
|
738
|
-
const deadline = Date.now() + timeoutMs;
|
|
739
|
-
const check = () => {
|
|
740
|
-
if (this.mdnsDiscovery?.getPeerByTeam(teamName)) {
|
|
741
|
-
resolve();
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
if (Date.now() >= deadline) {
|
|
745
|
-
reject(new Error(`mDNS timeout: team '${teamName}' not found`));
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
this.mdnsDiscovery?.discover();
|
|
749
|
-
setTimeout(check, 500);
|
|
750
|
-
};
|
|
751
|
-
check();
|
|
752
|
-
});
|
|
591
|
+
throw new Error(
|
|
592
|
+
`No connection to team '${teamName}'. Use the connect_peer tool to connect first.`
|
|
593
|
+
);
|
|
753
594
|
}
|
|
754
595
|
waitForResponse(filter, timeoutMs) {
|
|
755
596
|
return new Promise((resolve, reject) => {
|
|
@@ -773,9 +614,9 @@ var joinSchema = {
|
|
|
773
614
|
displayName: z.string().optional().describe('Display name for this terminal (default: team + " Claude")')
|
|
774
615
|
};
|
|
775
616
|
function registerJoinTool(server, client) {
|
|
776
|
-
server.tool("join", joinSchema, async (
|
|
777
|
-
const teamName =
|
|
778
|
-
const displayName =
|
|
617
|
+
server.tool("join", joinSchema, async (args) => {
|
|
618
|
+
const teamName = args.team;
|
|
619
|
+
const displayName = args.displayName ?? `${teamName} Claude`;
|
|
779
620
|
try {
|
|
780
621
|
const member = await client.join(teamName, displayName);
|
|
781
622
|
return {
|
|
@@ -804,14 +645,45 @@ Status: ${member.status}`
|
|
|
804
645
|
}
|
|
805
646
|
});
|
|
806
647
|
}
|
|
648
|
+
var connectPeerSchema = {
|
|
649
|
+
ip: z.string().describe('IP address of the peer to connect to (e.g., "172.16.40.137")'),
|
|
650
|
+
port: z.number().optional().describe(`Port the peer is listening on (default: ${config.p2p.port})`)
|
|
651
|
+
};
|
|
652
|
+
function registerConnectPeerTool(server, client) {
|
|
653
|
+
server.tool("connect_peer", connectPeerSchema, async (args) => {
|
|
654
|
+
const targetPort = args.port ?? config.p2p.port;
|
|
655
|
+
try {
|
|
656
|
+
const peerTeam = await client.connectPeer(args.ip, args.port);
|
|
657
|
+
return {
|
|
658
|
+
content: [
|
|
659
|
+
{
|
|
660
|
+
type: "text",
|
|
661
|
+
text: `Connected to peer "${peerTeam}" at ${args.ip}:${targetPort}.`
|
|
662
|
+
}
|
|
663
|
+
]
|
|
664
|
+
};
|
|
665
|
+
} catch (error) {
|
|
666
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
667
|
+
return {
|
|
668
|
+
content: [
|
|
669
|
+
{
|
|
670
|
+
type: "text",
|
|
671
|
+
text: `Failed to connect to ${args.ip}:${targetPort}: ${errorMessage}`
|
|
672
|
+
}
|
|
673
|
+
],
|
|
674
|
+
isError: true
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
}
|
|
807
679
|
var askSchema = {
|
|
808
680
|
team: z.string().describe('Target team name to ask (e.g., "backend", "frontend")'),
|
|
809
681
|
question: z.string().describe("The question to ask (supports markdown)")
|
|
810
682
|
};
|
|
811
683
|
function registerAskTool(server, client) {
|
|
812
|
-
server.tool("ask", askSchema, async (
|
|
813
|
-
const targetTeam =
|
|
814
|
-
const question =
|
|
684
|
+
server.tool("ask", askSchema, async (args) => {
|
|
685
|
+
const targetTeam = args.team;
|
|
686
|
+
const question = args.question;
|
|
815
687
|
try {
|
|
816
688
|
if (!client.currentTeamId) {
|
|
817
689
|
return {
|
|
@@ -873,8 +745,8 @@ var checkAnswerSchema = {
|
|
|
873
745
|
question_id: z.string().describe('The question ID returned by the "ask" tool')
|
|
874
746
|
};
|
|
875
747
|
function registerCheckAnswerTool(server, client) {
|
|
876
|
-
server.tool("check_answer", checkAnswerSchema, async (
|
|
877
|
-
const questionId =
|
|
748
|
+
server.tool("check_answer", checkAnswerSchema, async (args) => {
|
|
749
|
+
const questionId = args.question_id;
|
|
878
750
|
try {
|
|
879
751
|
if (!client.currentTeamId) {
|
|
880
752
|
return {
|
|
@@ -992,9 +864,9 @@ var replySchema = {
|
|
|
992
864
|
answer: z.string().describe("Your answer to the question (supports markdown)")
|
|
993
865
|
};
|
|
994
866
|
function registerReplyTool(server, client) {
|
|
995
|
-
server.tool("reply", replySchema, async (
|
|
996
|
-
const questionId =
|
|
997
|
-
const answer =
|
|
867
|
+
server.tool("reply", replySchema, async (args) => {
|
|
868
|
+
const questionId = args.questionId;
|
|
869
|
+
const answer = args.answer;
|
|
998
870
|
try {
|
|
999
871
|
if (!client.currentTeamId) {
|
|
1000
872
|
return {
|
|
@@ -1050,6 +922,7 @@ function createMcpServer(options) {
|
|
|
1050
922
|
}
|
|
1051
923
|
);
|
|
1052
924
|
registerJoinTool(server, client);
|
|
925
|
+
registerConnectPeerTool(server, client);
|
|
1053
926
|
registerAskTool(server, client);
|
|
1054
927
|
registerCheckAnswerTool(server, client);
|
|
1055
928
|
registerInboxTool(server, client);
|
|
@@ -1085,40 +958,10 @@ async function startMcpServer(options) {
|
|
|
1085
958
|
}
|
|
1086
959
|
|
|
1087
960
|
// src/mcp-main.ts
|
|
1088
|
-
var args = process.argv.slice(2);
|
|
1089
|
-
function parseArgs() {
|
|
1090
|
-
const options = {};
|
|
1091
|
-
for (let i = 0; i < args.length; i++) {
|
|
1092
|
-
const arg = args[i];
|
|
1093
|
-
const nextArg = args[i + 1];
|
|
1094
|
-
if (arg === "--team" && nextArg) {
|
|
1095
|
-
options.team = nextArg;
|
|
1096
|
-
i++;
|
|
1097
|
-
} else if (arg === "-h" || arg === "--help") {
|
|
1098
|
-
console.error(`
|
|
1099
|
-
Claude Collab MCP Client (P2P mode)
|
|
1100
|
-
|
|
1101
|
-
Usage:
|
|
1102
|
-
mcp-main [options]
|
|
1103
|
-
|
|
1104
|
-
Options:
|
|
1105
|
-
--team <team> Team to auto-join (optional)
|
|
1106
|
-
-h, --help Show this help message
|
|
1107
|
-
`);
|
|
1108
|
-
process.exit(0);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
return options;
|
|
1112
|
-
}
|
|
1113
961
|
async function main() {
|
|
1114
|
-
const options = parseArgs();
|
|
1115
962
|
const p2pNode = new P2PNode();
|
|
1116
963
|
try {
|
|
1117
964
|
await p2pNode.start();
|
|
1118
|
-
if (options.team) {
|
|
1119
|
-
await p2pNode.join(options.team, `${options.team} Claude`);
|
|
1120
|
-
console.error(`Auto-joined team: ${options.team}`);
|
|
1121
|
-
}
|
|
1122
965
|
} catch (error) {
|
|
1123
966
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1124
967
|
console.error(`Failed to start P2P node: ${errorMessage}`);
|