@dolusoft/claude-collab 1.5.0 → 1.6.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 +39 -85
- package/dist/cli.js +246 -418
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +154 -441
- package/dist/mcp-main.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp-main.js
CHANGED
|
@@ -1,145 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
3
|
import { v4 } from 'uuid';
|
|
4
|
-
import dgram from 'dgram';
|
|
5
|
-
import os, { tmpdir } from 'os';
|
|
6
4
|
import { EventEmitter } from 'events';
|
|
7
5
|
import { execFile } from 'child_process';
|
|
8
6
|
import { unlinkSync } from 'fs';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
9
8
|
import { join } from 'path';
|
|
10
9
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
11
|
import { z } from 'zod';
|
|
13
12
|
|
|
14
|
-
// src/infrastructure/
|
|
15
|
-
function
|
|
13
|
+
// src/infrastructure/hub/hub-protocol.ts
|
|
14
|
+
function serialize(msg) {
|
|
16
15
|
return JSON.stringify(msg);
|
|
17
16
|
}
|
|
18
|
-
function
|
|
17
|
+
function parse(data) {
|
|
19
18
|
return JSON.parse(data);
|
|
20
19
|
}
|
|
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
|
-
};
|
|
143
20
|
var CS_CONINJECT = `
|
|
144
21
|
using System;
|
|
145
22
|
using System.Collections.Generic;
|
|
@@ -358,125 +235,50 @@ var InjectionQueue = class extends EventEmitter {
|
|
|
358
235
|
};
|
|
359
236
|
var injectionQueue = new InjectionQueue();
|
|
360
237
|
|
|
361
|
-
// src/
|
|
362
|
-
var
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
* Port range for the WS server. Override with CLAUDE_COLLAB_PORT_MIN / MAX env vars.
|
|
369
|
-
*/
|
|
370
|
-
portRangeMin: Number(process.env["CLAUDE_COLLAB_PORT_MIN"] ?? 11700),
|
|
371
|
-
portRangeMax: Number(process.env["CLAUDE_COLLAB_PORT_MAX"] ?? 11750)
|
|
372
|
-
}};
|
|
373
|
-
|
|
374
|
-
// src/infrastructure/p2p/p2p-node.ts
|
|
375
|
-
var P2PNode = class {
|
|
376
|
-
wss = null;
|
|
377
|
-
port = 0;
|
|
378
|
-
discovery = new MulticastDiscovery();
|
|
379
|
-
// Connections indexed by remote peer name
|
|
380
|
-
peerConns = /* @__PURE__ */ new Map();
|
|
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();
|
|
387
|
-
// Questions we received from remote peers (our inbox)
|
|
238
|
+
// src/infrastructure/hub/hub-client.ts
|
|
239
|
+
var HubClient = class {
|
|
240
|
+
ws = null;
|
|
241
|
+
myName = "";
|
|
242
|
+
serverUrl = "";
|
|
243
|
+
reconnectTimer = null;
|
|
244
|
+
connectedPeers = /* @__PURE__ */ new Set();
|
|
388
245
|
incomingQuestions = /* @__PURE__ */ new Map();
|
|
389
|
-
// Answers we received for questions we asked
|
|
390
246
|
receivedAnswers = /* @__PURE__ */ new Map();
|
|
391
|
-
// Maps questionId → remote peer name (so we know who to poll)
|
|
392
247
|
questionToName = /* @__PURE__ */ new Map();
|
|
393
|
-
|
|
248
|
+
questionToSender = /* @__PURE__ */ new Map();
|
|
394
249
|
sentQuestions = /* @__PURE__ */ new Map();
|
|
395
|
-
// Pending response handlers (request-response correlation by filter)
|
|
396
250
|
pendingHandlers = /* @__PURE__ */ new Set();
|
|
397
|
-
|
|
398
|
-
|
|
251
|
+
setServerUrl(url) {
|
|
252
|
+
this.serverUrl = url;
|
|
253
|
+
}
|
|
399
254
|
get isConnected() {
|
|
400
|
-
return this.
|
|
255
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
401
256
|
}
|
|
402
257
|
get currentTeamId() {
|
|
403
|
-
return this.
|
|
404
|
-
}
|
|
405
|
-
/**
|
|
406
|
-
* Starts the WS server on a random available port within the configured range.
|
|
407
|
-
* Called automatically from join() if not yet started.
|
|
408
|
-
*/
|
|
409
|
-
async start() {
|
|
410
|
-
const { portRangeMin, portRangeMax } = config.p2p;
|
|
411
|
-
const range = portRangeMax - portRangeMin + 1;
|
|
412
|
-
const startOffset = Math.floor(Math.random() * range);
|
|
413
|
-
for (let i = 0; i < range; i++) {
|
|
414
|
-
const port = portRangeMin + (startOffset + i) % range;
|
|
415
|
-
const bound = await this.tryBind(port);
|
|
416
|
-
if (bound) {
|
|
417
|
-
this.port = port;
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
if (!this.wss) {
|
|
422
|
-
throw new Error(
|
|
423
|
-
`No available port in range ${portRangeMin}-${portRangeMax}. Override with CLAUDE_COLLAB_PORT_MIN / CLAUDE_COLLAB_PORT_MAX.`
|
|
424
|
-
);
|
|
425
|
-
}
|
|
426
|
-
this.setupWssHandlers();
|
|
427
|
-
this._isStarted = true;
|
|
428
|
-
console.error(`P2P node started on port ${this.port}`);
|
|
429
|
-
}
|
|
430
|
-
tryBind(port) {
|
|
431
|
-
return new Promise((resolve) => {
|
|
432
|
-
const wss = new WebSocketServer({ port });
|
|
433
|
-
wss.once("listening", () => {
|
|
434
|
-
this.wss = wss;
|
|
435
|
-
resolve(true);
|
|
436
|
-
});
|
|
437
|
-
wss.once("error", () => resolve(false));
|
|
438
|
-
});
|
|
258
|
+
return this.myName || void 0;
|
|
439
259
|
}
|
|
440
260
|
async join(name, displayName) {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const ws = this.peerConns.get(peerName);
|
|
452
|
-
if (ws) {
|
|
453
|
-
ws.close();
|
|
454
|
-
this.peerConns.delete(peerName);
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
return { memberId, teamId: name, teamName: name, displayName, status: "ONLINE", port: this.port };
|
|
261
|
+
this.myName = name;
|
|
262
|
+
await this.connectAndHello();
|
|
263
|
+
return {
|
|
264
|
+
memberId: v4(),
|
|
265
|
+
teamId: name,
|
|
266
|
+
teamName: name,
|
|
267
|
+
displayName,
|
|
268
|
+
status: "ONLINE",
|
|
269
|
+
port: 0
|
|
270
|
+
};
|
|
458
271
|
}
|
|
459
|
-
async ask(
|
|
460
|
-
const ws = await this.getPeerConnection(toName);
|
|
272
|
+
async ask(toPeer, content, format) {
|
|
461
273
|
const questionId = v4();
|
|
462
274
|
const requestId = v4();
|
|
463
|
-
this.questionToName.set(questionId,
|
|
464
|
-
this.sentQuestions.set(questionId, { toPeer
|
|
275
|
+
this.questionToName.set(questionId, toPeer);
|
|
276
|
+
this.sentQuestions.set(questionId, { toPeer, content, askedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
465
277
|
const ackPromise = this.waitForResponse(
|
|
466
|
-
(m) => m.type === "
|
|
278
|
+
(m) => m.type === "ASK_ACK" && m.requestId === requestId,
|
|
467
279
|
5e3
|
|
468
280
|
);
|
|
469
|
-
|
|
470
|
-
type: "P2P_ASK",
|
|
471
|
-
questionId,
|
|
472
|
-
fromMemberId: this.localMember.memberId,
|
|
473
|
-
fromTeam: this.localMember.name,
|
|
474
|
-
toTeam: toName,
|
|
475
|
-
content,
|
|
476
|
-
format,
|
|
477
|
-
requestId
|
|
478
|
-
};
|
|
479
|
-
ws.send(serializeP2PMsg(msg));
|
|
281
|
+
this.sendToHub({ type: "ASK", from: this.myName, to: toPeer, questionId, requestId, content, format });
|
|
480
282
|
await ackPromise;
|
|
481
283
|
return questionId;
|
|
482
284
|
}
|
|
@@ -485,40 +287,32 @@ var P2PNode = class {
|
|
|
485
287
|
if (cached) {
|
|
486
288
|
return {
|
|
487
289
|
questionId,
|
|
488
|
-
from: { displayName: `${cached.
|
|
290
|
+
from: { displayName: `${cached.fromName} Claude`, teamName: cached.fromName },
|
|
489
291
|
content: cached.content,
|
|
490
292
|
format: cached.format,
|
|
491
293
|
answeredAt: cached.answeredAt
|
|
492
294
|
};
|
|
493
295
|
}
|
|
494
|
-
const
|
|
495
|
-
if (!
|
|
496
|
-
const ws = this.peerConns.get(toName);
|
|
497
|
-
if (!ws || ws.readyState !== WebSocket.OPEN) return null;
|
|
296
|
+
const toPeer = this.questionToName.get(questionId);
|
|
297
|
+
if (!toPeer || !this.isConnected) return null;
|
|
498
298
|
const requestId = v4();
|
|
499
299
|
const responsePromise = this.waitForResponse(
|
|
500
|
-
(m) => m.type === "
|
|
300
|
+
(m) => m.type === "ANSWER" && m.questionId === questionId || m.type === "ANSWER_PENDING" && m.requestId === requestId,
|
|
501
301
|
5e3
|
|
502
302
|
);
|
|
503
|
-
|
|
504
|
-
type: "P2P_GET_ANSWER",
|
|
505
|
-
questionId,
|
|
506
|
-
requestId
|
|
507
|
-
};
|
|
508
|
-
ws.send(serializeP2PMsg(getMsg));
|
|
303
|
+
this.sendToHub({ type: "GET_ANSWER", from: this.myName, to: toPeer, questionId, requestId });
|
|
509
304
|
const response = await responsePromise;
|
|
510
|
-
if (response.type === "
|
|
305
|
+
if (response.type === "ANSWER_PENDING") return null;
|
|
511
306
|
const answer = response;
|
|
512
307
|
this.receivedAnswers.set(questionId, {
|
|
513
308
|
content: answer.content,
|
|
514
309
|
format: answer.format,
|
|
515
310
|
answeredAt: answer.answeredAt,
|
|
516
|
-
|
|
517
|
-
fromMemberId: answer.fromMemberId
|
|
311
|
+
fromName: answer.from
|
|
518
312
|
});
|
|
519
313
|
return {
|
|
520
314
|
questionId,
|
|
521
|
-
from: { displayName: `${answer.
|
|
315
|
+
from: { displayName: `${answer.from} Claude`, teamName: answer.from },
|
|
522
316
|
content: answer.content,
|
|
523
317
|
format: answer.format,
|
|
524
318
|
answeredAt: answer.answeredAt
|
|
@@ -526,45 +320,42 @@ var P2PNode = class {
|
|
|
526
320
|
}
|
|
527
321
|
async reply(questionId, content, format) {
|
|
528
322
|
const question = this.incomingQuestions.get(questionId);
|
|
529
|
-
if (!question) throw new Error(`Question ${questionId} not found
|
|
323
|
+
if (!question) throw new Error(`Question ${questionId} not found`);
|
|
530
324
|
question.answered = true;
|
|
531
325
|
question.answerContent = content;
|
|
532
326
|
question.answerFormat = format;
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
327
|
+
const senderName = this.questionToSender.get(questionId);
|
|
328
|
+
if (senderName) {
|
|
329
|
+
this.sendToHub({
|
|
330
|
+
type: "ANSWER",
|
|
331
|
+
from: this.myName,
|
|
332
|
+
to: senderName,
|
|
333
|
+
questionId,
|
|
334
|
+
content,
|
|
335
|
+
format,
|
|
336
|
+
answeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
337
|
+
});
|
|
544
338
|
}
|
|
339
|
+
injectionQueue.notifyReplied();
|
|
545
340
|
}
|
|
546
341
|
async getInbox() {
|
|
547
342
|
const now = Date.now();
|
|
548
343
|
const questions = [...this.incomingQuestions.values()].filter((q) => !q.answered).map((q) => ({
|
|
549
344
|
questionId: q.questionId,
|
|
550
|
-
from: { displayName: `${q.
|
|
345
|
+
from: { displayName: `${q.fromName} Claude`, teamName: q.fromName },
|
|
551
346
|
content: q.content,
|
|
552
347
|
format: q.format,
|
|
553
348
|
status: "PENDING",
|
|
554
349
|
createdAt: q.createdAt.toISOString(),
|
|
555
350
|
ageMs: now - q.createdAt.getTime()
|
|
556
351
|
}));
|
|
557
|
-
return {
|
|
558
|
-
questions,
|
|
559
|
-
totalCount: questions.length,
|
|
560
|
-
pendingCount: questions.length
|
|
561
|
-
};
|
|
352
|
+
return { questions, totalCount: questions.length, pendingCount: questions.length };
|
|
562
353
|
}
|
|
563
354
|
getInfo() {
|
|
564
355
|
return {
|
|
565
|
-
teamName: this.
|
|
566
|
-
port:
|
|
567
|
-
connectedPeers: [...this.
|
|
356
|
+
teamName: this.myName,
|
|
357
|
+
port: void 0,
|
|
358
|
+
connectedPeers: [...this.connectedPeers]
|
|
568
359
|
};
|
|
569
360
|
}
|
|
570
361
|
getHistory() {
|
|
@@ -585,7 +376,7 @@ var P2PNode = class {
|
|
|
585
376
|
entries.push({
|
|
586
377
|
direction: "received",
|
|
587
378
|
questionId,
|
|
588
|
-
peer: incoming.
|
|
379
|
+
peer: incoming.fromName,
|
|
589
380
|
question: incoming.content,
|
|
590
381
|
answer: incoming.answered ? incoming.answerContent : void 0,
|
|
591
382
|
askedAt: incoming.createdAt.toISOString(),
|
|
@@ -595,66 +386,41 @@ var P2PNode = class {
|
|
|
595
386
|
return entries.sort((a, b) => a.askedAt.localeCompare(b.askedAt));
|
|
596
387
|
}
|
|
597
388
|
async disconnect() {
|
|
598
|
-
this.
|
|
599
|
-
|
|
600
|
-
|
|
389
|
+
if (this.reconnectTimer) {
|
|
390
|
+
clearTimeout(this.reconnectTimer);
|
|
391
|
+
this.reconnectTimer = null;
|
|
601
392
|
}
|
|
602
|
-
this.
|
|
603
|
-
|
|
604
|
-
if (this.wss) {
|
|
605
|
-
this.wss.close(() => resolve());
|
|
606
|
-
} else {
|
|
607
|
-
resolve();
|
|
608
|
-
}
|
|
609
|
-
});
|
|
610
|
-
this._isStarted = false;
|
|
393
|
+
this.ws?.close();
|
|
394
|
+
this.ws = null;
|
|
611
395
|
}
|
|
612
396
|
// ---------------------------------------------------------------------------
|
|
613
|
-
// Private:
|
|
397
|
+
// Private: connection management
|
|
614
398
|
// ---------------------------------------------------------------------------
|
|
615
|
-
async
|
|
616
|
-
const
|
|
617
|
-
|
|
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);
|
|
399
|
+
async connectAndHello() {
|
|
400
|
+
const ws = new WebSocket(this.serverUrl);
|
|
401
|
+
this.ws = ws;
|
|
635
402
|
ws.on("message", (data) => {
|
|
636
403
|
try {
|
|
637
|
-
const msg =
|
|
638
|
-
this.handleMessage(
|
|
639
|
-
} catch
|
|
640
|
-
console.error("[p2p] Failed to parse message:", err);
|
|
404
|
+
const msg = parse(data.toString());
|
|
405
|
+
this.handleMessage(msg);
|
|
406
|
+
} catch {
|
|
641
407
|
}
|
|
642
408
|
});
|
|
643
|
-
ws.on("close", () =>
|
|
644
|
-
|
|
409
|
+
ws.on("close", () => {
|
|
410
|
+
this.connectedPeers.clear();
|
|
411
|
+
this.scheduleReconnect();
|
|
412
|
+
});
|
|
413
|
+
ws.on("error", (err) => {
|
|
414
|
+
console.error("[hub-client] error:", err.message);
|
|
415
|
+
});
|
|
645
416
|
await new Promise((resolve, reject) => {
|
|
646
417
|
const timeout = setTimeout(
|
|
647
|
-
() => reject(new Error(`
|
|
648
|
-
|
|
418
|
+
() => reject(new Error(`Cannot connect to hub at ${this.serverUrl}`)),
|
|
419
|
+
1e4
|
|
649
420
|
);
|
|
650
421
|
ws.on("open", () => {
|
|
651
422
|
clearTimeout(timeout);
|
|
652
|
-
|
|
653
|
-
type: "P2P_HELLO",
|
|
654
|
-
fromTeam: this.localMember.name,
|
|
655
|
-
fromMemberId: this.localMember.memberId
|
|
656
|
-
};
|
|
657
|
-
ws.send(serializeP2PMsg(hello));
|
|
423
|
+
ws.send(serialize({ type: "HELLO", name: this.myName }));
|
|
658
424
|
resolve();
|
|
659
425
|
});
|
|
660
426
|
ws.on("error", (err) => {
|
|
@@ -662,174 +428,115 @@ var P2PNode = class {
|
|
|
662
428
|
reject(err);
|
|
663
429
|
});
|
|
664
430
|
});
|
|
665
|
-
const
|
|
666
|
-
(m) => m.type === "
|
|
431
|
+
const ack = await this.waitForResponse(
|
|
432
|
+
(m) => m.type === "HELLO_ACK",
|
|
667
433
|
1e4
|
|
668
434
|
);
|
|
669
|
-
|
|
435
|
+
for (const peer of ack.peers) this.connectedPeers.add(peer);
|
|
436
|
+
console.error(`[hub-client] connected as "${this.myName}", peers: [${ack.peers.join(", ")}]`);
|
|
670
437
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
fromTeam: this.localMember.name,
|
|
685
|
-
fromMemberId: this.localMember.memberId
|
|
686
|
-
};
|
|
687
|
-
ws.send(serializeP2PMsg(hello));
|
|
688
|
-
}
|
|
689
|
-
this.handleMessage(ws, msg);
|
|
690
|
-
} catch (err) {
|
|
691
|
-
console.error("[p2p] Failed to parse incoming message:", err);
|
|
692
|
-
}
|
|
693
|
-
});
|
|
694
|
-
ws.on("close", () => this.cleanupWs(ws));
|
|
695
|
-
});
|
|
438
|
+
scheduleReconnect() {
|
|
439
|
+
if (this.reconnectTimer) return;
|
|
440
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
441
|
+
this.reconnectTimer = null;
|
|
442
|
+
try {
|
|
443
|
+
await this.connectAndHello();
|
|
444
|
+
console.error("[hub-client] reconnected to hub");
|
|
445
|
+
} catch (err) {
|
|
446
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
447
|
+
console.error(`[hub-client] reconnect failed: ${msg}, retrying in 5s...`);
|
|
448
|
+
this.scheduleReconnect();
|
|
449
|
+
}
|
|
450
|
+
}, 5e3);
|
|
696
451
|
}
|
|
697
452
|
// ---------------------------------------------------------------------------
|
|
698
|
-
// Private:
|
|
453
|
+
// Private: message handling
|
|
699
454
|
// ---------------------------------------------------------------------------
|
|
700
|
-
handleMessage(
|
|
701
|
-
for (const handler of this.pendingHandlers)
|
|
702
|
-
handler(msg);
|
|
703
|
-
}
|
|
455
|
+
handleMessage(msg) {
|
|
456
|
+
for (const handler of this.pendingHandlers) handler(msg);
|
|
704
457
|
switch (msg.type) {
|
|
705
|
-
case "
|
|
706
|
-
this.
|
|
458
|
+
case "PEER_JOINED":
|
|
459
|
+
this.connectedPeers.add(msg.name);
|
|
460
|
+
console.error(`[hub-client] peer joined: ${msg.name}`);
|
|
461
|
+
break;
|
|
462
|
+
case "PEER_LEFT":
|
|
463
|
+
this.connectedPeers.delete(msg.name);
|
|
464
|
+
console.error(`[hub-client] peer left: ${msg.name}`);
|
|
707
465
|
break;
|
|
708
|
-
case "
|
|
709
|
-
this.handleIncomingAsk(
|
|
466
|
+
case "ASK":
|
|
467
|
+
this.handleIncomingAsk(msg);
|
|
710
468
|
break;
|
|
711
|
-
case "
|
|
712
|
-
this.handleGetAnswer(
|
|
469
|
+
case "GET_ANSWER":
|
|
470
|
+
this.handleGetAnswer(msg);
|
|
713
471
|
break;
|
|
714
|
-
case "
|
|
472
|
+
case "ANSWER":
|
|
715
473
|
if (!this.receivedAnswers.has(msg.questionId)) {
|
|
716
474
|
this.receivedAnswers.set(msg.questionId, {
|
|
717
475
|
content: msg.content,
|
|
718
476
|
format: msg.format,
|
|
719
477
|
answeredAt: msg.answeredAt,
|
|
720
|
-
|
|
721
|
-
fromMemberId: msg.fromMemberId
|
|
478
|
+
fromName: msg.from
|
|
722
479
|
});
|
|
723
480
|
}
|
|
724
481
|
break;
|
|
725
|
-
case "
|
|
726
|
-
|
|
482
|
+
case "HUB_ERROR":
|
|
483
|
+
console.error(`[hub-client] hub error: ${msg.message}`);
|
|
727
484
|
break;
|
|
728
485
|
}
|
|
729
486
|
}
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
}
|
|
752
|
-
handleIncomingAsk(ws, msg) {
|
|
487
|
+
handleIncomingAsk(msg) {
|
|
488
|
+
this.questionToSender.set(msg.questionId, msg.from);
|
|
753
489
|
this.incomingQuestions.set(msg.questionId, {
|
|
754
490
|
questionId: msg.questionId,
|
|
755
|
-
|
|
756
|
-
fromMemberId: msg.fromMemberId,
|
|
491
|
+
fromName: msg.from,
|
|
757
492
|
content: msg.content,
|
|
758
493
|
format: msg.format,
|
|
759
494
|
createdAt: /* @__PURE__ */ new Date(),
|
|
760
|
-
ws,
|
|
761
495
|
answered: false
|
|
762
496
|
});
|
|
763
497
|
injectionQueue.enqueue({
|
|
764
498
|
questionId: msg.questionId,
|
|
765
|
-
from: {
|
|
766
|
-
displayName: `${msg.fromTeam} Claude`,
|
|
767
|
-
teamName: msg.fromTeam
|
|
768
|
-
},
|
|
499
|
+
from: { displayName: `${msg.from} Claude`, teamName: msg.from },
|
|
769
500
|
content: msg.content,
|
|
770
501
|
format: msg.format,
|
|
771
502
|
status: "PENDING",
|
|
772
503
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
773
504
|
ageMs: 0
|
|
774
505
|
});
|
|
775
|
-
const ack = {
|
|
776
|
-
type: "P2P_ASK_ACK",
|
|
777
|
-
questionId: msg.questionId,
|
|
778
|
-
requestId: msg.requestId
|
|
779
|
-
};
|
|
780
|
-
ws.send(serializeP2PMsg(ack));
|
|
781
506
|
}
|
|
782
|
-
handleGetAnswer(
|
|
507
|
+
handleGetAnswer(msg) {
|
|
783
508
|
const question = this.incomingQuestions.get(msg.questionId);
|
|
784
509
|
if (!question?.answered) {
|
|
785
|
-
|
|
786
|
-
type: "
|
|
510
|
+
this.sendToHub({
|
|
511
|
+
type: "ANSWER_PENDING",
|
|
512
|
+
to: msg.from,
|
|
787
513
|
questionId: msg.questionId,
|
|
788
514
|
requestId: msg.requestId
|
|
789
|
-
};
|
|
790
|
-
ws.send(serializeP2PMsg(pending));
|
|
515
|
+
});
|
|
791
516
|
return;
|
|
792
517
|
}
|
|
793
|
-
|
|
794
|
-
type: "
|
|
518
|
+
this.sendToHub({
|
|
519
|
+
type: "ANSWER",
|
|
520
|
+
from: this.myName,
|
|
521
|
+
to: msg.from,
|
|
795
522
|
questionId: msg.questionId,
|
|
796
523
|
content: question.answerContent,
|
|
797
524
|
format: question.answerFormat,
|
|
798
525
|
answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
799
|
-
fromTeam: this.localMember.name,
|
|
800
|
-
fromMemberId: this.localMember.memberId,
|
|
801
526
|
requestId: msg.requestId
|
|
802
|
-
};
|
|
803
|
-
ws.send(serializeP2PMsg(answer));
|
|
804
|
-
}
|
|
805
|
-
// ---------------------------------------------------------------------------
|
|
806
|
-
// Private: peer connection management
|
|
807
|
-
// ---------------------------------------------------------------------------
|
|
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);
|
|
527
|
+
});
|
|
818
528
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
return existing;
|
|
529
|
+
sendToHub(msg) {
|
|
530
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
531
|
+
throw new Error("Not connected to hub. Will retry automatically.");
|
|
823
532
|
}
|
|
824
|
-
|
|
825
|
-
`No connection to peer '${name}'. They may not be on the network yet \u2014 wait a moment and try again.`
|
|
826
|
-
);
|
|
533
|
+
this.ws.send(serialize(msg));
|
|
827
534
|
}
|
|
828
535
|
waitForResponse(filter, timeoutMs) {
|
|
829
536
|
return new Promise((resolve, reject) => {
|
|
830
537
|
const timeout = setTimeout(() => {
|
|
831
538
|
this.pendingHandlers.delete(handler);
|
|
832
|
-
reject(new Error("
|
|
539
|
+
reject(new Error("Hub request timed out"));
|
|
833
540
|
}, timeoutMs);
|
|
834
541
|
const handler = (msg) => {
|
|
835
542
|
if (filter(msg)) {
|
|
@@ -1024,20 +731,26 @@ async function startMcpServer(options) {
|
|
|
1024
731
|
}
|
|
1025
732
|
|
|
1026
733
|
// src/mcp-main.ts
|
|
1027
|
-
function
|
|
1028
|
-
const idx = process.argv.indexOf(
|
|
1029
|
-
|
|
1030
|
-
if (idx !== -1 && value) {
|
|
1031
|
-
return value;
|
|
1032
|
-
}
|
|
1033
|
-
console.error("Usage: claude-collab --name <your-name>");
|
|
1034
|
-
process.exit(1);
|
|
734
|
+
function getArg(flag) {
|
|
735
|
+
const idx = process.argv.indexOf(flag);
|
|
736
|
+
return idx !== -1 ? process.argv[idx + 1] : void 0;
|
|
1035
737
|
}
|
|
1036
738
|
async function main() {
|
|
1037
|
-
const name =
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
739
|
+
const name = getArg("--name");
|
|
740
|
+
const server = getArg("--server");
|
|
741
|
+
if (!name) {
|
|
742
|
+
console.error("--name is required");
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
if (!server) {
|
|
746
|
+
console.error("--server is required");
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
const url = server.startsWith("ws") ? server : `ws://${server}`;
|
|
750
|
+
const client = new HubClient();
|
|
751
|
+
client.setServerUrl(url);
|
|
752
|
+
await client.join(name, name);
|
|
753
|
+
await startMcpServer({ client });
|
|
1041
754
|
}
|
|
1042
755
|
main().catch((error) => {
|
|
1043
756
|
console.error("Unexpected error:", error);
|