@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 +63 -55
- package/dist/cli.js +80 -90
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +80 -90
- package/dist/mcp-main.js.map +1 -1
- package/package.json +1 -1
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
|
|
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
|
-
|
|
15
|
-
|
|
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 —
|
|
34
|
+
### Step 2 — Find your IP
|
|
31
35
|
|
|
32
|
-
Call `
|
|
36
|
+
Call `status()` in Claude Code:
|
|
33
37
|
|
|
34
38
|
```
|
|
35
|
-
|
|
39
|
+
status()
|
|
36
40
|
```
|
|
37
41
|
|
|
38
|
-
This
|
|
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
|
-
|
|
44
|
+
### Step 3 — Connect to a peer
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
Your teammate calls `connect()` with your IP:
|
|
46
47
|
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
| `
|
|
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
|
|
69
|
-
|
|
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
|
|
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
|
-
#
|
|
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:
|
|
110
|
-
Note over Alice Claude Code:
|
|
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
|
-
|
|
121
|
-
Note over Bob Claude Code: firewall
|
|
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
|
|
140
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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 peer → peer 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
|
-
- **
|
|
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
|
-
|
|
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
|
-
│ ├──
|
|
198
|
-
│ │
|
|
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
|
|
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
|
|
213
|
+
│ └── windows-injector.ts # Injects questions into Claude Code via WriteConsoleInput
|
|
206
214
|
└── presentation/
|
|
207
215
|
└── mcp/
|
|
208
|
-
├── server.ts
|
|
209
|
-
└── tools/
|
|
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/
|
|
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/
|
|
266
|
+
// src/infrastructure/mesh/mesh-node.ts
|
|
267
267
|
var FIXED_PORT = 12345;
|
|
268
|
-
var
|
|
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(`[
|
|
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
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
549
|
+
console.error(`[mesh] peer disconnected (inbound): ${name}`);
|
|
536
550
|
}
|
|
537
551
|
}
|
|
538
552
|
});
|
|
539
553
|
ws.on("error", (err) => {
|
|
540
|
-
console.error("[
|
|
554
|
+
console.error("[mesh] inbound ws error:", err.message);
|
|
541
555
|
});
|
|
542
556
|
});
|
|
543
557
|
wss.on("error", (err) => {
|
|
544
|
-
console.error("[
|
|
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(`[
|
|
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(`[
|
|
607
|
+
console.error(`[mesh] disconnected from mesh peer: ${peerName}`);
|
|
594
608
|
}
|
|
595
609
|
}
|
|
596
610
|
});
|
|
597
611
|
ws.on("error", (err) => {
|
|
598
|
-
console.error(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
1068
|
-
const
|
|
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
|
-
|
|
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
|
|
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}`);
|