@dolusoft/claude-collab 1.4.4 → 1.5.1

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