@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/dist/mcp-main.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { WebSocket } from 'ws';
|
|
2
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
3
3
|
import { v4 } from 'uuid';
|
|
4
4
|
import { EventEmitter } from 'events';
|
|
5
|
-
import { execFile } from 'child_process';
|
|
5
|
+
import { execFile, spawn } from 'child_process';
|
|
6
6
|
import { unlinkSync } from 'fs';
|
|
7
7
|
import { tmpdir } from 'os';
|
|
8
8
|
import { join } from 'path';
|
|
9
|
+
import { Bonjour } from 'bonjour-service';
|
|
9
10
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
12
|
import { z } from 'zod';
|
|
@@ -259,7 +260,9 @@ var HubClient = class {
|
|
|
259
260
|
}
|
|
260
261
|
async join(name, displayName) {
|
|
261
262
|
this.myName = name;
|
|
262
|
-
|
|
263
|
+
if (this.serverUrl) {
|
|
264
|
+
await this.connectAndHello();
|
|
265
|
+
}
|
|
263
266
|
return {
|
|
264
267
|
memberId: v4(),
|
|
265
268
|
teamId: name,
|
|
@@ -269,6 +272,10 @@ var HubClient = class {
|
|
|
269
272
|
port: 0
|
|
270
273
|
};
|
|
271
274
|
}
|
|
275
|
+
async connectToHub(url) {
|
|
276
|
+
this.serverUrl = url;
|
|
277
|
+
await this.connectAndHello();
|
|
278
|
+
}
|
|
272
279
|
async ask(toPeer, content, format) {
|
|
273
280
|
const questionId = v4();
|
|
274
281
|
const requestId = v4();
|
|
@@ -549,6 +556,195 @@ var HubClient = class {
|
|
|
549
556
|
});
|
|
550
557
|
}
|
|
551
558
|
};
|
|
559
|
+
var HubServer = class {
|
|
560
|
+
wss = null;
|
|
561
|
+
clients = /* @__PURE__ */ new Map();
|
|
562
|
+
// name → ws
|
|
563
|
+
wsToName = /* @__PURE__ */ new Map();
|
|
564
|
+
// ws → name
|
|
565
|
+
start(port) {
|
|
566
|
+
this.wss = new WebSocketServer({ port });
|
|
567
|
+
this.wss.on("listening", () => {
|
|
568
|
+
console.log(`claude-collab hub running on port ${port}`);
|
|
569
|
+
console.log("Waiting for peers...\n");
|
|
570
|
+
});
|
|
571
|
+
this.wss.on("connection", (ws) => {
|
|
572
|
+
ws.on("message", (data) => {
|
|
573
|
+
try {
|
|
574
|
+
const msg = parse(data.toString());
|
|
575
|
+
this.handleMessage(ws, msg);
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
ws.on("close", () => {
|
|
580
|
+
const name = this.wsToName.get(ws);
|
|
581
|
+
if (name) {
|
|
582
|
+
this.clients.delete(name);
|
|
583
|
+
this.wsToName.delete(ws);
|
|
584
|
+
this.broadcast({ type: "PEER_LEFT", name });
|
|
585
|
+
console.log(`\u2190 ${name} left (${this.clients.size} online: ${[...this.clients.keys()].join(", ") || "none"})`);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
ws.on("error", (err) => {
|
|
589
|
+
console.error("[hub] ws error:", err.message);
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
this.wss.on("error", (err) => {
|
|
593
|
+
console.error("[hub] server error:", err.message);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
stop() {
|
|
597
|
+
if (!this.wss) return;
|
|
598
|
+
for (const ws of this.clients.values()) ws.terminate();
|
|
599
|
+
this.clients.clear();
|
|
600
|
+
this.wsToName.clear();
|
|
601
|
+
this.wss.close();
|
|
602
|
+
this.wss = null;
|
|
603
|
+
console.log("claude-collab hub stopped");
|
|
604
|
+
}
|
|
605
|
+
handleMessage(ws, msg) {
|
|
606
|
+
switch (msg.type) {
|
|
607
|
+
case "HELLO": {
|
|
608
|
+
const existing = this.clients.get(msg.name);
|
|
609
|
+
const isReconnect = existing != null && existing !== ws;
|
|
610
|
+
if (isReconnect) {
|
|
611
|
+
this.wsToName.delete(existing);
|
|
612
|
+
existing.close();
|
|
613
|
+
}
|
|
614
|
+
this.clients.set(msg.name, ws);
|
|
615
|
+
this.wsToName.set(ws, msg.name);
|
|
616
|
+
const peers = [...this.clients.keys()].filter((n) => n !== msg.name);
|
|
617
|
+
const ack = { type: "HELLO_ACK", peers };
|
|
618
|
+
this.send(ws, ack);
|
|
619
|
+
if (!isReconnect) {
|
|
620
|
+
this.broadcast({ type: "PEER_JOINED", name: msg.name }, ws);
|
|
621
|
+
}
|
|
622
|
+
console.log(`\u2192 ${msg.name} joined (${this.clients.size} online: ${[...this.clients.keys()].join(", ")})`);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
case "ASK": {
|
|
626
|
+
const target = this.clients.get(msg.to);
|
|
627
|
+
if (!target) {
|
|
628
|
+
const err = {
|
|
629
|
+
type: "HUB_ERROR",
|
|
630
|
+
code: "PEER_NOT_FOUND",
|
|
631
|
+
message: `'${msg.to}' is not connected to the hub`
|
|
632
|
+
};
|
|
633
|
+
this.send(ws, err);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
this.send(target, msg);
|
|
637
|
+
const ack = { type: "ASK_ACK", questionId: msg.questionId, requestId: msg.requestId };
|
|
638
|
+
this.send(ws, ack);
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case "GET_ANSWER": {
|
|
642
|
+
const target = this.clients.get(msg.to);
|
|
643
|
+
if (!target) {
|
|
644
|
+
this.send(ws, { type: "ANSWER_PENDING", to: msg.from, questionId: msg.questionId, requestId: msg.requestId });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
this.send(target, msg);
|
|
648
|
+
break;
|
|
649
|
+
}
|
|
650
|
+
case "ANSWER":
|
|
651
|
+
case "ANSWER_PENDING": {
|
|
652
|
+
const target = this.clients.get(msg.to);
|
|
653
|
+
if (target) this.send(target, msg);
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
send(ws, msg) {
|
|
659
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
660
|
+
ws.send(serialize(msg));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
broadcast(msg, except) {
|
|
664
|
+
for (const ws of this.clients.values()) {
|
|
665
|
+
if (ws !== except) this.send(ws, msg);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
var SERVICE_TYPE = "claude-collab";
|
|
670
|
+
var MdnsAdvertiser = class {
|
|
671
|
+
bonjour = null;
|
|
672
|
+
start(port) {
|
|
673
|
+
this.bonjour = new Bonjour();
|
|
674
|
+
this.bonjour.publish({ name: "claude-collab-hub", type: SERVICE_TYPE, port });
|
|
675
|
+
console.error(`[mdns] advertising hub on port ${port}`);
|
|
676
|
+
}
|
|
677
|
+
stop() {
|
|
678
|
+
if (!this.bonjour) return;
|
|
679
|
+
this.bonjour.unpublishAll(() => {
|
|
680
|
+
this.bonjour?.destroy();
|
|
681
|
+
});
|
|
682
|
+
this.bonjour = null;
|
|
683
|
+
console.error("[mdns] stopped advertising");
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// src/infrastructure/hub/hub-manager.ts
|
|
688
|
+
var HubManager = class {
|
|
689
|
+
hubServer = null;
|
|
690
|
+
advertiser = null;
|
|
691
|
+
currentPort = null;
|
|
692
|
+
get isRunning() {
|
|
693
|
+
return this.hubServer !== null;
|
|
694
|
+
}
|
|
695
|
+
get port() {
|
|
696
|
+
return this.currentPort;
|
|
697
|
+
}
|
|
698
|
+
async start(port) {
|
|
699
|
+
if (this.isRunning) throw new Error("Hub is already running");
|
|
700
|
+
const server = new HubServer();
|
|
701
|
+
server.start(port);
|
|
702
|
+
this.hubServer = server;
|
|
703
|
+
this.currentPort = port;
|
|
704
|
+
await addFirewallRule(port);
|
|
705
|
+
const advertiser = new MdnsAdvertiser();
|
|
706
|
+
advertiser.start(port);
|
|
707
|
+
this.advertiser = advertiser;
|
|
708
|
+
}
|
|
709
|
+
async stop() {
|
|
710
|
+
if (!this.isRunning) throw new Error("Hub is not running");
|
|
711
|
+
if (this.advertiser) {
|
|
712
|
+
this.advertiser.stop();
|
|
713
|
+
this.advertiser = null;
|
|
714
|
+
}
|
|
715
|
+
const port = this.currentPort;
|
|
716
|
+
this.hubServer.stop();
|
|
717
|
+
this.hubServer = null;
|
|
718
|
+
this.currentPort = null;
|
|
719
|
+
await removeFirewallRule(port);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
function runElevated(netshArgs) {
|
|
723
|
+
return new Promise((resolve, reject) => {
|
|
724
|
+
const ps = spawn("powershell", [
|
|
725
|
+
"-NoProfile",
|
|
726
|
+
"-Command",
|
|
727
|
+
`Start-Process -FilePath "netsh" -ArgumentList "${netshArgs}" -Verb RunAs -Wait`
|
|
728
|
+
]);
|
|
729
|
+
ps.on("close", (code) => {
|
|
730
|
+
if (code === 0) resolve();
|
|
731
|
+
else reject(new Error(`Firewall command failed (exit code ${code}). User may have cancelled the UAC prompt.`));
|
|
732
|
+
});
|
|
733
|
+
ps.on("error", (err) => {
|
|
734
|
+
reject(new Error(`Failed to launch PowerShell for firewall elevation: ${err.message}`));
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
async function addFirewallRule(port) {
|
|
739
|
+
await runElevated(
|
|
740
|
+
`advfirewall firewall add rule name="claude-collab-${port}" protocol=TCP dir=in localport=${port} action=allow`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
async function removeFirewallRule(port) {
|
|
744
|
+
await runElevated(
|
|
745
|
+
`advfirewall firewall delete rule name="claude-collab-${port}"`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
552
748
|
var askSchema = {
|
|
553
749
|
peer: z.string().describe('Name of the peer to ask (e.g., "alice", "backend")'),
|
|
554
750
|
question: z.string().describe("The question to ask (supports markdown)")
|
|
@@ -710,10 +906,101 @@ ${answerLine}`;
|
|
|
710
906
|
};
|
|
711
907
|
});
|
|
712
908
|
}
|
|
909
|
+
function registerStartHubTool(server, client, hubManager) {
|
|
910
|
+
server.tool(
|
|
911
|
+
"start_hub",
|
|
912
|
+
"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.",
|
|
913
|
+
{ port: z.number().min(1024).max(65535).optional().describe("Port to listen on (default: 9999)") },
|
|
914
|
+
async ({ port = 9999 }) => {
|
|
915
|
+
if (hubManager.isRunning) {
|
|
916
|
+
return {
|
|
917
|
+
content: [{
|
|
918
|
+
type: "text",
|
|
919
|
+
text: `Hub is already running on port ${hubManager.port}.`
|
|
920
|
+
}]
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
try {
|
|
924
|
+
await hubManager.start(port);
|
|
925
|
+
} catch (err) {
|
|
926
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
927
|
+
return {
|
|
928
|
+
content: [{
|
|
929
|
+
type: "text",
|
|
930
|
+
text: `Failed to start hub: ${msg}`
|
|
931
|
+
}]
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
await client.connectToHub(`ws://localhost:${port}`);
|
|
936
|
+
} catch (err) {
|
|
937
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
938
|
+
return {
|
|
939
|
+
content: [{
|
|
940
|
+
type: "text",
|
|
941
|
+
text: `Hub started on port ${port}, but failed to self-connect: ${msg}`
|
|
942
|
+
}]
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
content: [{
|
|
947
|
+
type: "text",
|
|
948
|
+
text: [
|
|
949
|
+
`Hub started on port ${port}.`,
|
|
950
|
+
`Firewall rule added (claude-collab-${port}).`,
|
|
951
|
+
`Others on the LAN will auto-discover and connect \u2014 no IP sharing needed.`,
|
|
952
|
+
`Use stop_hub when you are done to close the hub and remove the firewall rule.`
|
|
953
|
+
].join("\n")
|
|
954
|
+
}]
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/presentation/mcp/tools/stop-hub.tool.ts
|
|
961
|
+
function registerStopHubTool(server, hubManager) {
|
|
962
|
+
server.tool(
|
|
963
|
+
"stop_hub",
|
|
964
|
+
"Stop the running hub server. Removes the firewall rule (UAC prompt) and stops LAN advertising. Connected peers will be disconnected.",
|
|
965
|
+
{},
|
|
966
|
+
async () => {
|
|
967
|
+
if (!hubManager.isRunning) {
|
|
968
|
+
return {
|
|
969
|
+
content: [{
|
|
970
|
+
type: "text",
|
|
971
|
+
text: "No hub is currently running on this machine."
|
|
972
|
+
}]
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
const port = hubManager.port;
|
|
976
|
+
try {
|
|
977
|
+
await hubManager.stop();
|
|
978
|
+
} catch (err) {
|
|
979
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
980
|
+
return {
|
|
981
|
+
content: [{
|
|
982
|
+
type: "text",
|
|
983
|
+
text: `Failed to stop hub: ${msg}`
|
|
984
|
+
}]
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
content: [{
|
|
989
|
+
type: "text",
|
|
990
|
+
text: [
|
|
991
|
+
`Hub stopped (was on port ${port}).`,
|
|
992
|
+
`Firewall rule removed (claude-collab-${port}).`,
|
|
993
|
+
`All peers have been disconnected.`
|
|
994
|
+
].join("\n")
|
|
995
|
+
}]
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
);
|
|
999
|
+
}
|
|
713
1000
|
|
|
714
1001
|
// src/presentation/mcp/server.ts
|
|
715
1002
|
function createMcpServer(options) {
|
|
716
|
-
const { client } = options;
|
|
1003
|
+
const { client, hubManager } = options;
|
|
717
1004
|
const server = new McpServer({
|
|
718
1005
|
name: "claude-collab",
|
|
719
1006
|
version: "0.1.0"
|
|
@@ -722,6 +1009,8 @@ function createMcpServer(options) {
|
|
|
722
1009
|
registerReplyTool(server, client);
|
|
723
1010
|
registerPeersTool(server, client);
|
|
724
1011
|
registerHistoryTool(server, client);
|
|
1012
|
+
registerStartHubTool(server, client, hubManager);
|
|
1013
|
+
registerStopHubTool(server, hubManager);
|
|
725
1014
|
return server;
|
|
726
1015
|
}
|
|
727
1016
|
async function startMcpServer(options) {
|
|
@@ -729,6 +1018,37 @@ async function startMcpServer(options) {
|
|
|
729
1018
|
const transport = new StdioServerTransport();
|
|
730
1019
|
await server.connect(transport);
|
|
731
1020
|
}
|
|
1021
|
+
var SERVICE_TYPE2 = "claude-collab";
|
|
1022
|
+
function discoverHub(timeoutMs = 5e3) {
|
|
1023
|
+
return new Promise((resolve) => {
|
|
1024
|
+
const bonjour = new Bonjour();
|
|
1025
|
+
const browser = bonjour.find({ type: SERVICE_TYPE2 });
|
|
1026
|
+
let settled = false;
|
|
1027
|
+
const finish = (result) => {
|
|
1028
|
+
if (settled) return;
|
|
1029
|
+
settled = true;
|
|
1030
|
+
clearTimeout(timer);
|
|
1031
|
+
browser.stop();
|
|
1032
|
+
bonjour.destroy();
|
|
1033
|
+
resolve(result);
|
|
1034
|
+
};
|
|
1035
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
1036
|
+
browser.on("up", (svc) => {
|
|
1037
|
+
finish({ host: svc.host, port: svc.port });
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
function watchForHub(onFound) {
|
|
1042
|
+
const bonjour = new Bonjour();
|
|
1043
|
+
const browser = bonjour.find({ type: SERVICE_TYPE2 });
|
|
1044
|
+
browser.on("up", (svc) => {
|
|
1045
|
+
onFound({ host: svc.host, port: svc.port });
|
|
1046
|
+
});
|
|
1047
|
+
return () => {
|
|
1048
|
+
browser.stop();
|
|
1049
|
+
bonjour.destroy();
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
732
1052
|
|
|
733
1053
|
// src/mcp-main.ts
|
|
734
1054
|
function getArg(flag) {
|
|
@@ -742,15 +1062,38 @@ async function main() {
|
|
|
742
1062
|
console.error("--name is required");
|
|
743
1063
|
process.exit(1);
|
|
744
1064
|
}
|
|
745
|
-
|
|
746
|
-
console.error("--server is required");
|
|
747
|
-
process.exit(1);
|
|
748
|
-
}
|
|
749
|
-
const url = server.startsWith("ws") ? server : `ws://${server}`;
|
|
1065
|
+
const hubManager = new HubManager();
|
|
750
1066
|
const client = new HubClient();
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1067
|
+
if (server) {
|
|
1068
|
+
const url = server.startsWith("ws") ? server : `ws://${server}`;
|
|
1069
|
+
client.setServerUrl(url);
|
|
1070
|
+
await client.join(name, name);
|
|
1071
|
+
} else {
|
|
1072
|
+
await client.join(name, name);
|
|
1073
|
+
console.error("[mcp-main] no --server given, searching for hub on LAN (5s)...");
|
|
1074
|
+
const hub = await discoverHub(5e3);
|
|
1075
|
+
if (hub) {
|
|
1076
|
+
console.error(`[mcp-main] hub found at ${hub.host}:${hub.port}, connecting...`);
|
|
1077
|
+
await client.connectToHub(`ws://${hub.host}:${hub.port}`);
|
|
1078
|
+
} else {
|
|
1079
|
+
console.error("[mcp-main] no hub found. Use start_hub tool to host, or wait \u2014 auto-connect will happen when a hub appears.");
|
|
1080
|
+
const stopWatch = watchForHub(async (hub2) => {
|
|
1081
|
+
if (client.isConnected) {
|
|
1082
|
+
stopWatch();
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
try {
|
|
1086
|
+
console.error(`[mcp-main] hub appeared at ${hub2.host}:${hub2.port}, connecting...`);
|
|
1087
|
+
await client.connectToHub(`ws://${hub2.host}:${hub2.port}`);
|
|
1088
|
+
stopWatch();
|
|
1089
|
+
} catch (err) {
|
|
1090
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1091
|
+
console.error(`[mcp-main] auto-connect failed: ${msg}`);
|
|
1092
|
+
}
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
await startMcpServer({ client, hubManager });
|
|
754
1097
|
}
|
|
755
1098
|
main().catch((error) => {
|
|
756
1099
|
console.error("Unexpected error:", error);
|