@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/dist/mcp-main.js CHANGED
@@ -1,145 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { WebSocketServer, WebSocket } from 'ws';
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/p2p/p2p-message-protocol.ts
15
- function serializeP2PMsg(msg) {
13
+ // src/infrastructure/hub/hub-protocol.ts
14
+ function serialize(msg) {
16
15
  return JSON.stringify(msg);
17
16
  }
18
- function parseP2PMsg(data) {
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/config/index.ts
362
- var config = {
363
- /**
364
- * P2P node configuration
365
- */
366
- p2p: {
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
- // Questions we sent for history
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
- localMember = null;
398
- _isStarted = false;
251
+ setServerUrl(url) {
252
+ this.serverUrl = url;
253
+ }
399
254
  get isConnected() {
400
- return this._isStarted;
255
+ return this.ws?.readyState === WebSocket.OPEN;
401
256
  }
402
257
  get currentTeamId() {
403
- return this.localMember?.name;
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
- if (!this._isStarted) {
442
- await this.start();
443
- }
444
- const memberId = v4();
445
- this.localMember = { memberId, name, displayName };
446
- this.discovery.start(name, this.port);
447
- this.discovery.on("peer-found", ({ name: peerName, ip, wsPort }) => {
448
- void this.autoConnect(peerName, ip, wsPort);
449
- });
450
- this.discovery.on("peer-lost", (peerName) => {
451
- const ws = this.peerConns.get(peerName);
452
- if (ws) {
453
- ws.close();
454
- this.peerConns.delete(peerName);
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(toName, content, format) {
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, toName);
464
- this.sentQuestions.set(questionId, { toPeer: toName, content, askedAt: (/* @__PURE__ */ new Date()).toISOString() });
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 === "P2P_ASK_ACK" && m.requestId === requestId,
278
+ (m) => m.type === "ASK_ACK" && m.requestId === requestId,
467
279
  5e3
468
280
  );
469
- const msg = {
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.fromTeam} Claude`, teamName: cached.fromTeam },
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 toName = this.questionToName.get(questionId);
495
- if (!toName) return null;
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 === "P2P_ANSWER" && m.questionId === questionId || m.type === "P2P_ANSWER_PENDING" && m.requestId === requestId,
300
+ (m) => m.type === "ANSWER" && m.questionId === questionId || m.type === "ANSWER_PENDING" && m.requestId === requestId,
501
301
  5e3
502
302
  );
503
- const getMsg = {
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 === "P2P_ANSWER_PENDING") return null;
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
- fromTeam: answer.fromTeam,
517
- fromMemberId: answer.fromMemberId
311
+ fromName: answer.from
518
312
  });
519
313
  return {
520
314
  questionId,
521
- from: { displayName: `${answer.fromTeam} Claude`, teamName: answer.fromTeam },
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 in inbox`);
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 answerMsg = {
534
- type: "P2P_ANSWER",
535
- questionId,
536
- content,
537
- format,
538
- answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
539
- fromTeam: this.localMember.name,
540
- fromMemberId: this.localMember.memberId
541
- };
542
- if (question.ws.readyState === WebSocket.OPEN) {
543
- question.ws.send(serializeP2PMsg(answerMsg));
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.fromTeam} Claude`, teamName: q.fromTeam },
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.localMember?.name,
566
- port: this._isStarted ? this.port : void 0,
567
- connectedPeers: [...this.peerConns.keys()]
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.fromTeam,
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.discovery.stop();
599
- for (const ws of this.peerConns.values()) {
600
- ws.close();
389
+ if (this.reconnectTimer) {
390
+ clearTimeout(this.reconnectTimer);
391
+ this.reconnectTimer = null;
601
392
  }
602
- this.peerConns.clear();
603
- await new Promise((resolve) => {
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: auto-connect from multicast discovery
397
+ // Private: connection management
614
398
  // ---------------------------------------------------------------------------
615
- async autoConnect(peerName, ip, wsPort) {
616
- const existing = this.peerConns.get(peerName);
617
- if (existing && existing.readyState === WebSocket.OPEN) return;
618
- try {
619
- await this.connectToPeer(ip, wsPort);
620
- } catch (err) {
621
- const msg = err instanceof Error ? err.message : String(err);
622
- console.error(`[p2p] auto-connect to ${peerName} @ ${ip}:${wsPort} failed: ${msg}`);
623
- }
624
- }
625
- /**
626
- * Internal: open a WebSocket connection to a peer and perform HELLO handshake.
627
- */
628
- async connectToPeer(ip, port) {
629
- if (!this.localMember) {
630
- throw new Error("Must call join() before connecting to peers");
631
- }
632
- const ws = new WebSocket(`ws://${ip}:${port}`);
633
- this.wsOutgoing.add(ws);
634
- this.wsToIp.set(ws, ip);
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 = parseP2PMsg(data.toString());
638
- this.handleMessage(ws, msg);
639
- } catch (err) {
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", () => this.cleanupWs(ws));
644
- ws.on("error", (err) => console.error("[p2p] ws error:", err.message));
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(`Connection timeout to ${ip}:${port}`)),
648
- 5e3
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
- const hello = {
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 helloMsg = await this.waitForResponse(
666
- (m) => m.type === "P2P_HELLO",
431
+ const ack = await this.waitForResponse(
432
+ (m) => m.type === "HELLO_ACK",
667
433
  1e4
668
434
  );
669
- return helloMsg.fromTeam;
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
- // Private: WebSocket server setup
673
- // ---------------------------------------------------------------------------
674
- setupWssHandlers() {
675
- this.wss.on("connection", (ws, req) => {
676
- const remoteIp = (req.socket.remoteAddress ?? "").replace("::ffff:", "");
677
- this.wsToIp.set(ws, remoteIp);
678
- ws.on("message", (data) => {
679
- try {
680
- const msg = parseP2PMsg(data.toString());
681
- if (msg.type === "P2P_HELLO" && this.localMember) {
682
- const hello = {
683
- type: "P2P_HELLO",
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: unified message handler
453
+ // Private: message handling
699
454
  // ---------------------------------------------------------------------------
700
- handleMessage(ws, msg) {
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 "P2P_HELLO":
706
- this.handleHello(ws, msg);
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 "P2P_ASK":
709
- this.handleIncomingAsk(ws, msg);
466
+ case "ASK":
467
+ this.handleIncomingAsk(msg);
710
468
  break;
711
- case "P2P_GET_ANSWER":
712
- this.handleGetAnswer(ws, msg);
469
+ case "GET_ANSWER":
470
+ this.handleGetAnswer(msg);
713
471
  break;
714
- case "P2P_ANSWER":
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
- fromTeam: msg.fromTeam,
721
- fromMemberId: msg.fromMemberId
478
+ fromName: msg.from
722
479
  });
723
480
  }
724
481
  break;
725
- case "P2P_PING":
726
- ws.send(serializeP2PMsg({ type: "P2P_PONG" }));
482
+ case "HUB_ERROR":
483
+ console.error(`[hub-client] hub error: ${msg.message}`);
727
484
  break;
728
485
  }
729
486
  }
730
- handleHello(ws, msg) {
731
- const peerName = msg.fromTeam;
732
- const existing = this.peerConns.get(peerName);
733
- if (existing && existing.readyState === WebSocket.OPEN) {
734
- const myIp = this.discovery.getMyIp();
735
- const peerIp = this.wsToIp.get(ws) ?? "";
736
- const iShouldInitiate = myIp < peerIp;
737
- const thisIsOutgoing = this.wsOutgoing.has(ws);
738
- if (iShouldInitiate && !thisIsOutgoing) {
739
- ws.close();
740
- return;
741
- } else if (!iShouldInitiate && thisIsOutgoing) {
742
- ws.close();
743
- return;
744
- }
745
- ws.close();
746
- return;
747
- }
748
- this.wsToName.set(ws, peerName);
749
- this.peerConns.set(peerName, ws);
750
- console.error(`[p2p] connected to peer: ${peerName}`);
751
- }
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
- fromTeam: msg.fromTeam,
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(ws, msg) {
507
+ handleGetAnswer(msg) {
783
508
  const question = this.incomingQuestions.get(msg.questionId);
784
509
  if (!question?.answered) {
785
- const pending = {
786
- type: "P2P_ANSWER_PENDING",
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
- const answer = {
794
- type: "P2P_ANSWER",
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
- async getPeerConnection(name) {
820
- const existing = this.peerConns.get(name);
821
- if (existing && existing.readyState === WebSocket.OPEN) {
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
- throw new Error(
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("P2P request timed out"));
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 parseName() {
1028
- const idx = process.argv.indexOf("--name");
1029
- const value = process.argv[idx + 1];
1030
- if (idx !== -1 && value) {
1031
- return value;
1032
- }
1033
- console.error("Usage: claude-collab --name <your-name>");
1034
- process.exit(1);
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 = parseName();
1038
- const p2pNode = new P2PNode();
1039
- await p2pNode.join(name, name);
1040
- await startMcpServer({ client: p2pNode });
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);