@dolusoft/claude-collab 1.11.1 → 1.11.3

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,8 @@ 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 { exec, spawn, execFile } from 'child_process';
7
+ import { promisify } from 'util';
7
8
  import { EventEmitter } from 'events';
8
9
  import { unlinkSync } from 'fs';
9
10
  import { join } from 'path';
@@ -11,7 +12,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
12
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
13
  import { z } from 'zod';
13
14
 
14
- // src/infrastructure/p2p/p2p-protocol.ts
15
+ // src/infrastructure/mesh/protocol.ts
15
16
  function serialize(msg) {
16
17
  return JSON.stringify(msg);
17
18
  }
@@ -263,9 +264,10 @@ var InjectionQueue = class extends EventEmitter {
263
264
  };
264
265
  var injectionQueue = new InjectionQueue();
265
266
 
266
- // src/infrastructure/p2p/p2p-node.ts
267
+ // src/infrastructure/mesh/mesh-node.ts
268
+ var execAsync = promisify(exec);
267
269
  var FIXED_PORT = 12345;
268
- var P2PNode = class {
270
+ var MeshNode = class {
269
271
  server = null;
270
272
  myName = "";
271
273
  running = false;
@@ -280,12 +282,11 @@ var P2PNode = class {
280
282
  connectingPeers = /* @__PURE__ */ new Set();
281
283
  incomingQuestions = /* @__PURE__ */ new Map();
282
284
  receivedAnswers = /* @__PURE__ */ new Map();
283
- sentQuestions = /* @__PURE__ */ new Map();
284
285
  questionToSender = /* @__PURE__ */ new Map();
285
286
  pendingHandlers = /* @__PURE__ */ new Set();
286
287
  // Push-based answer resolution: questionId → resolve callback
287
288
  answerWaiters = /* @__PURE__ */ new Map();
288
- // Answers queued for offline peers: peerName → AnswerMsg (delivered on reconnect)
289
+ // Answers queued for offline peers: peerName → AnswerMsg[] (delivered on reconnect)
289
290
  pendingOutboundAnswers = /* @__PURE__ */ new Map();
290
291
  // ---------------------------------------------------------------------------
291
292
  // ICollabClient implementation
@@ -346,19 +347,17 @@ var P2PNode = class {
346
347
  });
347
348
  ws.on("close", () => {
348
349
  clearTimeout(timeout);
349
- this.connectingPeers.delete(ip);
350
350
  const name = this.wsToName.get(ws);
351
351
  if (name) {
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
  });
359
359
  ws.on("error", (err) => {
360
360
  clearTimeout(timeout);
361
- this.connectingPeers.delete(ip);
362
361
  reject(new Error(`Failed to connect to ${ip}:${FIXED_PORT} \u2014 ${err.message}`));
363
362
  });
364
363
  });
@@ -366,10 +365,9 @@ var P2PNode = class {
366
365
  async ask(toPeer, content, format) {
367
366
  const ws = this.peerConnections.get(toPeer);
368
367
  if (!ws || ws.readyState !== WebSocket.OPEN) {
369
- throw new Error(`Peer "${toPeer}" is not connected. Use peers() to see who's online.`);
368
+ throw new Error(`Peer "${toPeer}" is not connected. Use status() to see who's online.`);
370
369
  }
371
370
  const questionId = v4();
372
- this.sentQuestions.set(questionId, { toPeer, content, askedAt: (/* @__PURE__ */ new Date()).toISOString() });
373
371
  const ackPromise = this.waitForResponse(
374
372
  (m) => m.type === "ASK_ACK" && m.questionId === questionId,
375
373
  5e3
@@ -412,6 +410,8 @@ var P2PNode = class {
412
410
  question.answerContent = content;
413
411
  question.answerFormat = format;
414
412
  const senderName = this.questionToSender.get(questionId);
413
+ this.incomingQuestions.delete(questionId);
414
+ this.questionToSender.delete(questionId);
415
415
  if (senderName) {
416
416
  const answerMsg = {
417
417
  type: "ANSWER",
@@ -425,15 +425,17 @@ var P2PNode = class {
425
425
  if (ws && ws.readyState === WebSocket.OPEN) {
426
426
  this.sendToWs(ws, answerMsg);
427
427
  } else {
428
- this.pendingOutboundAnswers.set(senderName, answerMsg);
429
- console.error(`[p2p] "${senderName}" is offline, answer queued for delivery on reconnect`);
428
+ const queue = this.pendingOutboundAnswers.get(senderName) ?? [];
429
+ queue.push(answerMsg);
430
+ this.pendingOutboundAnswers.set(senderName, queue);
431
+ console.error(`[mesh] "${senderName}" is offline, answer queued for delivery on reconnect`);
430
432
  }
431
433
  }
432
434
  injectionQueue.notifyReplied();
433
435
  }
434
436
  async getInbox() {
435
437
  const now = Date.now();
436
- const questions = [...this.incomingQuestions.values()].filter((q) => !q.answered).map((q) => ({
438
+ const questions = [...this.incomingQuestions.values()].map((q) => ({
437
439
  questionId: q.questionId,
438
440
  from: { displayName: `${q.fromName} Claude`, teamName: q.fromName },
439
441
  content: q.content,
@@ -463,31 +465,6 @@ var P2PNode = class {
463
465
  }
464
466
  return result;
465
467
  }
466
- getHistory() {
467
- const entries = [];
468
- for (const [questionId, sent] of this.sentQuestions) {
469
- const answer = this.receivedAnswers.get(questionId);
470
- entries.push({
471
- direction: "sent",
472
- questionId,
473
- peer: sent.toPeer,
474
- question: sent.content,
475
- askedAt: sent.askedAt,
476
- ...answer ? { answer: answer.content, answeredAt: answer.answeredAt } : {}
477
- });
478
- }
479
- for (const [, incoming] of this.incomingQuestions) {
480
- entries.push({
481
- direction: "received",
482
- questionId: incoming.questionId,
483
- peer: incoming.fromName,
484
- question: incoming.content,
485
- askedAt: incoming.createdAt.toISOString(),
486
- ...incoming.answered && incoming.answerContent ? { answer: incoming.answerContent, answeredAt: (/* @__PURE__ */ new Date()).toISOString() } : {}
487
- });
488
- }
489
- return entries.sort((a, b) => a.askedAt.localeCompare(b.askedAt));
490
- }
491
468
  async disconnect() {
492
469
  for (const ws of this.peerConnections.values()) ws.close();
493
470
  this.peerConnections.clear();
@@ -500,13 +477,27 @@ var P2PNode = class {
500
477
  // ---------------------------------------------------------------------------
501
478
  // Private: server startup
502
479
  // ---------------------------------------------------------------------------
503
- startServer() {
480
+ async startServer() {
481
+ try {
482
+ await this.tryBind();
483
+ } catch (err) {
484
+ const nodeErr = err;
485
+ if (nodeErr.code === "EADDRINUSE") {
486
+ console.error(`[mesh] port ${FIXED_PORT} in use \u2014 killing existing process`);
487
+ await killProcessOnPort(FIXED_PORT);
488
+ await this.tryBind();
489
+ } else {
490
+ throw err;
491
+ }
492
+ }
493
+ }
494
+ tryBind() {
504
495
  return new Promise((resolve, reject) => {
505
496
  const wss = new WebSocketServer({ port: FIXED_PORT });
506
497
  wss.once("listening", () => {
507
498
  this.server = wss;
508
499
  this.running = true;
509
- console.error(`[p2p] listening on port ${FIXED_PORT} as "${this.myName}"`);
500
+ console.error(`[mesh] listening on port ${FIXED_PORT} as "${this.myName}"`);
510
501
  this.attachServerHandlers(wss);
511
502
  resolve();
512
503
  });
@@ -532,16 +523,16 @@ var P2PNode = class {
532
523
  this.wsToName.delete(ws);
533
524
  if (this.peerConnections.get(name) === ws) {
534
525
  this.peerConnections.delete(name);
535
- console.error(`[p2p] peer disconnected (inbound): ${name}`);
526
+ console.error(`[mesh] peer disconnected (inbound): ${name}`);
536
527
  }
537
528
  }
538
529
  });
539
530
  ws.on("error", (err) => {
540
- console.error("[p2p] inbound ws error:", err.message);
531
+ console.error("[mesh] inbound ws error:", err.message);
541
532
  });
542
533
  });
543
534
  wss.on("error", (err) => {
544
- console.error("[p2p] server error:", err.message);
535
+ console.error("[mesh] server error:", err.message);
545
536
  });
546
537
  }
547
538
  // ---------------------------------------------------------------------------
@@ -565,7 +556,7 @@ var P2PNode = class {
565
556
  this.wsToName.set(ws, name);
566
557
  this.peerIPs.set(name, ip);
567
558
  this.connectingPeers.delete(name);
568
- console.error(`[p2p] peer registered: ${name} @ ${ip}`);
559
+ console.error(`[mesh] peer registered: ${name} @ ${ip}`);
569
560
  this.deliverPendingAnswer(name, ws);
570
561
  }
571
562
  /** Connect outbound to a peer discovered via PEER_LIST or PEER_ANNOUNCE. */
@@ -590,12 +581,12 @@ var P2PNode = class {
590
581
  this.wsToName.delete(ws);
591
582
  if (this.peerConnections.get(peerName) === ws) {
592
583
  this.peerConnections.delete(peerName);
593
- console.error(`[p2p] disconnected from mesh peer: ${peerName}`);
584
+ console.error(`[mesh] disconnected from mesh peer: ${peerName}`);
594
585
  }
595
586
  }
596
587
  });
597
588
  ws.on("error", (err) => {
598
- console.error(`[p2p] mesh connect to "${name}" @ ${ip} failed: ${err.message}`);
589
+ console.error(`[mesh] connect to "${name}" @ ${ip} failed: ${err.message}`);
599
590
  this.connectingPeers.delete(name);
600
591
  });
601
592
  }
@@ -604,7 +595,6 @@ var P2PNode = class {
604
595
  // ---------------------------------------------------------------------------
605
596
  /** Handles messages on inbound connections (server side — we know the remote IP). */
606
597
  handleInboundMessage(ws, remoteIp, msg) {
607
- for (const handler of this.pendingHandlers) handler(msg);
608
598
  if (msg.type === "HELLO") {
609
599
  if (this.peerConnections.has(msg.name)) {
610
600
  ws.terminate();
@@ -612,7 +602,7 @@ var P2PNode = class {
612
602
  }
613
603
  this.registerPeer(msg.name, remoteIp, ws);
614
604
  this.sendToWs(ws, { type: "HELLO_ACK", name: this.myName });
615
- console.error(`[p2p] peer joined (inbound): ${msg.name}`);
605
+ console.error(`[mesh] peer joined (inbound): ${msg.name}`);
616
606
  this.afterHandshake(msg.name, ws);
617
607
  return;
618
608
  }
@@ -627,7 +617,7 @@ var P2PNode = class {
627
617
  this.peerConnections.set(msg.name, ws);
628
618
  this.wsToName.set(ws, msg.name);
629
619
  this.connectingPeers.delete(msg.name);
630
- console.error(`[p2p] connected to mesh peer: ${msg.name}`);
620
+ console.error(`[mesh] connected to mesh peer: ${msg.name}`);
631
621
  this.deliverPendingAnswer(msg.name, ws);
632
622
  this.afterHandshake(msg.name, ws);
633
623
  }
@@ -635,7 +625,7 @@ var P2PNode = class {
635
625
  case "PEER_LIST":
636
626
  for (const peer of msg.peers) {
637
627
  if (peer.name !== this.myName && !this.peerConnections.has(peer.name)) {
638
- console.error(`[p2p] mesh: connecting to ${peer.name} @ ${peer.ip} via PEER_LIST`);
628
+ console.error(`[mesh] connecting to ${peer.name} @ ${peer.ip} via PEER_LIST`);
639
629
  this.peerIPs.set(peer.name, peer.ip);
640
630
  this.connectMeshPeer(peer.name, peer.ip);
641
631
  }
@@ -643,7 +633,7 @@ var P2PNode = class {
643
633
  break;
644
634
  case "PEER_ANNOUNCE":
645
635
  if (msg.name !== this.myName && !this.peerConnections.has(msg.name)) {
646
- console.error(`[p2p] mesh: connecting to ${msg.name} @ ${msg.ip} via PEER_ANNOUNCE`);
636
+ console.error(`[mesh] connecting to ${msg.name} @ ${msg.ip} via PEER_ANNOUNCE`);
647
637
  this.peerIPs.set(msg.name, msg.ip);
648
638
  this.connectMeshPeer(msg.name, msg.ip);
649
639
  }
@@ -692,10 +682,10 @@ var P2PNode = class {
692
682
  }
693
683
  deliverPendingAnswer(peerName, ws) {
694
684
  const pending = this.pendingOutboundAnswers.get(peerName);
695
- if (pending) {
685
+ if (pending && pending.length > 0) {
696
686
  this.pendingOutboundAnswers.delete(peerName);
697
- this.sendToWs(ws, pending);
698
- console.error(`[p2p] delivered queued answer to "${peerName}" after reconnect`);
687
+ for (const msg of pending) this.sendToWs(ws, msg);
688
+ console.error(`[mesh] delivered ${pending.length} queued answer(s) to "${peerName}" after reconnect`);
699
689
  }
700
690
  }
701
691
  sendToWs(ws, msg) {
@@ -720,6 +710,31 @@ var P2PNode = class {
720
710
  });
721
711
  }
722
712
  };
713
+ async function killProcessOnPort(port) {
714
+ try {
715
+ if (process.platform === "win32") {
716
+ const { stdout } = await execAsync(`netstat -ano | findstr ":${port} "`);
717
+ const pids = /* @__PURE__ */ new Set();
718
+ for (const line of stdout.split("\n")) {
719
+ const parts = line.trim().split(/\s+/);
720
+ if (parts.length >= 5 && parts[3] === "LISTENING" && parts[4]) {
721
+ pids.add(parts[4]);
722
+ }
723
+ }
724
+ for (const pid of pids) {
725
+ try {
726
+ await execAsync(`taskkill /PID ${pid} /F`);
727
+ console.error(`[mesh] killed PID ${pid} on port ${port}`);
728
+ } catch {
729
+ }
730
+ }
731
+ } else {
732
+ await execAsync(`fuser -k ${port}/tcp`);
733
+ }
734
+ } catch {
735
+ }
736
+ await new Promise((resolve) => setTimeout(resolve, 300));
737
+ }
723
738
  var CONNECT_DESCRIPTION = `Connect to another Claude instance by their IP address.
724
739
 
725
740
  WHEN TO USE:
@@ -822,7 +837,7 @@ function registerAskTool(server, client) {
822
837
  return {
823
838
  content: [{
824
839
  type: "text",
825
- text: "P2P node is not ready yet. Wait a moment and try again."
840
+ text: "Node is not ready yet. Wait a moment and try again."
826
841
  }],
827
842
  isError: true
828
843
  };
@@ -903,7 +918,7 @@ function registerReplyTool(server, client) {
903
918
  return {
904
919
  content: [{
905
920
  type: "text",
906
- text: "P2P node is not ready yet. Wait a moment and try again."
921
+ text: "Node is not ready yet. Wait a moment and try again."
907
922
  }],
908
923
  isError: true
909
924
  };
@@ -930,49 +945,6 @@ function registerReplyTool(server, client) {
930
945
  });
931
946
  }
932
947
 
933
- // src/presentation/mcp/tools/history.tool.ts
934
- var HISTORY_DESCRIPTION = `Show all questions and answers exchanged this session \u2014 both sent and received.
935
-
936
- WHEN TO USE:
937
- - To review what was already discussed before asking a follow-up question
938
- - To check if a previously sent question has been answered yet
939
- - To find a questionId you need to reference
940
- - To catch up on received questions you may have missed
941
-
942
- OUTPUT FORMAT:
943
- - \u2192 peer: question you sent, with their answer below
944
- - \u2190 peer: question they sent you, with your reply below
945
- - (no answer yet) means the question is still waiting for a response`;
946
- function registerHistoryTool(server, client) {
947
- server.tool("history", HISTORY_DESCRIPTION, {}, async () => {
948
- const entries = client.getHistory();
949
- if (entries.length === 0) {
950
- return {
951
- content: [{ type: "text", text: "No questions exchanged yet this session." }]
952
- };
953
- }
954
- const unanswered = entries.filter((e) => !e.answer);
955
- const lines = entries.map((e) => {
956
- const time = new Date(e.askedAt).toLocaleTimeString();
957
- if (e.direction === "sent") {
958
- const answerLine = e.answer ? ` \u21B3 ${e.peer}: ${e.answer}` : ` \u21B3 (no answer yet)`;
959
- return `[${time}] \u2192 ${e.peer}: ${e.question}
960
- ${answerLine}`;
961
- } else {
962
- const answerLine = e.answer ? ` \u21B3 you: ${e.answer}` : ` \u21B3 (not replied yet \u2014 use reply() if you haven't answered)`;
963
- return `[${time}] \u2190 ${e.peer}: ${e.question}
964
- ${answerLine}`;
965
- }
966
- });
967
- const summary = unanswered.length > 0 ? `
968
-
969
- \u26A0 ${unanswered.length} question(s) still waiting for a response.` : "";
970
- return {
971
- content: [{ type: "text", text: lines.join("\n\n") + summary }]
972
- };
973
- });
974
- }
975
-
976
948
  // src/presentation/mcp/tools/status.tool.ts
977
949
  var STATUS_DESCRIPTION = `Show your identity and network address on the collaboration network.
978
950
 
@@ -998,7 +970,7 @@ function registerStatusTool(server, client) {
998
970
  };
999
971
  }
1000
972
  let ips = [];
1001
- if (client instanceof P2PNode) {
973
+ if (client instanceof MeshNode) {
1002
974
  ips = client.getLocalIps();
1003
975
  }
1004
976
  const ipLine = ips.length > 0 ? ips.join(", ") : "(could not detect \u2014 check your network interface)";
@@ -1041,7 +1013,6 @@ function createMcpServer(options) {
1041
1013
  registerStatusTool(server, client);
1042
1014
  registerAskTool(server, client);
1043
1015
  registerReplyTool(server, client);
1044
- registerHistoryTool(server, client);
1045
1016
  return server;
1046
1017
  }
1047
1018
  async function startMcpServer(options) {
@@ -1053,7 +1024,7 @@ async function startMcpServer(options) {
1053
1024
  // src/cli.ts
1054
1025
  var program = new Command();
1055
1026
  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) => {
1056
- const node = new P2PNode();
1027
+ const node = new MeshNode();
1057
1028
  const mcpReady = startMcpServer({ client: node });
1058
1029
  node.join(options.name, options.name).catch((err) => {
1059
1030
  console.error(`[cli] Failed to start on port 12345: ${err.message}`);