@dolusoft/claude-collab 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -24
- package/dist/cli.js +193 -4
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +355 -12
- package/dist/mcp-main.js.map +1 -1
- package/package.json +81 -80
package/README.md
CHANGED
|
@@ -7,57 +7,73 @@ 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 with each other in real-time. One machine
|
|
10
|
+
Claude Collab lets multiple Claude Code terminals communicate with each other in real-time. One machine starts a hub server via MCP tool — others on the same LAN auto-discover and connect via mDNS. No manual IP sharing, no terminal commands.
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
alice ──outbound──→ ┌─────────────────┐ ←──outbound── bob
|
|
14
14
|
charlie─outbound──→ │ hub server │ ←──outbound── dave
|
|
15
|
-
│
|
|
15
|
+
│ (auto-discover) │
|
|
16
16
|
└─────────────────┘
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
## Setup
|
|
20
20
|
|
|
21
|
-
### Step 1 —
|
|
22
|
-
|
|
23
|
-
Pick any machine on your network. Run in a terminal:
|
|
21
|
+
### Step 1 — Add to Claude Code (each machine, one time)
|
|
24
22
|
|
|
25
23
|
```bash
|
|
26
|
-
npx @dolusoft/claude-collab --
|
|
24
|
+
claude mcp add claude-collab -- npx -y @dolusoft/claude-collab --name <your-name>
|
|
27
25
|
```
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
| Placeholder | What to put |
|
|
28
|
+
|-------------|-------------|
|
|
29
|
+
| `<your-name>` | Your identifier on the network (e.g. `alice`, `backend`, `frontend`) |
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
> Your name is saved permanently in Claude Code's MCP config. No server address needed — the hub is discovered automatically on the LAN.
|
|
32
|
+
|
|
33
|
+
### Step 2 — One person starts the hub (via MCP tool)
|
|
34
|
+
|
|
35
|
+
In Claude Code, call the `start_hub` tool:
|
|
32
36
|
|
|
33
|
-
```
|
|
34
|
-
|
|
37
|
+
```
|
|
38
|
+
start_hub() # uses default port 9999
|
|
39
|
+
start_hub(port=8888) # custom port
|
|
35
40
|
```
|
|
36
41
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
| `<hub-port>` | Port the hub is listening on (e.g. `9999`) |
|
|
42
|
+
This will:
|
|
43
|
+
1. Start the hub server on your machine
|
|
44
|
+
2. Show a **Windows UAC prompt** once — click **Yes** to open the firewall port
|
|
45
|
+
3. Advertise the hub on your LAN via mDNS
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
Everyone else will auto-connect within seconds. No IP address sharing needed.
|
|
44
48
|
|
|
45
|
-
###
|
|
49
|
+
### Step 3 — Stop the hub when done
|
|
46
50
|
|
|
47
|
-
```
|
|
48
|
-
|
|
51
|
+
```
|
|
52
|
+
stop_hub()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This stops the server and removes the firewall rule (UAC prompt shown once).
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
### Manual / advanced mode
|
|
60
|
+
|
|
61
|
+
If you prefer to manage the hub yourself (e.g. on a server), you can still use the legacy approach:
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
```bash
|
|
64
|
+
# Start hub manually in a terminal:
|
|
65
|
+
npx @dolusoft/claude-collab --hub --port 9999
|
|
52
66
|
|
|
53
|
-
#
|
|
54
|
-
claude mcp add claude-collab -- claude-collab --name <your-name> --server <hub-ip>:<hub-port>
|
|
67
|
+
# Add to Claude Code with explicit server address:
|
|
68
|
+
claude mcp add claude-collab -- npx -y @dolusoft/claude-collab --name <your-name> --server <hub-ip>:<hub-port>
|
|
55
69
|
```
|
|
56
70
|
|
|
57
71
|
## MCP Tools
|
|
58
72
|
|
|
59
73
|
| Tool | Description |
|
|
60
74
|
|------|-------------|
|
|
75
|
+
| `start_hub` | Start a hub server on this machine. Opens firewall via UAC, advertises on LAN via mDNS. |
|
|
76
|
+
| `stop_hub` | Stop the running hub. Removes firewall rule via UAC. |
|
|
61
77
|
| `ask` | Ask another peer a question by name. Waits up to 5 minutes for a response. |
|
|
62
78
|
| `reply` | Reply to an incoming question (called automatically by Claude when a question arrives). |
|
|
63
79
|
| `peers` | Show currently connected peers and your own name. |
|
|
@@ -83,10 +99,14 @@ src/
|
|
|
83
99
|
│ ├── hub/
|
|
84
100
|
│ │ ├── hub-server.ts # WebSocket hub — routes messages by name
|
|
85
101
|
│ │ ├── hub-client.ts # Connects to hub, implements ICollabClient
|
|
102
|
+
│ │ ├── hub-manager.ts # Orchestrates hub + firewall + mDNS advertising
|
|
86
103
|
│ │ └── hub-protocol.ts # Wire protocol types
|
|
104
|
+
│ ├── mdns/
|
|
105
|
+
│ │ ├── mdns-advertiser.ts # Broadcasts hub presence on LAN via mDNS
|
|
106
|
+
│ │ └── mdns-discovery.ts # Discovers hub on LAN via mDNS
|
|
87
107
|
│ └── terminal-injector/ # Injects incoming questions into Claude Code
|
|
88
108
|
└── presentation/
|
|
89
|
-
└── mcp/ # MCP server + tools (ask, reply, peers, history)
|
|
109
|
+
└── mcp/ # MCP server + tools (start_hub, stop_hub, ask, reply, peers, history)
|
|
90
110
|
```
|
|
91
111
|
|
|
92
112
|
## Development
|
package/dist/cli.js
CHANGED
|
@@ -3,10 +3,11 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
4
4
|
import { v4 } from 'uuid';
|
|
5
5
|
import { EventEmitter } from 'events';
|
|
6
|
-
import { execFile } from 'child_process';
|
|
6
|
+
import { execFile, spawn } from 'child_process';
|
|
7
7
|
import { unlinkSync } from 'fs';
|
|
8
8
|
import { tmpdir } from 'os';
|
|
9
9
|
import { join } from 'path';
|
|
10
|
+
import { Bonjour } from 'bonjour-service';
|
|
10
11
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
12
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
13
|
import { z } from 'zod';
|
|
@@ -57,6 +58,15 @@ var HubServer = class {
|
|
|
57
58
|
console.error("[hub] server error:", err.message);
|
|
58
59
|
});
|
|
59
60
|
}
|
|
61
|
+
stop() {
|
|
62
|
+
if (!this.wss) return;
|
|
63
|
+
for (const ws of this.clients.values()) ws.terminate();
|
|
64
|
+
this.clients.clear();
|
|
65
|
+
this.wsToName.clear();
|
|
66
|
+
this.wss.close();
|
|
67
|
+
this.wss = null;
|
|
68
|
+
console.log("claude-collab hub stopped");
|
|
69
|
+
}
|
|
60
70
|
handleMessage(ws, msg) {
|
|
61
71
|
switch (msg.type) {
|
|
62
72
|
case "HELLO": {
|
|
@@ -363,7 +373,9 @@ var HubClient = class {
|
|
|
363
373
|
}
|
|
364
374
|
async join(name, displayName) {
|
|
365
375
|
this.myName = name;
|
|
366
|
-
|
|
376
|
+
if (this.serverUrl) {
|
|
377
|
+
await this.connectAndHello();
|
|
378
|
+
}
|
|
367
379
|
return {
|
|
368
380
|
memberId: v4(),
|
|
369
381
|
teamId: name,
|
|
@@ -373,6 +385,10 @@ var HubClient = class {
|
|
|
373
385
|
port: 0
|
|
374
386
|
};
|
|
375
387
|
}
|
|
388
|
+
async connectToHub(url) {
|
|
389
|
+
this.serverUrl = url;
|
|
390
|
+
await this.connectAndHello();
|
|
391
|
+
}
|
|
376
392
|
async ask(toPeer, content, format) {
|
|
377
393
|
const questionId = v4();
|
|
378
394
|
const requestId = v4();
|
|
@@ -653,6 +669,85 @@ var HubClient = class {
|
|
|
653
669
|
});
|
|
654
670
|
}
|
|
655
671
|
};
|
|
672
|
+
var SERVICE_TYPE = "claude-collab";
|
|
673
|
+
var MdnsAdvertiser = class {
|
|
674
|
+
bonjour = null;
|
|
675
|
+
start(port) {
|
|
676
|
+
this.bonjour = new Bonjour();
|
|
677
|
+
this.bonjour.publish({ name: "claude-collab-hub", type: SERVICE_TYPE, port });
|
|
678
|
+
console.error(`[mdns] advertising hub on port ${port}`);
|
|
679
|
+
}
|
|
680
|
+
stop() {
|
|
681
|
+
if (!this.bonjour) return;
|
|
682
|
+
this.bonjour.unpublishAll(() => {
|
|
683
|
+
this.bonjour?.destroy();
|
|
684
|
+
});
|
|
685
|
+
this.bonjour = null;
|
|
686
|
+
console.error("[mdns] stopped advertising");
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// src/infrastructure/hub/hub-manager.ts
|
|
691
|
+
var HubManager = class {
|
|
692
|
+
hubServer = null;
|
|
693
|
+
advertiser = null;
|
|
694
|
+
currentPort = null;
|
|
695
|
+
get isRunning() {
|
|
696
|
+
return this.hubServer !== null;
|
|
697
|
+
}
|
|
698
|
+
get port() {
|
|
699
|
+
return this.currentPort;
|
|
700
|
+
}
|
|
701
|
+
async start(port) {
|
|
702
|
+
if (this.isRunning) throw new Error("Hub is already running");
|
|
703
|
+
const server = new HubServer();
|
|
704
|
+
server.start(port);
|
|
705
|
+
this.hubServer = server;
|
|
706
|
+
this.currentPort = port;
|
|
707
|
+
await addFirewallRule(port);
|
|
708
|
+
const advertiser = new MdnsAdvertiser();
|
|
709
|
+
advertiser.start(port);
|
|
710
|
+
this.advertiser = advertiser;
|
|
711
|
+
}
|
|
712
|
+
async stop() {
|
|
713
|
+
if (!this.isRunning) throw new Error("Hub is not running");
|
|
714
|
+
if (this.advertiser) {
|
|
715
|
+
this.advertiser.stop();
|
|
716
|
+
this.advertiser = null;
|
|
717
|
+
}
|
|
718
|
+
const port = this.currentPort;
|
|
719
|
+
this.hubServer.stop();
|
|
720
|
+
this.hubServer = null;
|
|
721
|
+
this.currentPort = null;
|
|
722
|
+
await removeFirewallRule(port);
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
function runElevated(netshArgs) {
|
|
726
|
+
return new Promise((resolve, reject) => {
|
|
727
|
+
const ps = spawn("powershell", [
|
|
728
|
+
"-NoProfile",
|
|
729
|
+
"-Command",
|
|
730
|
+
`Start-Process -FilePath "netsh" -ArgumentList "${netshArgs}" -Verb RunAs -Wait`
|
|
731
|
+
]);
|
|
732
|
+
ps.on("close", (code) => {
|
|
733
|
+
if (code === 0) resolve();
|
|
734
|
+
else reject(new Error(`Firewall command failed (exit code ${code}). User may have cancelled the UAC prompt.`));
|
|
735
|
+
});
|
|
736
|
+
ps.on("error", (err) => {
|
|
737
|
+
reject(new Error(`Failed to launch PowerShell for firewall elevation: ${err.message}`));
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
async function addFirewallRule(port) {
|
|
742
|
+
await runElevated(
|
|
743
|
+
`advfirewall firewall add rule name="claude-collab-${port}" protocol=TCP dir=in localport=${port} action=allow`
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
async function removeFirewallRule(port) {
|
|
747
|
+
await runElevated(
|
|
748
|
+
`advfirewall firewall delete rule name="claude-collab-${port}"`
|
|
749
|
+
);
|
|
750
|
+
}
|
|
656
751
|
var askSchema = {
|
|
657
752
|
peer: z.string().describe('Name of the peer to ask (e.g., "alice", "backend")'),
|
|
658
753
|
question: z.string().describe("The question to ask (supports markdown)")
|
|
@@ -814,10 +909,101 @@ ${answerLine}`;
|
|
|
814
909
|
};
|
|
815
910
|
});
|
|
816
911
|
}
|
|
912
|
+
function registerStartHubTool(server, client, hubManager) {
|
|
913
|
+
server.tool(
|
|
914
|
+
"start_hub",
|
|
915
|
+
"Start a hub server on this machine. Opens a firewall rule (UAC prompt) and advertises on LAN via mDNS \u2014 peers connect automatically, no IP sharing needed.",
|
|
916
|
+
{ port: z.number().min(1024).max(65535).optional().describe("Port to listen on (default: 9999)") },
|
|
917
|
+
async ({ port = 9999 }) => {
|
|
918
|
+
if (hubManager.isRunning) {
|
|
919
|
+
return {
|
|
920
|
+
content: [{
|
|
921
|
+
type: "text",
|
|
922
|
+
text: `Hub is already running on port ${hubManager.port}.`
|
|
923
|
+
}]
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
try {
|
|
927
|
+
await hubManager.start(port);
|
|
928
|
+
} catch (err) {
|
|
929
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
930
|
+
return {
|
|
931
|
+
content: [{
|
|
932
|
+
type: "text",
|
|
933
|
+
text: `Failed to start hub: ${msg}`
|
|
934
|
+
}]
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
try {
|
|
938
|
+
await client.connectToHub(`ws://localhost:${port}`);
|
|
939
|
+
} catch (err) {
|
|
940
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
941
|
+
return {
|
|
942
|
+
content: [{
|
|
943
|
+
type: "text",
|
|
944
|
+
text: `Hub started on port ${port}, but failed to self-connect: ${msg}`
|
|
945
|
+
}]
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
content: [{
|
|
950
|
+
type: "text",
|
|
951
|
+
text: [
|
|
952
|
+
`Hub started on port ${port}.`,
|
|
953
|
+
`Firewall rule added (claude-collab-${port}).`,
|
|
954
|
+
`Others on the LAN will auto-discover and connect \u2014 no IP sharing needed.`,
|
|
955
|
+
`Use stop_hub when you are done to close the hub and remove the firewall rule.`
|
|
956
|
+
].join("\n")
|
|
957
|
+
}]
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/presentation/mcp/tools/stop-hub.tool.ts
|
|
964
|
+
function registerStopHubTool(server, hubManager) {
|
|
965
|
+
server.tool(
|
|
966
|
+
"stop_hub",
|
|
967
|
+
"Stop the running hub server. Removes the firewall rule (UAC prompt) and stops LAN advertising. Connected peers will be disconnected.",
|
|
968
|
+
{},
|
|
969
|
+
async () => {
|
|
970
|
+
if (!hubManager.isRunning) {
|
|
971
|
+
return {
|
|
972
|
+
content: [{
|
|
973
|
+
type: "text",
|
|
974
|
+
text: "No hub is currently running on this machine."
|
|
975
|
+
}]
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
const port = hubManager.port;
|
|
979
|
+
try {
|
|
980
|
+
await hubManager.stop();
|
|
981
|
+
} catch (err) {
|
|
982
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
983
|
+
return {
|
|
984
|
+
content: [{
|
|
985
|
+
type: "text",
|
|
986
|
+
text: `Failed to stop hub: ${msg}`
|
|
987
|
+
}]
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
content: [{
|
|
992
|
+
type: "text",
|
|
993
|
+
text: [
|
|
994
|
+
`Hub stopped (was on port ${port}).`,
|
|
995
|
+
`Firewall rule removed (claude-collab-${port}).`,
|
|
996
|
+
`All peers have been disconnected.`
|
|
997
|
+
].join("\n")
|
|
998
|
+
}]
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
817
1003
|
|
|
818
1004
|
// src/presentation/mcp/server.ts
|
|
819
1005
|
function createMcpServer(options) {
|
|
820
|
-
const { client } = options;
|
|
1006
|
+
const { client, hubManager } = options;
|
|
821
1007
|
const server = new McpServer({
|
|
822
1008
|
name: "claude-collab",
|
|
823
1009
|
version: "0.1.0"
|
|
@@ -826,6 +1012,8 @@ function createMcpServer(options) {
|
|
|
826
1012
|
registerReplyTool(server, client);
|
|
827
1013
|
registerPeersTool(server, client);
|
|
828
1014
|
registerHistoryTool(server, client);
|
|
1015
|
+
registerStartHubTool(server, client, hubManager);
|
|
1016
|
+
registerStopHubTool(server, hubManager);
|
|
829
1017
|
return server;
|
|
830
1018
|
}
|
|
831
1019
|
async function startMcpServer(options) {
|
|
@@ -860,9 +1048,10 @@ program.name("claude-collab").description("P2P collaboration between Claude Code
|
|
|
860
1048
|
}
|
|
861
1049
|
const url = options.server.startsWith("ws") ? options.server : `ws://${options.server}`;
|
|
862
1050
|
const client = new HubClient();
|
|
1051
|
+
const hubManager = new HubManager();
|
|
863
1052
|
client.setServerUrl(url);
|
|
864
1053
|
await client.join(options.name, options.name);
|
|
865
|
-
await startMcpServer({ client });
|
|
1054
|
+
await startMcpServer({ client, hubManager });
|
|
866
1055
|
}
|
|
867
1056
|
});
|
|
868
1057
|
program.parse();
|