@dolusoft/claude-collab 1.7.1 → 1.8.1

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,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 runs a lightweight hub server; everyone else connects to it no firewall rules needed for clients.
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
- 192.168.1.5:9999
15
+ (auto-discover)
16
16
  └─────────────────┘
17
17
  ```
18
18
 
19
19
  ## Setup
20
20
 
21
- ### Step 1 — Start the hub (one machine, one time)
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 --hub --port 9999
24
+ claude mcp add claude-collab -- npx -y @dolusoft/claude-collab --name <your-name>
27
25
  ```
28
26
 
29
- Keep this terminal open. The hub routes messages between everyone.
27
+ | Placeholder | What to put |
28
+ |-------------|-------------|
29
+ | `<your-name>` | Your identifier on the network (e.g. `alice`, `backend`, `frontend`) |
30
30
 
31
- ### Step 2 Add to Claude Code (each machine, one time)
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
- ```bash
34
- claude mcp add claude-collab -- npx -y @dolusoft/claude-collab --name <your-name> --server <hub-ip>:<hub-port>
37
+ ```
38
+ start_hub() # uses default port 9999
39
+ start_hub(port=8888) # custom port
35
40
  ```
36
41
 
37
- | Placeholder | What to put |
38
- |-------------|-------------|
39
- | `<your-name>` | Your identifier on the network (e.g. `alice`, `backend`, `frontend`) |
40
- | `<hub-ip>` | IP address of the machine running the hub (e.g. `192.168.1.5`) |
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
- > Your name and server address are saved permanently in Claude Code's MCP config. Every time Claude Code opens, it connects automatically — you never need to reconfigure.
47
+ Everyone else will auto-connect within seconds. No IP address sharing needed.
44
48
 
45
- ### With global install
49
+ ### Step 3 — Stop the hub when done
46
50
 
47
- ```bash
48
- npm install -g @dolusoft/claude-collab
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
- # Hub:
51
- claude-collab --hub --port <hub-port>
63
+ ```bash
64
+ # Start hub manually in a terminal:
65
+ npx @dolusoft/claude-collab --hub --port 9999
52
66
 
53
- # Client (add to Claude Code):
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
- await this.connectAndHello();
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();