@dolusoft/claude-collab 1.4.3 → 1.5.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,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 } 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,128 @@ function serializeP2PMsg(msg) {
17
18
  function parseP2PMsg(data) {
18
19
  return JSON.parse(data);
19
20
  }
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
+ };
20
143
  var CS_CONINJECT = `
21
144
  using System;
22
145
  using System.Collections.Generic;
@@ -252,16 +375,23 @@ var config = {
252
375
  var P2PNode = class {
253
376
  wss = null;
254
377
  port = 0;
255
- // Connections indexed by remote team name
378
+ discovery = new MulticastDiscovery();
379
+ // Connections indexed by remote peer name
256
380
  peerConns = /* @__PURE__ */ new Map();
257
- // Reverse lookup: ws → teamName (for cleanup)
258
- wsToTeam = /* @__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();
259
387
  // Questions we received from remote peers (our inbox)
260
388
  incomingQuestions = /* @__PURE__ */ new Map();
261
389
  // Answers we received for questions we asked
262
390
  receivedAnswers = /* @__PURE__ */ new Map();
263
- // Maps questionId → remote teamName (so we know who to poll)
264
- questionToTeam = /* @__PURE__ */ new Map();
391
+ // Maps questionId → remote peer name (so we know who to poll)
392
+ questionToName = /* @__PURE__ */ new Map();
393
+ // Questions we sent — for history
394
+ sentQuestions = /* @__PURE__ */ new Map();
265
395
  // Pending response handlers (request-response correlation by filter)
266
396
  pendingHandlers = /* @__PURE__ */ new Set();
267
397
  localMember = null;
@@ -270,7 +400,7 @@ var P2PNode = class {
270
400
  return this._isStarted;
271
401
  }
272
402
  get currentTeamId() {
273
- return this.localMember?.teamName;
403
+ return this.localMember?.name;
274
404
  }
275
405
  /**
276
406
  * Starts the WS server on a random available port within the configured range.
@@ -307,71 +437,31 @@ var P2PNode = class {
307
437
  wss.once("error", () => resolve(false));
308
438
  });
309
439
  }
310
- async join(teamName, displayName) {
440
+ async join(name, displayName) {
311
441
  if (!this._isStarted) {
312
442
  await this.start();
313
443
  }
314
444
  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
- }
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);
334
449
  });
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);
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);
342
455
  }
343
456
  });
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;
457
+ return { memberId, teamId: name, teamName: name, displayName, status: "ONLINE", port: this.port };
369
458
  }
370
- async ask(toTeam, content, format) {
371
- const ws = await this.getPeerConnection(toTeam);
459
+ async ask(toName, content, format) {
460
+ const ws = await this.getPeerConnection(toName);
372
461
  const questionId = v4();
373
462
  const requestId = v4();
374
- this.questionToTeam.set(questionId, toTeam);
463
+ this.questionToName.set(questionId, toName);
464
+ this.sentQuestions.set(questionId, { toPeer: toName, content, askedAt: (/* @__PURE__ */ new Date()).toISOString() });
375
465
  const ackPromise = this.waitForResponse(
376
466
  (m) => m.type === "P2P_ASK_ACK" && m.requestId === requestId,
377
467
  5e3
@@ -380,8 +470,8 @@ var P2PNode = class {
380
470
  type: "P2P_ASK",
381
471
  questionId,
382
472
  fromMemberId: this.localMember.memberId,
383
- fromTeam: this.localMember.teamName,
384
- toTeam,
473
+ fromTeam: this.localMember.name,
474
+ toTeam: toName,
385
475
  content,
386
476
  format,
387
477
  requestId
@@ -401,9 +491,9 @@ var P2PNode = class {
401
491
  answeredAt: cached.answeredAt
402
492
  };
403
493
  }
404
- const toTeam = this.questionToTeam.get(questionId);
405
- if (!toTeam) return null;
406
- const ws = this.peerConns.get(toTeam);
494
+ const toName = this.questionToName.get(questionId);
495
+ if (!toName) return null;
496
+ const ws = this.peerConns.get(toName);
407
497
  if (!ws || ws.readyState !== WebSocket.OPEN) return null;
408
498
  const requestId = v4();
409
499
  const responsePromise = this.waitForResponse(
@@ -446,7 +536,7 @@ var P2PNode = class {
446
536
  content,
447
537
  format,
448
538
  answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
449
- fromTeam: this.localMember.teamName,
539
+ fromTeam: this.localMember.name,
450
540
  fromMemberId: this.localMember.memberId
451
541
  };
452
542
  if (question.ws.readyState === WebSocket.OPEN) {
@@ -470,7 +560,42 @@ var P2PNode = class {
470
560
  pendingCount: questions.length
471
561
  };
472
562
  }
563
+ getInfo() {
564
+ return {
565
+ teamName: this.localMember?.name,
566
+ port: this._isStarted ? this.port : void 0,
567
+ connectedPeers: [...this.peerConns.keys()]
568
+ };
569
+ }
570
+ getHistory() {
571
+ const entries = [];
572
+ for (const [questionId, sent] of this.sentQuestions) {
573
+ const answer = this.receivedAnswers.get(questionId);
574
+ entries.push({
575
+ direction: "sent",
576
+ questionId,
577
+ peer: sent.toPeer,
578
+ question: sent.content,
579
+ answer: answer?.content,
580
+ askedAt: sent.askedAt,
581
+ answeredAt: answer?.answeredAt
582
+ });
583
+ }
584
+ for (const [questionId, incoming] of this.incomingQuestions) {
585
+ entries.push({
586
+ direction: "received",
587
+ questionId,
588
+ peer: incoming.fromTeam,
589
+ question: incoming.content,
590
+ answer: incoming.answered ? incoming.answerContent : void 0,
591
+ askedAt: incoming.createdAt.toISOString(),
592
+ answeredAt: incoming.answered ? (/* @__PURE__ */ new Date()).toISOString() : void 0
593
+ });
594
+ }
595
+ return entries.sort((a, b) => a.askedAt.localeCompare(b.askedAt));
596
+ }
473
597
  async disconnect() {
598
+ this.discovery.stop();
474
599
  for (const ws of this.peerConns.values()) {
475
600
  ws.close();
476
601
  }
@@ -485,35 +610,88 @@ var P2PNode = class {
485
610
  this._isStarted = false;
486
611
  }
487
612
  // ---------------------------------------------------------------------------
613
+ // Private: auto-connect from multicast discovery
614
+ // ---------------------------------------------------------------------------
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);
635
+ ws.on("message", (data) => {
636
+ try {
637
+ const msg = parseP2PMsg(data.toString());
638
+ this.handleMessage(ws, msg);
639
+ } catch (err) {
640
+ console.error("[p2p] Failed to parse message:", err);
641
+ }
642
+ });
643
+ ws.on("close", () => this.cleanupWs(ws));
644
+ ws.on("error", (err) => console.error("[p2p] ws error:", err.message));
645
+ await new Promise((resolve, reject) => {
646
+ const timeout = setTimeout(
647
+ () => reject(new Error(`Connection timeout to ${ip}:${port}`)),
648
+ 5e3
649
+ );
650
+ ws.on("open", () => {
651
+ 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));
658
+ resolve();
659
+ });
660
+ ws.on("error", (err) => {
661
+ clearTimeout(timeout);
662
+ reject(err);
663
+ });
664
+ });
665
+ const helloMsg = await this.waitForResponse(
666
+ (m) => m.type === "P2P_HELLO",
667
+ 1e4
668
+ );
669
+ return helloMsg.fromTeam;
670
+ }
671
+ // ---------------------------------------------------------------------------
488
672
  // Private: WebSocket server setup
489
673
  // ---------------------------------------------------------------------------
490
674
  setupWssHandlers() {
491
- this.wss.on("connection", (ws) => {
675
+ this.wss.on("connection", (ws, req) => {
676
+ const remoteIp = (req.socket.remoteAddress ?? "").replace("::ffff:", "");
677
+ this.wsToIp.set(ws, remoteIp);
492
678
  ws.on("message", (data) => {
493
679
  try {
494
680
  const msg = parseP2PMsg(data.toString());
495
681
  if (msg.type === "P2P_HELLO" && this.localMember) {
496
682
  const hello = {
497
683
  type: "P2P_HELLO",
498
- fromTeam: this.localMember.teamName,
684
+ fromTeam: this.localMember.name,
499
685
  fromMemberId: this.localMember.memberId
500
686
  };
501
687
  ws.send(serializeP2PMsg(hello));
502
688
  }
503
689
  this.handleMessage(ws, msg);
504
690
  } catch (err) {
505
- console.error("Failed to parse incoming P2P message:", err);
506
- }
507
- });
508
- ws.on("close", () => {
509
- const team = this.wsToTeam.get(ws);
510
- if (team) {
511
- if (this.peerConns.get(team) === ws) {
512
- this.peerConns.delete(team);
513
- }
514
- this.wsToTeam.delete(ws);
691
+ console.error("[p2p] Failed to parse incoming message:", err);
515
692
  }
516
693
  });
694
+ ws.on("close", () => this.cleanupWs(ws));
517
695
  });
518
696
  }
519
697
  // ---------------------------------------------------------------------------
@@ -525,9 +703,7 @@ var P2PNode = class {
525
703
  }
526
704
  switch (msg.type) {
527
705
  case "P2P_HELLO":
528
- this.wsToTeam.set(ws, msg.fromTeam);
529
- this.peerConns.set(msg.fromTeam, ws);
530
- console.error(`Peer identified: ${msg.fromTeam}`);
706
+ this.handleHello(ws, msg);
531
707
  break;
532
708
  case "P2P_ASK":
533
709
  this.handleIncomingAsk(ws, msg);
@@ -551,6 +727,28 @@ var P2PNode = class {
551
727
  break;
552
728
  }
553
729
  }
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
+ }
554
752
  handleIncomingAsk(ws, msg) {
555
753
  this.incomingQuestions.set(msg.questionId, {
556
754
  questionId: msg.questionId,
@@ -598,7 +796,7 @@ var P2PNode = class {
598
796
  content: question.answerContent,
599
797
  format: question.answerFormat,
600
798
  answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
601
- fromTeam: this.localMember.teamName,
799
+ fromTeam: this.localMember.name,
602
800
  fromMemberId: this.localMember.memberId,
603
801
  requestId: msg.requestId
604
802
  };
@@ -607,13 +805,24 @@ var P2PNode = class {
607
805
  // ---------------------------------------------------------------------------
608
806
  // Private: peer connection management
609
807
  // ---------------------------------------------------------------------------
610
- async getPeerConnection(teamName) {
611
- const existing = this.peerConns.get(teamName);
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);
818
+ }
819
+ async getPeerConnection(name) {
820
+ const existing = this.peerConns.get(name);
612
821
  if (existing && existing.readyState === WebSocket.OPEN) {
613
822
  return existing;
614
823
  }
615
824
  throw new Error(
616
- `No connection to team '${teamName}'. Use the connect_peer tool to connect first.`
825
+ `No connection to peer '${name}'. They may not be on the network yet \u2014 wait a moment and try again.`
617
826
  );
618
827
  }
619
828
  waitForResponse(filter, timeoutMs) {
@@ -633,81 +842,13 @@ var P2PNode = class {
633
842
  });
634
843
  }
635
844
  };
636
- var joinSchema = {
637
- team: z.string().describe('Team name to join (e.g., "frontend", "backend", "devops")'),
638
- displayName: z.string().optional().describe('Display name for this terminal (default: team + " Claude")')
639
- };
640
- function registerJoinTool(server, client) {
641
- server.tool("join", joinSchema, async (args) => {
642
- const teamName = args.team;
643
- const displayName = args.displayName ?? `${teamName} Claude`;
644
- try {
645
- const member = await client.join(teamName, displayName);
646
- return {
647
- content: [
648
- {
649
- type: "text",
650
- text: `Successfully joined team "${member.teamName}" as "${member.displayName}".
651
-
652
- Your member ID: ${member.memberId}
653
- Status: ${member.status}
654
- Listening on port: ${member.port}
655
-
656
- Share this port with the other terminal so they can run: connect_peer("<your-ip>", ${member.port})`
657
- }
658
- ]
659
- };
660
- } catch (error) {
661
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
662
- return {
663
- content: [
664
- {
665
- type: "text",
666
- text: `Failed to join team: ${errorMessage}`
667
- }
668
- ],
669
- isError: true
670
- };
671
- }
672
- });
673
- }
674
- var connectPeerSchema = {
675
- ip: z.string().describe('IP address of the peer to connect to (e.g., "172.16.40.137")'),
676
- port: z.number().describe("Port the peer is listening on (shown in their join response)")
677
- };
678
- function registerConnectPeerTool(server, client) {
679
- server.tool("connect_peer", connectPeerSchema, async (args) => {
680
- try {
681
- const peerTeam = await client.connectPeer(args.ip, args.port);
682
- return {
683
- content: [
684
- {
685
- type: "text",
686
- text: `Connected to peer "${peerTeam}" at ${args.ip}:${args.port}.`
687
- }
688
- ]
689
- };
690
- } catch (error) {
691
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
692
- return {
693
- content: [
694
- {
695
- type: "text",
696
- text: `Failed to connect to ${args.ip}:${args.port}: ${errorMessage}`
697
- }
698
- ],
699
- isError: true
700
- };
701
- }
702
- });
703
- }
704
845
  var askSchema = {
705
- team: z.string().describe('Target team name to ask (e.g., "backend", "frontend")'),
846
+ peer: z.string().describe('Name of the peer to ask (e.g., "alice", "backend")'),
706
847
  question: z.string().describe("The question to ask (supports markdown)")
707
848
  };
708
849
  function registerAskTool(server, client) {
709
850
  server.tool("ask", askSchema, async (args) => {
710
- const targetTeam = args.team;
851
+ const targetPeer = args.peer;
711
852
  const question = args.question;
712
853
  try {
713
854
  if (!client.currentTeamId) {
@@ -715,13 +856,13 @@ function registerAskTool(server, client) {
715
856
  content: [
716
857
  {
717
858
  type: "text",
718
- text: 'You must join a team first. Use the "join" tool to join a team.'
859
+ text: "Node is not ready yet. Wait a moment and try again."
719
860
  }
720
861
  ],
721
862
  isError: true
722
863
  };
723
864
  }
724
- const questionId = await client.ask(targetTeam, question, "markdown");
865
+ const questionId = await client.ask(targetPeer, question, "markdown");
725
866
  const POLL_INTERVAL_MS = 5e3;
726
867
  const MAX_WAIT_MS = 5 * 60 * 1e3;
727
868
  const deadline = Date.now() + MAX_WAIT_MS;
@@ -766,124 +907,6 @@ Manuel kontrol i\xE7in "check_answer" tool'unu kullanabilirsin.`
766
907
  }
767
908
  });
768
909
  }
769
- var checkAnswerSchema = {
770
- question_id: z.string().describe('The question ID returned by the "ask" tool')
771
- };
772
- function registerCheckAnswerTool(server, client) {
773
- server.tool("check_answer", checkAnswerSchema, async (args) => {
774
- const questionId = args.question_id;
775
- try {
776
- if (!client.currentTeamId) {
777
- return {
778
- content: [
779
- {
780
- type: "text",
781
- text: 'You must join a team first. Use the "join" tool to join a team.'
782
- }
783
- ],
784
- isError: true
785
- };
786
- }
787
- const answer = await client.checkAnswer(questionId);
788
- if (!answer) {
789
- return {
790
- content: [
791
- {
792
- type: "text",
793
- text: `No answer yet for question \`${questionId}\`. The other team hasn't replied yet. You can continue working and check again later.`
794
- }
795
- ]
796
- };
797
- }
798
- return {
799
- content: [
800
- {
801
- type: "text",
802
- text: `**Answer from ${answer.from.displayName} (${answer.from.teamName}):**
803
-
804
- ${answer.content}`
805
- }
806
- ]
807
- };
808
- } catch (error) {
809
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
810
- return {
811
- content: [
812
- {
813
- type: "text",
814
- text: `Failed to check answer: ${errorMessage}`
815
- }
816
- ],
817
- isError: true
818
- };
819
- }
820
- });
821
- }
822
-
823
- // src/presentation/mcp/tools/inbox.tool.ts
824
- var inboxSchema = {};
825
- function registerInboxTool(server, client) {
826
- server.tool("inbox", inboxSchema, async () => {
827
- try {
828
- if (!client.currentTeamId) {
829
- return {
830
- content: [
831
- {
832
- type: "text",
833
- text: 'You must join a team first. Use the "join" tool to join a team.'
834
- }
835
- ],
836
- isError: true
837
- };
838
- }
839
- const inbox = await client.getInbox();
840
- if (inbox.questions.length === 0) {
841
- return {
842
- content: [
843
- {
844
- type: "text",
845
- text: "No pending questions in your inbox."
846
- }
847
- ]
848
- };
849
- }
850
- const questionsList = inbox.questions.map((q, i) => {
851
- const ageSeconds = Math.floor(q.ageMs / 1e3);
852
- const ageStr = ageSeconds < 60 ? `${ageSeconds}s ago` : `${Math.floor(ageSeconds / 60)}m ago`;
853
- return `### ${i + 1}. Question from ${q.from.displayName} (${q.from.teamName}) - ${ageStr}
854
- **ID:** \`${q.questionId}\`
855
- **Status:** ${q.status}
856
-
857
- ${q.content}
858
-
859
- ---`;
860
- }).join("\n\n");
861
- return {
862
- content: [
863
- {
864
- type: "text",
865
- text: `# Inbox (${inbox.pendingCount} pending, ${inbox.totalCount} total)
866
-
867
- ${questionsList}
868
-
869
- Use the "reply" tool with the question ID to answer a question.`
870
- }
871
- ]
872
- };
873
- } catch (error) {
874
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
875
- return {
876
- content: [
877
- {
878
- type: "text",
879
- text: `Failed to get inbox: ${errorMessage}`
880
- }
881
- ],
882
- isError: true
883
- };
884
- }
885
- });
886
- }
887
910
  var replySchema = {
888
911
  questionId: z.string().describe("The ID of the question to reply to (from inbox)"),
889
912
  answer: z.string().describe("Your answer to the question (supports markdown)")
@@ -929,51 +952,69 @@ function registerReplyTool(server, client) {
929
952
  });
930
953
  }
931
954
 
955
+ // src/presentation/mcp/tools/peers.tool.ts
956
+ function registerPeersTool(server, client) {
957
+ server.tool("peers", {}, async () => {
958
+ const info = client.getInfo();
959
+ const myName = info.teamName ?? "(starting...)";
960
+ const connected = info.connectedPeers;
961
+ if (connected.length === 0) {
962
+ return {
963
+ content: [{
964
+ type: "text",
965
+ text: `You are "${myName}". No peers connected yet \u2014 they will appear automatically when they come online.`
966
+ }]
967
+ };
968
+ }
969
+ const list = connected.map((name) => ` \u2022 ${name}`).join("\n");
970
+ return {
971
+ content: [{
972
+ type: "text",
973
+ text: `You are "${myName}". Connected peers (${connected.length}):
974
+ ${list}`
975
+ }]
976
+ };
977
+ });
978
+ }
979
+
980
+ // src/presentation/mcp/tools/history.tool.ts
981
+ function registerHistoryTool(server, client) {
982
+ server.tool("history", {}, async () => {
983
+ const entries = client.getHistory();
984
+ if (entries.length === 0) {
985
+ return {
986
+ content: [{ type: "text", text: "No questions yet this session." }]
987
+ };
988
+ }
989
+ const lines = entries.map((e) => {
990
+ const time = new Date(e.askedAt).toLocaleTimeString();
991
+ if (e.direction === "sent") {
992
+ const answerLine = e.answer ? ` \u21B3 ${e.peer}: ${e.answer}` : ` \u21B3 (no answer yet)`;
993
+ return `[${time}] \u2192 ${e.peer}: ${e.question}
994
+ ${answerLine}`;
995
+ } else {
996
+ const answerLine = e.answer ? ` \u21B3 you: ${e.answer}` : ` \u21B3 (not replied yet)`;
997
+ return `[${time}] \u2190 ${e.peer}: ${e.question}
998
+ ${answerLine}`;
999
+ }
1000
+ });
1001
+ return {
1002
+ content: [{ type: "text", text: lines.join("\n\n") }]
1003
+ };
1004
+ });
1005
+ }
1006
+
932
1007
  // src/presentation/mcp/server.ts
933
1008
  function createMcpServer(options) {
934
1009
  const { client } = options;
935
- const server = new McpServer(
936
- {
937
- name: "claude-collab",
938
- version: "0.1.2"
939
- },
940
- {
941
- capabilities: {
942
- resources: {
943
- subscribe: true,
944
- listChanged: true
945
- }
946
- }
947
- }
948
- );
949
- registerJoinTool(server, client);
950
- registerConnectPeerTool(server, client);
1010
+ const server = new McpServer({
1011
+ name: "claude-collab",
1012
+ version: "0.1.0"
1013
+ });
951
1014
  registerAskTool(server, client);
952
- registerCheckAnswerTool(server, client);
953
- registerInboxTool(server, client);
954
1015
  registerReplyTool(server, client);
955
- server.resource(
956
- "inbox-questions",
957
- "inbox://questions",
958
- { description: "Your inbox of pending questions from other teams", mimeType: "application/json" },
959
- async () => {
960
- try {
961
- const inbox = await client.getInbox();
962
- return {
963
- contents: [
964
- {
965
- uri: "inbox://questions",
966
- mimeType: "application/json",
967
- text: JSON.stringify(inbox, null, 2)
968
- }
969
- ]
970
- };
971
- } catch (error) {
972
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
973
- throw new Error(`Failed to read inbox: ${errorMessage}`);
974
- }
975
- }
976
- );
1016
+ registerPeersTool(server, client);
1017
+ registerHistoryTool(server, client);
977
1018
  return server;
978
1019
  }
979
1020
  async function startMcpServer(options) {
@@ -983,8 +1024,19 @@ async function startMcpServer(options) {
983
1024
  }
984
1025
 
985
1026
  // 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);
1035
+ }
986
1036
  async function main() {
1037
+ const name = parseName();
987
1038
  const p2pNode = new P2PNode();
1039
+ await p2pNode.join(name, name);
988
1040
  await startMcpServer({ client: p2pNode });
989
1041
  }
990
1042
  main().catch((error) => {