mono-pilot 0.2.9 → 0.2.12

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.
Files changed (158) hide show
  1. package/README.md +270 -7
  2. package/dist/src/agents-paths.js +36 -0
  3. package/dist/src/brief/blocks.js +83 -0
  4. package/dist/src/brief/defaults.js +60 -0
  5. package/dist/src/brief/frontmatter.js +53 -0
  6. package/dist/src/brief/paths.js +10 -0
  7. package/dist/src/brief/reflection.js +27 -0
  8. package/dist/src/cli.js +62 -5
  9. package/dist/src/cluster/bus.js +102 -0
  10. package/dist/src/cluster/follower.js +137 -0
  11. package/dist/src/cluster/init.js +182 -0
  12. package/dist/src/cluster/leader.js +97 -0
  13. package/dist/src/cluster/log.js +49 -0
  14. package/dist/src/cluster/protocol.js +34 -0
  15. package/dist/src/cluster/services/bus.js +243 -0
  16. package/dist/src/cluster/services/embedding.js +12 -0
  17. package/dist/src/cluster/socket.js +86 -0
  18. package/dist/src/cluster/test-bus.js +175 -0
  19. package/dist/src/cluster_v2/connection-lifecycle.js +31 -0
  20. package/dist/src/cluster_v2/connection-lifecycle.test.js +24 -0
  21. package/dist/src/cluster_v2/connection.js +159 -0
  22. package/dist/src/cluster_v2/connection.test.js +55 -0
  23. package/dist/src/cluster_v2/events.js +102 -0
  24. package/dist/src/cluster_v2/index.js +2 -0
  25. package/dist/src/cluster_v2/observability.js +99 -0
  26. package/dist/src/cluster_v2/observability.test.js +46 -0
  27. package/dist/src/cluster_v2/rpc.js +389 -0
  28. package/dist/src/cluster_v2/rpc.test.js +110 -0
  29. package/dist/src/cluster_v2/runtime.failover.integration.test.js +156 -0
  30. package/dist/src/cluster_v2/runtime.js +531 -0
  31. package/dist/src/cluster_v2/runtime.lease-compromise.integration.test.js +91 -0
  32. package/dist/src/cluster_v2/runtime.lifecycle.integration.test.js +225 -0
  33. package/dist/src/cluster_v2/services/bus.integration.test.js +140 -0
  34. package/dist/src/cluster_v2/services/bus.js +450 -0
  35. package/dist/src/cluster_v2/services/discord/auth-store.js +82 -0
  36. package/dist/src/cluster_v2/services/discord/collector.js +569 -0
  37. package/dist/src/cluster_v2/services/discord/index.js +1 -0
  38. package/dist/src/cluster_v2/services/discord/oauth.js +87 -0
  39. package/dist/src/cluster_v2/services/discord/rpc-client.js +325 -0
  40. package/dist/src/cluster_v2/services/embedding.js +66 -0
  41. package/dist/src/cluster_v2/services/registry-cache.js +107 -0
  42. package/dist/src/cluster_v2/services/registry-cache.test.js +66 -0
  43. package/dist/src/cluster_v2/services/registry.js +36 -0
  44. package/dist/src/cluster_v2/services/twitter/collector.js +1055 -0
  45. package/dist/src/cluster_v2/services/twitter/index.js +1 -0
  46. package/dist/src/config/digest.js +78 -0
  47. package/dist/src/config/discord.js +143 -0
  48. package/dist/src/config/image-gen.js +48 -0
  49. package/dist/src/config/mono-pilot.js +31 -0
  50. package/dist/src/config/twitter.js +100 -0
  51. package/dist/src/extensions/cluster.js +311 -0
  52. package/dist/src/extensions/commands/build-memory.js +76 -0
  53. package/dist/src/extensions/commands/digest/backfill.js +779 -0
  54. package/dist/src/extensions/commands/digest/index.js +1133 -0
  55. package/dist/src/extensions/commands/image-model.js +214 -0
  56. package/dist/src/extensions/game/bus-injection.js +47 -0
  57. package/dist/src/extensions/game/identity.js +83 -0
  58. package/dist/src/extensions/game/mailbox.js +61 -0
  59. package/dist/src/extensions/game/system-prompt.js +134 -0
  60. package/dist/src/extensions/game/tools.js +28 -0
  61. package/dist/src/extensions/lifecycle.js +337 -0
  62. package/dist/src/extensions/mode-runtime.js +26 -2
  63. package/dist/src/extensions/mono-game.js +66 -0
  64. package/dist/src/extensions/mono-pilot.js +100 -18
  65. package/dist/src/extensions/nvim.js +47 -0
  66. package/dist/src/extensions/session-hints.js +60 -35
  67. package/dist/src/extensions/sftp.js +897 -0
  68. package/dist/src/extensions/status.js +676 -0
  69. package/dist/src/extensions/system-events.js +478 -0
  70. package/dist/src/extensions/system-prompt.js +24 -14
  71. package/dist/src/extensions/user-message.js +94 -50
  72. package/dist/src/lsp/client.js +235 -0
  73. package/dist/src/lsp/index.js +165 -0
  74. package/dist/src/lsp/runtime.js +67 -0
  75. package/dist/src/lsp/server.js +242 -0
  76. package/dist/src/mcp/config.js +112 -0
  77. package/dist/src/{utils/mcp-client.js → mcp/protocol.js} +1 -100
  78. package/dist/src/mcp/servers.js +90 -0
  79. package/dist/src/memory/build-memory.js +103 -0
  80. package/dist/src/memory/config/defaults.js +55 -0
  81. package/dist/src/memory/config/loader.js +29 -0
  82. package/dist/src/memory/config/paths.js +9 -0
  83. package/dist/src/memory/config/resolve.js +90 -0
  84. package/dist/src/memory/config/types.js +1 -0
  85. package/dist/src/memory/embeddings/batch-runner.js +39 -0
  86. package/dist/src/memory/embeddings/cache.js +47 -0
  87. package/dist/src/memory/embeddings/chunk-limits.js +26 -0
  88. package/dist/src/memory/embeddings/input-limits.js +48 -0
  89. package/dist/src/memory/embeddings/local.js +108 -0
  90. package/dist/src/memory/embeddings/types.js +1 -0
  91. package/dist/src/memory/index-manager.js +552 -0
  92. package/dist/src/memory/indexing/embeddings.js +67 -0
  93. package/dist/src/memory/indexing/files.js +180 -0
  94. package/dist/src/memory/indexing/index-file.js +105 -0
  95. package/dist/src/memory/log.js +38 -0
  96. package/dist/src/memory/paths.js +15 -0
  97. package/dist/src/memory/runtime/index.js +299 -0
  98. package/dist/src/memory/runtime/thread.js +116 -0
  99. package/dist/src/memory/search/fts.js +57 -0
  100. package/dist/src/memory/search/hybrid.js +50 -0
  101. package/dist/src/memory/search/text.js +30 -0
  102. package/dist/src/memory/search/vector.js +43 -0
  103. package/dist/src/memory/session/content-hash.js +7 -0
  104. package/dist/src/memory/session/entry.js +33 -0
  105. package/dist/src/memory/session/flush-policy.js +34 -0
  106. package/dist/src/memory/session/hook.js +191 -0
  107. package/dist/src/memory/session/paths.js +15 -0
  108. package/dist/src/memory/session/session-reader.js +88 -0
  109. package/dist/src/memory/session/transcript/content-hash.js +7 -0
  110. package/dist/src/memory/session/transcript/entry.js +28 -0
  111. package/dist/src/memory/session/transcript/flush.js +56 -0
  112. package/dist/src/memory/session/transcript/paths.js +28 -0
  113. package/dist/src/memory/session/transcript/reader.js +112 -0
  114. package/dist/src/memory/session/transcript/state.js +31 -0
  115. package/dist/src/memory/store/schema.js +89 -0
  116. package/dist/src/memory/store/sqlite.js +89 -0
  117. package/dist/src/memory/types.js +1 -0
  118. package/dist/src/memory/warm.js +25 -0
  119. package/dist/src/rules/discovery.js +41 -0
  120. package/dist/{tools → src/tools}/README.md +29 -3
  121. package/dist/{tools → src/tools}/apply-patch-description.md +8 -2
  122. package/dist/{tools → src/tools}/apply-patch.js +174 -104
  123. package/dist/{tools → src/tools}/apply-patch.test.js +52 -1
  124. package/dist/{tools/ask-question.js → src/tools/ask-user-question.js} +3 -3
  125. package/dist/src/tools/ast-grep.js +357 -0
  126. package/dist/src/tools/brief-write.js +122 -0
  127. package/dist/src/tools/bus-send.js +100 -0
  128. package/dist/{tools → src/tools}/call-mcp-tool.js +40 -124
  129. package/dist/src/tools/codex-apply-patch-description.md +52 -0
  130. package/dist/src/tools/codex-apply-patch.js +540 -0
  131. package/dist/{tools → src/tools}/delete.js +24 -0
  132. package/dist/src/tools/exit-plan-mode.js +83 -0
  133. package/dist/{tools → src/tools}/fetch-mcp-resource.js +56 -100
  134. package/dist/src/tools/generate-image.js +567 -0
  135. package/dist/{tools → src/tools}/glob.js +55 -1
  136. package/dist/{tools → src/tools}/list-mcp-resources.js +46 -57
  137. package/dist/{tools → src/tools}/list-mcp-tools.js +52 -63
  138. package/dist/src/tools/ls.js +48 -0
  139. package/dist/src/tools/lsp-diagnostics.js +67 -0
  140. package/dist/src/tools/lsp-symbols.js +54 -0
  141. package/dist/src/tools/mailbox.js +85 -0
  142. package/dist/src/tools/memory-get.js +90 -0
  143. package/dist/src/tools/memory-search.js +180 -0
  144. package/dist/{tools → src/tools}/plan-mode-reminder.md +3 -4
  145. package/dist/{tools → src/tools}/read-file.js +8 -19
  146. package/dist/{tools → src/tools}/rg.js +10 -20
  147. package/dist/{tools → src/tools}/shell.js +19 -42
  148. package/dist/{tools → src/tools}/subagent.js +255 -6
  149. package/dist/{tools → src/tools}/switch-mode.js +37 -6
  150. package/dist/{tools → src/tools}/web-fetch.js +105 -7
  151. package/dist/{tools → src/tools}/web-search.js +29 -1
  152. package/package.json +21 -9
  153. /package/dist/{tools → src/tools}/ask-mode-reminder.md +0 -0
  154. /package/dist/{tools → src/tools}/rg.test.js +0 -0
  155. /package/dist/{tools → src/tools}/semantic-search-description.md +0 -0
  156. /package/dist/{tools → src/tools}/semantic-search.js +0 -0
  157. /package/dist/{tools → src/tools}/shell-description.md +0 -0
  158. /package/dist/{tools → src/tools}/subagent-description.md +0 -0
@@ -61,6 +61,71 @@ function compact(value, maxLength) {
61
61
  return normalized;
62
62
  return `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
63
63
  }
64
+ function compactTail(value, maxLength) {
65
+ const normalized = value.replace(/\r/g, "");
66
+ if (normalized.length <= maxLength)
67
+ return normalized;
68
+ return `…${normalized.slice(Math.max(0, normalized.length - (maxLength - 1)))}`;
69
+ }
70
+ function safeStringify(value) {
71
+ try {
72
+ return JSON.stringify(value);
73
+ }
74
+ catch {
75
+ return String(value);
76
+ }
77
+ }
78
+ function appendTrace(stats, line) {
79
+ const trimmed = line.trim();
80
+ if (!trimmed)
81
+ return;
82
+ stats.traceLines.push(trimmed);
83
+ const TRACE_MAX_LINES = 240;
84
+ if (stats.traceLines.length > TRACE_MAX_LINES) {
85
+ stats.traceLines.splice(0, stats.traceLines.length - TRACE_MAX_LINES);
86
+ }
87
+ }
88
+ function getAssistantTraceLines(message) {
89
+ if (!message || typeof message !== "object")
90
+ return [];
91
+ const record = message;
92
+ if (record.role !== "assistant")
93
+ return [];
94
+ const parts = record.content;
95
+ if (!Array.isArray(parts))
96
+ return [];
97
+ const lines = [];
98
+ for (const part of parts) {
99
+ if (!part || typeof part !== "object")
100
+ continue;
101
+ const partRecord = part;
102
+ if (partRecord.type === "thinking" && typeof partRecord.thinking === "string") {
103
+ lines.push(`[thinking] ${partRecord.thinking}`);
104
+ continue;
105
+ }
106
+ if (partRecord.type === "toolCall") {
107
+ const name = typeof partRecord.name === "string" ? partRecord.name : "(unknown)";
108
+ const args = safeStringify(partRecord.arguments ?? {});
109
+ lines.push(`[tool_call] ${name} ${args}`);
110
+ continue;
111
+ }
112
+ if (partRecord.type === "text" && typeof partRecord.text === "string") {
113
+ lines.push(`[text] ${partRecord.text}`);
114
+ }
115
+ }
116
+ return lines;
117
+ }
118
+ function getCurrentLine(text) {
119
+ const lines = text.replace(/\r/g, "").split("\n");
120
+ const last = lines[lines.length - 1] ?? "";
121
+ if (last.trim().length > 0)
122
+ return last;
123
+ for (let i = lines.length - 2; i >= 0; i--) {
124
+ if (lines[i].trim().length > 0)
125
+ return lines[i];
126
+ }
127
+ return "";
128
+ }
64
129
  function parseBooleanLike(value) {
65
130
  if (typeof value === "boolean")
66
131
  return value;
@@ -457,10 +522,63 @@ function parseMessageMeta(message) {
457
522
  errorMessage: typeof record.errorMessage === "string" ? record.errorMessage : undefined,
458
523
  };
459
524
  }
525
+ function getToolResultText(result) {
526
+ if (!result || typeof result !== "object")
527
+ return undefined;
528
+ const record = result;
529
+ const content = record.content;
530
+ if (!Array.isArray(content))
531
+ return undefined;
532
+ const textParts = [];
533
+ for (const part of content) {
534
+ if (!part || typeof part !== "object")
535
+ continue;
536
+ const partRecord = part;
537
+ if (partRecord.type === "text" && typeof partRecord.text === "string") {
538
+ textParts.push(partRecord.text);
539
+ }
540
+ }
541
+ if (textParts.length === 0)
542
+ return undefined;
543
+ return textParts.join("\n\n").trim();
544
+ }
460
545
  function getProgressPreview(stats) {
461
- if (stats.lastAssistantText.length > 0)
462
- return compact(stats.lastAssistantText, 220);
463
- return "(running...)";
546
+ if (stats.lastAssistantText.length > 0) {
547
+ const line = getCurrentLine(stats.lastAssistantText);
548
+ if (line.length > 0)
549
+ return compactTail(line, 220);
550
+ return compactTail(stats.lastAssistantText.replace(/\s+/g, " ").trim(), 220);
551
+ }
552
+ if (stats.traceLines.length > 0) {
553
+ return compactTail(stats.traceLines[stats.traceLines.length - 1], 220);
554
+ }
555
+ return "running...";
556
+ }
557
+ function getTraceOutput(stats) {
558
+ if (stats.traceLines.length === 0)
559
+ return "";
560
+ return stats.traceLines.join("\n");
561
+ }
562
+ function getLiveOutput(stats) {
563
+ const trace = getTraceOutput(stats);
564
+ if (trace.length > 0) {
565
+ const MAX_TRACE_CHARS = 12000;
566
+ if (trace.length <= MAX_TRACE_CHARS)
567
+ return trace;
568
+ return `...\n${trace.slice(trace.length - MAX_TRACE_CHARS)}`;
569
+ }
570
+ const raw = stats.lastAssistantText.replace(/\r/g, "").trim();
571
+ if (!raw)
572
+ return "running...";
573
+ const MAX_CHARS = 8000;
574
+ if (raw.length <= MAX_CHARS)
575
+ return raw;
576
+ return `...\n${raw.slice(raw.length - MAX_CHARS)}`;
577
+ }
578
+ const RUNNING_SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
579
+ function getRunningSpinner(seed) {
580
+ const index = Math.abs(Math.floor(seed)) % RUNNING_SPINNER_FRAMES.length;
581
+ return RUNNING_SPINNER_FRAMES[index] ?? RUNNING_SPINNER_FRAMES[0];
464
582
  }
465
583
  async function runSubagentForeground(piCliPath, args, cwd, signal, onAssistantUpdate) {
466
584
  return new Promise((resolveRun, rejectRun) => {
@@ -481,6 +599,17 @@ async function runSubagentForeground(piCliPath, args, cwd, signal, onAssistantUp
481
599
  parsedEvents: 0,
482
600
  assistantMessages: 0,
483
601
  lastAssistantText: "",
602
+ traceLines: [],
603
+ };
604
+ // Throttle streaming updates to avoid excessive re-renders
605
+ let lastUpdateTime = 0;
606
+ const THROTTLE_MS = 150;
607
+ const throttledUpdate = () => {
608
+ const now = Date.now();
609
+ if (now - lastUpdateTime >= THROTTLE_MS) {
610
+ lastUpdateTime = now;
611
+ onAssistantUpdate?.({ ...stats });
612
+ }
484
613
  };
485
614
  const parseLine = (line) => {
486
615
  const trimmed = line.trim();
@@ -494,7 +623,35 @@ async function runSubagentForeground(piCliPath, args, cwd, signal, onAssistantUp
494
623
  return;
495
624
  }
496
625
  stats.parsedEvents++;
497
- if (event.type === "message_end" && event.message) {
626
+ // Streaming: capture partial assistant text for real-time preview
627
+ if (event.type === "message_update" && event.message) {
628
+ const text = getAssistantTextFromMessage(event.message);
629
+ if (text) {
630
+ stats.lastAssistantText = text;
631
+ throttledUpdate();
632
+ }
633
+ }
634
+ else if (event.type === "tool_execution_start") {
635
+ const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
636
+ stats.lastAssistantText = `[tool:${toolName}] running`;
637
+ throttledUpdate();
638
+ }
639
+ else if (event.type === "tool_execution_update") {
640
+ const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
641
+ const partialText = getToolResultText(event.partialResult);
642
+ stats.lastAssistantText = partialText ? `[tool:${toolName}] ${partialText}` : `[tool:${toolName}] running`;
643
+ throttledUpdate();
644
+ }
645
+ else if (event.type === "tool_execution_end") {
646
+ const toolName = typeof event.toolName === "string" ? event.toolName : "tool";
647
+ const resultText = getToolResultText(event.result);
648
+ stats.lastAssistantText = resultText ? `[tool:${toolName}] ${resultText}` : `[tool:${toolName}] done`;
649
+ throttledUpdate();
650
+ }
651
+ else if (event.type === "message_end" && event.message) {
652
+ for (const traceLine of getAssistantTraceLines(event.message)) {
653
+ appendTrace(stats, traceLine);
654
+ }
498
655
  const text = getAssistantTextFromMessage(event.message);
499
656
  const meta = parseMessageMeta(event.message);
500
657
  if (meta.stopReason)
@@ -750,7 +907,10 @@ async function executeTask(task, index, options) {
750
907
  selected_model: selectedModel?.modelId,
751
908
  parsed_events: 0,
752
909
  assistant_messages: 0,
753
- preview: "(running...)",
910
+ preview: "running...",
911
+ live_output: "running...",
912
+ trace_output: "",
913
+ spinner_tick: 0,
754
914
  };
755
915
  options.onProgress?.({ ...detail });
756
916
  if (isBackground) {
@@ -788,12 +948,23 @@ async function executeTask(task, index, options) {
788
948
  output_path: session.outputPath,
789
949
  started_at: new Date().toISOString(),
790
950
  });
951
+ let spinnerTimer = null;
791
952
  try {
953
+ const SPINNER_HEARTBEAT_MS = 80;
954
+ spinnerTimer = setInterval(() => {
955
+ if (detail.status !== "running")
956
+ return;
957
+ detail.spinner_tick = (detail.spinner_tick ?? 0) + 1;
958
+ options.onProgress?.({ ...detail });
959
+ }, SPINNER_HEARTBEAT_MS);
960
+ spinnerTimer.unref?.();
792
961
  const runResult = await runSubagentForeground(options.piCliPath, args, options.ctx.cwd, options.signal, (stats) => {
793
962
  detail.status = "running";
794
963
  detail.parsed_events = stats.parsedEvents;
795
964
  detail.assistant_messages = stats.assistantMessages;
796
965
  detail.preview = getProgressPreview(stats);
966
+ detail.live_output = getLiveOutput(stats);
967
+ detail.trace_output = getTraceOutput(stats);
797
968
  options.onProgress?.({ ...detail });
798
969
  });
799
970
  const finalOutput = runResult.stats.lastAssistantText.trim() || runResult.stdout.trim();
@@ -802,6 +973,7 @@ async function executeTask(task, index, options) {
802
973
  detail.parsed_events = runResult.stats.parsedEvents;
803
974
  detail.assistant_messages = runResult.stats.assistantMessages;
804
975
  detail.stderr = runResult.stderr.trim() || undefined;
976
+ detail.trace_output = getTraceOutput(runResult.stats);
805
977
  if (isError) {
806
978
  const errorText = runResult.stats.errorMessage || runResult.stderr.trim() || finalOutput || "Subagent execution failed.";
807
979
  detail.status = "failed";
@@ -822,6 +994,7 @@ async function executeTask(task, index, options) {
822
994
  detail.status = "completed";
823
995
  detail.final_output = finalOutput || "(no output)";
824
996
  detail.preview = compact(detail.final_output, 220);
997
+ detail.live_output = undefined;
825
998
  writeSubagentState(session.statePath, {
826
999
  id: session.id,
827
1000
  status: "completed",
@@ -840,6 +1013,7 @@ async function executeTask(task, index, options) {
840
1013
  detail.exit_code = 1;
841
1014
  detail.stderr = message;
842
1015
  detail.preview = compact(message, 220);
1016
+ detail.live_output = undefined;
843
1017
  detail.final_output = message;
844
1018
  writeSubagentState(session.statePath, {
845
1019
  id: session.id,
@@ -853,6 +1027,11 @@ async function executeTask(task, index, options) {
853
1027
  });
854
1028
  return detail;
855
1029
  }
1030
+ finally {
1031
+ if (spinnerTimer) {
1032
+ clearInterval(spinnerTimer);
1033
+ }
1034
+ }
856
1035
  }
857
1036
  export default function (pi) {
858
1037
  pi.registerTool({
@@ -895,6 +1074,76 @@ export default function (pi) {
895
1074
  }
896
1075
  return new Text(text, 0, 0);
897
1076
  },
1077
+ renderResult(result, { expanded, isPartial }, theme) {
1078
+ const details = result.details;
1079
+ if (!details) {
1080
+ const raw = result.content?.map((c) => c.text ?? "").join("") ?? "";
1081
+ return new Text(raw || "(no output)", 0, 0);
1082
+ }
1083
+ const isParallel = details.mode === "parallel";
1084
+ const hasFailed = details.failed_tasks > 0;
1085
+ // Streaming: collapsed shows one-line preview, expanded shows live multiline output.
1086
+ if (isPartial) {
1087
+ if (expanded) {
1088
+ const lines = [];
1089
+ for (const task of details.results) {
1090
+ if (isParallel) {
1091
+ const spinner = getRunningSpinner((task.spinner_tick ?? 0) + task.task_index);
1092
+ lines.push(theme.fg("warning", `${spinner} [${task.task_index + 1}] ${task.description}`));
1093
+ }
1094
+ const live = task.trace_output ?? task.live_output ?? task.preview ?? "running...";
1095
+ lines.push(live);
1096
+ if (isParallel && task.task_index < details.results.length - 1)
1097
+ lines.push("");
1098
+ }
1099
+ return new Text(lines.join("\n"), 0, 0);
1100
+ }
1101
+ if (isParallel) {
1102
+ const running = details.results.filter((r) => r.status === "running");
1103
+ const done = details.completed_tasks;
1104
+ const label = `${done}/${details.total_tasks}`;
1105
+ const preview = running.length > 0 ? compact(running[0].preview ?? "", 90) : "...";
1106
+ const seed = running.length > 0 ? (running[0]?.spinner_tick ?? 0) : 0;
1107
+ const spinner = getRunningSpinner(seed);
1108
+ return new Text(theme.fg("warning", `${spinner} ${label} `) + theme.fg("dim", preview), 0, 0);
1109
+ }
1110
+ const task = details.results[0];
1111
+ const preview = task?.preview ?? "running...";
1112
+ const spinner = getRunningSpinner(task?.spinner_tick ?? 0);
1113
+ return new Text(theme.fg("warning", `${spinner} `) + theme.fg("dim", compact(preview, 110)), 0, 0);
1114
+ }
1115
+ // Completed, collapsed: compact summary
1116
+ if (!expanded) {
1117
+ if (isParallel) {
1118
+ const icon = hasFailed ? theme.fg("error", "✗") : theme.fg("success", "✓");
1119
+ const summary = `${details.completed_tasks}/${details.total_tasks} completed`;
1120
+ const failText = hasFailed ? `, ${details.failed_tasks} failed` : "";
1121
+ return new Text(`${icon} ${summary}${failText}`, 0, 0);
1122
+ }
1123
+ const task = details.results[0];
1124
+ const icon = task?.status === "failed" ? theme.fg("error", "✗") : theme.fg("success", "✓");
1125
+ const preview = compact(task?.final_output ?? task?.preview ?? "(no output)", 110);
1126
+ return new Text(`${icon} ${preview}`, 0, 0);
1127
+ }
1128
+ // Expanded: full output
1129
+ const lines = [];
1130
+ for (const task of details.results) {
1131
+ if (isParallel) {
1132
+ const icon = task.status === "failed" ? "✗" : "✓";
1133
+ lines.push(theme.fg(task.status === "failed" ? "error" : "success", `${icon} [${task.task_index + 1}] ${task.description}`));
1134
+ }
1135
+ if (task.trace_output && task.trace_output.trim().length > 0) {
1136
+ lines.push(task.trace_output);
1137
+ if (task.final_output && task.final_output.trim().length > 0)
1138
+ lines.push("");
1139
+ }
1140
+ const output = task.final_output ?? task.preview ?? "(no output)";
1141
+ lines.push(output);
1142
+ if (isParallel && task.task_index < details.results.length - 1)
1143
+ lines.push("");
1144
+ }
1145
+ return new Text(lines.join("\n"), 0, 0);
1146
+ },
898
1147
  async execute(_toolCallId, params, signal, onUpdate, ctx) {
899
1148
  try {
900
1149
  const piCliPath = resolvePiCliPath();
@@ -963,7 +1212,7 @@ export default function (pi) {
963
1212
  onProgress: (detail) => {
964
1213
  lastProgress = detail;
965
1214
  onUpdate?.({
966
- content: [{ type: "text", text: detail.preview ?? "(running...)" }],
1215
+ content: [{ type: "text", text: detail.preview ?? "running..." }],
967
1216
  details: buildSubagentDetails("single", [detail]),
968
1217
  });
969
1218
  },
@@ -1,6 +1,8 @@
1
1
  import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
2
2
  import { Type } from "@sinclair/typebox";
3
- import { createModeStateData, deriveInitialModeState, modeRuntimeStore, MODE_STATE_ENTRY_TYPE, parseModeStateEntry, } from "../src/extensions/mode-runtime.js";
3
+ import { isAbsolute, resolve } from "node:path";
4
+ import process from "node:process";
5
+ import { createModeStateData, deriveInitialModeState, modeRuntimeStore, MODE_STATE_ENTRY_TYPE, parseModeStateEntry, } from "../extensions/mode-runtime.js";
4
6
  const MODE_STATUS_KEY = "mono-pilot-mode";
5
7
  const DESCRIPTION = `Switch the interaction mode to better match the current task. Each mode is optimized for a specific type of work.
6
8
 
@@ -60,7 +62,20 @@ const switchModeSchema = Type.Object({
60
62
  explanation: Type.Optional(Type.String({
61
63
  description: "Optional explanation for why the mode switch is requested. This helps the user understand why you're switching modes.",
62
64
  })),
65
+ plan_file: Type.Optional(Type.String({
66
+ description: "Optional plan file path for Plan mode. If relative, it is resolved against the current workspace.",
67
+ })),
63
68
  });
69
+ function normalizePlanFilePath(value) {
70
+ if (typeof value !== "string") {
71
+ return undefined;
72
+ }
73
+ const trimmed = value.trim();
74
+ if (trimmed.length === 0) {
75
+ return undefined;
76
+ }
77
+ return isAbsolute(trimmed) ? trimmed : resolve(process.cwd(), trimmed);
78
+ }
64
79
  function sanitizeStatusText(text) {
65
80
  return text
66
81
  .replace(/[\r\n\t]/g, " ")
@@ -261,8 +276,8 @@ function updateModeStatus(ctx) {
261
276
  function persistModeState(pi) {
262
277
  pi.appendEntry(MODE_STATE_ENTRY_TYPE, createModeStateData(modeRuntimeStore.getSnapshot()));
263
278
  }
264
- function setMode(pi, nextMode, ctx) {
265
- const { changed } = modeRuntimeStore.setMode(nextMode);
279
+ function setMode(pi, nextMode, ctx, options) {
280
+ const { changed } = modeRuntimeStore.setMode(nextMode, options);
266
281
  if (changed) {
267
282
  persistModeState(pi);
268
283
  }
@@ -318,16 +333,32 @@ export default function switchModeExtension(pi) {
318
333
  description: DESCRIPTION,
319
334
  parameters: switchModeSchema,
320
335
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
321
- const { changed } = setMode(pi, "plan", ctx);
336
+ const before = modeRuntimeStore.getSnapshot();
337
+ const planFilePath = normalizePlanFilePath(params.plan_file);
338
+ const { changed } = setMode(pi, "plan", ctx, { planFilePath });
339
+ const after = modeRuntimeStore.getSnapshot();
322
340
  const explanation = params.explanation?.trim();
323
341
  const details = {
324
342
  active_mode: "plan",
325
343
  explanation: explanation || undefined,
344
+ plan_file: after.planFilePath,
326
345
  };
327
346
  const explanationSuffix = explanation ? ` Reason: ${explanation}` : "";
328
- const prefix = changed ? "Switched to Plan mode." : "Plan mode is already active.";
347
+ const modeChanged = before.activeMode !== after.activeMode;
348
+ const planFileChanged = before.planFilePath !== after.planFilePath;
349
+ let prefix;
350
+ if (modeChanged) {
351
+ prefix = "Switched to Plan mode.";
352
+ }
353
+ else if (planFileChanged) {
354
+ prefix = "Plan mode is already active. Updated plan file.";
355
+ }
356
+ else {
357
+ prefix = changed ? "Switched to Plan mode." : "Plan mode is already active.";
358
+ }
359
+ const planFileSuffix = after.planFilePath ? ` Plan file: ${after.planFilePath}` : "";
329
360
  return {
330
- content: [{ type: "text", text: `${prefix}${explanationSuffix}` }],
361
+ content: [{ type: "text", text: `${prefix}${planFileSuffix}${explanationSuffix}` }],
331
362
  details,
332
363
  };
333
364
  },
@@ -1,20 +1,26 @@
1
1
  import { lookup } from "node:dns/promises";
2
2
  import { isIP } from "node:net";
3
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail, } from "@mariozechner/pi-coding-agent";
3
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, keyHint, truncateTail, } from "@mariozechner/pi-coding-agent";
4
+ import { Text } from "@mariozechner/pi-tui";
4
5
  import { Type } from "@sinclair/typebox";
5
6
  const DESCRIPTION = `Fetch content from a specified URL and return its contents in a readable markdown format. Use this tool when you need to retrieve and analyze web content.
6
7
 
7
8
  - The URL must be a fully-formed, valid URL.
8
9
  - This tool is read-only and will not work for requests intended to have side effects.
9
10
  - This fetch tries to return live results but may return previously cached content.
10
- - This fetch runs from an isolated server - hosts like localhost or private IPs will not work.
11
+ - This fetch runs in the current runtime network context, and blocks localhost/private IP targets for safety.
11
12
  - Authentication is not supported, and an error will be returned if the URL requires authentication.
12
13
  - If the URL is returning a non-200 status code, e.g. 404, the tool will not return the content and will instead return an error message.
13
- - The tool prefers markdown content negotiation via \`Accept: text/markdown\` when supported by the target site, and falls back to HTML-to-markdown conversion.
14
+ - The tool prefers readable text output and converts HTML responses into markdown-like text when needed.
14
15
  - If present, metadata like \`x-markdown-tokens\` and \`content-signal\` may be returned in tool details.`;
15
16
  const REQUEST_TIMEOUT_MS = 20_000;
16
17
  const CACHE_TTL_MS = 5 * 60 * 1000;
17
- const ACCEPT_MARKDOWN_HEADER = "text/markdown, text/html;q=0.9, application/xhtml+xml;q=0.8, application/json;q=0.7, text/plain;q=0.6, */*;q=0.5";
18
+ const DEFAULT_BROWSER_ACCEPT = "text/markdown, text/html;q=0.95, application/xhtml+xml;q=0.9, application/xml;q=0.85, image/avif,image/webp,*/*;q=0.8";
19
+ const DEFAULT_BROWSER_ACCEPT_LANGUAGE = "en-US,en;q=0.9";
20
+ const DEFAULT_BROWSER_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";
21
+ const WECHAT_MOBILE_ACCEPT = "text/markdown, text/html;q=0.95, application/xhtml+xml;q=0.9, application/xml;q=0.85,*/*;q=0.8";
22
+ const WECHAT_MOBILE_ACCEPT_LANGUAGE = "zh-CN,zh;q=0.9";
23
+ const WECHAT_MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/7.0.20(0x17001422) NetType/WIFI Language/zh_CN";
18
24
  const webFetchSchema = Type.Object({
19
25
  url: Type.String({
20
26
  description: "The URL to fetch. The content will be converted to a readable markdown format.",
@@ -190,12 +196,29 @@ function normalizeFetchedBody(rawText, contentType) {
190
196
  body: rawText.trim(),
191
197
  };
192
198
  }
199
+ function getFetchProfile(url) {
200
+ if (url.hostname.toLowerCase() === "mp.weixin.qq.com") {
201
+ return {
202
+ name: "wechat_mobile",
203
+ accept: WECHAT_MOBILE_ACCEPT,
204
+ acceptLanguage: WECHAT_MOBILE_ACCEPT_LANGUAGE,
205
+ userAgent: WECHAT_MOBILE_USER_AGENT,
206
+ };
207
+ }
208
+ return {
209
+ name: "browser_default",
210
+ accept: DEFAULT_BROWSER_ACCEPT,
211
+ acceptLanguage: DEFAULT_BROWSER_ACCEPT_LANGUAGE,
212
+ userAgent: DEFAULT_BROWSER_USER_AGENT,
213
+ };
214
+ }
193
215
  function formatFetchedOutput(entry) {
194
216
  const lines = [];
195
217
  lines.push(`Source URL: ${entry.finalUrl}`);
196
218
  if (entry.url !== entry.finalUrl) {
197
219
  lines.push(`Requested URL: ${entry.url}`);
198
220
  }
221
+ lines.push(`Request profile: ${entry.requestProfile}`);
199
222
  lines.push(`Fetched at: ${entry.fetchedAtIso}`);
200
223
  lines.push(`Content-Type: ${entry.contentType || "unknown"}`);
201
224
  lines.push(`Markdown negotiated: ${entry.markdownNegotiated ? "yes" : "no"}`);
@@ -209,6 +232,27 @@ function formatFetchedOutput(entry) {
209
232
  lines.push(entry.markdownContent.length > 0 ? entry.markdownContent : "(No readable text content found.)");
210
233
  return lines.join("\n");
211
234
  }
235
+ function isBlockedInterstital(content) {
236
+ const finalUrlLower = content.finalUrl.toLowerCase();
237
+ const textLower = content.rawText.toLowerCase();
238
+ const looksLikeWechatCaptcha = finalUrlLower.includes("wappoc_appmsgcaptcha") ||
239
+ textLower.includes("wappoc_appmsgcaptcha") ||
240
+ (content.contentType.includes("text/html") && textLower.includes("环境异常") && textLower.includes("去验证"));
241
+ if (looksLikeWechatCaptcha) {
242
+ return {
243
+ blockedBy: "wechat_risk_control",
244
+ message: "WebFetch blocked by WeChat risk-control/captcha interstitial. Try local fetch with WeChat mobile UA or provide article text manually.",
245
+ };
246
+ }
247
+ return undefined;
248
+ }
249
+ function isBlockedCachedEntry(entry) {
250
+ return isBlockedInterstital({
251
+ finalUrl: entry.finalUrl,
252
+ contentType: entry.contentType,
253
+ rawText: entry.markdownContent,
254
+ }) !== undefined;
255
+ }
212
256
  function getCachedEntry(url) {
213
257
  const cached = responseCache.get(url);
214
258
  if (!cached)
@@ -217,13 +261,18 @@ function getCachedEntry(url) {
217
261
  responseCache.delete(url);
218
262
  return undefined;
219
263
  }
264
+ if (isBlockedCachedEntry(cached)) {
265
+ responseCache.delete(url);
266
+ return undefined;
267
+ }
220
268
  return cached;
221
269
  }
222
- function formatErrorResult(url, error) {
270
+ function formatErrorResult(url, error, extraDetails) {
223
271
  return {
224
272
  content: [{ type: "text", text: error }],
225
273
  details: {
226
274
  url,
275
+ ...extraDetails,
227
276
  error,
228
277
  },
229
278
  };
@@ -235,6 +284,32 @@ export default function webFetchExtension(pi) {
235
284
  label: "WebFetch",
236
285
  description: DESCRIPTION,
237
286
  parameters: webFetchSchema,
287
+ renderCall(args, theme) {
288
+ const input = args;
289
+ const url = typeof input.url === "string" && input.url.trim().length > 0 ? input.url.trim() : "(missing url)";
290
+ const displayUrl = url.length > 120 ? `${url.slice(0, 119)}…` : url;
291
+ let text = theme.fg("toolTitle", theme.bold("WebFetch"));
292
+ text += ` ${theme.fg("toolOutput", displayUrl)}`;
293
+ return new Text(text, 0, 0);
294
+ },
295
+ renderResult(result, { expanded, isPartial }, theme) {
296
+ if (isPartial) {
297
+ return new Text(theme.fg("muted", "Fetching URL..."), 0, 0);
298
+ }
299
+ const textBlock = result.content.find((entry) => entry.type === "text" && typeof entry.text === "string");
300
+ if (!textBlock) {
301
+ return new Text(theme.fg("error", "No text result returned."), 0, 0);
302
+ }
303
+ const fullText = textBlock.text;
304
+ const lineCount = fullText.split("\n").length;
305
+ if (!expanded) {
306
+ const summary = `${lineCount} lines (click or ${keyHint("expandTools", "to expand")})`;
307
+ return new Text(theme.fg("muted", summary), 0, 0);
308
+ }
309
+ let text = fullText.split("\n").map((line) => theme.fg("toolOutput", line)).join("\n");
310
+ text += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
311
+ return new Text(text, 0, 0);
312
+ },
238
313
  async execute(_toolCallId, params, signal) {
239
314
  let parsedUrl;
240
315
  try {
@@ -258,6 +333,7 @@ export default function webFetchExtension(pi) {
258
333
  details: {
259
334
  url: normalizedUrl,
260
335
  final_url: cached.finalUrl,
336
+ request_profile: cached.requestProfile,
261
337
  status: cached.status,
262
338
  content_type: cached.contentType,
263
339
  markdown_negotiated: cached.markdownNegotiated,
@@ -274,14 +350,16 @@ export default function webFetchExtension(pi) {
274
350
  const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
275
351
  const onAbort = () => controller.abort();
276
352
  signal?.addEventListener("abort", onAbort, { once: true });
353
+ const fetchProfile = getFetchProfile(parsedUrl);
277
354
  try {
278
355
  const response = await fetch(normalizedUrl, {
279
356
  method: "GET",
280
357
  redirect: "follow",
281
358
  signal: controller.signal,
282
359
  headers: {
283
- Accept: ACCEPT_MARKDOWN_HEADER,
284
- "User-Agent": "mono-pilot-web-fetch/0.1",
360
+ Accept: fetchProfile.accept,
361
+ "Accept-Language": fetchProfile.acceptLanguage,
362
+ "User-Agent": fetchProfile.userAgent,
285
363
  },
286
364
  });
287
365
  const finalUrl = response.url || normalizedUrl;
@@ -300,6 +378,24 @@ export default function webFetchExtension(pi) {
300
378
  const markdownTokens = parsePositiveInt(response.headers.get("x-markdown-tokens"));
301
379
  const contentSignal = response.headers.get("content-signal") ?? undefined;
302
380
  const rawText = await response.text();
381
+ const blocked = isBlockedInterstital({
382
+ finalUrl: finalParsed.toString(),
383
+ contentType,
384
+ rawText,
385
+ });
386
+ if (blocked) {
387
+ return formatErrorResult(normalizedUrl, blocked.message, {
388
+ final_url: finalParsed.toString(),
389
+ request_profile: fetchProfile.name,
390
+ status: response.status,
391
+ content_type: contentType,
392
+ markdown_negotiated: markdownNegotiated,
393
+ markdown_tokens: markdownTokens,
394
+ content_signal: contentSignal,
395
+ blocked_by: blocked.blockedBy,
396
+ bytes_received: Buffer.byteLength(rawText, "utf-8"),
397
+ });
398
+ }
303
399
  const normalizedBody = normalizeFetchedBody(rawText, contentType);
304
400
  const markdownBody = normalizedBody.title
305
401
  ? `# ${normalizedBody.title}\n\n${normalizedBody.body}`.trim()
@@ -308,6 +404,7 @@ export default function webFetchExtension(pi) {
308
404
  const cacheEntry = {
309
405
  url: normalizedUrl,
310
406
  finalUrl: finalParsed.toString(),
407
+ requestProfile: fetchProfile.name,
311
408
  status: response.status,
312
409
  contentType,
313
410
  markdownNegotiated,
@@ -329,6 +426,7 @@ export default function webFetchExtension(pi) {
329
426
  details: {
330
427
  url: normalizedUrl,
331
428
  final_url: cacheEntry.finalUrl,
429
+ request_profile: cacheEntry.requestProfile,
332
430
  status: cacheEntry.status,
333
431
  content_type: cacheEntry.contentType,
334
432
  markdown_negotiated: cacheEntry.markdownNegotiated,
@@ -1,4 +1,5 @@
1
- import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateTail, } from "@mariozechner/pi-coding-agent";
1
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, keyHint, truncateTail, } from "@mariozechner/pi-coding-agent";
2
+ import { Text } from "@mariozechner/pi-tui";
2
3
  import { Type } from "@sinclair/typebox";
3
4
  const DESCRIPTION = "Search web for real-time info on any topic; use for up-to-date facts not in training data, like current events or tech updates. Results include snippets and URLs.";
4
5
  const PROVIDER_NAME = "Brave Search API";
@@ -190,6 +191,33 @@ export default function webSearchExtension(pi) {
190
191
  label: "WebSearch",
191
192
  description: DESCRIPTION,
192
193
  parameters: webSearchSchema,
194
+ renderCall(args, theme) {
195
+ const input = args;
196
+ const term = typeof input.search_term === "string" && input.search_term.trim().length > 0 ? input.search_term.trim() : "(missing search_term)";
197
+ const displayTerm = term.length > 120 ? `${term.slice(0, 119)}…` : term;
198
+ let text = theme.fg("toolTitle", theme.bold("WebSearch"));
199
+ text += ` ${theme.fg("toolOutput", displayTerm)}`;
200
+ return new Text(text, 0, 0);
201
+ },
202
+ renderResult(result, { expanded, isPartial }, theme) {
203
+ if (isPartial) {
204
+ return new Text(theme.fg("muted", "Searching..."), 0, 0);
205
+ }
206
+ const textBlock = result.content.find((entry) => entry.type === "text" && typeof entry.text === "string");
207
+ if (!textBlock) {
208
+ return new Text(theme.fg("error", "No text result returned."), 0, 0);
209
+ }
210
+ const fullText = textBlock.text;
211
+ const details = result.details;
212
+ const count = details?.result_count ?? 0;
213
+ if (!expanded) {
214
+ const summary = `${count} results (click or ${keyHint("expandTools", "to expand")})`;
215
+ return new Text(theme.fg("muted", summary), 0, 0);
216
+ }
217
+ let text = fullText.split("\n").map((line) => theme.fg("toolOutput", line)).join("\n");
218
+ text += theme.fg("muted", `\n(click or ${keyHint("expandTools", "to collapse")})`);
219
+ return new Text(text, 0, 0);
220
+ },
193
221
  async execute(_toolCallId, params, signal) {
194
222
  const query = normalizeSearchTerm(params.search_term);
195
223
  if (query.length === 0) {