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