@dolusoft/claude-collab 1.9.2 → 1.9.4

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/mcp-main.js CHANGED
@@ -26,7 +26,7 @@ var PeerBroadcaster = class {
26
26
  timer = null;
27
27
  start(name, port) {
28
28
  if (this.socket) return;
29
- const socket = createSocket("udp4");
29
+ const socket = createSocket({ type: "udp4", reuseAddr: true });
30
30
  this.socket = socket;
31
31
  socket.on("error", (err) => {
32
32
  console.error("[peer-broadcaster] error:", err.message);
@@ -56,9 +56,9 @@ var PeerBroadcaster = class {
56
56
  }
57
57
  };
58
58
  function watchForPeer(onFound) {
59
- const socket = createSocket("udp4");
59
+ const socket = createSocket({ type: "udp4", reuseAddr: true });
60
60
  socket.on("error", (err) => {
61
- console.error("[peer-listener] error:", err.message);
61
+ console.error("[peer-listener] bind failed on port", PEER_DISCOVERY_PORT, "\u2014", err.message);
62
62
  });
63
63
  socket.on("message", (msg, rinfo) => {
64
64
  try {
@@ -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
- if (!cached) return null;
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
- type: "ANSWER",
383
- from: this.myName,
384
- questionId,
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
- this.receivedAnswers.set(msg.questionId, {
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('Name of the peer to ask (e.g., "alice", "backend")'),
637
- question: z.string().describe("The question to ask (supports markdown)")
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
- type: "text",
649
- text: "Node is not ready yet. Wait a moment and try again."
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 POLL_INTERVAL_MS = 5e3;
657
- const MAX_WAIT_MS = 5 * 60 * 1e3;
658
- const deadline = Date.now() + MAX_WAIT_MS;
659
- while (Date.now() < deadline) {
660
- await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
661
- const answer = await client.checkAnswer(questionId);
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
- type: "text",
679
- text: `Soru g\xF6nderildi ancak 5 dakika i\xE7inde cevap gelmedi.
680
- Question ID: \`${questionId}\`
681
-
682
- Manuel kontrol i\xE7in "check_answer" tool'unu kullanabilirsin.`
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
- type: "text",
692
- text: `Failed to send question: ${errorMessage}`
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("The ID of the question to reply to (from inbox)"),
702
- answer: z.string().describe("Your answer to the question (supports markdown)")
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
- type: "text",
714
- text: 'You must join a team first. Use the "join" tool to join a team.'
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
- type: "text",
726
- text: `Reply sent successfully to question \`${questionId}\`.`
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
- type: "text",
736
- text: `Failed to send reply: ${errorMessage}`
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: `P2P server is not running yet. Port ${myPort} may be in use. Check the MCP logs for details.`
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: `You are "${myName}" (listening on port ${myPort}). No peers connected yet.
766
- Use firewall_open to allow inbound connections, or wait for peers to connect to you.`
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
- "Open a Windows Firewall inbound rule for your P2P listen port. A UAC popup will appear \u2014 accept it to allow peers to connect to you directly.",
1001
+ FIREWALL_OPEN_DESCRIPTION,
868
1002
  {
869
- port: z.number().min(1024).max(65535).optional().describe("Port to open (defaults to your current listen port)")
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: "Could not determine port. Pass port explicitly." }],
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 (process already elevated)" : "applied via UAC elevation popup";
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} (rule name: claude-collab-${targetPort}).`,
887
- `Method: ${how}.`,
888
- `Peers on the LAN can now connect to you directly.`
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: [{ type: "text", text: `Failed to open firewall: ${msg}` }],
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
- "Remove the Windows Firewall inbound rule for your P2P listen port. A UAC popup will appear \u2014 accept it to close the rule.",
1063
+ FIREWALL_CLOSE_DESCRIPTION,
906
1064
  {
907
- port: z.number().min(1024).max(65535).optional().describe("Port to close (defaults to your current listen port)")
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: "Could not determine port. Pass port explicitly." }],
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 (process already elevated)" : "applied via UAC elevation popup";
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} (rule name: claude-collab-${targetPort}). Method: ${how}.`
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: [{ type: "text", text: `Failed to close firewall: ${msg}` }],
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
  }