@dolusoft/claude-collab 1.3.1 → 1.4.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/dist/cli.js +103 -266
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +102 -261
- package/dist/mcp-main.js.map +1 -1
- package/package.json +1 -3
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);
|
|
@@ -365,35 +211,18 @@ var config = {
|
|
|
365
211
|
*/
|
|
366
212
|
p2p: {
|
|
367
213
|
/**
|
|
368
|
-
*
|
|
369
|
-
*/
|
|
370
|
-
portRangeMin: 1e4,
|
|
371
|
-
/**
|
|
372
|
-
* Maximum port for the random WS server port range
|
|
214
|
+
* Fixed port for the WS server. Override with CLAUDE_COLLAB_PORT env var.
|
|
373
215
|
*/
|
|
374
|
-
|
|
216
|
+
port: Number(process.env["CLAUDE_COLLAB_PORT"] ?? 11777)
|
|
375
217
|
}};
|
|
376
218
|
|
|
377
219
|
// 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
220
|
var P2PNode = class {
|
|
391
221
|
wss = null;
|
|
392
|
-
mdnsDiscovery = null;
|
|
393
222
|
port = 0;
|
|
394
223
|
// Connections indexed by remote team name
|
|
395
224
|
peerConns = /* @__PURE__ */ new Map();
|
|
396
|
-
// Reverse lookup: ws → teamName (for cleanup
|
|
225
|
+
// Reverse lookup: ws → teamName (for cleanup)
|
|
397
226
|
wsToTeam = /* @__PURE__ */ new Map();
|
|
398
227
|
// Questions we received from remote peers (our inbox)
|
|
399
228
|
incomingQuestions = /* @__PURE__ */ new Map();
|
|
@@ -412,14 +241,13 @@ var P2PNode = class {
|
|
|
412
241
|
return this.localMember?.teamName;
|
|
413
242
|
}
|
|
414
243
|
/**
|
|
415
|
-
* Starts the WS server on
|
|
244
|
+
* Starts the WS server on the configured fixed port.
|
|
416
245
|
* Called automatically from join() if not yet started.
|
|
417
246
|
*/
|
|
418
247
|
async start() {
|
|
419
|
-
this.port =
|
|
248
|
+
this.port = config.p2p.port;
|
|
420
249
|
this.wss = new WebSocketServer({ port: this.port });
|
|
421
250
|
this.setupWssHandlers();
|
|
422
|
-
this.mdnsDiscovery = new MdnsDiscovery();
|
|
423
251
|
this._isStarted = true;
|
|
424
252
|
console.error(`P2P node started on port ${this.port}`);
|
|
425
253
|
}
|
|
@@ -429,17 +257,60 @@ var P2PNode = class {
|
|
|
429
257
|
}
|
|
430
258
|
const memberId = v4();
|
|
431
259
|
this.localMember = { memberId, teamName, displayName };
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
260
|
+
return { memberId, teamId: teamName, teamName, displayName, status: "ONLINE" };
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Connects to a peer at the given IP and port.
|
|
264
|
+
* Performs a bidirectional HELLO handshake and returns the peer's team name.
|
|
265
|
+
*/
|
|
266
|
+
async connectPeer(ip, port) {
|
|
267
|
+
if (!this.localMember) {
|
|
268
|
+
throw new Error("Must call join() before connectPeer()");
|
|
269
|
+
}
|
|
270
|
+
const targetPort = port ?? config.p2p.port;
|
|
271
|
+
const ws = new WebSocket(`ws://${ip}:${targetPort}`);
|
|
272
|
+
ws.on("message", (data) => {
|
|
273
|
+
try {
|
|
274
|
+
const msg = parseP2PMsg(data.toString());
|
|
275
|
+
this.handleMessage(ws, msg);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error("Failed to parse P2P message:", err);
|
|
438
278
|
}
|
|
439
279
|
});
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
280
|
+
ws.on("close", () => {
|
|
281
|
+
const team = this.wsToTeam.get(ws);
|
|
282
|
+
if (team) {
|
|
283
|
+
if (this.peerConns.get(team) === ws) {
|
|
284
|
+
this.peerConns.delete(team);
|
|
285
|
+
}
|
|
286
|
+
this.wsToTeam.delete(ws);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
await new Promise((resolve, reject) => {
|
|
290
|
+
const timeout = setTimeout(
|
|
291
|
+
() => reject(new Error(`Connection timeout to ${ip}:${targetPort}`)),
|
|
292
|
+
5e3
|
|
293
|
+
);
|
|
294
|
+
ws.on("open", () => {
|
|
295
|
+
clearTimeout(timeout);
|
|
296
|
+
const hello = {
|
|
297
|
+
type: "P2P_HELLO",
|
|
298
|
+
fromTeam: this.localMember.teamName,
|
|
299
|
+
fromMemberId: this.localMember.memberId
|
|
300
|
+
};
|
|
301
|
+
ws.send(serializeP2PMsg(hello));
|
|
302
|
+
resolve();
|
|
303
|
+
});
|
|
304
|
+
ws.on("error", (err) => {
|
|
305
|
+
clearTimeout(timeout);
|
|
306
|
+
reject(err);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
const helloMsg = await this.waitForResponse(
|
|
310
|
+
(m) => m.type === "P2P_HELLO",
|
|
311
|
+
1e4
|
|
312
|
+
);
|
|
313
|
+
return helloMsg.fromTeam;
|
|
443
314
|
}
|
|
444
315
|
async ask(toTeam, content, format) {
|
|
445
316
|
const ws = await this.getPeerConnection(toTeam);
|
|
@@ -545,7 +416,6 @@ var P2PNode = class {
|
|
|
545
416
|
};
|
|
546
417
|
}
|
|
547
418
|
async disconnect() {
|
|
548
|
-
this.mdnsDiscovery?.destroy();
|
|
549
419
|
for (const ws of this.peerConns.values()) {
|
|
550
420
|
ws.close();
|
|
551
421
|
}
|
|
@@ -567,6 +437,14 @@ var P2PNode = class {
|
|
|
567
437
|
ws.on("message", (data) => {
|
|
568
438
|
try {
|
|
569
439
|
const msg = parseP2PMsg(data.toString());
|
|
440
|
+
if (msg.type === "P2P_HELLO" && this.localMember) {
|
|
441
|
+
const hello = {
|
|
442
|
+
type: "P2P_HELLO",
|
|
443
|
+
fromTeam: this.localMember.teamName,
|
|
444
|
+
fromMemberId: this.localMember.memberId
|
|
445
|
+
};
|
|
446
|
+
ws.send(serializeP2PMsg(hello));
|
|
447
|
+
}
|
|
570
448
|
this.handleMessage(ws, msg);
|
|
571
449
|
} catch (err) {
|
|
572
450
|
console.error("Failed to parse incoming P2P message:", err);
|
|
@@ -584,7 +462,7 @@ var P2PNode = class {
|
|
|
584
462
|
});
|
|
585
463
|
}
|
|
586
464
|
// ---------------------------------------------------------------------------
|
|
587
|
-
// Private: unified message handler
|
|
465
|
+
// Private: unified message handler
|
|
588
466
|
// ---------------------------------------------------------------------------
|
|
589
467
|
handleMessage(ws, msg) {
|
|
590
468
|
for (const handler of this.pendingHandlers) {
|
|
@@ -679,78 +557,9 @@ var P2PNode = class {
|
|
|
679
557
|
if (existing && existing.readyState === WebSocket.OPEN) {
|
|
680
558
|
return existing;
|
|
681
559
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
});
|
|
560
|
+
throw new Error(
|
|
561
|
+
`No connection to team '${teamName}'. Use the connect_peer tool to connect first.`
|
|
562
|
+
);
|
|
754
563
|
}
|
|
755
564
|
waitForResponse(filter, timeoutMs) {
|
|
756
565
|
return new Promise((resolve, reject) => {
|
|
@@ -805,6 +614,37 @@ Status: ${member.status}`
|
|
|
805
614
|
}
|
|
806
615
|
});
|
|
807
616
|
}
|
|
617
|
+
var connectPeerSchema = {
|
|
618
|
+
ip: z.string().describe('IP address of the peer to connect to (e.g., "172.16.40.137")'),
|
|
619
|
+
port: z.number().optional().describe(`Port the peer is listening on (default: ${config.p2p.port})`)
|
|
620
|
+
};
|
|
621
|
+
function registerConnectPeerTool(server, client) {
|
|
622
|
+
server.tool("connect_peer", connectPeerSchema, async (args) => {
|
|
623
|
+
const targetPort = args.port ?? config.p2p.port;
|
|
624
|
+
try {
|
|
625
|
+
const peerTeam = await client.connectPeer(args.ip, args.port);
|
|
626
|
+
return {
|
|
627
|
+
content: [
|
|
628
|
+
{
|
|
629
|
+
type: "text",
|
|
630
|
+
text: `Connected to peer "${peerTeam}" at ${args.ip}:${targetPort}.`
|
|
631
|
+
}
|
|
632
|
+
]
|
|
633
|
+
};
|
|
634
|
+
} catch (error) {
|
|
635
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
636
|
+
return {
|
|
637
|
+
content: [
|
|
638
|
+
{
|
|
639
|
+
type: "text",
|
|
640
|
+
text: `Failed to connect to ${args.ip}:${targetPort}: ${errorMessage}`
|
|
641
|
+
}
|
|
642
|
+
],
|
|
643
|
+
isError: true
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
}
|
|
808
648
|
var askSchema = {
|
|
809
649
|
team: z.string().describe('Target team name to ask (e.g., "backend", "frontend")'),
|
|
810
650
|
question: z.string().describe("The question to ask (supports markdown)")
|
|
@@ -1051,6 +891,7 @@ function createMcpServer(options) {
|
|
|
1051
891
|
}
|
|
1052
892
|
);
|
|
1053
893
|
registerJoinTool(server, client);
|
|
894
|
+
registerConnectPeerTool(server, client);
|
|
1054
895
|
registerAskTool(server, client);
|
|
1055
896
|
registerCheckAnswerTool(server, client);
|
|
1056
897
|
registerInboxTool(server, client);
|
|
@@ -1088,14 +929,10 @@ async function startMcpServer(options) {
|
|
|
1088
929
|
// src/cli.ts
|
|
1089
930
|
var program = new Command();
|
|
1090
931
|
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)").
|
|
932
|
+
program.command("client").description("Start MCP client (P2P mode, connects to Claude Code)").action(async () => {
|
|
1092
933
|
const p2pNode = new P2PNode();
|
|
1093
934
|
try {
|
|
1094
935
|
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
936
|
} catch (error) {
|
|
1100
937
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
1101
938
|
console.error(`Failed to start P2P node: ${errorMessage}`);
|