@dolusoft/claude-collab 1.9.2 → 1.9.3
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 +261 -94
- package/dist/cli.js.map +1 -1
- package/dist/mcp-main.js +261 -94
- package/dist/mcp-main.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp-main.js
CHANGED
|
@@ -314,6 +314,10 @@ var P2PNode = class {
|
|
|
314
314
|
sentQuestions = /* @__PURE__ */ new Map();
|
|
315
315
|
questionToSender = /* @__PURE__ */ new Map();
|
|
316
316
|
pendingHandlers = /* @__PURE__ */ new Set();
|
|
317
|
+
// Push-based answer resolution: questionId → resolve callback
|
|
318
|
+
answerWaiters = /* @__PURE__ */ new Map();
|
|
319
|
+
// Answers queued for offline peers: peerName → AnswerMsg (delivered on reconnect)
|
|
320
|
+
pendingOutboundAnswers = /* @__PURE__ */ new Map();
|
|
317
321
|
broadcaster = null;
|
|
318
322
|
stopPeerWatcher = null;
|
|
319
323
|
boundPort = 0;
|
|
@@ -357,9 +361,27 @@ var P2PNode = class {
|
|
|
357
361
|
await ackPromise;
|
|
358
362
|
return questionId;
|
|
359
363
|
}
|
|
364
|
+
waitForAnswer(questionId, timeoutMs) {
|
|
365
|
+
const cached = this.receivedAnswers.get(questionId);
|
|
366
|
+
if (cached) {
|
|
367
|
+
return Promise.resolve(this.formatAnswer(questionId, cached));
|
|
368
|
+
}
|
|
369
|
+
return new Promise((resolve) => {
|
|
370
|
+
const timeout = setTimeout(() => {
|
|
371
|
+
this.answerWaiters.delete(questionId);
|
|
372
|
+
resolve(null);
|
|
373
|
+
}, timeoutMs);
|
|
374
|
+
this.answerWaiters.set(questionId, (result) => {
|
|
375
|
+
clearTimeout(timeout);
|
|
376
|
+
resolve(result);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
}
|
|
360
380
|
async checkAnswer(questionId) {
|
|
361
381
|
const cached = this.receivedAnswers.get(questionId);
|
|
362
|
-
|
|
382
|
+
return cached ? this.formatAnswer(questionId, cached) : null;
|
|
383
|
+
}
|
|
384
|
+
formatAnswer(questionId, cached) {
|
|
363
385
|
return {
|
|
364
386
|
questionId,
|
|
365
387
|
from: { displayName: `${cached.fromName} Claude`, teamName: cached.fromName },
|
|
@@ -376,16 +398,20 @@ var P2PNode = class {
|
|
|
376
398
|
question.answerFormat = format;
|
|
377
399
|
const senderName = this.questionToSender.get(questionId);
|
|
378
400
|
if (senderName) {
|
|
401
|
+
const answerMsg = {
|
|
402
|
+
type: "ANSWER",
|
|
403
|
+
from: this.myName,
|
|
404
|
+
questionId,
|
|
405
|
+
content,
|
|
406
|
+
format,
|
|
407
|
+
answeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
408
|
+
};
|
|
379
409
|
const ws = this.peerConnections.get(senderName);
|
|
380
410
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
381
|
-
this.sendToWs(ws,
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
content,
|
|
386
|
-
format,
|
|
387
|
-
answeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
388
|
-
});
|
|
411
|
+
this.sendToWs(ws, answerMsg);
|
|
412
|
+
} else {
|
|
413
|
+
this.pendingOutboundAnswers.set(senderName, answerMsg);
|
|
414
|
+
console.error(`[p2p] "${senderName}" is offline, answer queued for delivery on reconnect`);
|
|
389
415
|
}
|
|
390
416
|
}
|
|
391
417
|
injectionQueue.notifyReplied();
|
|
@@ -561,6 +587,7 @@ var P2PNode = class {
|
|
|
561
587
|
this.connectingPeers.delete(msg.name);
|
|
562
588
|
this.sendToWs(ws, { type: "HELLO_ACK", name: this.myName });
|
|
563
589
|
console.error(`[p2p] peer joined (inbound): ${msg.name}`);
|
|
590
|
+
this.deliverPendingAnswer(msg.name, ws);
|
|
564
591
|
break;
|
|
565
592
|
}
|
|
566
593
|
case "HELLO_ACK": {
|
|
@@ -572,6 +599,7 @@ var P2PNode = class {
|
|
|
572
599
|
this.wsToName.set(ws, msg.name);
|
|
573
600
|
this.connectingPeers.delete(msg.name);
|
|
574
601
|
console.error(`[p2p] connected to peer: ${msg.name}`);
|
|
602
|
+
this.deliverPendingAnswer(msg.name, ws);
|
|
575
603
|
break;
|
|
576
604
|
}
|
|
577
605
|
case "ASK":
|
|
@@ -579,12 +607,18 @@ var P2PNode = class {
|
|
|
579
607
|
break;
|
|
580
608
|
case "ANSWER":
|
|
581
609
|
if (!this.receivedAnswers.has(msg.questionId)) {
|
|
582
|
-
|
|
610
|
+
const record = {
|
|
583
611
|
content: msg.content,
|
|
584
612
|
format: msg.format,
|
|
585
613
|
answeredAt: msg.answeredAt,
|
|
586
614
|
fromName: msg.from
|
|
587
|
-
}
|
|
615
|
+
};
|
|
616
|
+
this.receivedAnswers.set(msg.questionId, record);
|
|
617
|
+
const waiter = this.answerWaiters.get(msg.questionId);
|
|
618
|
+
if (waiter) {
|
|
619
|
+
this.answerWaiters.delete(msg.questionId);
|
|
620
|
+
waiter(this.formatAnswer(msg.questionId, record));
|
|
621
|
+
}
|
|
588
622
|
}
|
|
589
623
|
break;
|
|
590
624
|
}
|
|
@@ -610,6 +644,14 @@ var P2PNode = class {
|
|
|
610
644
|
ageMs: 0
|
|
611
645
|
});
|
|
612
646
|
}
|
|
647
|
+
deliverPendingAnswer(peerName, ws) {
|
|
648
|
+
const pending = this.pendingOutboundAnswers.get(peerName);
|
|
649
|
+
if (pending) {
|
|
650
|
+
this.pendingOutboundAnswers.delete(peerName);
|
|
651
|
+
this.sendToWs(ws, pending);
|
|
652
|
+
console.error(`[p2p] delivered queued answer to "${peerName}" after reconnect`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
613
655
|
sendToWs(ws, msg) {
|
|
614
656
|
if (ws.readyState === WebSocket.OPEN) {
|
|
615
657
|
ws.send(serialize(msg));
|
|
@@ -632,110 +674,144 @@ var P2PNode = class {
|
|
|
632
674
|
});
|
|
633
675
|
}
|
|
634
676
|
};
|
|
677
|
+
var ASK_DESCRIPTION = `Send a question to another Claude instance on the LAN and wait for their answer.
|
|
678
|
+
|
|
679
|
+
WHEN TO USE:
|
|
680
|
+
- You need input, a decision, or work output from a specific peer Claude
|
|
681
|
+
- You want to delegate a subtask to another Claude and use their result
|
|
682
|
+
- You need to coordinate or synchronize work across multiple Claudes
|
|
683
|
+
|
|
684
|
+
WORKFLOW:
|
|
685
|
+
1. Call peers() first to confirm the target is online and get their exact name
|
|
686
|
+
2. Call ask(peer, question) \u2014 this blocks until they reply (up to 5 minutes)
|
|
687
|
+
3. Use their answer directly in your ongoing task
|
|
688
|
+
|
|
689
|
+
WRITING GOOD QUESTIONS:
|
|
690
|
+
- Include all context the other Claude needs \u2014 they cannot see your conversation
|
|
691
|
+
- Be specific about what format or level of detail you expect in the answer
|
|
692
|
+
- If you need code, specify language and constraints
|
|
693
|
+
- One focused question per call works better than multiple combined
|
|
694
|
+
|
|
695
|
+
DO NOT use this tool if:
|
|
696
|
+
- The peer is not listed in peers() \u2014 the call will fail immediately
|
|
697
|
+
- You just want to share information without needing a response (there is no broadcast tool yet)`;
|
|
635
698
|
var askSchema = {
|
|
636
|
-
peer: z.string().describe(
|
|
637
|
-
question: z.string().describe(
|
|
699
|
+
peer: z.string().describe("Exact name of the peer to ask. Use peers() first to see who is online and get the correct name."),
|
|
700
|
+
question: z.string().describe(
|
|
701
|
+
"Your question in markdown. Include all necessary context \u2014 the other Claude cannot see your conversation history. Be specific about what kind of answer you need."
|
|
702
|
+
)
|
|
638
703
|
};
|
|
639
704
|
function registerAskTool(server, client) {
|
|
640
|
-
server.tool("ask", askSchema, async (args) => {
|
|
705
|
+
server.tool("ask", ASK_DESCRIPTION, askSchema, async (args) => {
|
|
641
706
|
const targetPeer = args.peer;
|
|
642
707
|
const question = args.question;
|
|
643
708
|
try {
|
|
644
709
|
if (!client.currentTeamId) {
|
|
645
710
|
return {
|
|
646
|
-
content: [
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}
|
|
651
|
-
],
|
|
711
|
+
content: [{
|
|
712
|
+
type: "text",
|
|
713
|
+
text: "P2P node is not ready yet. Wait a moment and try again."
|
|
714
|
+
}],
|
|
652
715
|
isError: true
|
|
653
716
|
};
|
|
654
717
|
}
|
|
655
718
|
const questionId = await client.ask(targetPeer, question, "markdown");
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if (answer !== null) {
|
|
663
|
-
return {
|
|
664
|
-
content: [
|
|
665
|
-
{
|
|
666
|
-
type: "text",
|
|
667
|
-
text: `**${answer.from.displayName} (${answer.from.teamName}) cevaplad\u0131:**
|
|
719
|
+
const answer = await client.waitForAnswer(questionId, 5 * 60 * 1e3);
|
|
720
|
+
if (answer !== null) {
|
|
721
|
+
return {
|
|
722
|
+
content: [{
|
|
723
|
+
type: "text",
|
|
724
|
+
text: `**${answer.from.displayName} answered:**
|
|
668
725
|
|
|
669
726
|
${answer.content}`
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
};
|
|
673
|
-
}
|
|
727
|
+
}]
|
|
728
|
+
};
|
|
674
729
|
}
|
|
675
730
|
return {
|
|
676
|
-
content: [
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
Question ID: \`${questionId}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
731
|
+
content: [{
|
|
732
|
+
type: "text",
|
|
733
|
+
text: [
|
|
734
|
+
`Question sent to "${targetPeer}" but no answer arrived within 5 minutes.`,
|
|
735
|
+
`Question ID: \`${questionId}\``,
|
|
736
|
+
``,
|
|
737
|
+
`The peer may be busy or offline. You can:`,
|
|
738
|
+
`- Call peers() to check if they are still connected`,
|
|
739
|
+
`- Continue with your task and follow up later`
|
|
740
|
+
].join("\n")
|
|
741
|
+
}]
|
|
685
742
|
};
|
|
686
743
|
} catch (error) {
|
|
687
744
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
745
|
+
const isPeerOffline = errorMessage.includes("not connected");
|
|
688
746
|
return {
|
|
689
|
-
content: [
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
694
|
-
],
|
|
747
|
+
content: [{
|
|
748
|
+
type: "text",
|
|
749
|
+
text: isPeerOffline ? `Peer "${targetPeer}" is not connected. Call peers() to see who is currently online.` : `Failed to send question: ${errorMessage}`
|
|
750
|
+
}],
|
|
695
751
|
isError: true
|
|
696
752
|
};
|
|
697
753
|
}
|
|
698
754
|
});
|
|
699
755
|
}
|
|
756
|
+
var REPLY_DESCRIPTION = `Send your answer back to a Claude instance that asked you a question.
|
|
757
|
+
|
|
758
|
+
WHEN TO USE:
|
|
759
|
+
- A question has been injected into your terminal by another Claude (you will see it appear automatically)
|
|
760
|
+
- You have finished thinking through the answer and are ready to respond
|
|
761
|
+
|
|
762
|
+
WORKFLOW:
|
|
763
|
+
1. Read the injected question carefully \u2014 it includes a questionId at the top
|
|
764
|
+
2. Think through a complete answer
|
|
765
|
+
3. Call reply(questionId, answer) \u2014 your answer is sent directly back to the asking Claude
|
|
766
|
+
4. The asking Claude's ask() call unblocks and they receive your answer immediately
|
|
767
|
+
|
|
768
|
+
WRITING GOOD ANSWERS:
|
|
769
|
+
- Be thorough \u2014 the asking Claude will use your answer directly in their task
|
|
770
|
+
- Use markdown for code blocks, lists, and structure
|
|
771
|
+
- Include any caveats, assumptions, or follow-up suggestions that would help them
|
|
772
|
+
- If you cannot answer fully, say so clearly and explain why
|
|
773
|
+
|
|
774
|
+
IMPORTANT:
|
|
775
|
+
- Each question can only be replied to once
|
|
776
|
+
- The questionId is a UUID shown in the injected question prompt \u2014 copy it exactly`;
|
|
700
777
|
var replySchema = {
|
|
701
|
-
questionId: z.string().describe(
|
|
702
|
-
|
|
778
|
+
questionId: z.string().describe(
|
|
779
|
+
"The UUID of the question to reply to. Shown at the top of the injected question prompt in your terminal."
|
|
780
|
+
),
|
|
781
|
+
answer: z.string().describe(
|
|
782
|
+
"Your complete answer in markdown. Be thorough \u2014 the asking Claude is waiting and will use your response directly in their work."
|
|
783
|
+
)
|
|
703
784
|
};
|
|
704
785
|
function registerReplyTool(server, client) {
|
|
705
|
-
server.tool("reply", replySchema, async (args) => {
|
|
786
|
+
server.tool("reply", REPLY_DESCRIPTION, replySchema, async (args) => {
|
|
706
787
|
const questionId = args.questionId;
|
|
707
788
|
const answer = args.answer;
|
|
708
789
|
try {
|
|
709
790
|
if (!client.currentTeamId) {
|
|
710
791
|
return {
|
|
711
|
-
content: [
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
}
|
|
716
|
-
],
|
|
792
|
+
content: [{
|
|
793
|
+
type: "text",
|
|
794
|
+
text: "P2P node is not ready yet. Wait a moment and try again."
|
|
795
|
+
}],
|
|
717
796
|
isError: true
|
|
718
797
|
};
|
|
719
798
|
}
|
|
720
799
|
await client.reply(questionId, answer, "markdown");
|
|
721
800
|
injectionQueue.notifyReplied();
|
|
722
801
|
return {
|
|
723
|
-
content: [
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
]
|
|
802
|
+
content: [{
|
|
803
|
+
type: "text",
|
|
804
|
+
text: `Answer sent. The peer's ask() call has been unblocked and they received your response.`
|
|
805
|
+
}]
|
|
729
806
|
};
|
|
730
807
|
} catch (error) {
|
|
731
808
|
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
809
|
+
const isNotFound = errorMessage.includes("not found");
|
|
732
810
|
return {
|
|
733
|
-
content: [
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
],
|
|
811
|
+
content: [{
|
|
812
|
+
type: "text",
|
|
813
|
+
text: isNotFound ? `Question \`${questionId}\` not found. Check that you copied the questionId exactly from the injected prompt.` : `Failed to send reply: ${errorMessage}`
|
|
814
|
+
}],
|
|
739
815
|
isError: true
|
|
740
816
|
};
|
|
741
817
|
}
|
|
@@ -743,8 +819,24 @@ function registerReplyTool(server, client) {
|
|
|
743
819
|
}
|
|
744
820
|
|
|
745
821
|
// src/presentation/mcp/tools/peers.tool.ts
|
|
822
|
+
var PEERS_DESCRIPTION = `Show your identity on the P2P network and list all currently connected peers.
|
|
823
|
+
|
|
824
|
+
WHEN TO USE:
|
|
825
|
+
- Before calling ask() \u2014 to confirm the target peer is online and get their exact name
|
|
826
|
+
- After startup \u2014 to verify you joined the network successfully
|
|
827
|
+
- When a peer seems unreachable \u2014 to check if they are still connected
|
|
828
|
+
|
|
829
|
+
WHAT IT SHOWS:
|
|
830
|
+
- Your own name and the port you are listening on
|
|
831
|
+
- All peers who have established a direct connection to you
|
|
832
|
+
|
|
833
|
+
IF NO PEERS ARE LISTED:
|
|
834
|
+
- Peers discover each other automatically via LAN broadcast every 3 seconds
|
|
835
|
+
- If a peer just started, wait a few seconds and call peers() again
|
|
836
|
+
- Call firewall_open() so peers on other machines can connect inbound to you
|
|
837
|
+
- If still no peers, verify all machines are on the same LAN/subnet`;
|
|
746
838
|
function registerPeersTool(server, client) {
|
|
747
|
-
server.tool("peers", {}, async () => {
|
|
839
|
+
server.tool("peers", PEERS_DESCRIPTION, {}, async () => {
|
|
748
840
|
const info = client.getInfo();
|
|
749
841
|
const myName = info.teamName ?? "(starting...)";
|
|
750
842
|
const myPort = info.port ?? "?";
|
|
@@ -753,7 +845,10 @@ function registerPeersTool(server, client) {
|
|
|
753
845
|
return {
|
|
754
846
|
content: [{
|
|
755
847
|
type: "text",
|
|
756
|
-
text:
|
|
848
|
+
text: [
|
|
849
|
+
`P2P server is not running yet (port ${myPort} may be in use).`,
|
|
850
|
+
`Check the MCP process logs for the error details.`
|
|
851
|
+
].join("\n")
|
|
757
852
|
}],
|
|
758
853
|
isError: true
|
|
759
854
|
};
|
|
@@ -762,8 +857,13 @@ function registerPeersTool(server, client) {
|
|
|
762
857
|
return {
|
|
763
858
|
content: [{
|
|
764
859
|
type: "text",
|
|
765
|
-
text:
|
|
766
|
-
|
|
860
|
+
text: [
|
|
861
|
+
`You are "${myName}" (listening on port ${myPort}).`,
|
|
862
|
+
`No peers connected yet.`,
|
|
863
|
+
``,
|
|
864
|
+
`Peers auto-discover via LAN broadcast \u2014 wait a few seconds if they just started.`,
|
|
865
|
+
`Call firewall_open() if peers on other machines cannot reach you.`
|
|
866
|
+
].join("\n")
|
|
767
867
|
}]
|
|
768
868
|
};
|
|
769
869
|
}
|
|
@@ -779,14 +879,27 @@ ${list}`
|
|
|
779
879
|
}
|
|
780
880
|
|
|
781
881
|
// src/presentation/mcp/tools/history.tool.ts
|
|
882
|
+
var HISTORY_DESCRIPTION = `Show all questions and answers exchanged this session \u2014 both sent and received.
|
|
883
|
+
|
|
884
|
+
WHEN TO USE:
|
|
885
|
+
- To review what was already discussed before asking a follow-up question
|
|
886
|
+
- To check if a previously sent question has been answered yet
|
|
887
|
+
- To find a questionId you need to reference
|
|
888
|
+
- To catch up on received questions you may have missed
|
|
889
|
+
|
|
890
|
+
OUTPUT FORMAT:
|
|
891
|
+
- \u2192 peer: question you sent, with their answer below
|
|
892
|
+
- \u2190 peer: question they sent you, with your reply below
|
|
893
|
+
- (no answer yet) means the question is still waiting for a response`;
|
|
782
894
|
function registerHistoryTool(server, client) {
|
|
783
|
-
server.tool("history", {}, async () => {
|
|
895
|
+
server.tool("history", HISTORY_DESCRIPTION, {}, async () => {
|
|
784
896
|
const entries = client.getHistory();
|
|
785
897
|
if (entries.length === 0) {
|
|
786
898
|
return {
|
|
787
|
-
content: [{ type: "text", text: "No questions yet this session." }]
|
|
899
|
+
content: [{ type: "text", text: "No questions exchanged yet this session." }]
|
|
788
900
|
};
|
|
789
901
|
}
|
|
902
|
+
const unanswered = entries.filter((e) => !e.answer);
|
|
790
903
|
const lines = entries.map((e) => {
|
|
791
904
|
const time = new Date(e.askedAt).toLocaleTimeString();
|
|
792
905
|
if (e.direction === "sent") {
|
|
@@ -794,13 +907,16 @@ function registerHistoryTool(server, client) {
|
|
|
794
907
|
return `[${time}] \u2192 ${e.peer}: ${e.question}
|
|
795
908
|
${answerLine}`;
|
|
796
909
|
} else {
|
|
797
|
-
const answerLine = e.answer ? ` \u21B3 you: ${e.answer}` : ` \u21B3 (not replied yet)`;
|
|
910
|
+
const answerLine = e.answer ? ` \u21B3 you: ${e.answer}` : ` \u21B3 (not replied yet \u2014 use reply() if you haven't answered)`;
|
|
798
911
|
return `[${time}] \u2190 ${e.peer}: ${e.question}
|
|
799
912
|
${answerLine}`;
|
|
800
913
|
}
|
|
801
914
|
});
|
|
915
|
+
const summary = unanswered.length > 0 ? `
|
|
916
|
+
|
|
917
|
+
\u26A0 ${unanswered.length} question(s) still waiting for a response.` : "";
|
|
802
918
|
return {
|
|
803
|
-
content: [{ type: "text", text: lines.join("\n\n") }]
|
|
919
|
+
content: [{ type: "text", text: lines.join("\n\n") + summary }]
|
|
804
920
|
};
|
|
805
921
|
});
|
|
806
922
|
}
|
|
@@ -861,72 +977,123 @@ async function removeFirewallRule(port) {
|
|
|
861
977
|
}
|
|
862
978
|
|
|
863
979
|
// src/presentation/mcp/tools/firewall-open.tool.ts
|
|
980
|
+
var FIREWALL_OPEN_DESCRIPTION = `Open a Windows Firewall inbound rule so peers on the LAN can connect directly to you.
|
|
981
|
+
|
|
982
|
+
WHEN YOU NEED THIS:
|
|
983
|
+
- Other peers cannot see you in their peers() list even though you are on the same LAN
|
|
984
|
+
- You just started and want to make sure all peers can reach you
|
|
985
|
+
- A peer explicitly says they cannot connect to you
|
|
986
|
+
|
|
987
|
+
WHEN YOU DO NOT NEED THIS:
|
|
988
|
+
- You can already see peers in peers() \u2014 the connection is working
|
|
989
|
+
- You are connecting outbound to others (outbound connections do not require firewall rules)
|
|
990
|
+
|
|
991
|
+
WHAT HAPPENS:
|
|
992
|
+
- On most systems: the rule is applied immediately (terminal is already elevated)
|
|
993
|
+
- On standard Windows: a UAC popup appears \u2014 the user must click Yes
|
|
994
|
+
- On VMs or headless setups: applied directly if running as Administrator
|
|
995
|
+
|
|
996
|
+
The rule is named "claude-collab-{port}" and allows inbound TCP on your current listen port.
|
|
997
|
+
Call firewall_close() when you are done to clean up the rule.`;
|
|
864
998
|
function registerFirewallOpenTool(server, client) {
|
|
865
999
|
server.tool(
|
|
866
1000
|
"firewall_open",
|
|
867
|
-
|
|
1001
|
+
FIREWALL_OPEN_DESCRIPTION,
|
|
868
1002
|
{
|
|
869
|
-
port: z.number().min(1024).max(65535).optional().describe(
|
|
1003
|
+
port: z.number().min(1024).max(65535).optional().describe(
|
|
1004
|
+
"Port to open. Defaults to your current listen port \u2014 omit this unless you have a specific reason to override."
|
|
1005
|
+
)
|
|
870
1006
|
},
|
|
871
1007
|
async ({ port }) => {
|
|
872
1008
|
const targetPort = port ?? client.getInfo().port;
|
|
873
1009
|
if (!targetPort) {
|
|
874
1010
|
return {
|
|
875
|
-
content: [{ type: "text", text: "
|
|
1011
|
+
content: [{ type: "text", text: "P2P node is not running yet \u2014 port unknown. Try again in a moment." }],
|
|
876
1012
|
isError: true
|
|
877
1013
|
};
|
|
878
1014
|
}
|
|
879
1015
|
try {
|
|
880
1016
|
const { method } = await addFirewallRule(targetPort);
|
|
881
|
-
const how = method === "direct" ? "applied directly (
|
|
1017
|
+
const how = method === "direct" ? "applied directly (already elevated)" : "applied via UAC popup";
|
|
882
1018
|
return {
|
|
883
1019
|
content: [{
|
|
884
1020
|
type: "text",
|
|
885
1021
|
text: [
|
|
886
|
-
`Firewall rule opened for port ${targetPort} (
|
|
887
|
-
`
|
|
888
|
-
`
|
|
1022
|
+
`Firewall rule opened for port ${targetPort} (claude-collab-${targetPort}). ${how}.`,
|
|
1023
|
+
`Peers on the LAN can now connect to you inbound.`,
|
|
1024
|
+
`Call firewall_close() when you are done with this session.`
|
|
889
1025
|
].join("\n")
|
|
890
1026
|
}]
|
|
891
1027
|
};
|
|
892
1028
|
} catch (err) {
|
|
893
1029
|
const msg = err instanceof Error ? err.message : String(err);
|
|
894
1030
|
return {
|
|
895
|
-
content: [{
|
|
1031
|
+
content: [{
|
|
1032
|
+
type: "text",
|
|
1033
|
+
text: [
|
|
1034
|
+
`Failed to open firewall: ${msg}`,
|
|
1035
|
+
``,
|
|
1036
|
+
`Try running your terminal as Administrator and call firewall_open() again.`
|
|
1037
|
+
].join("\n")
|
|
1038
|
+
}],
|
|
896
1039
|
isError: true
|
|
897
1040
|
};
|
|
898
1041
|
}
|
|
899
1042
|
}
|
|
900
1043
|
);
|
|
901
1044
|
}
|
|
1045
|
+
var FIREWALL_CLOSE_DESCRIPTION = `Remove the Windows Firewall inbound rule that was opened by firewall_open().
|
|
1046
|
+
|
|
1047
|
+
WHEN TO USE:
|
|
1048
|
+
- At the end of a collaboration session to clean up
|
|
1049
|
+
- When you no longer want to accept inbound peer connections
|
|
1050
|
+
- As general hygiene \u2014 open firewall rules should not be left indefinitely
|
|
1051
|
+
|
|
1052
|
+
WHAT HAPPENS:
|
|
1053
|
+
- The inbound TCP rule "claude-collab-{port}" is deleted from Windows Firewall
|
|
1054
|
+
- Existing active connections are not dropped \u2014 only new inbound connections are blocked
|
|
1055
|
+
- On standard Windows: a UAC popup appears \u2014 the user must click Yes
|
|
1056
|
+
- On VMs or admin terminals: applied directly without a popup
|
|
1057
|
+
|
|
1058
|
+
NOTE: Because ports are random each session, each startup creates a new rule name.
|
|
1059
|
+
Old rules from previous sessions can be removed by passing the port explicitly.`;
|
|
902
1060
|
function registerFirewallCloseTool(server, client) {
|
|
903
1061
|
server.tool(
|
|
904
1062
|
"firewall_close",
|
|
905
|
-
|
|
1063
|
+
FIREWALL_CLOSE_DESCRIPTION,
|
|
906
1064
|
{
|
|
907
|
-
port: z.number().min(1024).max(65535).optional().describe(
|
|
1065
|
+
port: z.number().min(1024).max(65535).optional().describe(
|
|
1066
|
+
"Port whose rule to remove. Defaults to your current listen port. Pass a specific port to clean up a rule from a previous session."
|
|
1067
|
+
)
|
|
908
1068
|
},
|
|
909
1069
|
async ({ port }) => {
|
|
910
1070
|
const targetPort = port ?? client.getInfo().port;
|
|
911
1071
|
if (!targetPort) {
|
|
912
1072
|
return {
|
|
913
|
-
content: [{ type: "text", text: "
|
|
1073
|
+
content: [{ type: "text", text: "P2P node is not running yet \u2014 port unknown. Try again in a moment." }],
|
|
914
1074
|
isError: true
|
|
915
1075
|
};
|
|
916
1076
|
}
|
|
917
1077
|
try {
|
|
918
1078
|
const { method } = await removeFirewallRule(targetPort);
|
|
919
|
-
const how = method === "direct" ? "applied directly (
|
|
1079
|
+
const how = method === "direct" ? "applied directly (already elevated)" : "applied via UAC popup";
|
|
920
1080
|
return {
|
|
921
1081
|
content: [{
|
|
922
1082
|
type: "text",
|
|
923
|
-
text: `Firewall rule removed for port ${targetPort} (
|
|
1083
|
+
text: `Firewall rule removed for port ${targetPort} (claude-collab-${targetPort}). ${how}.`
|
|
924
1084
|
}]
|
|
925
1085
|
};
|
|
926
1086
|
} catch (err) {
|
|
927
1087
|
const msg = err instanceof Error ? err.message : String(err);
|
|
928
1088
|
return {
|
|
929
|
-
content: [{
|
|
1089
|
+
content: [{
|
|
1090
|
+
type: "text",
|
|
1091
|
+
text: [
|
|
1092
|
+
`Failed to remove firewall rule: ${msg}`,
|
|
1093
|
+
``,
|
|
1094
|
+
`The rule may not exist (if firewall_open was never called this session), or try running as Administrator.`
|
|
1095
|
+
].join("\n")
|
|
1096
|
+
}],
|
|
930
1097
|
isError: true
|
|
931
1098
|
};
|
|
932
1099
|
}
|