@dolusoft/claude-collab 1.8.5 → 1.9.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/dist/cli.js +300 -553
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +306 -542
- package/dist/mcp-main.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,136 +1,83 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
|
-
import {
|
|
3
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
5
|
+
import { createSocket } from 'dgram';
|
|
5
6
|
import { EventEmitter } from 'events';
|
|
6
7
|
import { execFile, spawn } from 'child_process';
|
|
7
8
|
import { unlinkSync } from 'fs';
|
|
8
9
|
import { tmpdir } from 'os';
|
|
9
10
|
import { join } from 'path';
|
|
10
|
-
import { createSocket } from 'dgram';
|
|
11
11
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
12
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
13
|
import { z } from 'zod';
|
|
14
14
|
|
|
15
|
-
// src/infrastructure/
|
|
15
|
+
// src/infrastructure/p2p/p2p-protocol.ts
|
|
16
16
|
function serialize(msg) {
|
|
17
17
|
return JSON.stringify(msg);
|
|
18
18
|
}
|
|
19
19
|
function parse(data) {
|
|
20
20
|
return JSON.parse(data);
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
var
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.
|
|
32
|
-
|
|
33
|
-
console.
|
|
34
|
-
console.log("Waiting for peers...\n");
|
|
35
|
-
});
|
|
36
|
-
this.wss.on("connection", (ws) => {
|
|
37
|
-
ws.on("message", (data) => {
|
|
38
|
-
try {
|
|
39
|
-
const msg = parse(data.toString());
|
|
40
|
-
this.handleMessage(ws, msg);
|
|
41
|
-
} catch {
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
ws.on("close", () => {
|
|
45
|
-
const name = this.wsToName.get(ws);
|
|
46
|
-
if (name) {
|
|
47
|
-
this.clients.delete(name);
|
|
48
|
-
this.wsToName.delete(ws);
|
|
49
|
-
this.broadcast({ type: "PEER_LEFT", name });
|
|
50
|
-
console.log(`\u2190 ${name} left (${this.clients.size} online: ${[...this.clients.keys()].join(", ") || "none"})`);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
ws.on("error", (err) => {
|
|
54
|
-
console.error("[hub] ws error:", err.message);
|
|
55
|
-
});
|
|
22
|
+
var PEER_DISCOVERY_PORT = 9998;
|
|
23
|
+
var BROADCAST_INTERVAL_MS = 3e3;
|
|
24
|
+
var BROADCAST_ADDRESS = "255.255.255.255";
|
|
25
|
+
var PeerBroadcaster = class {
|
|
26
|
+
socket = null;
|
|
27
|
+
timer = null;
|
|
28
|
+
start(name, port) {
|
|
29
|
+
if (this.socket) return;
|
|
30
|
+
const socket = createSocket("udp4");
|
|
31
|
+
this.socket = socket;
|
|
32
|
+
socket.on("error", (err) => {
|
|
33
|
+
console.error("[peer-broadcaster] error:", err.message);
|
|
56
34
|
});
|
|
57
|
-
|
|
58
|
-
|
|
35
|
+
socket.bind(0, () => {
|
|
36
|
+
socket.setBroadcast(true);
|
|
37
|
+
const send = () => {
|
|
38
|
+
if (!this.socket) return;
|
|
39
|
+
const msg = Buffer.from(JSON.stringify({ type: "claude-collab-peer", name, port }));
|
|
40
|
+
socket.send(msg, 0, msg.length, PEER_DISCOVERY_PORT, BROADCAST_ADDRESS, (err) => {
|
|
41
|
+
if (err) console.error("[peer-broadcaster] send error:", err.message);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
send();
|
|
45
|
+
this.timer = setInterval(send, BROADCAST_INTERVAL_MS);
|
|
59
46
|
});
|
|
60
47
|
}
|
|
61
48
|
stop() {
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
this.wsToName.clear();
|
|
66
|
-
this.wss.close();
|
|
67
|
-
this.wss = null;
|
|
68
|
-
console.log("claude-collab hub stopped");
|
|
69
|
-
}
|
|
70
|
-
handleMessage(ws, msg) {
|
|
71
|
-
switch (msg.type) {
|
|
72
|
-
case "HELLO": {
|
|
73
|
-
const existing = this.clients.get(msg.name);
|
|
74
|
-
const isReconnect = existing != null && existing !== ws;
|
|
75
|
-
if (isReconnect) {
|
|
76
|
-
this.wsToName.delete(existing);
|
|
77
|
-
existing.close();
|
|
78
|
-
}
|
|
79
|
-
this.clients.set(msg.name, ws);
|
|
80
|
-
this.wsToName.set(ws, msg.name);
|
|
81
|
-
const peers = [...this.clients.keys()].filter((n) => n !== msg.name);
|
|
82
|
-
const ack = { type: "HELLO_ACK", peers };
|
|
83
|
-
this.send(ws, ack);
|
|
84
|
-
if (!isReconnect) {
|
|
85
|
-
this.broadcast({ type: "PEER_JOINED", name: msg.name }, ws);
|
|
86
|
-
}
|
|
87
|
-
console.log(`\u2192 ${msg.name} joined (${this.clients.size} online: ${[...this.clients.keys()].join(", ")})`);
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
case "ASK": {
|
|
91
|
-
const target = this.clients.get(msg.to);
|
|
92
|
-
if (!target) {
|
|
93
|
-
const err = {
|
|
94
|
-
type: "HUB_ERROR",
|
|
95
|
-
code: "PEER_NOT_FOUND",
|
|
96
|
-
message: `'${msg.to}' is not connected to the hub`
|
|
97
|
-
};
|
|
98
|
-
this.send(ws, err);
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
this.send(target, msg);
|
|
102
|
-
const ack = { type: "ASK_ACK", questionId: msg.questionId, requestId: msg.requestId };
|
|
103
|
-
this.send(ws, ack);
|
|
104
|
-
break;
|
|
105
|
-
}
|
|
106
|
-
case "GET_ANSWER": {
|
|
107
|
-
const target = this.clients.get(msg.to);
|
|
108
|
-
if (!target) {
|
|
109
|
-
this.send(ws, { type: "ANSWER_PENDING", to: msg.from, questionId: msg.questionId, requestId: msg.requestId });
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
this.send(target, msg);
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
case "ANSWER":
|
|
116
|
-
case "ANSWER_PENDING": {
|
|
117
|
-
const target = this.clients.get(msg.to);
|
|
118
|
-
if (target) this.send(target, msg);
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
send(ws, msg) {
|
|
124
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
125
|
-
ws.send(serialize(msg));
|
|
49
|
+
if (this.timer) {
|
|
50
|
+
clearInterval(this.timer);
|
|
51
|
+
this.timer = null;
|
|
126
52
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (ws !== except) this.send(ws, msg);
|
|
53
|
+
if (this.socket) {
|
|
54
|
+
this.socket.close();
|
|
55
|
+
this.socket = null;
|
|
131
56
|
}
|
|
132
57
|
}
|
|
133
58
|
};
|
|
59
|
+
function watchForPeer(onFound) {
|
|
60
|
+
const socket = createSocket("udp4");
|
|
61
|
+
socket.on("error", (err) => {
|
|
62
|
+
console.error("[peer-listener] error:", err.message);
|
|
63
|
+
});
|
|
64
|
+
socket.on("message", (msg, rinfo) => {
|
|
65
|
+
try {
|
|
66
|
+
const data = JSON.parse(msg.toString());
|
|
67
|
+
if (data.type === "claude-collab-peer" && typeof data.name === "string" && typeof data.port === "number") {
|
|
68
|
+
onFound({ name: data.name, host: rinfo.address, port: data.port });
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
socket.bind(PEER_DISCOVERY_PORT, "0.0.0.0");
|
|
74
|
+
return () => {
|
|
75
|
+
try {
|
|
76
|
+
socket.close();
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
134
81
|
var CS_CONINJECT = `
|
|
135
82
|
using System;
|
|
136
83
|
using System.Collections.Generic;
|
|
@@ -349,93 +296,73 @@ var InjectionQueue = class extends EventEmitter {
|
|
|
349
296
|
};
|
|
350
297
|
var injectionQueue = new InjectionQueue();
|
|
351
298
|
|
|
352
|
-
// src/infrastructure/
|
|
353
|
-
var
|
|
354
|
-
|
|
299
|
+
// src/infrastructure/p2p/p2p-node.ts
|
|
300
|
+
var P2PNode = class {
|
|
301
|
+
constructor(port) {
|
|
302
|
+
this.port = port;
|
|
303
|
+
}
|
|
304
|
+
server = null;
|
|
355
305
|
myName = "";
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
306
|
+
running = false;
|
|
307
|
+
// One connection per peer (inbound or outbound — whichever was established first)
|
|
308
|
+
peerConnections = /* @__PURE__ */ new Map();
|
|
309
|
+
// Reverse map: ws → peer name (only for registered connections)
|
|
310
|
+
wsToName = /* @__PURE__ */ new Map();
|
|
311
|
+
// Prevent duplicate outbound connect attempts
|
|
312
|
+
connectingPeers = /* @__PURE__ */ new Set();
|
|
359
313
|
incomingQuestions = /* @__PURE__ */ new Map();
|
|
360
314
|
receivedAnswers = /* @__PURE__ */ new Map();
|
|
361
|
-
questionToName = /* @__PURE__ */ new Map();
|
|
362
|
-
questionToSender = /* @__PURE__ */ new Map();
|
|
363
315
|
sentQuestions = /* @__PURE__ */ new Map();
|
|
316
|
+
questionToSender = /* @__PURE__ */ new Map();
|
|
364
317
|
pendingHandlers = /* @__PURE__ */ new Set();
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
318
|
+
broadcaster = null;
|
|
319
|
+
stopPeerWatcher = null;
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
// ICollabClient implementation
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
368
323
|
get isConnected() {
|
|
369
|
-
return this.
|
|
324
|
+
return this.running;
|
|
370
325
|
}
|
|
371
326
|
get currentTeamId() {
|
|
372
327
|
return this.myName || void 0;
|
|
373
328
|
}
|
|
374
329
|
async join(name, displayName) {
|
|
375
330
|
this.myName = name;
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
331
|
+
await this.startServer();
|
|
332
|
+
this.startDiscovery();
|
|
379
333
|
return {
|
|
380
334
|
memberId: v4(),
|
|
381
335
|
teamId: name,
|
|
382
336
|
teamName: name,
|
|
383
337
|
displayName,
|
|
384
338
|
status: "ONLINE",
|
|
385
|
-
port:
|
|
339
|
+
port: this.port
|
|
386
340
|
};
|
|
387
341
|
}
|
|
388
|
-
async connectToHub(url) {
|
|
389
|
-
this.serverUrl = url;
|
|
390
|
-
await this.connectAndHello();
|
|
391
|
-
}
|
|
392
342
|
async ask(toPeer, content, format) {
|
|
343
|
+
const ws = this.peerConnections.get(toPeer);
|
|
344
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
345
|
+
throw new Error(`Peer "${toPeer}" is not connected. Use peers() to see who's online.`);
|
|
346
|
+
}
|
|
393
347
|
const questionId = v4();
|
|
394
|
-
const requestId = v4();
|
|
395
|
-
this.questionToName.set(questionId, toPeer);
|
|
396
348
|
this.sentQuestions.set(questionId, { toPeer, content, askedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
397
349
|
const ackPromise = this.waitForResponse(
|
|
398
|
-
(m) => m.type === "ASK_ACK" && m.
|
|
350
|
+
(m) => m.type === "ASK_ACK" && m.questionId === questionId,
|
|
399
351
|
5e3
|
|
400
352
|
);
|
|
401
|
-
this.
|
|
353
|
+
this.sendToWs(ws, { type: "ASK", from: this.myName, questionId, content, format });
|
|
402
354
|
await ackPromise;
|
|
403
355
|
return questionId;
|
|
404
356
|
}
|
|
405
357
|
async checkAnswer(questionId) {
|
|
406
358
|
const cached = this.receivedAnswers.get(questionId);
|
|
407
|
-
if (cached)
|
|
408
|
-
return {
|
|
409
|
-
questionId,
|
|
410
|
-
from: { displayName: `${cached.fromName} Claude`, teamName: cached.fromName },
|
|
411
|
-
content: cached.content,
|
|
412
|
-
format: cached.format,
|
|
413
|
-
answeredAt: cached.answeredAt
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
const toPeer = this.questionToName.get(questionId);
|
|
417
|
-
if (!toPeer || !this.isConnected) return null;
|
|
418
|
-
const requestId = v4();
|
|
419
|
-
const responsePromise = this.waitForResponse(
|
|
420
|
-
(m) => m.type === "ANSWER" && m.questionId === questionId || m.type === "ANSWER_PENDING" && m.requestId === requestId,
|
|
421
|
-
5e3
|
|
422
|
-
);
|
|
423
|
-
this.sendToHub({ type: "GET_ANSWER", from: this.myName, to: toPeer, questionId, requestId });
|
|
424
|
-
const response = await responsePromise;
|
|
425
|
-
if (response.type === "ANSWER_PENDING") return null;
|
|
426
|
-
const answer = response;
|
|
427
|
-
this.receivedAnswers.set(questionId, {
|
|
428
|
-
content: answer.content,
|
|
429
|
-
format: answer.format,
|
|
430
|
-
answeredAt: answer.answeredAt,
|
|
431
|
-
fromName: answer.from
|
|
432
|
-
});
|
|
359
|
+
if (!cached) return null;
|
|
433
360
|
return {
|
|
434
361
|
questionId,
|
|
435
|
-
from: { displayName: `${
|
|
436
|
-
content:
|
|
437
|
-
format:
|
|
438
|
-
answeredAt:
|
|
362
|
+
from: { displayName: `${cached.fromName} Claude`, teamName: cached.fromName },
|
|
363
|
+
content: cached.content,
|
|
364
|
+
format: cached.format,
|
|
365
|
+
answeredAt: cached.answeredAt
|
|
439
366
|
};
|
|
440
367
|
}
|
|
441
368
|
async reply(questionId, content, format) {
|
|
@@ -446,15 +373,17 @@ var HubClient = class {
|
|
|
446
373
|
question.answerFormat = format;
|
|
447
374
|
const senderName = this.questionToSender.get(questionId);
|
|
448
375
|
if (senderName) {
|
|
449
|
-
this.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
376
|
+
const ws = this.peerConnections.get(senderName);
|
|
377
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
378
|
+
this.sendToWs(ws, {
|
|
379
|
+
type: "ANSWER",
|
|
380
|
+
from: this.myName,
|
|
381
|
+
questionId,
|
|
382
|
+
content,
|
|
383
|
+
format,
|
|
384
|
+
answeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
385
|
+
});
|
|
386
|
+
}
|
|
458
387
|
}
|
|
459
388
|
injectionQueue.notifyReplied();
|
|
460
389
|
}
|
|
@@ -474,8 +403,8 @@ var HubClient = class {
|
|
|
474
403
|
getInfo() {
|
|
475
404
|
return {
|
|
476
405
|
teamName: this.myName,
|
|
477
|
-
port:
|
|
478
|
-
connectedPeers: [...this.
|
|
406
|
+
port: this.port,
|
|
407
|
+
connectedPeers: [...this.peerConnections.keys()]
|
|
479
408
|
};
|
|
480
409
|
}
|
|
481
410
|
getHistory() {
|
|
@@ -487,107 +416,143 @@ var HubClient = class {
|
|
|
487
416
|
questionId,
|
|
488
417
|
peer: sent.toPeer,
|
|
489
418
|
question: sent.content,
|
|
490
|
-
answer: answer?.content,
|
|
491
419
|
askedAt: sent.askedAt,
|
|
492
|
-
answeredAt: answer
|
|
420
|
+
...answer ? { answer: answer.content, answeredAt: answer.answeredAt } : {}
|
|
493
421
|
});
|
|
494
422
|
}
|
|
495
|
-
for (const [
|
|
423
|
+
for (const [, incoming] of this.incomingQuestions) {
|
|
496
424
|
entries.push({
|
|
497
425
|
direction: "received",
|
|
498
|
-
questionId,
|
|
426
|
+
questionId: incoming.questionId,
|
|
499
427
|
peer: incoming.fromName,
|
|
500
428
|
question: incoming.content,
|
|
501
|
-
answer: incoming.answered ? incoming.answerContent : void 0,
|
|
502
429
|
askedAt: incoming.createdAt.toISOString(),
|
|
503
|
-
|
|
430
|
+
...incoming.answered && incoming.answerContent ? { answer: incoming.answerContent, answeredAt: (/* @__PURE__ */ new Date()).toISOString() } : {}
|
|
504
431
|
});
|
|
505
432
|
}
|
|
506
433
|
return entries.sort((a, b) => a.askedAt.localeCompare(b.askedAt));
|
|
507
434
|
}
|
|
508
435
|
async disconnect() {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
this.
|
|
514
|
-
this.
|
|
436
|
+
this.stopPeerWatcher?.();
|
|
437
|
+
this.broadcaster?.stop();
|
|
438
|
+
for (const ws of this.peerConnections.values()) ws.close();
|
|
439
|
+
this.peerConnections.clear();
|
|
440
|
+
this.wsToName.clear();
|
|
441
|
+
this.server?.close();
|
|
442
|
+
this.server = null;
|
|
443
|
+
this.running = false;
|
|
444
|
+
}
|
|
445
|
+
// ---------------------------------------------------------------------------
|
|
446
|
+
// Private: server startup
|
|
447
|
+
// ---------------------------------------------------------------------------
|
|
448
|
+
startServer() {
|
|
449
|
+
return new Promise((resolve, reject) => {
|
|
450
|
+
const wss = new WebSocketServer({ port: this.port });
|
|
451
|
+
this.server = wss;
|
|
452
|
+
wss.on("listening", () => {
|
|
453
|
+
this.running = true;
|
|
454
|
+
console.error(`[p2p] listening on port ${this.port} as "${this.myName}"`);
|
|
455
|
+
resolve();
|
|
456
|
+
});
|
|
457
|
+
wss.on("error", (err) => {
|
|
458
|
+
if (!this.running) reject(err);
|
|
459
|
+
else console.error("[p2p] server error:", err.message);
|
|
460
|
+
});
|
|
461
|
+
wss.on("connection", (ws) => {
|
|
462
|
+
ws.on("message", (data) => {
|
|
463
|
+
try {
|
|
464
|
+
this.handleMessage(ws, parse(data.toString()));
|
|
465
|
+
} catch {
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
ws.on("close", () => {
|
|
469
|
+
const name = this.wsToName.get(ws);
|
|
470
|
+
if (name) {
|
|
471
|
+
this.wsToName.delete(ws);
|
|
472
|
+
if (this.peerConnections.get(name) === ws) {
|
|
473
|
+
this.peerConnections.delete(name);
|
|
474
|
+
console.error(`[p2p] peer disconnected (inbound): ${name}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
ws.on("error", (err) => {
|
|
479
|
+
console.error("[p2p] inbound ws error:", err.message);
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
});
|
|
515
483
|
}
|
|
516
484
|
// ---------------------------------------------------------------------------
|
|
517
|
-
// Private:
|
|
485
|
+
// Private: discovery + outbound connections
|
|
518
486
|
// ---------------------------------------------------------------------------
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
this.
|
|
487
|
+
startDiscovery() {
|
|
488
|
+
this.broadcaster = new PeerBroadcaster();
|
|
489
|
+
this.broadcaster.start(this.myName, this.port);
|
|
490
|
+
this.stopPeerWatcher = watchForPeer((peer) => {
|
|
491
|
+
if (peer.name === this.myName) return;
|
|
492
|
+
if (this.peerConnections.has(peer.name)) return;
|
|
493
|
+
if (this.connectingPeers.has(peer.name)) return;
|
|
494
|
+
this.connectToPeer(peer.name, peer.host, peer.port);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
connectToPeer(peerName, host, port) {
|
|
498
|
+
this.connectingPeers.add(peerName);
|
|
499
|
+
const ws = new WebSocket(`ws://${host}:${port}`);
|
|
500
|
+
ws.on("open", () => {
|
|
501
|
+
this.sendToWs(ws, { type: "HELLO", name: this.myName });
|
|
502
|
+
});
|
|
522
503
|
ws.on("message", (data) => {
|
|
523
504
|
try {
|
|
524
|
-
|
|
525
|
-
this.handleMessage(msg);
|
|
505
|
+
this.handleMessage(ws, parse(data.toString()));
|
|
526
506
|
} catch {
|
|
527
507
|
}
|
|
528
508
|
});
|
|
529
509
|
ws.on("close", () => {
|
|
530
|
-
this.
|
|
531
|
-
this.
|
|
510
|
+
this.connectingPeers.delete(peerName);
|
|
511
|
+
const name = this.wsToName.get(ws);
|
|
512
|
+
if (name) {
|
|
513
|
+
this.wsToName.delete(ws);
|
|
514
|
+
if (this.peerConnections.get(name) === ws) {
|
|
515
|
+
this.peerConnections.delete(name);
|
|
516
|
+
console.error(`[p2p] disconnected from peer: ${name}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
532
519
|
});
|
|
533
520
|
ws.on("error", (err) => {
|
|
534
|
-
console.error(
|
|
535
|
-
|
|
536
|
-
await new Promise((resolve, reject) => {
|
|
537
|
-
const timeout = setTimeout(
|
|
538
|
-
() => reject(new Error(`Cannot connect to hub at ${this.serverUrl}`)),
|
|
539
|
-
1e4
|
|
540
|
-
);
|
|
541
|
-
ws.on("open", () => {
|
|
542
|
-
clearTimeout(timeout);
|
|
543
|
-
ws.send(serialize({ type: "HELLO", name: this.myName }));
|
|
544
|
-
resolve();
|
|
545
|
-
});
|
|
546
|
-
ws.on("error", (err) => {
|
|
547
|
-
clearTimeout(timeout);
|
|
548
|
-
reject(err);
|
|
549
|
-
});
|
|
521
|
+
console.error(`[p2p] connect to "${peerName}" failed: ${err.message}`);
|
|
522
|
+
this.connectingPeers.delete(peerName);
|
|
550
523
|
});
|
|
551
|
-
const ack = await this.waitForResponse(
|
|
552
|
-
(m) => m.type === "HELLO_ACK",
|
|
553
|
-
1e4
|
|
554
|
-
);
|
|
555
|
-
for (const peer of ack.peers) this.connectedPeers.add(peer);
|
|
556
|
-
console.error(`[hub-client] connected as "${this.myName}", peers: [${ack.peers.join(", ")}]`);
|
|
557
|
-
}
|
|
558
|
-
scheduleReconnect() {
|
|
559
|
-
if (this.reconnectTimer) return;
|
|
560
|
-
this.reconnectTimer = setTimeout(async () => {
|
|
561
|
-
this.reconnectTimer = null;
|
|
562
|
-
try {
|
|
563
|
-
await this.connectAndHello();
|
|
564
|
-
console.error("[hub-client] reconnected to hub");
|
|
565
|
-
} catch (err) {
|
|
566
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
567
|
-
console.error(`[hub-client] reconnect failed: ${msg}, retrying in 5s...`);
|
|
568
|
-
this.scheduleReconnect();
|
|
569
|
-
}
|
|
570
|
-
}, 5e3);
|
|
571
524
|
}
|
|
572
525
|
// ---------------------------------------------------------------------------
|
|
573
526
|
// Private: message handling
|
|
574
527
|
// ---------------------------------------------------------------------------
|
|
575
|
-
handleMessage(msg) {
|
|
528
|
+
handleMessage(ws, msg) {
|
|
576
529
|
for (const handler of this.pendingHandlers) handler(msg);
|
|
577
530
|
switch (msg.type) {
|
|
578
|
-
case "
|
|
579
|
-
this.
|
|
580
|
-
|
|
531
|
+
case "HELLO": {
|
|
532
|
+
if (this.peerConnections.has(msg.name)) {
|
|
533
|
+
ws.terminate();
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
this.peerConnections.set(msg.name, ws);
|
|
537
|
+
this.wsToName.set(ws, msg.name);
|
|
538
|
+
this.connectingPeers.delete(msg.name);
|
|
539
|
+
this.sendToWs(ws, { type: "HELLO_ACK", name: this.myName });
|
|
540
|
+
console.error(`[p2p] peer joined (inbound): ${msg.name}`);
|
|
581
541
|
break;
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
542
|
+
}
|
|
543
|
+
case "HELLO_ACK": {
|
|
544
|
+
if (this.peerConnections.has(msg.name)) {
|
|
545
|
+
ws.terminate();
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
this.peerConnections.set(msg.name, ws);
|
|
549
|
+
this.wsToName.set(ws, msg.name);
|
|
550
|
+
this.connectingPeers.delete(msg.name);
|
|
551
|
+
console.error(`[p2p] connected to peer: ${msg.name}`);
|
|
585
552
|
break;
|
|
553
|
+
}
|
|
586
554
|
case "ASK":
|
|
587
|
-
this.handleIncomingAsk(msg);
|
|
588
|
-
break;
|
|
589
|
-
case "GET_ANSWER":
|
|
590
|
-
this.handleGetAnswer(msg);
|
|
555
|
+
this.handleIncomingAsk(ws, msg);
|
|
591
556
|
break;
|
|
592
557
|
case "ANSWER":
|
|
593
558
|
if (!this.receivedAnswers.has(msg.questionId)) {
|
|
@@ -599,12 +564,9 @@ var HubClient = class {
|
|
|
599
564
|
});
|
|
600
565
|
}
|
|
601
566
|
break;
|
|
602
|
-
case "HUB_ERROR":
|
|
603
|
-
console.error(`[hub-client] hub error: ${msg.message}`);
|
|
604
|
-
break;
|
|
605
567
|
}
|
|
606
568
|
}
|
|
607
|
-
handleIncomingAsk(msg) {
|
|
569
|
+
handleIncomingAsk(ws, msg) {
|
|
608
570
|
this.questionToSender.set(msg.questionId, msg.from);
|
|
609
571
|
this.incomingQuestions.set(msg.questionId, {
|
|
610
572
|
questionId: msg.questionId,
|
|
@@ -614,6 +576,7 @@ var HubClient = class {
|
|
|
614
576
|
createdAt: /* @__PURE__ */ new Date(),
|
|
615
577
|
answered: false
|
|
616
578
|
});
|
|
579
|
+
this.sendToWs(ws, { type: "ASK_ACK", questionId: msg.questionId });
|
|
617
580
|
injectionQueue.enqueue({
|
|
618
581
|
questionId: msg.questionId,
|
|
619
582
|
from: { displayName: `${msg.from} Claude`, teamName: msg.from },
|
|
@@ -624,39 +587,16 @@ var HubClient = class {
|
|
|
624
587
|
ageMs: 0
|
|
625
588
|
});
|
|
626
589
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
this.sendToHub({
|
|
631
|
-
type: "ANSWER_PENDING",
|
|
632
|
-
to: msg.from,
|
|
633
|
-
questionId: msg.questionId,
|
|
634
|
-
requestId: msg.requestId
|
|
635
|
-
});
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
this.sendToHub({
|
|
639
|
-
type: "ANSWER",
|
|
640
|
-
from: this.myName,
|
|
641
|
-
to: msg.from,
|
|
642
|
-
questionId: msg.questionId,
|
|
643
|
-
content: question.answerContent,
|
|
644
|
-
format: question.answerFormat,
|
|
645
|
-
answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
646
|
-
requestId: msg.requestId
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
sendToHub(msg) {
|
|
650
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
651
|
-
throw new Error("Not connected to hub. Will retry automatically.");
|
|
590
|
+
sendToWs(ws, msg) {
|
|
591
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
592
|
+
ws.send(serialize(msg));
|
|
652
593
|
}
|
|
653
|
-
this.ws.send(serialize(msg));
|
|
654
594
|
}
|
|
655
595
|
waitForResponse(filter, timeoutMs) {
|
|
656
596
|
return new Promise((resolve, reject) => {
|
|
657
597
|
const timeout = setTimeout(() => {
|
|
658
598
|
this.pendingHandlers.delete(handler);
|
|
659
|
-
reject(new Error("
|
|
599
|
+
reject(new Error("Request timed out"));
|
|
660
600
|
}, timeoutMs);
|
|
661
601
|
const handler = (msg) => {
|
|
662
602
|
if (filter(msg)) {
|
|
@@ -669,129 +609,6 @@ var HubClient = class {
|
|
|
669
609
|
});
|
|
670
610
|
}
|
|
671
611
|
};
|
|
672
|
-
var DISCOVERY_PORT = 9998;
|
|
673
|
-
var BROADCAST_INTERVAL_MS = 3e3;
|
|
674
|
-
var BROADCAST_ADDRESS = "255.255.255.255";
|
|
675
|
-
var HubBroadcaster = class {
|
|
676
|
-
socket = null;
|
|
677
|
-
timer = null;
|
|
678
|
-
start(hubPort) {
|
|
679
|
-
if (this.socket) return;
|
|
680
|
-
const socket = createSocket("udp4");
|
|
681
|
-
this.socket = socket;
|
|
682
|
-
socket.on("error", (err) => {
|
|
683
|
-
console.error("[hub-broadcaster] error:", err.message);
|
|
684
|
-
});
|
|
685
|
-
socket.bind(0, () => {
|
|
686
|
-
socket.setBroadcast(true);
|
|
687
|
-
const send = () => {
|
|
688
|
-
if (!this.socket) return;
|
|
689
|
-
const msg = Buffer.from(JSON.stringify({ type: "claude-collab-hub", port: hubPort }));
|
|
690
|
-
socket.send(msg, 0, msg.length, DISCOVERY_PORT, BROADCAST_ADDRESS, (err) => {
|
|
691
|
-
if (err) console.error("[hub-broadcaster] send error:", err.message);
|
|
692
|
-
});
|
|
693
|
-
};
|
|
694
|
-
send();
|
|
695
|
-
this.timer = setInterval(send, BROADCAST_INTERVAL_MS);
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
stop() {
|
|
699
|
-
if (this.timer) {
|
|
700
|
-
clearInterval(this.timer);
|
|
701
|
-
this.timer = null;
|
|
702
|
-
}
|
|
703
|
-
if (this.socket) {
|
|
704
|
-
this.socket.close();
|
|
705
|
-
this.socket = null;
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
// src/infrastructure/hub/hub-manager.ts
|
|
711
|
-
var HubManager = class {
|
|
712
|
-
hubServer = null;
|
|
713
|
-
broadcaster = null;
|
|
714
|
-
currentPort = null;
|
|
715
|
-
get isRunning() {
|
|
716
|
-
return this.hubServer !== null;
|
|
717
|
-
}
|
|
718
|
-
get port() {
|
|
719
|
-
return this.currentPort;
|
|
720
|
-
}
|
|
721
|
-
async start(port) {
|
|
722
|
-
if (this.isRunning) throw new Error("Hub is already running");
|
|
723
|
-
const server = new HubServer();
|
|
724
|
-
server.start(port);
|
|
725
|
-
this.hubServer = server;
|
|
726
|
-
this.currentPort = port;
|
|
727
|
-
const broadcaster = new HubBroadcaster();
|
|
728
|
-
broadcaster.start(port);
|
|
729
|
-
this.broadcaster = broadcaster;
|
|
730
|
-
let firewallAdded = false;
|
|
731
|
-
try {
|
|
732
|
-
await addFirewallRule(port);
|
|
733
|
-
firewallAdded = true;
|
|
734
|
-
} catch (err) {
|
|
735
|
-
console.error("[hub-manager] firewall rule failed:", err);
|
|
736
|
-
}
|
|
737
|
-
return { firewallAdded };
|
|
738
|
-
}
|
|
739
|
-
async stop() {
|
|
740
|
-
if (!this.isRunning) throw new Error("Hub is not running");
|
|
741
|
-
if (this.broadcaster) {
|
|
742
|
-
this.broadcaster.stop();
|
|
743
|
-
this.broadcaster = null;
|
|
744
|
-
}
|
|
745
|
-
const port = this.currentPort;
|
|
746
|
-
this.hubServer.stop();
|
|
747
|
-
this.hubServer = null;
|
|
748
|
-
this.currentPort = null;
|
|
749
|
-
let firewallRemoved = false;
|
|
750
|
-
try {
|
|
751
|
-
await removeFirewallRule(port);
|
|
752
|
-
firewallRemoved = true;
|
|
753
|
-
} catch (err) {
|
|
754
|
-
console.error("[hub-manager] firewall rule removal failed:", err);
|
|
755
|
-
}
|
|
756
|
-
return { firewallRemoved };
|
|
757
|
-
}
|
|
758
|
-
};
|
|
759
|
-
function runElevated(argArray) {
|
|
760
|
-
const argList = argArray.map((a) => `"${a}"`).join(",");
|
|
761
|
-
const psCommand = `Start-Process -FilePath "netsh" -ArgumentList @(${argList}) -Verb RunAs -Wait`;
|
|
762
|
-
return new Promise((resolve, reject) => {
|
|
763
|
-
const ps = spawn("powershell", ["-NoProfile", "-Command", psCommand]);
|
|
764
|
-
ps.on("close", (code) => {
|
|
765
|
-
if (code === 0) resolve();
|
|
766
|
-
else reject(new Error(`Firewall UAC prompt was cancelled or denied (exit code ${code}).`));
|
|
767
|
-
});
|
|
768
|
-
ps.on("error", (err) => {
|
|
769
|
-
reject(new Error(`Failed to launch PowerShell: ${err.message}`));
|
|
770
|
-
});
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
async function addFirewallRule(port) {
|
|
774
|
-
await runElevated([
|
|
775
|
-
"advfirewall",
|
|
776
|
-
"firewall",
|
|
777
|
-
"add",
|
|
778
|
-
"rule",
|
|
779
|
-
`name=claude-collab-${port}`,
|
|
780
|
-
"protocol=TCP",
|
|
781
|
-
"dir=in",
|
|
782
|
-
`localport=${port}`,
|
|
783
|
-
"action=allow"
|
|
784
|
-
]);
|
|
785
|
-
}
|
|
786
|
-
async function removeFirewallRule(port) {
|
|
787
|
-
await runElevated([
|
|
788
|
-
"advfirewall",
|
|
789
|
-
"firewall",
|
|
790
|
-
"delete",
|
|
791
|
-
"rule",
|
|
792
|
-
`name=claude-collab-${port}`
|
|
793
|
-
]);
|
|
794
|
-
}
|
|
795
612
|
var askSchema = {
|
|
796
613
|
peer: z.string().describe('Name of the peer to ask (e.g., "alice", "backend")'),
|
|
797
614
|
question: z.string().describe("The question to ask (supports markdown)")
|
|
@@ -907,12 +724,14 @@ function registerPeersTool(server, client) {
|
|
|
907
724
|
server.tool("peers", {}, async () => {
|
|
908
725
|
const info = client.getInfo();
|
|
909
726
|
const myName = info.teamName ?? "(starting...)";
|
|
727
|
+
const myPort = info.port ?? "?";
|
|
910
728
|
const connected = info.connectedPeers;
|
|
911
729
|
if (connected.length === 0) {
|
|
912
730
|
return {
|
|
913
731
|
content: [{
|
|
914
732
|
type: "text",
|
|
915
|
-
text: `You are "${myName}". No peers connected yet
|
|
733
|
+
text: `You are "${myName}" (listening on port ${myPort}). No peers connected yet.
|
|
734
|
+
Use firewall_open to allow inbound connections, or wait for peers to connect to you.`
|
|
916
735
|
}]
|
|
917
736
|
};
|
|
918
737
|
}
|
|
@@ -920,7 +739,7 @@ function registerPeersTool(server, client) {
|
|
|
920
739
|
return {
|
|
921
740
|
content: [{
|
|
922
741
|
type: "text",
|
|
923
|
-
text: `You are "${myName}". Connected peers (${connected.length}):
|
|
742
|
+
text: `You are "${myName}" (port ${myPort}). Connected peers (${connected.length}):
|
|
924
743
|
${list}`
|
|
925
744
|
}]
|
|
926
745
|
};
|
|
@@ -953,92 +772,117 @@ ${answerLine}`;
|
|
|
953
772
|
};
|
|
954
773
|
});
|
|
955
774
|
}
|
|
956
|
-
function
|
|
775
|
+
function runElevated(argArray) {
|
|
776
|
+
const argList = argArray.map((a) => `"${a}"`).join(",");
|
|
777
|
+
const psCommand = `Start-Process -FilePath "netsh" -ArgumentList @(${argList}) -Verb RunAs -Wait`;
|
|
778
|
+
return new Promise((resolve, reject) => {
|
|
779
|
+
const ps = spawn("powershell", ["-NoProfile", "-Command", psCommand]);
|
|
780
|
+
ps.on("close", (code) => {
|
|
781
|
+
if (code === 0) resolve();
|
|
782
|
+
else reject(new Error(`Firewall UAC prompt was cancelled or denied (exit code ${code}).`));
|
|
783
|
+
});
|
|
784
|
+
ps.on("error", (err) => {
|
|
785
|
+
reject(new Error(`Failed to launch PowerShell: ${err.message}`));
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
async function addFirewallRule(port) {
|
|
790
|
+
await runElevated([
|
|
791
|
+
"advfirewall",
|
|
792
|
+
"firewall",
|
|
793
|
+
"add",
|
|
794
|
+
"rule",
|
|
795
|
+
`name=claude-collab-${port}`,
|
|
796
|
+
"protocol=TCP",
|
|
797
|
+
"dir=in",
|
|
798
|
+
`localport=${port}`,
|
|
799
|
+
"action=allow"
|
|
800
|
+
]);
|
|
801
|
+
}
|
|
802
|
+
async function removeFirewallRule(port) {
|
|
803
|
+
await runElevated([
|
|
804
|
+
"advfirewall",
|
|
805
|
+
"firewall",
|
|
806
|
+
"delete",
|
|
807
|
+
"rule",
|
|
808
|
+
`name=claude-collab-${port}`
|
|
809
|
+
]);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// src/presentation/mcp/tools/firewall-open.tool.ts
|
|
813
|
+
function registerFirewallOpenTool(server, client) {
|
|
957
814
|
server.tool(
|
|
958
|
-
"
|
|
959
|
-
"
|
|
960
|
-
{
|
|
961
|
-
|
|
962
|
-
|
|
815
|
+
"firewall_open",
|
|
816
|
+
"Open a Windows Firewall inbound rule for your P2P listen port. A UAC popup will appear \u2014 accept it to allow peers to connect to you directly.",
|
|
817
|
+
{
|
|
818
|
+
port: z.number().min(1024).max(65535).optional().describe("Port to open (defaults to your current listen port)")
|
|
819
|
+
},
|
|
820
|
+
async ({ port }) => {
|
|
821
|
+
const targetPort = port ?? client.getInfo().port;
|
|
822
|
+
if (!targetPort) {
|
|
963
823
|
return {
|
|
964
|
-
content: [{
|
|
965
|
-
|
|
966
|
-
text: `Hub is already running on port ${hubManager.port}.`
|
|
967
|
-
}]
|
|
824
|
+
content: [{ type: "text", text: "Could not determine port. Pass port explicitly." }],
|
|
825
|
+
isError: true
|
|
968
826
|
};
|
|
969
827
|
}
|
|
970
|
-
let firewallAdded = false;
|
|
971
828
|
try {
|
|
972
|
-
|
|
973
|
-
firewallAdded = result.firewallAdded;
|
|
974
|
-
} catch (err) {
|
|
975
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
829
|
+
await addFirewallRule(targetPort);
|
|
976
830
|
return {
|
|
977
|
-
content: [{
|
|
831
|
+
content: [{
|
|
832
|
+
type: "text",
|
|
833
|
+
text: [
|
|
834
|
+
`Firewall rule opened for port ${targetPort} (rule name: claude-collab-${targetPort}).`,
|
|
835
|
+
`Peers on the LAN can now connect to you directly.`
|
|
836
|
+
].join("\n")
|
|
837
|
+
}]
|
|
978
838
|
};
|
|
979
|
-
}
|
|
980
|
-
try {
|
|
981
|
-
await client.connectToHub(`ws://localhost:${port}`);
|
|
982
839
|
} catch (err) {
|
|
983
840
|
const msg = err instanceof Error ? err.message : String(err);
|
|
984
841
|
return {
|
|
985
|
-
content: [{ type: "text", text: `
|
|
842
|
+
content: [{ type: "text", text: `Failed to open firewall: ${msg}` }],
|
|
843
|
+
isError: true
|
|
986
844
|
};
|
|
987
845
|
}
|
|
988
|
-
const lines = [
|
|
989
|
-
`Hub started on port ${port}.`,
|
|
990
|
-
firewallAdded ? `Firewall rule added (claude-collab-${port}) \u2014 LAN peers can connect.` : `WARNING: Firewall rule could not be added (UAC was cancelled or denied). Peers on other machines may be blocked by Windows Firewall. Run start_hub again and accept the UAC prompt to fix this.`,
|
|
991
|
-
`Others on the LAN will auto-discover and connect via mDNS.`,
|
|
992
|
-
`Use stop_hub when you are done.`
|
|
993
|
-
];
|
|
994
|
-
return {
|
|
995
|
-
content: [{ type: "text", text: lines.join("\n") }]
|
|
996
|
-
};
|
|
997
846
|
}
|
|
998
847
|
);
|
|
999
848
|
}
|
|
1000
|
-
|
|
1001
|
-
// src/presentation/mcp/tools/stop-hub.tool.ts
|
|
1002
|
-
function registerStopHubTool(server, hubManager) {
|
|
849
|
+
function registerFirewallCloseTool(server, client) {
|
|
1003
850
|
server.tool(
|
|
1004
|
-
"
|
|
1005
|
-
"
|
|
1006
|
-
{
|
|
1007
|
-
|
|
1008
|
-
|
|
851
|
+
"firewall_close",
|
|
852
|
+
"Remove the Windows Firewall inbound rule for your P2P listen port. A UAC popup will appear \u2014 accept it to close the rule.",
|
|
853
|
+
{
|
|
854
|
+
port: z.number().min(1024).max(65535).optional().describe("Port to close (defaults to your current listen port)")
|
|
855
|
+
},
|
|
856
|
+
async ({ port }) => {
|
|
857
|
+
const targetPort = port ?? client.getInfo().port;
|
|
858
|
+
if (!targetPort) {
|
|
859
|
+
return {
|
|
860
|
+
content: [{ type: "text", text: "Could not determine port. Pass port explicitly." }],
|
|
861
|
+
isError: true
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
try {
|
|
865
|
+
await removeFirewallRule(targetPort);
|
|
1009
866
|
return {
|
|
1010
867
|
content: [{
|
|
1011
868
|
type: "text",
|
|
1012
|
-
text:
|
|
869
|
+
text: `Firewall rule removed for port ${targetPort} (rule name: claude-collab-${targetPort}).`
|
|
1013
870
|
}]
|
|
1014
871
|
};
|
|
1015
|
-
}
|
|
1016
|
-
const port = hubManager.port;
|
|
1017
|
-
let firewallRemoved = false;
|
|
1018
|
-
try {
|
|
1019
|
-
const result = await hubManager.stop();
|
|
1020
|
-
firewallRemoved = result.firewallRemoved;
|
|
1021
872
|
} catch (err) {
|
|
1022
873
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1023
874
|
return {
|
|
1024
|
-
content: [{ type: "text", text: `Failed to
|
|
875
|
+
content: [{ type: "text", text: `Failed to close firewall: ${msg}` }],
|
|
876
|
+
isError: true
|
|
1025
877
|
};
|
|
1026
878
|
}
|
|
1027
|
-
const lines = [
|
|
1028
|
-
`Hub stopped (was on port ${port}).`,
|
|
1029
|
-
firewallRemoved ? `Firewall rule removed (claude-collab-${port}).` : `WARNING: Firewall rule could not be removed (UAC was cancelled). Remove it manually: netsh advfirewall firewall delete rule name="claude-collab-${port}"`,
|
|
1030
|
-
`All peers have been disconnected.`
|
|
1031
|
-
];
|
|
1032
|
-
return {
|
|
1033
|
-
content: [{ type: "text", text: lines.join("\n") }]
|
|
1034
|
-
};
|
|
1035
879
|
}
|
|
1036
880
|
);
|
|
1037
881
|
}
|
|
1038
882
|
|
|
1039
883
|
// src/presentation/mcp/server.ts
|
|
1040
884
|
function createMcpServer(options) {
|
|
1041
|
-
const { client
|
|
885
|
+
const { client } = options;
|
|
1042
886
|
const server = new McpServer({
|
|
1043
887
|
name: "claude-collab",
|
|
1044
888
|
version: "0.1.0"
|
|
@@ -1047,8 +891,8 @@ function createMcpServer(options) {
|
|
|
1047
891
|
registerReplyTool(server, client);
|
|
1048
892
|
registerPeersTool(server, client);
|
|
1049
893
|
registerHistoryTool(server, client);
|
|
1050
|
-
|
|
1051
|
-
|
|
894
|
+
registerFirewallOpenTool(server, client);
|
|
895
|
+
registerFirewallCloseTool(server, client);
|
|
1052
896
|
return server;
|
|
1053
897
|
}
|
|
1054
898
|
async function startMcpServer(options) {
|
|
@@ -1056,111 +900,14 @@ async function startMcpServer(options) {
|
|
|
1056
900
|
const transport = new StdioServerTransport();
|
|
1057
901
|
await server.connect(transport);
|
|
1058
902
|
}
|
|
1059
|
-
function discoverHub(timeoutMs = 1e4) {
|
|
1060
|
-
return new Promise((resolve) => {
|
|
1061
|
-
const socket = createSocket("udp4");
|
|
1062
|
-
let settled = false;
|
|
1063
|
-
const finish = (result) => {
|
|
1064
|
-
if (settled) return;
|
|
1065
|
-
settled = true;
|
|
1066
|
-
clearTimeout(timer);
|
|
1067
|
-
try {
|
|
1068
|
-
socket.close();
|
|
1069
|
-
} catch {
|
|
1070
|
-
}
|
|
1071
|
-
resolve(result);
|
|
1072
|
-
};
|
|
1073
|
-
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
1074
|
-
socket.on("error", () => finish(null));
|
|
1075
|
-
socket.on("message", (msg, rinfo) => {
|
|
1076
|
-
try {
|
|
1077
|
-
const data = JSON.parse(msg.toString());
|
|
1078
|
-
if (data.type === "claude-collab-hub" && typeof data.port === "number") {
|
|
1079
|
-
finish({ host: rinfo.address, port: data.port });
|
|
1080
|
-
}
|
|
1081
|
-
} catch {
|
|
1082
|
-
}
|
|
1083
|
-
});
|
|
1084
|
-
socket.bind(DISCOVERY_PORT, "0.0.0.0");
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
function watchForHub(onFound) {
|
|
1088
|
-
const socket = createSocket("udp4");
|
|
1089
|
-
socket.on("error", (err) => {
|
|
1090
|
-
console.error("[hub-listener] error:", err.message);
|
|
1091
|
-
});
|
|
1092
|
-
socket.on("message", (msg, rinfo) => {
|
|
1093
|
-
try {
|
|
1094
|
-
const data = JSON.parse(msg.toString());
|
|
1095
|
-
if (data.type === "claude-collab-hub" && typeof data.port === "number") {
|
|
1096
|
-
onFound({ host: rinfo.address, port: data.port });
|
|
1097
|
-
}
|
|
1098
|
-
} catch {
|
|
1099
|
-
}
|
|
1100
|
-
});
|
|
1101
|
-
socket.bind(DISCOVERY_PORT, "0.0.0.0");
|
|
1102
|
-
return () => {
|
|
1103
|
-
try {
|
|
1104
|
-
socket.close();
|
|
1105
|
-
} catch {
|
|
1106
|
-
}
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
903
|
|
|
1110
904
|
// src/cli.ts
|
|
905
|
+
var P2P_PORT = 9999;
|
|
1111
906
|
var program = new Command();
|
|
1112
|
-
program.name("claude-collab").description("P2P collaboration between Claude Code terminals via
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
console.error(`Invalid port: ${options.port}`);
|
|
1117
|
-
process.exit(1);
|
|
1118
|
-
}
|
|
1119
|
-
const hub = new HubServer();
|
|
1120
|
-
hub.start(port);
|
|
1121
|
-
process.on("SIGINT", () => {
|
|
1122
|
-
console.log("\nHub stopped.");
|
|
1123
|
-
process.exit(0);
|
|
1124
|
-
});
|
|
1125
|
-
} else {
|
|
1126
|
-
if (!options.name) {
|
|
1127
|
-
console.error("--name is required");
|
|
1128
|
-
process.exit(1);
|
|
1129
|
-
}
|
|
1130
|
-
const client = new HubClient();
|
|
1131
|
-
const hubManager = new HubManager();
|
|
1132
|
-
if (options.server) {
|
|
1133
|
-
const url = options.server.startsWith("ws") ? options.server : `ws://${options.server}`;
|
|
1134
|
-
client.setServerUrl(url);
|
|
1135
|
-
await client.join(options.name, options.name);
|
|
1136
|
-
} else {
|
|
1137
|
-
await client.join(options.name, options.name);
|
|
1138
|
-
}
|
|
1139
|
-
const mcpReady = startMcpServer({ client, hubManager });
|
|
1140
|
-
if (!options.server) {
|
|
1141
|
-
discoverHub(5e3).then(async (hub) => {
|
|
1142
|
-
if (!hub || client.isConnected) return;
|
|
1143
|
-
try {
|
|
1144
|
-
await client.connectToHub(`ws://${hub.host}:${hub.port}`);
|
|
1145
|
-
} catch {
|
|
1146
|
-
}
|
|
1147
|
-
});
|
|
1148
|
-
const stopWatch = watchForHub(async (hub) => {
|
|
1149
|
-
if (client.isConnected) {
|
|
1150
|
-
stopWatch();
|
|
1151
|
-
return;
|
|
1152
|
-
}
|
|
1153
|
-
try {
|
|
1154
|
-
await client.connectToHub(`ws://${hub.host}:${hub.port}`);
|
|
1155
|
-
stopWatch();
|
|
1156
|
-
} catch (err) {
|
|
1157
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1158
|
-
console.error(`[cli] auto-connect failed: ${msg}`);
|
|
1159
|
-
}
|
|
1160
|
-
});
|
|
1161
|
-
}
|
|
1162
|
-
await mcpReady;
|
|
1163
|
-
}
|
|
907
|
+
program.name("claude-collab").description("P2P collaboration between Claude Code terminals via MCP").version("0.1.0").requiredOption("--name <name>", 'Your name on the network (e.g. "alice")').action(async (options) => {
|
|
908
|
+
const node = new P2PNode(P2P_PORT);
|
|
909
|
+
await node.join(options.name, options.name);
|
|
910
|
+
await startMcpServer({ client: node });
|
|
1164
911
|
});
|
|
1165
912
|
program.parse();
|
|
1166
913
|
//# sourceMappingURL=cli.js.map
|