@dolusoft/claude-collab 1.4.3 → 1.5.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 +70 -156
- package/dist/cli.js +360 -319
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +369 -317
- package/dist/mcp-main.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp-main.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
3
3
|
import { v4 } from 'uuid';
|
|
4
|
+
import dgram from 'dgram';
|
|
5
|
+
import os, { tmpdir } from 'os';
|
|
4
6
|
import { EventEmitter } from 'events';
|
|
5
7
|
import { execFile } from 'child_process';
|
|
6
8
|
import { unlinkSync } from 'fs';
|
|
7
|
-
import { tmpdir } from 'os';
|
|
8
9
|
import { join } from 'path';
|
|
9
10
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -17,6 +18,128 @@ function serializeP2PMsg(msg) {
|
|
|
17
18
|
function parseP2PMsg(data) {
|
|
18
19
|
return JSON.parse(data);
|
|
19
20
|
}
|
|
21
|
+
var MULTICAST_ADDR = "239.255.42.42";
|
|
22
|
+
var MULTICAST_PORT = 11776;
|
|
23
|
+
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
24
|
+
var PEER_TIMEOUT_MS = 95e3;
|
|
25
|
+
var MulticastDiscovery = class extends EventEmitter {
|
|
26
|
+
socket = null;
|
|
27
|
+
heartbeatTimer = null;
|
|
28
|
+
timeoutTimer = null;
|
|
29
|
+
peers = /* @__PURE__ */ new Map();
|
|
30
|
+
myName = "";
|
|
31
|
+
myWsPort = 0;
|
|
32
|
+
myIp = "";
|
|
33
|
+
start(name, wsPort) {
|
|
34
|
+
this.myName = name;
|
|
35
|
+
this.myWsPort = wsPort;
|
|
36
|
+
this.myIp = this.resolveLocalIp();
|
|
37
|
+
const socket = dgram.createSocket({ type: "udp4", reuseAddr: true });
|
|
38
|
+
this.socket = socket;
|
|
39
|
+
socket.on("error", (err) => {
|
|
40
|
+
console.error("[multicast] socket error:", err.message);
|
|
41
|
+
});
|
|
42
|
+
socket.on("message", (buf, rinfo) => {
|
|
43
|
+
try {
|
|
44
|
+
const msg = JSON.parse(buf.toString());
|
|
45
|
+
this.handleMessage(msg, rinfo.address);
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
socket.bind(MULTICAST_PORT, () => {
|
|
50
|
+
try {
|
|
51
|
+
socket.addMembership(MULTICAST_ADDR);
|
|
52
|
+
socket.setMulticastTTL(1);
|
|
53
|
+
socket.setMulticastLoopback(false);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error("[multicast] membership error:", err);
|
|
56
|
+
}
|
|
57
|
+
this.announce();
|
|
58
|
+
this.heartbeatTimer = setInterval(() => this.announce(), HEARTBEAT_INTERVAL_MS);
|
|
59
|
+
this.timeoutTimer = setInterval(() => this.checkTimeouts(), 1e4);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
stop() {
|
|
63
|
+
if (this.heartbeatTimer) {
|
|
64
|
+
clearInterval(this.heartbeatTimer);
|
|
65
|
+
this.heartbeatTimer = null;
|
|
66
|
+
}
|
|
67
|
+
if (this.timeoutTimer) {
|
|
68
|
+
clearInterval(this.timeoutTimer);
|
|
69
|
+
this.timeoutTimer = null;
|
|
70
|
+
}
|
|
71
|
+
if (this.socket) {
|
|
72
|
+
this.sendMessage({ type: "LEAVE", name: this.myName });
|
|
73
|
+
try {
|
|
74
|
+
this.socket.dropMembership(MULTICAST_ADDR);
|
|
75
|
+
this.socket.close();
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
this.socket = null;
|
|
79
|
+
}
|
|
80
|
+
this.peers.clear();
|
|
81
|
+
}
|
|
82
|
+
getMyIp() {
|
|
83
|
+
return this.myIp;
|
|
84
|
+
}
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
// Private
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
announce() {
|
|
89
|
+
this.sendMessage({ type: "ANNOUNCE", name: this.myName, wsPort: this.myWsPort });
|
|
90
|
+
}
|
|
91
|
+
sendMessage(msg) {
|
|
92
|
+
if (!this.socket) return;
|
|
93
|
+
const buf = Buffer.from(JSON.stringify(msg));
|
|
94
|
+
this.socket.send(buf, MULTICAST_PORT, MULTICAST_ADDR, (err) => {
|
|
95
|
+
if (err) console.error("[multicast] send error:", err.message);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
handleMessage(msg, fromIp) {
|
|
99
|
+
if (msg.type === "ANNOUNCE") {
|
|
100
|
+
if (msg.name === this.myName) return;
|
|
101
|
+
const existing = this.peers.get(msg.name);
|
|
102
|
+
if (!existing) {
|
|
103
|
+
const peer = { name: msg.name, ip: fromIp, wsPort: msg.wsPort, lastSeen: Date.now() };
|
|
104
|
+
this.peers.set(msg.name, peer);
|
|
105
|
+
this.emit("peer-found", { name: peer.name, ip: peer.ip, wsPort: peer.wsPort });
|
|
106
|
+
console.error(`[multicast] discovered peer: ${msg.name} @ ${fromIp}:${msg.wsPort}`);
|
|
107
|
+
} else {
|
|
108
|
+
existing.lastSeen = Date.now();
|
|
109
|
+
existing.ip = fromIp;
|
|
110
|
+
existing.wsPort = msg.wsPort;
|
|
111
|
+
}
|
|
112
|
+
} else if (msg.type === "LEAVE") {
|
|
113
|
+
if (this.peers.has(msg.name)) {
|
|
114
|
+
this.peers.delete(msg.name);
|
|
115
|
+
this.emit("peer-lost", msg.name);
|
|
116
|
+
console.error(`[multicast] peer left: ${msg.name}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
checkTimeouts() {
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
for (const [name, peer] of this.peers) {
|
|
123
|
+
if (now - peer.lastSeen > PEER_TIMEOUT_MS) {
|
|
124
|
+
this.peers.delete(name);
|
|
125
|
+
this.emit("peer-lost", name);
|
|
126
|
+
console.error(`[multicast] peer timed out: ${name}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
resolveLocalIp() {
|
|
131
|
+
const interfaces = os.networkInterfaces();
|
|
132
|
+
for (const iface of Object.values(interfaces)) {
|
|
133
|
+
if (!iface) continue;
|
|
134
|
+
for (const addr of iface) {
|
|
135
|
+
if (addr.family === "IPv4" && !addr.internal) {
|
|
136
|
+
return addr.address;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return "127.0.0.1";
|
|
141
|
+
}
|
|
142
|
+
};
|
|
20
143
|
var CS_CONINJECT = `
|
|
21
144
|
using System;
|
|
22
145
|
using System.Collections.Generic;
|
|
@@ -252,16 +375,23 @@ var config = {
|
|
|
252
375
|
var P2PNode = class {
|
|
253
376
|
wss = null;
|
|
254
377
|
port = 0;
|
|
255
|
-
|
|
378
|
+
discovery = new MulticastDiscovery();
|
|
379
|
+
// Connections indexed by remote peer name
|
|
256
380
|
peerConns = /* @__PURE__ */ new Map();
|
|
257
|
-
// Reverse lookup: ws →
|
|
258
|
-
|
|
381
|
+
// Reverse lookup: ws → peerName (for cleanup)
|
|
382
|
+
wsToName = /* @__PURE__ */ new Map();
|
|
383
|
+
// Track which connections we initiated (for dedup tiebreaker)
|
|
384
|
+
wsOutgoing = /* @__PURE__ */ new Set();
|
|
385
|
+
// Remote IP per connection (for dedup tiebreaker)
|
|
386
|
+
wsToIp = /* @__PURE__ */ new Map();
|
|
259
387
|
// Questions we received from remote peers (our inbox)
|
|
260
388
|
incomingQuestions = /* @__PURE__ */ new Map();
|
|
261
389
|
// Answers we received for questions we asked
|
|
262
390
|
receivedAnswers = /* @__PURE__ */ new Map();
|
|
263
|
-
// Maps questionId → remote
|
|
264
|
-
|
|
391
|
+
// Maps questionId → remote peer name (so we know who to poll)
|
|
392
|
+
questionToName = /* @__PURE__ */ new Map();
|
|
393
|
+
// Questions we sent — for history
|
|
394
|
+
sentQuestions = /* @__PURE__ */ new Map();
|
|
265
395
|
// Pending response handlers (request-response correlation by filter)
|
|
266
396
|
pendingHandlers = /* @__PURE__ */ new Set();
|
|
267
397
|
localMember = null;
|
|
@@ -270,7 +400,7 @@ var P2PNode = class {
|
|
|
270
400
|
return this._isStarted;
|
|
271
401
|
}
|
|
272
402
|
get currentTeamId() {
|
|
273
|
-
return this.localMember?.
|
|
403
|
+
return this.localMember?.name;
|
|
274
404
|
}
|
|
275
405
|
/**
|
|
276
406
|
* Starts the WS server on a random available port within the configured range.
|
|
@@ -307,71 +437,31 @@ var P2PNode = class {
|
|
|
307
437
|
wss.once("error", () => resolve(false));
|
|
308
438
|
});
|
|
309
439
|
}
|
|
310
|
-
async join(
|
|
440
|
+
async join(name, displayName) {
|
|
311
441
|
if (!this._isStarted) {
|
|
312
442
|
await this.start();
|
|
313
443
|
}
|
|
314
444
|
const memberId = v4();
|
|
315
|
-
this.localMember = { memberId,
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
* Connects to a peer at the given IP and port.
|
|
320
|
-
* Performs a bidirectional HELLO handshake and returns the peer's team name.
|
|
321
|
-
*/
|
|
322
|
-
async connectPeer(ip, port) {
|
|
323
|
-
if (!this.localMember) {
|
|
324
|
-
throw new Error("Must call join() before connectPeer()");
|
|
325
|
-
}
|
|
326
|
-
const ws = new WebSocket(`ws://${ip}:${port}`);
|
|
327
|
-
ws.on("message", (data) => {
|
|
328
|
-
try {
|
|
329
|
-
const msg = parseP2PMsg(data.toString());
|
|
330
|
-
this.handleMessage(ws, msg);
|
|
331
|
-
} catch (err) {
|
|
332
|
-
console.error("Failed to parse P2P message:", err);
|
|
333
|
-
}
|
|
445
|
+
this.localMember = { memberId, name, displayName };
|
|
446
|
+
this.discovery.start(name, this.port);
|
|
447
|
+
this.discovery.on("peer-found", ({ name: peerName, ip, wsPort }) => {
|
|
448
|
+
void this.autoConnect(peerName, ip, wsPort);
|
|
334
449
|
});
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
this.wsToTeam.delete(ws);
|
|
450
|
+
this.discovery.on("peer-lost", (peerName) => {
|
|
451
|
+
const ws = this.peerConns.get(peerName);
|
|
452
|
+
if (ws) {
|
|
453
|
+
ws.close();
|
|
454
|
+
this.peerConns.delete(peerName);
|
|
342
455
|
}
|
|
343
456
|
});
|
|
344
|
-
|
|
345
|
-
const timeout = setTimeout(
|
|
346
|
-
() => reject(new Error(`Connection timeout to ${ip}:${port}`)),
|
|
347
|
-
5e3
|
|
348
|
-
);
|
|
349
|
-
ws.on("open", () => {
|
|
350
|
-
clearTimeout(timeout);
|
|
351
|
-
const hello = {
|
|
352
|
-
type: "P2P_HELLO",
|
|
353
|
-
fromTeam: this.localMember.teamName,
|
|
354
|
-
fromMemberId: this.localMember.memberId
|
|
355
|
-
};
|
|
356
|
-
ws.send(serializeP2PMsg(hello));
|
|
357
|
-
resolve();
|
|
358
|
-
});
|
|
359
|
-
ws.on("error", (err) => {
|
|
360
|
-
clearTimeout(timeout);
|
|
361
|
-
reject(err);
|
|
362
|
-
});
|
|
363
|
-
});
|
|
364
|
-
const helloMsg = await this.waitForResponse(
|
|
365
|
-
(m) => m.type === "P2P_HELLO",
|
|
366
|
-
1e4
|
|
367
|
-
);
|
|
368
|
-
return helloMsg.fromTeam;
|
|
457
|
+
return { memberId, teamId: name, teamName: name, displayName, status: "ONLINE", port: this.port };
|
|
369
458
|
}
|
|
370
|
-
async ask(
|
|
371
|
-
const ws = await this.getPeerConnection(
|
|
459
|
+
async ask(toName, content, format) {
|
|
460
|
+
const ws = await this.getPeerConnection(toName);
|
|
372
461
|
const questionId = v4();
|
|
373
462
|
const requestId = v4();
|
|
374
|
-
this.
|
|
463
|
+
this.questionToName.set(questionId, toName);
|
|
464
|
+
this.sentQuestions.set(questionId, { toPeer: toName, content, askedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
375
465
|
const ackPromise = this.waitForResponse(
|
|
376
466
|
(m) => m.type === "P2P_ASK_ACK" && m.requestId === requestId,
|
|
377
467
|
5e3
|
|
@@ -380,8 +470,8 @@ var P2PNode = class {
|
|
|
380
470
|
type: "P2P_ASK",
|
|
381
471
|
questionId,
|
|
382
472
|
fromMemberId: this.localMember.memberId,
|
|
383
|
-
fromTeam: this.localMember.
|
|
384
|
-
toTeam,
|
|
473
|
+
fromTeam: this.localMember.name,
|
|
474
|
+
toTeam: toName,
|
|
385
475
|
content,
|
|
386
476
|
format,
|
|
387
477
|
requestId
|
|
@@ -401,9 +491,9 @@ var P2PNode = class {
|
|
|
401
491
|
answeredAt: cached.answeredAt
|
|
402
492
|
};
|
|
403
493
|
}
|
|
404
|
-
const
|
|
405
|
-
if (!
|
|
406
|
-
const ws = this.peerConns.get(
|
|
494
|
+
const toName = this.questionToName.get(questionId);
|
|
495
|
+
if (!toName) return null;
|
|
496
|
+
const ws = this.peerConns.get(toName);
|
|
407
497
|
if (!ws || ws.readyState !== WebSocket.OPEN) return null;
|
|
408
498
|
const requestId = v4();
|
|
409
499
|
const responsePromise = this.waitForResponse(
|
|
@@ -446,7 +536,7 @@ var P2PNode = class {
|
|
|
446
536
|
content,
|
|
447
537
|
format,
|
|
448
538
|
answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
449
|
-
fromTeam: this.localMember.
|
|
539
|
+
fromTeam: this.localMember.name,
|
|
450
540
|
fromMemberId: this.localMember.memberId
|
|
451
541
|
};
|
|
452
542
|
if (question.ws.readyState === WebSocket.OPEN) {
|
|
@@ -470,7 +560,42 @@ var P2PNode = class {
|
|
|
470
560
|
pendingCount: questions.length
|
|
471
561
|
};
|
|
472
562
|
}
|
|
563
|
+
getInfo() {
|
|
564
|
+
return {
|
|
565
|
+
teamName: this.localMember?.name,
|
|
566
|
+
port: this._isStarted ? this.port : void 0,
|
|
567
|
+
connectedPeers: [...this.peerConns.keys()]
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
getHistory() {
|
|
571
|
+
const entries = [];
|
|
572
|
+
for (const [questionId, sent] of this.sentQuestions) {
|
|
573
|
+
const answer = this.receivedAnswers.get(questionId);
|
|
574
|
+
entries.push({
|
|
575
|
+
direction: "sent",
|
|
576
|
+
questionId,
|
|
577
|
+
peer: sent.toPeer,
|
|
578
|
+
question: sent.content,
|
|
579
|
+
answer: answer?.content,
|
|
580
|
+
askedAt: sent.askedAt,
|
|
581
|
+
answeredAt: answer?.answeredAt
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
for (const [questionId, incoming] of this.incomingQuestions) {
|
|
585
|
+
entries.push({
|
|
586
|
+
direction: "received",
|
|
587
|
+
questionId,
|
|
588
|
+
peer: incoming.fromTeam,
|
|
589
|
+
question: incoming.content,
|
|
590
|
+
answer: incoming.answered ? incoming.answerContent : void 0,
|
|
591
|
+
askedAt: incoming.createdAt.toISOString(),
|
|
592
|
+
answeredAt: incoming.answered ? (/* @__PURE__ */ new Date()).toISOString() : void 0
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
return entries.sort((a, b) => a.askedAt.localeCompare(b.askedAt));
|
|
596
|
+
}
|
|
473
597
|
async disconnect() {
|
|
598
|
+
this.discovery.stop();
|
|
474
599
|
for (const ws of this.peerConns.values()) {
|
|
475
600
|
ws.close();
|
|
476
601
|
}
|
|
@@ -485,35 +610,88 @@ var P2PNode = class {
|
|
|
485
610
|
this._isStarted = false;
|
|
486
611
|
}
|
|
487
612
|
// ---------------------------------------------------------------------------
|
|
613
|
+
// Private: auto-connect from multicast discovery
|
|
614
|
+
// ---------------------------------------------------------------------------
|
|
615
|
+
async autoConnect(peerName, ip, wsPort) {
|
|
616
|
+
const existing = this.peerConns.get(peerName);
|
|
617
|
+
if (existing && existing.readyState === WebSocket.OPEN) return;
|
|
618
|
+
try {
|
|
619
|
+
await this.connectToPeer(ip, wsPort);
|
|
620
|
+
} catch (err) {
|
|
621
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
622
|
+
console.error(`[p2p] auto-connect to ${peerName} @ ${ip}:${wsPort} failed: ${msg}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Internal: open a WebSocket connection to a peer and perform HELLO handshake.
|
|
627
|
+
*/
|
|
628
|
+
async connectToPeer(ip, port) {
|
|
629
|
+
if (!this.localMember) {
|
|
630
|
+
throw new Error("Must call join() before connecting to peers");
|
|
631
|
+
}
|
|
632
|
+
const ws = new WebSocket(`ws://${ip}:${port}`);
|
|
633
|
+
this.wsOutgoing.add(ws);
|
|
634
|
+
this.wsToIp.set(ws, ip);
|
|
635
|
+
ws.on("message", (data) => {
|
|
636
|
+
try {
|
|
637
|
+
const msg = parseP2PMsg(data.toString());
|
|
638
|
+
this.handleMessage(ws, msg);
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.error("[p2p] Failed to parse message:", err);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
ws.on("close", () => this.cleanupWs(ws));
|
|
644
|
+
ws.on("error", (err) => console.error("[p2p] ws error:", err.message));
|
|
645
|
+
await new Promise((resolve, reject) => {
|
|
646
|
+
const timeout = setTimeout(
|
|
647
|
+
() => reject(new Error(`Connection timeout to ${ip}:${port}`)),
|
|
648
|
+
5e3
|
|
649
|
+
);
|
|
650
|
+
ws.on("open", () => {
|
|
651
|
+
clearTimeout(timeout);
|
|
652
|
+
const hello = {
|
|
653
|
+
type: "P2P_HELLO",
|
|
654
|
+
fromTeam: this.localMember.name,
|
|
655
|
+
fromMemberId: this.localMember.memberId
|
|
656
|
+
};
|
|
657
|
+
ws.send(serializeP2PMsg(hello));
|
|
658
|
+
resolve();
|
|
659
|
+
});
|
|
660
|
+
ws.on("error", (err) => {
|
|
661
|
+
clearTimeout(timeout);
|
|
662
|
+
reject(err);
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
const helloMsg = await this.waitForResponse(
|
|
666
|
+
(m) => m.type === "P2P_HELLO",
|
|
667
|
+
1e4
|
|
668
|
+
);
|
|
669
|
+
return helloMsg.fromTeam;
|
|
670
|
+
}
|
|
671
|
+
// ---------------------------------------------------------------------------
|
|
488
672
|
// Private: WebSocket server setup
|
|
489
673
|
// ---------------------------------------------------------------------------
|
|
490
674
|
setupWssHandlers() {
|
|
491
|
-
this.wss.on("connection", (ws) => {
|
|
675
|
+
this.wss.on("connection", (ws, req) => {
|
|
676
|
+
const remoteIp = (req.socket.remoteAddress ?? "").replace("::ffff:", "");
|
|
677
|
+
this.wsToIp.set(ws, remoteIp);
|
|
492
678
|
ws.on("message", (data) => {
|
|
493
679
|
try {
|
|
494
680
|
const msg = parseP2PMsg(data.toString());
|
|
495
681
|
if (msg.type === "P2P_HELLO" && this.localMember) {
|
|
496
682
|
const hello = {
|
|
497
683
|
type: "P2P_HELLO",
|
|
498
|
-
fromTeam: this.localMember.
|
|
684
|
+
fromTeam: this.localMember.name,
|
|
499
685
|
fromMemberId: this.localMember.memberId
|
|
500
686
|
};
|
|
501
687
|
ws.send(serializeP2PMsg(hello));
|
|
502
688
|
}
|
|
503
689
|
this.handleMessage(ws, msg);
|
|
504
690
|
} catch (err) {
|
|
505
|
-
console.error("Failed to parse incoming
|
|
506
|
-
}
|
|
507
|
-
});
|
|
508
|
-
ws.on("close", () => {
|
|
509
|
-
const team = this.wsToTeam.get(ws);
|
|
510
|
-
if (team) {
|
|
511
|
-
if (this.peerConns.get(team) === ws) {
|
|
512
|
-
this.peerConns.delete(team);
|
|
513
|
-
}
|
|
514
|
-
this.wsToTeam.delete(ws);
|
|
691
|
+
console.error("[p2p] Failed to parse incoming message:", err);
|
|
515
692
|
}
|
|
516
693
|
});
|
|
694
|
+
ws.on("close", () => this.cleanupWs(ws));
|
|
517
695
|
});
|
|
518
696
|
}
|
|
519
697
|
// ---------------------------------------------------------------------------
|
|
@@ -525,9 +703,7 @@ var P2PNode = class {
|
|
|
525
703
|
}
|
|
526
704
|
switch (msg.type) {
|
|
527
705
|
case "P2P_HELLO":
|
|
528
|
-
this.
|
|
529
|
-
this.peerConns.set(msg.fromTeam, ws);
|
|
530
|
-
console.error(`Peer identified: ${msg.fromTeam}`);
|
|
706
|
+
this.handleHello(ws, msg);
|
|
531
707
|
break;
|
|
532
708
|
case "P2P_ASK":
|
|
533
709
|
this.handleIncomingAsk(ws, msg);
|
|
@@ -551,6 +727,28 @@ var P2PNode = class {
|
|
|
551
727
|
break;
|
|
552
728
|
}
|
|
553
729
|
}
|
|
730
|
+
handleHello(ws, msg) {
|
|
731
|
+
const peerName = msg.fromTeam;
|
|
732
|
+
const existing = this.peerConns.get(peerName);
|
|
733
|
+
if (existing && existing.readyState === WebSocket.OPEN) {
|
|
734
|
+
const myIp = this.discovery.getMyIp();
|
|
735
|
+
const peerIp = this.wsToIp.get(ws) ?? "";
|
|
736
|
+
const iShouldInitiate = myIp < peerIp;
|
|
737
|
+
const thisIsOutgoing = this.wsOutgoing.has(ws);
|
|
738
|
+
if (iShouldInitiate && !thisIsOutgoing) {
|
|
739
|
+
ws.close();
|
|
740
|
+
return;
|
|
741
|
+
} else if (!iShouldInitiate && thisIsOutgoing) {
|
|
742
|
+
ws.close();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
ws.close();
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
this.wsToName.set(ws, peerName);
|
|
749
|
+
this.peerConns.set(peerName, ws);
|
|
750
|
+
console.error(`[p2p] connected to peer: ${peerName}`);
|
|
751
|
+
}
|
|
554
752
|
handleIncomingAsk(ws, msg) {
|
|
555
753
|
this.incomingQuestions.set(msg.questionId, {
|
|
556
754
|
questionId: msg.questionId,
|
|
@@ -598,7 +796,7 @@ var P2PNode = class {
|
|
|
598
796
|
content: question.answerContent,
|
|
599
797
|
format: question.answerFormat,
|
|
600
798
|
answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
601
|
-
fromTeam: this.localMember.
|
|
799
|
+
fromTeam: this.localMember.name,
|
|
602
800
|
fromMemberId: this.localMember.memberId,
|
|
603
801
|
requestId: msg.requestId
|
|
604
802
|
};
|
|
@@ -607,13 +805,24 @@ var P2PNode = class {
|
|
|
607
805
|
// ---------------------------------------------------------------------------
|
|
608
806
|
// Private: peer connection management
|
|
609
807
|
// ---------------------------------------------------------------------------
|
|
610
|
-
|
|
611
|
-
const
|
|
808
|
+
cleanupWs(ws) {
|
|
809
|
+
const name = this.wsToName.get(ws);
|
|
810
|
+
if (name) {
|
|
811
|
+
if (this.peerConns.get(name) === ws) {
|
|
812
|
+
this.peerConns.delete(name);
|
|
813
|
+
}
|
|
814
|
+
this.wsToName.delete(ws);
|
|
815
|
+
}
|
|
816
|
+
this.wsOutgoing.delete(ws);
|
|
817
|
+
this.wsToIp.delete(ws);
|
|
818
|
+
}
|
|
819
|
+
async getPeerConnection(name) {
|
|
820
|
+
const existing = this.peerConns.get(name);
|
|
612
821
|
if (existing && existing.readyState === WebSocket.OPEN) {
|
|
613
822
|
return existing;
|
|
614
823
|
}
|
|
615
824
|
throw new Error(
|
|
616
|
-
`No connection to
|
|
825
|
+
`No connection to peer '${name}'. They may not be on the network yet \u2014 wait a moment and try again.`
|
|
617
826
|
);
|
|
618
827
|
}
|
|
619
828
|
waitForResponse(filter, timeoutMs) {
|
|
@@ -633,81 +842,13 @@ var P2PNode = class {
|
|
|
633
842
|
});
|
|
634
843
|
}
|
|
635
844
|
};
|
|
636
|
-
var joinSchema = {
|
|
637
|
-
team: z.string().describe('Team name to join (e.g., "frontend", "backend", "devops")'),
|
|
638
|
-
displayName: z.string().optional().describe('Display name for this terminal (default: team + " Claude")')
|
|
639
|
-
};
|
|
640
|
-
function registerJoinTool(server, client) {
|
|
641
|
-
server.tool("join", joinSchema, async (args) => {
|
|
642
|
-
const teamName = args.team;
|
|
643
|
-
const displayName = args.displayName ?? `${teamName} Claude`;
|
|
644
|
-
try {
|
|
645
|
-
const member = await client.join(teamName, displayName);
|
|
646
|
-
return {
|
|
647
|
-
content: [
|
|
648
|
-
{
|
|
649
|
-
type: "text",
|
|
650
|
-
text: `Successfully joined team "${member.teamName}" as "${member.displayName}".
|
|
651
|
-
|
|
652
|
-
Your member ID: ${member.memberId}
|
|
653
|
-
Status: ${member.status}
|
|
654
|
-
Listening on port: ${member.port}
|
|
655
|
-
|
|
656
|
-
Share this port with the other terminal so they can run: connect_peer("<your-ip>", ${member.port})`
|
|
657
|
-
}
|
|
658
|
-
]
|
|
659
|
-
};
|
|
660
|
-
} catch (error) {
|
|
661
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
662
|
-
return {
|
|
663
|
-
content: [
|
|
664
|
-
{
|
|
665
|
-
type: "text",
|
|
666
|
-
text: `Failed to join team: ${errorMessage}`
|
|
667
|
-
}
|
|
668
|
-
],
|
|
669
|
-
isError: true
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
var connectPeerSchema = {
|
|
675
|
-
ip: z.string().describe('IP address of the peer to connect to (e.g., "172.16.40.137")'),
|
|
676
|
-
port: z.number().describe("Port the peer is listening on (shown in their join response)")
|
|
677
|
-
};
|
|
678
|
-
function registerConnectPeerTool(server, client) {
|
|
679
|
-
server.tool("connect_peer", connectPeerSchema, async (args) => {
|
|
680
|
-
try {
|
|
681
|
-
const peerTeam = await client.connectPeer(args.ip, args.port);
|
|
682
|
-
return {
|
|
683
|
-
content: [
|
|
684
|
-
{
|
|
685
|
-
type: "text",
|
|
686
|
-
text: `Connected to peer "${peerTeam}" at ${args.ip}:${args.port}.`
|
|
687
|
-
}
|
|
688
|
-
]
|
|
689
|
-
};
|
|
690
|
-
} catch (error) {
|
|
691
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
692
|
-
return {
|
|
693
|
-
content: [
|
|
694
|
-
{
|
|
695
|
-
type: "text",
|
|
696
|
-
text: `Failed to connect to ${args.ip}:${args.port}: ${errorMessage}`
|
|
697
|
-
}
|
|
698
|
-
],
|
|
699
|
-
isError: true
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
845
|
var askSchema = {
|
|
705
|
-
|
|
846
|
+
peer: z.string().describe('Name of the peer to ask (e.g., "alice", "backend")'),
|
|
706
847
|
question: z.string().describe("The question to ask (supports markdown)")
|
|
707
848
|
};
|
|
708
849
|
function registerAskTool(server, client) {
|
|
709
850
|
server.tool("ask", askSchema, async (args) => {
|
|
710
|
-
const
|
|
851
|
+
const targetPeer = args.peer;
|
|
711
852
|
const question = args.question;
|
|
712
853
|
try {
|
|
713
854
|
if (!client.currentTeamId) {
|
|
@@ -715,13 +856,13 @@ function registerAskTool(server, client) {
|
|
|
715
856
|
content: [
|
|
716
857
|
{
|
|
717
858
|
type: "text",
|
|
718
|
-
text:
|
|
859
|
+
text: "Node is not ready yet. Wait a moment and try again."
|
|
719
860
|
}
|
|
720
861
|
],
|
|
721
862
|
isError: true
|
|
722
863
|
};
|
|
723
864
|
}
|
|
724
|
-
const questionId = await client.ask(
|
|
865
|
+
const questionId = await client.ask(targetPeer, question, "markdown");
|
|
725
866
|
const POLL_INTERVAL_MS = 5e3;
|
|
726
867
|
const MAX_WAIT_MS = 5 * 60 * 1e3;
|
|
727
868
|
const deadline = Date.now() + MAX_WAIT_MS;
|
|
@@ -766,124 +907,6 @@ Manuel kontrol i\xE7in "check_answer" tool'unu kullanabilirsin.`
|
|
|
766
907
|
}
|
|
767
908
|
});
|
|
768
909
|
}
|
|
769
|
-
var checkAnswerSchema = {
|
|
770
|
-
question_id: z.string().describe('The question ID returned by the "ask" tool')
|
|
771
|
-
};
|
|
772
|
-
function registerCheckAnswerTool(server, client) {
|
|
773
|
-
server.tool("check_answer", checkAnswerSchema, async (args) => {
|
|
774
|
-
const questionId = args.question_id;
|
|
775
|
-
try {
|
|
776
|
-
if (!client.currentTeamId) {
|
|
777
|
-
return {
|
|
778
|
-
content: [
|
|
779
|
-
{
|
|
780
|
-
type: "text",
|
|
781
|
-
text: 'You must join a team first. Use the "join" tool to join a team.'
|
|
782
|
-
}
|
|
783
|
-
],
|
|
784
|
-
isError: true
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
const answer = await client.checkAnswer(questionId);
|
|
788
|
-
if (!answer) {
|
|
789
|
-
return {
|
|
790
|
-
content: [
|
|
791
|
-
{
|
|
792
|
-
type: "text",
|
|
793
|
-
text: `No answer yet for question \`${questionId}\`. The other team hasn't replied yet. You can continue working and check again later.`
|
|
794
|
-
}
|
|
795
|
-
]
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
return {
|
|
799
|
-
content: [
|
|
800
|
-
{
|
|
801
|
-
type: "text",
|
|
802
|
-
text: `**Answer from ${answer.from.displayName} (${answer.from.teamName}):**
|
|
803
|
-
|
|
804
|
-
${answer.content}`
|
|
805
|
-
}
|
|
806
|
-
]
|
|
807
|
-
};
|
|
808
|
-
} catch (error) {
|
|
809
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
810
|
-
return {
|
|
811
|
-
content: [
|
|
812
|
-
{
|
|
813
|
-
type: "text",
|
|
814
|
-
text: `Failed to check answer: ${errorMessage}`
|
|
815
|
-
}
|
|
816
|
-
],
|
|
817
|
-
isError: true
|
|
818
|
-
};
|
|
819
|
-
}
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// src/presentation/mcp/tools/inbox.tool.ts
|
|
824
|
-
var inboxSchema = {};
|
|
825
|
-
function registerInboxTool(server, client) {
|
|
826
|
-
server.tool("inbox", inboxSchema, async () => {
|
|
827
|
-
try {
|
|
828
|
-
if (!client.currentTeamId) {
|
|
829
|
-
return {
|
|
830
|
-
content: [
|
|
831
|
-
{
|
|
832
|
-
type: "text",
|
|
833
|
-
text: 'You must join a team first. Use the "join" tool to join a team.'
|
|
834
|
-
}
|
|
835
|
-
],
|
|
836
|
-
isError: true
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
const inbox = await client.getInbox();
|
|
840
|
-
if (inbox.questions.length === 0) {
|
|
841
|
-
return {
|
|
842
|
-
content: [
|
|
843
|
-
{
|
|
844
|
-
type: "text",
|
|
845
|
-
text: "No pending questions in your inbox."
|
|
846
|
-
}
|
|
847
|
-
]
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
const questionsList = inbox.questions.map((q, i) => {
|
|
851
|
-
const ageSeconds = Math.floor(q.ageMs / 1e3);
|
|
852
|
-
const ageStr = ageSeconds < 60 ? `${ageSeconds}s ago` : `${Math.floor(ageSeconds / 60)}m ago`;
|
|
853
|
-
return `### ${i + 1}. Question from ${q.from.displayName} (${q.from.teamName}) - ${ageStr}
|
|
854
|
-
**ID:** \`${q.questionId}\`
|
|
855
|
-
**Status:** ${q.status}
|
|
856
|
-
|
|
857
|
-
${q.content}
|
|
858
|
-
|
|
859
|
-
---`;
|
|
860
|
-
}).join("\n\n");
|
|
861
|
-
return {
|
|
862
|
-
content: [
|
|
863
|
-
{
|
|
864
|
-
type: "text",
|
|
865
|
-
text: `# Inbox (${inbox.pendingCount} pending, ${inbox.totalCount} total)
|
|
866
|
-
|
|
867
|
-
${questionsList}
|
|
868
|
-
|
|
869
|
-
Use the "reply" tool with the question ID to answer a question.`
|
|
870
|
-
}
|
|
871
|
-
]
|
|
872
|
-
};
|
|
873
|
-
} catch (error) {
|
|
874
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
875
|
-
return {
|
|
876
|
-
content: [
|
|
877
|
-
{
|
|
878
|
-
type: "text",
|
|
879
|
-
text: `Failed to get inbox: ${errorMessage}`
|
|
880
|
-
}
|
|
881
|
-
],
|
|
882
|
-
isError: true
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
910
|
var replySchema = {
|
|
888
911
|
questionId: z.string().describe("The ID of the question to reply to (from inbox)"),
|
|
889
912
|
answer: z.string().describe("Your answer to the question (supports markdown)")
|
|
@@ -929,51 +952,69 @@ function registerReplyTool(server, client) {
|
|
|
929
952
|
});
|
|
930
953
|
}
|
|
931
954
|
|
|
955
|
+
// src/presentation/mcp/tools/peers.tool.ts
|
|
956
|
+
function registerPeersTool(server, client) {
|
|
957
|
+
server.tool("peers", {}, async () => {
|
|
958
|
+
const info = client.getInfo();
|
|
959
|
+
const myName = info.teamName ?? "(starting...)";
|
|
960
|
+
const connected = info.connectedPeers;
|
|
961
|
+
if (connected.length === 0) {
|
|
962
|
+
return {
|
|
963
|
+
content: [{
|
|
964
|
+
type: "text",
|
|
965
|
+
text: `You are "${myName}". No peers connected yet \u2014 they will appear automatically when they come online.`
|
|
966
|
+
}]
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
const list = connected.map((name) => ` \u2022 ${name}`).join("\n");
|
|
970
|
+
return {
|
|
971
|
+
content: [{
|
|
972
|
+
type: "text",
|
|
973
|
+
text: `You are "${myName}". Connected peers (${connected.length}):
|
|
974
|
+
${list}`
|
|
975
|
+
}]
|
|
976
|
+
};
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// src/presentation/mcp/tools/history.tool.ts
|
|
981
|
+
function registerHistoryTool(server, client) {
|
|
982
|
+
server.tool("history", {}, async () => {
|
|
983
|
+
const entries = client.getHistory();
|
|
984
|
+
if (entries.length === 0) {
|
|
985
|
+
return {
|
|
986
|
+
content: [{ type: "text", text: "No questions yet this session." }]
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
const lines = entries.map((e) => {
|
|
990
|
+
const time = new Date(e.askedAt).toLocaleTimeString();
|
|
991
|
+
if (e.direction === "sent") {
|
|
992
|
+
const answerLine = e.answer ? ` \u21B3 ${e.peer}: ${e.answer}` : ` \u21B3 (no answer yet)`;
|
|
993
|
+
return `[${time}] \u2192 ${e.peer}: ${e.question}
|
|
994
|
+
${answerLine}`;
|
|
995
|
+
} else {
|
|
996
|
+
const answerLine = e.answer ? ` \u21B3 you: ${e.answer}` : ` \u21B3 (not replied yet)`;
|
|
997
|
+
return `[${time}] \u2190 ${e.peer}: ${e.question}
|
|
998
|
+
${answerLine}`;
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
return {
|
|
1002
|
+
content: [{ type: "text", text: lines.join("\n\n") }]
|
|
1003
|
+
};
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
932
1007
|
// src/presentation/mcp/server.ts
|
|
933
1008
|
function createMcpServer(options) {
|
|
934
1009
|
const { client } = options;
|
|
935
|
-
const server = new McpServer(
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
},
|
|
940
|
-
{
|
|
941
|
-
capabilities: {
|
|
942
|
-
resources: {
|
|
943
|
-
subscribe: true,
|
|
944
|
-
listChanged: true
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
);
|
|
949
|
-
registerJoinTool(server, client);
|
|
950
|
-
registerConnectPeerTool(server, client);
|
|
1010
|
+
const server = new McpServer({
|
|
1011
|
+
name: "claude-collab",
|
|
1012
|
+
version: "0.1.0"
|
|
1013
|
+
});
|
|
951
1014
|
registerAskTool(server, client);
|
|
952
|
-
registerCheckAnswerTool(server, client);
|
|
953
|
-
registerInboxTool(server, client);
|
|
954
1015
|
registerReplyTool(server, client);
|
|
955
|
-
server
|
|
956
|
-
|
|
957
|
-
"inbox://questions",
|
|
958
|
-
{ description: "Your inbox of pending questions from other teams", mimeType: "application/json" },
|
|
959
|
-
async () => {
|
|
960
|
-
try {
|
|
961
|
-
const inbox = await client.getInbox();
|
|
962
|
-
return {
|
|
963
|
-
contents: [
|
|
964
|
-
{
|
|
965
|
-
uri: "inbox://questions",
|
|
966
|
-
mimeType: "application/json",
|
|
967
|
-
text: JSON.stringify(inbox, null, 2)
|
|
968
|
-
}
|
|
969
|
-
]
|
|
970
|
-
};
|
|
971
|
-
} catch (error) {
|
|
972
|
-
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
973
|
-
throw new Error(`Failed to read inbox: ${errorMessage}`);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
);
|
|
1016
|
+
registerPeersTool(server, client);
|
|
1017
|
+
registerHistoryTool(server, client);
|
|
977
1018
|
return server;
|
|
978
1019
|
}
|
|
979
1020
|
async function startMcpServer(options) {
|
|
@@ -983,8 +1024,19 @@ async function startMcpServer(options) {
|
|
|
983
1024
|
}
|
|
984
1025
|
|
|
985
1026
|
// src/mcp-main.ts
|
|
1027
|
+
function parseName() {
|
|
1028
|
+
const idx = process.argv.indexOf("--name");
|
|
1029
|
+
const value = process.argv[idx + 1];
|
|
1030
|
+
if (idx !== -1 && value) {
|
|
1031
|
+
return value;
|
|
1032
|
+
}
|
|
1033
|
+
console.error("Usage: claude-collab --name <your-name>");
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
986
1036
|
async function main() {
|
|
1037
|
+
const name = parseName();
|
|
987
1038
|
const p2pNode = new P2PNode();
|
|
1039
|
+
await p2pNode.join(name, name);
|
|
988
1040
|
await startMcpServer({ client: p2pNode });
|
|
989
1041
|
}
|
|
990
1042
|
main().catch((error) => {
|