@dolusoft/claude-collab 1.11.0 → 1.11.2

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/README.md CHANGED
@@ -7,12 +7,16 @@ Real-time collaboration between Claude Code terminals via MCP (Model Context Pro
7
7
 
8
8
  ## Overview
9
9
 
10
- Claude Collab lets multiple Claude Code terminals communicate directly — no central server needed. Each peer runs its own node; peers discover each other automatically on the LAN via UDP multicast and connect directly over WebSocket.
10
+ Claude Collab lets multiple Claude Code terminals communicate directly — no central server needed. Each node listens on a fixed port (12345). Peers connect manually by IP address and form a **full mesh**: when you connect to one peer, everyone connects to everyone automatically.
11
11
 
12
12
  ```
13
13
  alice ◄──────────────────► bob
14
- direct WebSocket
15
- (auto-discovered)
14
+ direct WebSocket
15
+ (manual IP connect)
16
+
17
+
18
+ carol
19
+ (auto-connected via mesh)
16
20
  ```
17
21
 
18
22
  ## Setup
@@ -27,28 +31,33 @@ claude mcp add claude-collab -- npx -y @dolusoft/claude-collab --name <your-name
27
31
  |-------------|-------------|
28
32
  | `<your-name>` | Your identifier on the network (e.g. `alice`, `backend`, `frontend`) |
29
33
 
30
- ### Step 2 — Connect to peers
34
+ ### Step 2 — Find your IP
31
35
 
32
- Call `peer_find` in Claude Code:
36
+ Call `status()` in Claude Code:
33
37
 
34
38
  ```
35
- peer_find()
39
+ status()
36
40
  ```
37
41
 
38
- This will:
39
- 1. Open a **Windows UAC prompt** — click **Yes** to open your firewall
40
- 2. Wait 30 seconds while peers are discovered and connected
41
- 3. Close the firewall (another UAC prompt) — existing connections persist
42
+ This shows your LAN IP address. Share it with your teammate.
42
43
 
43
- Everyone on the team should call `peer_find` at the same time when setting up.
44
+ ### Step 3 Connect to a peer
44
45
 
45
- ### Step 3 Adding a new peer later
46
+ Your teammate calls `connect()` with your IP:
46
47
 
47
- Only the **new peer** needs to call `peer_find`. Existing peers will connect to them automatically — no action needed from others.
48
+ ```
49
+ connect("192.168.1.5")
50
+ ```
51
+
52
+ On the **first call only**, a Windows UAC popup appears — click **Yes** to open port 12345 on your firewall. This stays open permanently.
53
+
54
+ ### Step 4 — Full mesh forms automatically
48
55
 
49
- ### Step 4After a disconnect or restart
56
+ If Alice is already connected to Bob, and Carol connects to Alice Carol automatically connects to Bob as well. No extra steps needed.
50
57
 
51
- The disconnecting peer calls `peer_find` again. Others reconnect automatically.
58
+ ### Step 5 After a disconnect or restart
59
+
60
+ The disconnecting peer calls `connect(ip)` again to rejoin. Others do not need to do anything.
52
61
 
53
62
  ---
54
63
 
@@ -56,22 +65,30 @@ The disconnecting peer calls `peer_find` again. Others reconnect automatically.
56
65
 
57
66
  | Tool | Description |
58
67
  |------|-------------|
59
- | `peer_find` | Discover and connect to peers. Opens firewall, waits 30s, closes firewall. |
68
+ | `connect` | Connect to a peer by their LAN IP. Opens firewall on first use (UAC popup). |
69
+ | `status` | Show your name, LAN IP, port, and connected peers. |
60
70
  | `ask` | Ask a peer a question by name. Waits up to 5 minutes for a reply. |
61
71
  | `reply` | Reply to an incoming question. |
62
- | `peers` | Show connected peers and your own name/port. |
63
72
  | `history` | Show past questions and answers from this session. |
64
73
 
65
74
  ## Example
66
75
 
67
76
  ```
68
- # Alice asks Bob
69
- ask("bob", "What's the response format for the /users endpoint?")
77
+ # Alice checks her IP and shares it with Bob
78
+ status()
79
+ # → Name: alice IP: 192.168.1.5 Port: 12345
80
+
81
+ # Bob connects to Alice
82
+ connect("192.168.1.5")
83
+ # → Connected to "alice". Use status() to see all connected peers.
70
84
 
71
- # Bob sees the question injected into his terminal and replies
85
+ # Bob asks Alice a question
86
+ ask("alice", "What's the response format for the /users endpoint?")
87
+
88
+ # Alice sees the question injected into her terminal and replies
72
89
  reply("<question-id>", "Returns JSON: { id, name, email }")
73
90
 
74
- # Alice receives the answer
91
+ # Bob receives the answer
75
92
  ```
76
93
 
77
94
  ## Use Case: Sharing Context Between Agents
@@ -106,19 +123,13 @@ sequenceDiagram
106
123
  participant Alice Claude Code
107
124
  participant Bob Claude Code
108
125
 
109
- Alice Claude Code->>Alice Claude Code: peer_find()
110
- Note over Alice Claude Code: UAC popup → firewall opens
111
- Bob Claude Code->>Bob Claude Code: peer_find()
112
- Note over Bob Claude Code: UAC popup → firewall opens
113
-
114
- Alice Claude Code-)Bob Claude Code: UDP multicast discovery
115
- Bob Claude Code-)Alice Claude Code: UDP multicast discovery
116
- Alice Claude Code->>Bob Claude Code: WebSocket HELLO
117
- Bob Claude Code-->>Alice Claude Code: HELLO_ACK
118
- Note over Alice Claude Code,Bob Claude Code: P2P connection established
126
+ Alice Claude Code->>Alice Claude Code: status()
127
+ Note over Alice Claude Code: IP: 192.168.1.5
119
128
 
120
- Note over Alice Claude Code: firewall closes (connection persists)
121
- Note over Bob Claude Code: firewall closes (connection persists)
129
+ Bob Claude Code->>Alice Claude Code: connect("192.168.1.5")
130
+ Note over Bob Claude Code: UAC popup → firewall opens (once)
131
+ Alice Claude Code-->>Bob Claude Code: HELLO_ACK
132
+ Note over Alice Claude Code,Bob Claude Code: Direct connection established
122
133
 
123
134
  User->>Alice Claude Code: "My API key is sk-abc123..."
124
135
  Note over Alice Claude Code: Stored in context
@@ -136,25 +147,24 @@ sequenceDiagram
136
147
 
137
148
  ```
138
149
  Startup:
139
- Each peer binds a WebSocket server on a random port (10000–19999)
140
- Each peer broadcasts UDP multicast (239.255.42.42:11776) every 5s
150
+ Each node binds a WebSocket server on fixed port 12345
151
+ If the port is already in use, the existing process is killed automatically
141
152
 
142
- Discovery:
143
- Peer A hears Peer B's multicast connects outbound to B's WS port
144
- If multicast is one-way (e.g. VMware + WiFi), the receiving side
145
- sends a unicast reply so both peers discover each other
153
+ Connecting:
154
+ connect("IP") opens firewall (first time only, UAC popup) WebSocket handshake
155
+ After handshake: both sides exchange PEER_LIST
156
+ Each side connects to any unknown peers in the list (full mesh)
157
+ Existing peers are notified via PEER_ANNOUNCE and connect directly too
146
158
 
147
- peer_find:
148
- Opens firewall (inbound TCP + UDP 11776) wait closes firewall
149
- Established connections persist after firewall closes (stateful TCP)
150
- New peers only need to open their own firewall — existing peers
151
- connect outbound to them automatically
159
+ Questions:
160
+ ask() sends ASK to peerpeer gets question injected into their terminal
161
+ reply() sends ANSWER back ask() call unblocks immediately (push, no polling)
152
162
  ```
153
163
 
154
164
  ## Limitations
155
165
 
156
166
  - **Windows only** — terminal injection uses `kernel32.dll` Win32 APIs (`AttachConsole`, `WriteConsoleInput`) compiled via PowerShell. macOS and Linux are not supported.
157
- - **LAN only** — UDP multicast TTL is set to 1, so packets cannot cross routers. Does not work over the internet or VPNs that don't forward multicast.
167
+ - **IP reachability required** — peers must be able to reach each other's IP on port 12345. Works on the same LAN, or any network where routing is possible (VPN, etc.).
158
168
  - **No encryption** — peer connections use plain `ws://` WebSocket. Traffic is unencrypted on the network.
159
169
  - **5-minute answer timeout** — if the peer does not reply within 5 minutes, `ask()` times out. The question is not retried automatically.
160
170
  - **One queued answer per offline peer** — if a peer is offline and you reply to multiple questions from them, only the last reply is queued and delivered on reconnect.
@@ -187,26 +197,24 @@ node dist/mcp-main.js --name alice
187
197
  node dist/mcp-main.js --name bob
188
198
  ```
189
199
 
190
- Both nodes will bind on random ports in the `10000–19999` range and discover each other via UDP multicast automatically.
200
+ Then in terminal 2, call `connect("127.0.0.1")` both nodes will connect on port 12345.
191
201
 
192
202
  ### Project structure
193
203
 
194
204
  ```
195
205
  src/
196
206
  ├── infrastructure/
197
- │ ├── discovery/
198
- │ │ └── multicast-discovery.ts # UDP multicast (239.255.42.42:11776) + unicast reply
207
+ │ ├── mesh/
208
+ │ │ ├── mesh-node.ts # WS server + client + peer management + mesh logic
209
+ │ │ └── protocol.ts # Wire protocol: HELLO, PEER_LIST, PEER_ANNOUNCE, ASK, ANSWER
199
210
  │ ├── firewall/
200
- │ │ └── firewall.ts # Windows Firewall via UAC-elevated netsh
201
- │ ├── p2p/
202
- │ │ ├── p2p-node.ts # WS server + client + peer management
203
- │ │ └── p2p-protocol.ts # Wire protocol: HELLO, ASK, ASK_ACK, ANSWER
211
+ │ │ └── firewall.ts # Windows Firewall via UAC-elevated netsh
204
212
  │ └── terminal-injector/
205
- │ └── windows-injector.ts # Injects questions into Claude Code via WriteConsoleInput
213
+ │ └── windows-injector.ts # Injects questions into Claude Code via WriteConsoleInput
206
214
  └── presentation/
207
215
  └── mcp/
208
- ├── server.ts # MCP server setup
209
- └── tools/ # ask, reply, peers, history, peer_find
216
+ ├── server.ts # MCP server setup
217
+ └── tools/ # connect, status, ask, reply, history
210
218
  ```
211
219
 
212
220
  ## License
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import { Command } from 'commander';
3
3
  import { WebSocket, WebSocketServer } from 'ws';
4
4
  import { v4 } from 'uuid';
5
5
  import os, { tmpdir } from 'os';
6
- import { spawn, execFile } from 'child_process';
6
+ import { execSync, spawn, execFile } from 'child_process';
7
7
  import { EventEmitter } from 'events';
8
8
  import { unlinkSync } from 'fs';
9
9
  import { join } from 'path';
@@ -11,7 +11,7 @@ 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/p2p/p2p-protocol.ts
14
+ // src/infrastructure/mesh/protocol.ts
15
15
  function serialize(msg) {
16
16
  return JSON.stringify(msg);
17
17
  }
@@ -263,9 +263,9 @@ var InjectionQueue = class extends EventEmitter {
263
263
  };
264
264
  var injectionQueue = new InjectionQueue();
265
265
 
266
- // src/infrastructure/p2p/p2p-node.ts
266
+ // src/infrastructure/mesh/mesh-node.ts
267
267
  var FIXED_PORT = 12345;
268
- var P2PNode = class {
268
+ var MeshNode = class {
269
269
  server = null;
270
270
  myName = "";
271
271
  running = false;
@@ -352,7 +352,7 @@ var P2PNode = class {
352
352
  this.wsToName.delete(ws);
353
353
  if (this.peerConnections.get(name) === ws) {
354
354
  this.peerConnections.delete(name);
355
- console.error(`[p2p] disconnected from peer: ${name}`);
355
+ console.error(`[mesh] disconnected from peer: ${name}`);
356
356
  }
357
357
  }
358
358
  });
@@ -366,7 +366,7 @@ var P2PNode = class {
366
366
  async ask(toPeer, content, format) {
367
367
  const ws = this.peerConnections.get(toPeer);
368
368
  if (!ws || ws.readyState !== WebSocket.OPEN) {
369
- throw new Error(`Peer "${toPeer}" is not connected. Use peers() to see who's online.`);
369
+ throw new Error(`Peer "${toPeer}" is not connected. Use status() to see who's online.`);
370
370
  }
371
371
  const questionId = v4();
372
372
  this.sentQuestions.set(questionId, { toPeer, content, askedAt: (/* @__PURE__ */ new Date()).toISOString() });
@@ -426,7 +426,7 @@ var P2PNode = class {
426
426
  this.sendToWs(ws, answerMsg);
427
427
  } else {
428
428
  this.pendingOutboundAnswers.set(senderName, answerMsg);
429
- console.error(`[p2p] "${senderName}" is offline, answer queued for delivery on reconnect`);
429
+ console.error(`[mesh] "${senderName}" is offline, answer queued for delivery on reconnect`);
430
430
  }
431
431
  }
432
432
  injectionQueue.notifyReplied();
@@ -500,13 +500,27 @@ var P2PNode = class {
500
500
  // ---------------------------------------------------------------------------
501
501
  // Private: server startup
502
502
  // ---------------------------------------------------------------------------
503
- startServer() {
503
+ async startServer() {
504
+ try {
505
+ await this.tryBind();
506
+ } catch (err) {
507
+ const nodeErr = err;
508
+ if (nodeErr.code === "EADDRINUSE") {
509
+ console.error(`[mesh] port ${FIXED_PORT} in use \u2014 killing existing process`);
510
+ await killProcessOnPort(FIXED_PORT);
511
+ await this.tryBind();
512
+ } else {
513
+ throw err;
514
+ }
515
+ }
516
+ }
517
+ tryBind() {
504
518
  return new Promise((resolve, reject) => {
505
519
  const wss = new WebSocketServer({ port: FIXED_PORT });
506
520
  wss.once("listening", () => {
507
521
  this.server = wss;
508
522
  this.running = true;
509
- console.error(`[p2p] listening on port ${FIXED_PORT} as "${this.myName}"`);
523
+ console.error(`[mesh] listening on port ${FIXED_PORT} as "${this.myName}"`);
510
524
  this.attachServerHandlers(wss);
511
525
  resolve();
512
526
  });
@@ -532,16 +546,16 @@ var P2PNode = class {
532
546
  this.wsToName.delete(ws);
533
547
  if (this.peerConnections.get(name) === ws) {
534
548
  this.peerConnections.delete(name);
535
- console.error(`[p2p] peer disconnected (inbound): ${name}`);
549
+ console.error(`[mesh] peer disconnected (inbound): ${name}`);
536
550
  }
537
551
  }
538
552
  });
539
553
  ws.on("error", (err) => {
540
- console.error("[p2p] inbound ws error:", err.message);
554
+ console.error("[mesh] inbound ws error:", err.message);
541
555
  });
542
556
  });
543
557
  wss.on("error", (err) => {
544
- console.error("[p2p] server error:", err.message);
558
+ console.error("[mesh] server error:", err.message);
545
559
  });
546
560
  }
547
561
  // ---------------------------------------------------------------------------
@@ -565,7 +579,7 @@ var P2PNode = class {
565
579
  this.wsToName.set(ws, name);
566
580
  this.peerIPs.set(name, ip);
567
581
  this.connectingPeers.delete(name);
568
- console.error(`[p2p] peer registered: ${name} @ ${ip}`);
582
+ console.error(`[mesh] peer registered: ${name} @ ${ip}`);
569
583
  this.deliverPendingAnswer(name, ws);
570
584
  }
571
585
  /** Connect outbound to a peer discovered via PEER_LIST or PEER_ANNOUNCE. */
@@ -590,12 +604,12 @@ var P2PNode = class {
590
604
  this.wsToName.delete(ws);
591
605
  if (this.peerConnections.get(peerName) === ws) {
592
606
  this.peerConnections.delete(peerName);
593
- console.error(`[p2p] disconnected from mesh peer: ${peerName}`);
607
+ console.error(`[mesh] disconnected from mesh peer: ${peerName}`);
594
608
  }
595
609
  }
596
610
  });
597
611
  ws.on("error", (err) => {
598
- console.error(`[p2p] mesh connect to "${name}" @ ${ip} failed: ${err.message}`);
612
+ console.error(`[mesh] connect to "${name}" @ ${ip} failed: ${err.message}`);
599
613
  this.connectingPeers.delete(name);
600
614
  });
601
615
  }
@@ -612,7 +626,7 @@ var P2PNode = class {
612
626
  }
613
627
  this.registerPeer(msg.name, remoteIp, ws);
614
628
  this.sendToWs(ws, { type: "HELLO_ACK", name: this.myName });
615
- console.error(`[p2p] peer joined (inbound): ${msg.name}`);
629
+ console.error(`[mesh] peer joined (inbound): ${msg.name}`);
616
630
  this.afterHandshake(msg.name, ws);
617
631
  return;
618
632
  }
@@ -627,7 +641,7 @@ var P2PNode = class {
627
641
  this.peerConnections.set(msg.name, ws);
628
642
  this.wsToName.set(ws, msg.name);
629
643
  this.connectingPeers.delete(msg.name);
630
- console.error(`[p2p] connected to mesh peer: ${msg.name}`);
644
+ console.error(`[mesh] connected to mesh peer: ${msg.name}`);
631
645
  this.deliverPendingAnswer(msg.name, ws);
632
646
  this.afterHandshake(msg.name, ws);
633
647
  }
@@ -635,7 +649,7 @@ var P2PNode = class {
635
649
  case "PEER_LIST":
636
650
  for (const peer of msg.peers) {
637
651
  if (peer.name !== this.myName && !this.peerConnections.has(peer.name)) {
638
- console.error(`[p2p] mesh: connecting to ${peer.name} @ ${peer.ip} via PEER_LIST`);
652
+ console.error(`[mesh] connecting to ${peer.name} @ ${peer.ip} via PEER_LIST`);
639
653
  this.peerIPs.set(peer.name, peer.ip);
640
654
  this.connectMeshPeer(peer.name, peer.ip);
641
655
  }
@@ -643,7 +657,7 @@ var P2PNode = class {
643
657
  break;
644
658
  case "PEER_ANNOUNCE":
645
659
  if (msg.name !== this.myName && !this.peerConnections.has(msg.name)) {
646
- console.error(`[p2p] mesh: connecting to ${msg.name} @ ${msg.ip} via PEER_ANNOUNCE`);
660
+ console.error(`[mesh] connecting to ${msg.name} @ ${msg.ip} via PEER_ANNOUNCE`);
647
661
  this.peerIPs.set(msg.name, msg.ip);
648
662
  this.connectMeshPeer(msg.name, msg.ip);
649
663
  }
@@ -695,7 +709,7 @@ var P2PNode = class {
695
709
  if (pending) {
696
710
  this.pendingOutboundAnswers.delete(peerName);
697
711
  this.sendToWs(ws, pending);
698
- console.error(`[p2p] delivered queued answer to "${peerName}" after reconnect`);
712
+ console.error(`[mesh] delivered queued answer to "${peerName}" after reconnect`);
699
713
  }
700
714
  }
701
715
  sendToWs(ws, msg) {
@@ -720,6 +734,34 @@ var P2PNode = class {
720
734
  });
721
735
  }
722
736
  };
737
+ async function killProcessOnPort(port) {
738
+ try {
739
+ if (process.platform === "win32") {
740
+ const out = execSync(
741
+ `netstat -ano | findstr ":${port} "`,
742
+ { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
743
+ );
744
+ const pids = /* @__PURE__ */ new Set();
745
+ for (const line of out.split("\n")) {
746
+ const parts = line.trim().split(/\s+/);
747
+ if (parts.length >= 5 && parts[3] === "LISTENING" && parts[4]) {
748
+ pids.add(parts[4]);
749
+ }
750
+ }
751
+ for (const pid of pids) {
752
+ try {
753
+ execSync(`taskkill /PID ${pid} /F`, { stdio: "ignore" });
754
+ console.error(`[mesh] killed PID ${pid} on port ${port}`);
755
+ } catch {
756
+ }
757
+ }
758
+ } else {
759
+ execSync(`fuser -k ${port}/tcp`, { stdio: "ignore" });
760
+ }
761
+ } catch {
762
+ }
763
+ await new Promise((resolve) => setTimeout(resolve, 300));
764
+ }
723
765
  var CONNECT_DESCRIPTION = `Connect to another Claude instance by their IP address.
724
766
 
725
767
  WHEN TO USE:
@@ -822,7 +864,7 @@ function registerAskTool(server, client) {
822
864
  return {
823
865
  content: [{
824
866
  type: "text",
825
- text: "P2P node is not ready yet. Wait a moment and try again."
867
+ text: "Node is not ready yet. Wait a moment and try again."
826
868
  }],
827
869
  isError: true
828
870
  };
@@ -903,7 +945,7 @@ function registerReplyTool(server, client) {
903
945
  return {
904
946
  content: [{
905
947
  type: "text",
906
- text: "P2P node is not ready yet. Wait a moment and try again."
948
+ text: "Node is not ready yet. Wait a moment and try again."
907
949
  }],
908
950
  isError: true
909
951
  };
@@ -930,68 +972,6 @@ function registerReplyTool(server, client) {
930
972
  });
931
973
  }
932
974
 
933
- // src/presentation/mcp/tools/peers.tool.ts
934
- var PEERS_DESCRIPTION = `List all currently active peer connections.
935
-
936
- WHEN TO USE:
937
- - Before calling ask() \u2014 to confirm the target peer is online and get their exact name
938
- - After connect() \u2014 to verify the connection succeeded and the mesh formed
939
- - When a peer seems unreachable \u2014 to check if they are still connected
940
-
941
- WHAT IT SHOWS:
942
- - Your own name and port
943
- - All peers with an active direct connection
944
-
945
- IF NO PEERS ARE LISTED:
946
- - Use connect(ip) to connect to a peer
947
- - Ask your teammate to run status() to get their IP`;
948
- function registerPeersTool(server, client) {
949
- server.tool("peers", PEERS_DESCRIPTION, {}, async () => {
950
- const info = client.getInfo();
951
- const myName = info.teamName ?? "(starting...)";
952
- const myPort = info.port ?? "?";
953
- const connected = info.connectedPeers;
954
- if (!client.isConnected) {
955
- return {
956
- content: [{
957
- type: "text",
958
- text: [
959
- `P2P server is not running yet (port ${myPort} may be in use).`,
960
- `Check the MCP process logs for the error details.`
961
- ].join("\n")
962
- }],
963
- isError: true
964
- };
965
- }
966
- if (connected.length === 0) {
967
- return {
968
- content: [{
969
- type: "text",
970
- text: [
971
- `You are "${myName}" (listening on port ${myPort}).`,
972
- `No peers connected yet.`,
973
- ``,
974
- `Use connect(ip) to connect to a peer.`,
975
- `Ask your teammate to run status() to get their IP.`
976
- ].join("\n")
977
- }]
978
- };
979
- }
980
- const peerIPs = info.peerIPs ?? {};
981
- const list = connected.map((name) => {
982
- const ip = peerIPs[name] ? ` (${peerIPs[name]})` : "";
983
- return ` \u2022 ${name}${ip}`;
984
- }).join("\n");
985
- return {
986
- content: [{
987
- type: "text",
988
- text: `You are "${myName}" (port ${myPort}). Connected peers (${connected.length}):
989
- ${list}`
990
- }]
991
- };
992
- });
993
- }
994
-
995
975
  // src/presentation/mcp/tools/history.tool.ts
996
976
  var HISTORY_DESCRIPTION = `Show all questions and answers exchanged this session \u2014 both sent and received.
997
977
 
@@ -1060,12 +1040,23 @@ function registerStatusTool(server, client) {
1060
1040
  };
1061
1041
  }
1062
1042
  let ips = [];
1063
- if (client instanceof P2PNode) {
1043
+ if (client instanceof MeshNode) {
1064
1044
  ips = client.getLocalIps();
1065
1045
  }
1066
1046
  const ipLine = ips.length > 0 ? ips.join(", ") : "(could not detect \u2014 check your network interface)";
1067
- const peerCount = info.connectedPeers.length;
1068
- const peerLine = peerCount === 0 ? "No peers connected yet. Share your IP so others can connect(ip)." : `Connected to ${peerCount} peer(s): ${info.connectedPeers.map((n) => `"${n}"`).join(", ")}`;
1047
+ const peerIPs = info.peerIPs ?? {};
1048
+ const connected = info.connectedPeers;
1049
+ let peersSection;
1050
+ if (connected.length === 0) {
1051
+ peersSection = "No peers connected yet. Share your IP so others can connect(ip).";
1052
+ } else {
1053
+ const list = connected.map((n) => {
1054
+ const ip = peerIPs[n] ? ` (${peerIPs[n]})` : "";
1055
+ return ` \u2022 ${n}${ip}`;
1056
+ }).join("\n");
1057
+ peersSection = `Connected peers (${connected.length}):
1058
+ ${list}`;
1059
+ }
1069
1060
  return {
1070
1061
  content: [{
1071
1062
  type: "text",
@@ -1074,7 +1065,7 @@ function registerStatusTool(server, client) {
1074
1065
  `IP: ${ipLine}`,
1075
1066
  `Port: ${port}`,
1076
1067
  ``,
1077
- peerLine
1068
+ peersSection
1078
1069
  ].join("\n")
1079
1070
  }]
1080
1071
  };
@@ -1092,7 +1083,6 @@ function createMcpServer(options) {
1092
1083
  registerStatusTool(server, client);
1093
1084
  registerAskTool(server, client);
1094
1085
  registerReplyTool(server, client);
1095
- registerPeersTool(server, client);
1096
1086
  registerHistoryTool(server, client);
1097
1087
  return server;
1098
1088
  }
@@ -1105,7 +1095,7 @@ async function startMcpServer(options) {
1105
1095
  // src/cli.ts
1106
1096
  var program = new Command();
1107
1097
  program.name("claude-collab").description("Collaboration between Claude Code terminals via MCP").version("0.1.0").requiredOption("--name <name>", 'Your name on the network (e.g. "alice")').action(async (options) => {
1108
- const node = new P2PNode();
1098
+ const node = new MeshNode();
1109
1099
  const mcpReady = startMcpServer({ client: node });
1110
1100
  node.join(options.name, options.name).catch((err) => {
1111
1101
  console.error(`[cli] Failed to start on port 12345: ${err.message}`);