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