@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 +63 -55
- package/dist/cli.js +74 -103
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +74 -103
- 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,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/
|
|
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/
|
|
267
|
+
// src/infrastructure/mesh/mesh-node.ts
|
|
268
|
+
var execAsync = promisify(exec);
|
|
267
269
|
var FIXED_PORT = 12345;
|
|
268
|
-
var
|
|
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(`[
|
|
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
|
|
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.
|
|
429
|
-
|
|
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()].
|
|
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(`[
|
|
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(`[
|
|
526
|
+
console.error(`[mesh] peer disconnected (inbound): ${name}`);
|
|
536
527
|
}
|
|
537
528
|
}
|
|
538
529
|
});
|
|
539
530
|
ws.on("error", (err) => {
|
|
540
|
-
console.error("[
|
|
531
|
+
console.error("[mesh] inbound ws error:", err.message);
|
|
541
532
|
});
|
|
542
533
|
});
|
|
543
534
|
wss.on("error", (err) => {
|
|
544
|
-
console.error("[
|
|
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(`[
|
|
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(`[
|
|
584
|
+
console.error(`[mesh] disconnected from mesh peer: ${peerName}`);
|
|
594
585
|
}
|
|
595
586
|
}
|
|
596
587
|
});
|
|
597
588
|
ws.on("error", (err) => {
|
|
598
|
-
console.error(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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,
|
|
698
|
-
console.error(`[
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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}`);
|