@dolusoft/claude-collab 1.3.1 → 1.4.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/dist/cli.js CHANGED
@@ -1,170 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
3
  import { WebSocketServer, WebSocket } from 'ws';
4
- import { createServer } from 'net';
5
4
  import { v4 } from 'uuid';
6
- import multicastDns from 'multicast-dns';
7
- import { networkInterfaces, tmpdir } from 'os';
8
5
  import { EventEmitter } from 'events';
9
6
  import { execFile } from 'child_process';
10
7
  import { unlinkSync } from 'fs';
8
+ import { tmpdir } from 'os';
11
9
  import { join } from 'path';
12
10
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
12
  import { z } from 'zod';
15
13
 
16
- function getLocalIp() {
17
- const nets = networkInterfaces();
18
- for (const name of Object.keys(nets)) {
19
- for (const net of nets[name] ?? []) {
20
- if (net.family === "IPv4" && !net.internal) {
21
- return net.address;
22
- }
23
- }
24
- }
25
- return "127.0.0.1";
26
- }
27
- var SERVICE_TYPE = "_claude-collab._tcp.local";
28
- var MdnsDiscovery = class {
29
- mdns;
30
- announced = false;
31
- port = 0;
32
- teamName = "";
33
- memberId = "";
34
- peersByTeam = /* @__PURE__ */ new Map();
35
- peersByMemberId = /* @__PURE__ */ new Map();
36
- onPeerFoundCb;
37
- onPeerLostCb;
38
- constructor() {
39
- this.mdns = multicastDns();
40
- this.setupHandlers();
41
- }
42
- get serviceName() {
43
- return `${this.memberId}.${SERVICE_TYPE}`;
44
- }
45
- buildAnswers() {
46
- return [
47
- {
48
- name: SERVICE_TYPE,
49
- type: "PTR",
50
- ttl: 300,
51
- data: this.serviceName
52
- },
53
- {
54
- name: this.serviceName,
55
- type: "SRV",
56
- ttl: 300,
57
- data: {
58
- priority: 0,
59
- weight: 0,
60
- port: this.port,
61
- target: getLocalIp()
62
- }
63
- },
64
- {
65
- name: this.serviceName,
66
- type: "TXT",
67
- ttl: 300,
68
- data: [
69
- Buffer.from(`team=${this.teamName}`),
70
- Buffer.from(`memberId=${this.memberId}`),
71
- Buffer.from("ver=1")
72
- ]
73
- }
74
- ];
75
- }
76
- setupHandlers() {
77
- this.mdns.on("query", (query) => {
78
- if (!this.announced) return;
79
- const questions = query.questions ?? [];
80
- const ptrQuery = questions.find(
81
- (q) => q.type === "PTR" && q.name === SERVICE_TYPE
82
- );
83
- if (!ptrQuery) return;
84
- this.mdns.respond({ answers: this.buildAnswers() });
85
- });
86
- this.mdns.on("response", (response) => {
87
- this.parseResponse(response);
88
- });
89
- }
90
- parseResponse(response) {
91
- const allRecords = [
92
- ...response.answers ?? [],
93
- ...response.additionals ?? []
94
- ];
95
- const ptrRecords = allRecords.filter(
96
- (r) => r.type === "PTR" && r.name === SERVICE_TYPE
97
- );
98
- for (const ptr of ptrRecords) {
99
- const instanceName = ptr.data;
100
- const srv = allRecords.find(
101
- (r) => r.type === "SRV" && r.name === instanceName
102
- );
103
- const txt = allRecords.find(
104
- (r) => r.type === "TXT" && r.name === instanceName
105
- );
106
- if (!srv) continue;
107
- const port = srv.data.port;
108
- const host = srv.data.target || "127.0.0.1";
109
- let teamName = "";
110
- let memberId = "";
111
- if (txt) {
112
- const txtData = txt.data ?? [];
113
- for (const entry of txtData) {
114
- const str = Buffer.isBuffer(entry) ? entry.toString() : String(entry);
115
- if (str.startsWith("team=")) teamName = str.slice(5);
116
- if (str.startsWith("memberId=")) memberId = str.slice(9);
117
- }
118
- }
119
- if (!teamName || !memberId) continue;
120
- if (memberId === this.memberId) continue;
121
- const ptrTtl = ptr.ttl ?? 300;
122
- const srvTtl = srv.ttl ?? 300;
123
- if (ptrTtl === 0 || srvTtl === 0) {
124
- this.peersByTeam.delete(teamName);
125
- this.peersByMemberId.delete(memberId);
126
- this.onPeerLostCb?.(memberId);
127
- continue;
128
- }
129
- const peer = { host, port, teamName, memberId };
130
- this.peersByTeam.set(teamName, peer);
131
- this.peersByMemberId.set(memberId, peer);
132
- this.onPeerFoundCb?.(peer);
133
- }
134
- }
135
- /**
136
- * Announce this node's service via mDNS.
137
- * Sends an unsolicited response so existing peers notice immediately.
138
- */
139
- announce(port, teamName, memberId) {
140
- this.port = port;
141
- this.teamName = teamName;
142
- this.memberId = memberId;
143
- this.announced = true;
144
- this.mdns.respond({ answers: this.buildAnswers() });
145
- }
146
- /**
147
- * Send a PTR query to discover existing peers.
148
- */
149
- discover() {
150
- this.mdns.query({
151
- questions: [{ name: SERVICE_TYPE, type: "PTR" }]
152
- });
153
- }
154
- getPeerByTeam(teamName) {
155
- return this.peersByTeam.get(teamName);
156
- }
157
- onPeerFound(cb) {
158
- this.onPeerFoundCb = cb;
159
- }
160
- onPeerLost(cb) {
161
- this.onPeerLostCb = cb;
162
- }
163
- destroy() {
164
- this.mdns.destroy();
165
- }
166
- };
167
-
168
14
  // src/infrastructure/p2p/p2p-message-protocol.ts
169
15
  function serializeP2PMsg(msg) {
170
16
  return JSON.stringify(msg);
@@ -180,8 +26,6 @@ using System.Runtime.InteropServices;
180
26
  public class ConInject {
181
27
  [DllImport("kernel32.dll")] public static extern bool FreeConsole();
182
28
  [DllImport("kernel32.dll")] public static extern bool AttachConsole(uint pid);
183
- [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow();
184
- [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hwnd);
185
29
  [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
186
30
  public static extern IntPtr CreateFile(
187
31
  string lpFileName, uint dwDesiredAccess, uint dwShareMode,
@@ -210,7 +54,7 @@ public class ConInject {
210
54
  return CreateFile("CONIN$", 0xC0000000, 3, IntPtr.Zero, 3, 0, IntPtr.Zero);
211
55
  }
212
56
 
213
- // Inject only text characters into console input buffer (no Ctrl keys, no Enter)
57
+ // Inject plain text characters into console input buffer
214
58
  public static int InjectText(uint pid, string text) {
215
59
  IntPtr hIn = OpenConin(pid);
216
60
  if (hIn == new IntPtr(-1)) return -1;
@@ -228,13 +72,56 @@ public class ConInject {
228
72
  return ok ? (int)written : -2;
229
73
  }
230
74
 
231
- // Focus the console window for WScript.Shell.SendKeys
232
- public static IntPtr FocusConsole(uint pid) {
233
- FreeConsole();
234
- if (!AttachConsole(pid)) return IntPtr.Zero;
235
- IntPtr hwnd = GetConsoleWindow();
236
- if (hwnd != IntPtr.Zero) SetForegroundWindow(hwnd);
237
- return hwnd;
75
+ // Inject Enter (VK_RETURN = 0x0D)
76
+ public static int InjectEnter(uint pid) {
77
+ IntPtr hIn = OpenConin(pid);
78
+ if (hIn == new IntPtr(-1)) return -1;
79
+
80
+ var records = new INPUT_RECORD[] {
81
+ new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, wVirtualKeyCode=0x0D, UnicodeChar=0x0D },
82
+ new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, wVirtualKeyCode=0x0D, UnicodeChar=0x0D }
83
+ };
84
+
85
+ uint written;
86
+ bool ok = WriteConsoleInput(hIn, records, (uint)records.Length, out written);
87
+ CloseHandle(hIn);
88
+ return ok ? (int)written : -2;
89
+ }
90
+
91
+ // Inject Ctrl+U (VK_U = 0x55, char = 0x15)
92
+ public static int InjectCtrlU(uint pid) {
93
+ IntPtr hIn = OpenConin(pid);
94
+ if (hIn == new IntPtr(-1)) return -1;
95
+
96
+ var records = new INPUT_RECORD[] {
97
+ new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, wVirtualKeyCode=0xA2, dwControlKeyState=LEFT_CTRL },
98
+ new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, wVirtualKeyCode=0x55, UnicodeChar=0x15, dwControlKeyState=LEFT_CTRL },
99
+ new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, wVirtualKeyCode=0x55, UnicodeChar=0x15, dwControlKeyState=LEFT_CTRL },
100
+ new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, wVirtualKeyCode=0xA2, dwControlKeyState=0 }
101
+ };
102
+
103
+ uint written;
104
+ bool ok = WriteConsoleInput(hIn, records, (uint)records.Length, out written);
105
+ CloseHandle(hIn);
106
+ return ok ? (int)written : -2;
107
+ }
108
+
109
+ // Inject Ctrl+Y (VK_Y = 0x59, char = 0x19)
110
+ public static int InjectCtrlY(uint pid) {
111
+ IntPtr hIn = OpenConin(pid);
112
+ if (hIn == new IntPtr(-1)) return -1;
113
+
114
+ var records = new INPUT_RECORD[] {
115
+ new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, wVirtualKeyCode=0xA2, dwControlKeyState=LEFT_CTRL },
116
+ new INPUT_RECORD { EventType=1, bKeyDown=1, wRepeatCount=1, wVirtualKeyCode=0x59, UnicodeChar=0x19, dwControlKeyState=LEFT_CTRL },
117
+ new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, wVirtualKeyCode=0x59, UnicodeChar=0x19, dwControlKeyState=LEFT_CTRL },
118
+ new INPUT_RECORD { EventType=1, bKeyDown=0, wRepeatCount=1, wVirtualKeyCode=0xA2, dwControlKeyState=0 }
119
+ };
120
+
121
+ uint written;
122
+ bool ok = WriteConsoleInput(hIn, records, (uint)records.Length, out written);
123
+ CloseHandle(hIn);
124
+ return ok ? (int)written : -2;
238
125
  }
239
126
  }
240
127
  `;
@@ -272,33 +159,24 @@ async function windowsInject(text) {
272
159
  const script = buildScript(claudePid, `
273
160
  $textBytes = [System.Convert]::FromBase64String('${textB64}')
274
161
  $text = [System.Text.Encoding]::Unicode.GetString($textBytes)
275
- $wsh = New-Object -ComObject WScript.Shell
276
162
 
277
- # 1. Focus console and send Ctrl+U to save user's current text to kill ring
278
- $hwnd = [ConInject]::FocusConsole([uint32]$claudePid)
279
- Start-Sleep -Milliseconds 150
280
- $wsh.SendKeys('^u')
281
- Start-Sleep -Milliseconds 150
163
+ # 1. Ctrl+U to save user's current text to kill ring
164
+ [ConInject]::InjectCtrlU([uint32]$claudePid) | Out-Null
165
+ Start-Sleep -Milliseconds 100
282
166
 
283
167
  # 2. Write question text into console input buffer
284
168
  [ConInject]::InjectText([uint32]$claudePid, $text) | Out-Null
169
+ Start-Sleep -Milliseconds 50
285
170
 
286
- # 3. Re-focus (InjectText calls FreeConsole internally, focus may be lost)
287
- [ConInject]::FocusConsole([uint32]$claudePid) | Out-Null
288
- Start-Sleep -Milliseconds 150
289
-
290
- # 4. Send Enter
291
- $wsh.SendKeys('~')
171
+ # 3. Send Enter
172
+ [ConInject]::InjectEnter([uint32]$claudePid) | Out-Null
292
173
  `);
293
174
  await run(script);
294
175
  }
295
176
  async function windowsInjectCtrlY() {
296
177
  const claudePid = process.ppid;
297
178
  const script = buildScript(claudePid, `
298
- $wsh = New-Object -ComObject WScript.Shell
299
- $hwnd = [ConInject]::FocusConsole([uint32]$claudePid)
300
- Start-Sleep -Milliseconds 150
301
- $wsh.SendKeys('^y')
179
+ [ConInject]::InjectCtrlY([uint32]$claudePid) | Out-Null
302
180
  `);
303
181
  await run(script);
304
182
  }
@@ -365,35 +243,18 @@ var config = {
365
243
  */
366
244
  p2p: {
367
245
  /**
368
- * Minimum port for the random WS server port range
369
- */
370
- portRangeMin: 1e4,
371
- /**
372
- * Maximum port for the random WS server port range
246
+ * Fixed port for the WS server. Override with CLAUDE_COLLAB_PORT env var.
373
247
  */
374
- portRangeMax: 19999
248
+ port: Number(process.env["CLAUDE_COLLAB_PORT"] ?? 11777)
375
249
  }};
376
250
 
377
251
  // src/infrastructure/p2p/p2p-node.ts
378
- function getRandomPort(min, max) {
379
- return new Promise((resolve) => {
380
- const port = Math.floor(Math.random() * (max - min + 1)) + min;
381
- const server = createServer();
382
- server.listen(port, () => {
383
- server.close(() => resolve(port));
384
- });
385
- server.on("error", () => {
386
- resolve(getRandomPort(min, max));
387
- });
388
- });
389
- }
390
252
  var P2PNode = class {
391
253
  wss = null;
392
- mdnsDiscovery = null;
393
254
  port = 0;
394
255
  // Connections indexed by remote team name
395
256
  peerConns = /* @__PURE__ */ new Map();
396
- // Reverse lookup: ws → teamName (for cleanup on incoming connections)
257
+ // Reverse lookup: ws → teamName (for cleanup)
397
258
  wsToTeam = /* @__PURE__ */ new Map();
398
259
  // Questions we received from remote peers (our inbox)
399
260
  incomingQuestions = /* @__PURE__ */ new Map();
@@ -412,14 +273,13 @@ var P2PNode = class {
412
273
  return this.localMember?.teamName;
413
274
  }
414
275
  /**
415
- * Starts the WS server on a random port and initialises mDNS.
276
+ * Starts the WS server on the configured fixed port.
416
277
  * Called automatically from join() if not yet started.
417
278
  */
418
279
  async start() {
419
- this.port = await getRandomPort(config.p2p.portRangeMin, config.p2p.portRangeMax);
280
+ this.port = config.p2p.port;
420
281
  this.wss = new WebSocketServer({ port: this.port });
421
282
  this.setupWssHandlers();
422
- this.mdnsDiscovery = new MdnsDiscovery();
423
283
  this._isStarted = true;
424
284
  console.error(`P2P node started on port ${this.port}`);
425
285
  }
@@ -429,17 +289,60 @@ var P2PNode = class {
429
289
  }
430
290
  const memberId = v4();
431
291
  this.localMember = { memberId, teamName, displayName };
432
- this.mdnsDiscovery.onPeerFound((peer) => {
433
- if (peer.teamName !== teamName) {
434
- console.error(`Discovered peer '${peer.teamName}' at ${peer.host}:${peer.port}`);
435
- this.connectToPeer(peer.teamName, peer.host, peer.port).catch((err) => {
436
- console.error(`Could not eagerly connect to ${peer.teamName}:`, err);
437
- });
292
+ return { memberId, teamId: teamName, teamName, displayName, status: "ONLINE" };
293
+ }
294
+ /**
295
+ * Connects to a peer at the given IP and port.
296
+ * Performs a bidirectional HELLO handshake and returns the peer's team name.
297
+ */
298
+ async connectPeer(ip, port) {
299
+ if (!this.localMember) {
300
+ throw new Error("Must call join() before connectPeer()");
301
+ }
302
+ const targetPort = port ?? config.p2p.port;
303
+ const ws = new WebSocket(`ws://${ip}:${targetPort}`);
304
+ ws.on("message", (data) => {
305
+ try {
306
+ const msg = parseP2PMsg(data.toString());
307
+ this.handleMessage(ws, msg);
308
+ } catch (err) {
309
+ console.error("Failed to parse P2P message:", err);
438
310
  }
439
311
  });
440
- this.mdnsDiscovery.announce(this.port, teamName, memberId);
441
- this.mdnsDiscovery.discover();
442
- return { memberId, teamId: teamName, teamName, displayName, status: "ONLINE" };
312
+ ws.on("close", () => {
313
+ const team = this.wsToTeam.get(ws);
314
+ if (team) {
315
+ if (this.peerConns.get(team) === ws) {
316
+ this.peerConns.delete(team);
317
+ }
318
+ this.wsToTeam.delete(ws);
319
+ }
320
+ });
321
+ await new Promise((resolve, reject) => {
322
+ const timeout = setTimeout(
323
+ () => reject(new Error(`Connection timeout to ${ip}:${targetPort}`)),
324
+ 5e3
325
+ );
326
+ ws.on("open", () => {
327
+ clearTimeout(timeout);
328
+ const hello = {
329
+ type: "P2P_HELLO",
330
+ fromTeam: this.localMember.teamName,
331
+ fromMemberId: this.localMember.memberId
332
+ };
333
+ ws.send(serializeP2PMsg(hello));
334
+ resolve();
335
+ });
336
+ ws.on("error", (err) => {
337
+ clearTimeout(timeout);
338
+ reject(err);
339
+ });
340
+ });
341
+ const helloMsg = await this.waitForResponse(
342
+ (m) => m.type === "P2P_HELLO",
343
+ 1e4
344
+ );
345
+ return helloMsg.fromTeam;
443
346
  }
444
347
  async ask(toTeam, content, format) {
445
348
  const ws = await this.getPeerConnection(toTeam);
@@ -545,7 +448,6 @@ var P2PNode = class {
545
448
  };
546
449
  }
547
450
  async disconnect() {
548
- this.mdnsDiscovery?.destroy();
549
451
  for (const ws of this.peerConns.values()) {
550
452
  ws.close();
551
453
  }
@@ -567,6 +469,14 @@ var P2PNode = class {
567
469
  ws.on("message", (data) => {
568
470
  try {
569
471
  const msg = parseP2PMsg(data.toString());
472
+ if (msg.type === "P2P_HELLO" && this.localMember) {
473
+ const hello = {
474
+ type: "P2P_HELLO",
475
+ fromTeam: this.localMember.teamName,
476
+ fromMemberId: this.localMember.memberId
477
+ };
478
+ ws.send(serializeP2PMsg(hello));
479
+ }
570
480
  this.handleMessage(ws, msg);
571
481
  } catch (err) {
572
482
  console.error("Failed to parse incoming P2P message:", err);
@@ -584,7 +494,7 @@ var P2PNode = class {
584
494
  });
585
495
  }
586
496
  // ---------------------------------------------------------------------------
587
- // Private: unified message handler (used for both incoming & outgoing sockets)
497
+ // Private: unified message handler
588
498
  // ---------------------------------------------------------------------------
589
499
  handleMessage(ws, msg) {
590
500
  for (const handler of this.pendingHandlers) {
@@ -679,78 +589,9 @@ var P2PNode = class {
679
589
  if (existing && existing.readyState === WebSocket.OPEN) {
680
590
  return existing;
681
591
  }
682
- let peer = this.mdnsDiscovery?.getPeerByTeam(teamName);
683
- if (!peer) {
684
- this.mdnsDiscovery?.discover();
685
- await this.waitForMdnsPeer(teamName, 1e4);
686
- peer = this.mdnsDiscovery?.getPeerByTeam(teamName);
687
- }
688
- if (!peer) {
689
- throw new Error(
690
- `Peer for team '${teamName}' not found via mDNS. Make sure the other terminal has joined with that team name.`
691
- );
692
- }
693
- return this.connectToPeer(teamName, peer.host, peer.port);
694
- }
695
- async connectToPeer(teamName, host, port) {
696
- const existing = this.peerConns.get(teamName);
697
- if (existing && existing.readyState === WebSocket.OPEN) {
698
- return existing;
699
- }
700
- const ws = new WebSocket(`ws://${host}:${port}`);
701
- await new Promise((resolve, reject) => {
702
- const timeout = setTimeout(
703
- () => reject(new Error(`Connection timeout to team '${teamName}'`)),
704
- 5e3
705
- );
706
- ws.on("open", () => {
707
- clearTimeout(timeout);
708
- const hello = {
709
- type: "P2P_HELLO",
710
- fromTeam: this.localMember?.teamName ?? "unknown",
711
- fromMemberId: this.localMember?.memberId ?? "unknown"
712
- };
713
- ws.send(serializeP2PMsg(hello));
714
- resolve();
715
- });
716
- ws.on("error", (err) => {
717
- clearTimeout(timeout);
718
- reject(err);
719
- });
720
- });
721
- ws.on("message", (data) => {
722
- try {
723
- const msg = parseP2PMsg(data.toString());
724
- this.handleMessage(ws, msg);
725
- } catch (err) {
726
- console.error("Failed to parse P2P message:", err);
727
- }
728
- });
729
- ws.on("close", () => {
730
- if (this.peerConns.get(teamName) === ws) {
731
- this.peerConns.delete(teamName);
732
- }
733
- });
734
- this.peerConns.set(teamName, ws);
735
- return ws;
736
- }
737
- waitForMdnsPeer(teamName, timeoutMs) {
738
- return new Promise((resolve, reject) => {
739
- const deadline = Date.now() + timeoutMs;
740
- const check = () => {
741
- if (this.mdnsDiscovery?.getPeerByTeam(teamName)) {
742
- resolve();
743
- return;
744
- }
745
- if (Date.now() >= deadline) {
746
- reject(new Error(`mDNS timeout: team '${teamName}' not found`));
747
- return;
748
- }
749
- this.mdnsDiscovery?.discover();
750
- setTimeout(check, 500);
751
- };
752
- check();
753
- });
592
+ throw new Error(
593
+ `No connection to team '${teamName}'. Use the connect_peer tool to connect first.`
594
+ );
754
595
  }
755
596
  waitForResponse(filter, timeoutMs) {
756
597
  return new Promise((resolve, reject) => {
@@ -805,6 +646,37 @@ Status: ${member.status}`
805
646
  }
806
647
  });
807
648
  }
649
+ var connectPeerSchema = {
650
+ ip: z.string().describe('IP address of the peer to connect to (e.g., "172.16.40.137")'),
651
+ port: z.number().optional().describe(`Port the peer is listening on (default: ${config.p2p.port})`)
652
+ };
653
+ function registerConnectPeerTool(server, client) {
654
+ server.tool("connect_peer", connectPeerSchema, async (args) => {
655
+ const targetPort = args.port ?? config.p2p.port;
656
+ try {
657
+ const peerTeam = await client.connectPeer(args.ip, args.port);
658
+ return {
659
+ content: [
660
+ {
661
+ type: "text",
662
+ text: `Connected to peer "${peerTeam}" at ${args.ip}:${targetPort}.`
663
+ }
664
+ ]
665
+ };
666
+ } catch (error) {
667
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
668
+ return {
669
+ content: [
670
+ {
671
+ type: "text",
672
+ text: `Failed to connect to ${args.ip}:${targetPort}: ${errorMessage}`
673
+ }
674
+ ],
675
+ isError: true
676
+ };
677
+ }
678
+ });
679
+ }
808
680
  var askSchema = {
809
681
  team: z.string().describe('Target team name to ask (e.g., "backend", "frontend")'),
810
682
  question: z.string().describe("The question to ask (supports markdown)")
@@ -1051,6 +923,7 @@ function createMcpServer(options) {
1051
923
  }
1052
924
  );
1053
925
  registerJoinTool(server, client);
926
+ registerConnectPeerTool(server, client);
1054
927
  registerAskTool(server, client);
1055
928
  registerCheckAnswerTool(server, client);
1056
929
  registerInboxTool(server, client);
@@ -1088,14 +961,10 @@ async function startMcpServer(options) {
1088
961
  // src/cli.ts
1089
962
  var program = new Command();
1090
963
  program.name("claude-collab").description("Real-time P2P team collaboration between Claude Code terminals").version("0.1.0");
1091
- program.command("client").description("Start MCP client (P2P mode, connects to Claude Code)").option("-t, --team <team>", "Team to auto-join (e.g., frontend, backend)").action(async (options) => {
964
+ program.command("client").description("Start MCP client (P2P mode, connects to Claude Code)").action(async () => {
1092
965
  const p2pNode = new P2PNode();
1093
966
  try {
1094
967
  await p2pNode.start();
1095
- if (options.team) {
1096
- await p2pNode.join(options.team, `${options.team} Claude`);
1097
- console.error(`Auto-joined team: ${options.team}`);
1098
- }
1099
968
  } catch (error) {
1100
969
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
1101
970
  console.error(`Failed to start P2P node: ${errorMessage}`);