@herbertgao/pi-extensions 2026.8.14 → 2026.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/README.md +19 -13
  2. package/THIRD_PARTY_NOTICES.md +26 -1
  3. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +8 -1
  4. package/node_modules/@czottmann/pi-automode/README.md +4 -0
  5. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +71 -9
  6. package/node_modules/@czottmann/pi-automode/package.json +1 -1
  7. package/node_modules/@herbertgao/pi-bark/LICENSE +21 -0
  8. package/node_modules/@herbertgao/pi-bark/README.md +44 -0
  9. package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
  10. package/node_modules/@herbertgao/pi-bark/package.json +53 -0
  11. package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +20 -0
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +46 -3
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
  15. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +6 -0
  16. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +5 -0
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -30
  18. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  19. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  20. package/node_modules/@narumitw/pi-btw/README.md +129 -88
  21. package/node_modules/@narumitw/pi-btw/dist/index.ts +283 -59
  22. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  23. package/node_modules/@narumitw/pi-btw/package.json +5 -8
  24. package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
  25. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +264 -24
  26. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
  27. package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
  28. package/node_modules/@narumitw/pi-btw/src/text.ts +18 -0
  29. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +5 -16
  30. package/node_modules/@narumitw/pi-caffeinate/LICENSE +21 -0
  31. package/node_modules/@narumitw/pi-caffeinate/README.md +224 -0
  32. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +1112 -0
  33. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +7 -0
  34. package/node_modules/@narumitw/pi-caffeinate/package.json +53 -0
  35. package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +811 -0
  36. package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +153 -0
  37. package/node_modules/@narumitw/pi-caffeinate/src/index.ts +1 -0
  38. package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +43 -0
  39. package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +167 -0
  40. package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +202 -0
  41. package/node_modules/@tifan/pi-handoff/README.md +39 -13
  42. package/node_modules/@tifan/pi-handoff/package.json +2 -2
  43. package/node_modules/@tifan/pi-handoff/src/index.ts +166 -5
  44. package/node_modules/@tifan/pi-recap/README.md +1 -1
  45. package/node_modules/@tifan/pi-recap/package.json +2 -2
  46. package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
  47. package/node_modules/@tifan/pi-rename/README.md +42 -21
  48. package/node_modules/@tifan/pi-rename/package.json +1 -1
  49. package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
  50. package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
  51. package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
  52. package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
  53. package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
  54. package/node_modules/pi-lens/CHANGELOG.md +320 -0
  55. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
  56. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
  57. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
  58. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
  59. package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
  60. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
  61. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
  62. package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
  63. package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
  64. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
  65. package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
  66. package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
  67. package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
  68. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
  69. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
  70. package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
  71. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
  72. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
  73. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
  74. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
  75. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
  76. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
  77. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
  93. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
  94. package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
  95. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
  96. package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
  97. package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
  98. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
  99. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
  100. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
  101. package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
  102. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
  103. package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
  104. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
  105. package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
  106. package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
  107. package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
  108. package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
  109. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  110. package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
  111. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
  112. package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
  113. package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
  114. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
  115. package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
  116. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
  117. package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
  118. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
  119. package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
  120. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
  121. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
  122. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  123. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
  124. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
  125. package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
  126. package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
  127. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
  128. package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
  129. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
  130. package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
  131. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
  132. package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
  133. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
  134. package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
  135. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  136. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
  137. package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
  138. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
  139. package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
  140. package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
  141. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
  142. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
  143. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
  144. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
  145. package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
  146. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
  147. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
  148. package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
  149. package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
  150. package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
  151. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
  152. package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
  153. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
  154. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
  155. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
  156. package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
  157. package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
  158. package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
  159. package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
  160. package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
  161. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
  162. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
  163. package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
  164. package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
  165. package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
  166. package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
  167. package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
  168. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
  169. package/node_modules/pi-lens/dist/index.js +11243 -6651
  170. package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
  171. package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
  172. package/node_modules/pi-lens/dist/mcp/server.js +6 -1
  173. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
  174. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
  175. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
  176. package/node_modules/pi-lens/docs/agent-guide.md +4 -2
  177. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
  178. package/node_modules/pi-lens/docs/astplayground.md +26 -17
  179. package/node_modules/pi-lens/package.json +5 -1
  180. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  181. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  182. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  183. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  184. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  185. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  186. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  187. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  188. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  189. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  190. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  191. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  192. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  193. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  194. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  195. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  196. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  197. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  198. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  199. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  200. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  201. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  202. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  203. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  204. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  205. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  206. package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  207. package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  208. package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  209. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  210. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  211. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  212. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  213. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  214. package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  215. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  216. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  217. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  218. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  219. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  220. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  221. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  222. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  223. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
  224. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  225. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  226. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  227. package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  228. package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  229. package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  230. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  231. package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  232. package/node_modules/pi-mcp-adapter/CHANGELOG.md +42 -0
  233. package/node_modules/pi-mcp-adapter/README.md +15 -15
  234. package/node_modules/pi-mcp-adapter/commands.ts +59 -22
  235. package/node_modules/pi-mcp-adapter/config.ts +18 -4
  236. package/node_modules/pi-mcp-adapter/direct-tools.ts +26 -6
  237. package/node_modules/pi-mcp-adapter/dist/config.d.ts +4 -0
  238. package/node_modules/pi-mcp-adapter/dist/config.js +13 -4
  239. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  240. package/node_modules/pi-mcp-adapter/dist/types.d.ts +12 -4
  241. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  242. package/node_modules/pi-mcp-adapter/error-signal.ts +15 -4
  243. package/node_modules/pi-mcp-adapter/errors.ts +141 -0
  244. package/node_modules/pi-mcp-adapter/host-html-template.ts +58 -5
  245. package/node_modules/pi-mcp-adapter/index.ts +2 -3
  246. package/node_modules/pi-mcp-adapter/init.ts +5 -0
  247. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +105 -32
  248. package/node_modules/pi-mcp-adapter/mcp-panel.ts +30 -9
  249. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +66 -28
  250. package/node_modules/pi-mcp-adapter/mcp-status.ts +2 -0
  251. package/node_modules/pi-mcp-adapter/package.json +2 -1
  252. package/node_modules/pi-mcp-adapter/proxy-modes.ts +88 -25
  253. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +217 -0
  254. package/node_modules/pi-mcp-adapter/server-manager.ts +343 -6
  255. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +1 -0
  256. package/node_modules/pi-mcp-adapter/types.ts +19 -4
  257. package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +18 -2
  258. package/node_modules/pi-mcp-adapter/ui-server.ts +179 -54
  259. package/node_modules/pi-mcp-adapter/ui-session.ts +28 -8
  260. package/node_modules/pi-web-access/CHANGELOG.md +20 -0
  261. package/node_modules/pi-web-access/README.md +15 -2
  262. package/node_modules/pi-web-access/extract.ts +57 -12
  263. package/node_modules/pi-web-access/github-extract.ts +47 -3
  264. package/node_modules/pi-web-access/index.ts +1 -1
  265. package/node_modules/pi-web-access/package.json +1 -1
  266. package/node_modules/pi-web-access/page-query.ts +61 -8
  267. package/package.json +21 -17
  268. package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
  269. package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
  270. package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
@@ -12,6 +12,11 @@ import type { ConsentManager } from "./consent-manager.ts";
12
12
  import { ServerError, wrapError } from "./errors.ts";
13
13
  import { formatAuthRequiredMessage, normalizeToolArguments } from "./utils.ts";
14
14
  import { buildHostHtmlTemplate, buildCspMetaContent } from "./host-html-template.ts";
15
+ import {
16
+ buildSandboxProxyCsp,
17
+ buildSandboxProxyHtml,
18
+ SANDBOX_PROXY_PATH,
19
+ } from "./sandbox-proxy-template.ts";
15
20
  import { logger } from "./logger.ts";
16
21
  import type { McpServerManager } from "./server-manager.ts";
17
22
  import type { McpExtensionState } from "./state.ts";
@@ -99,6 +104,11 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
99
104
  const eventLog: Array<{ id: number; name: string; payload: unknown }> = [];
100
105
  let latestCheckpointEventId: number | undefined;
101
106
  let streamSummary: UiStreamSummary | undefined;
107
+ let server: http.Server | null = null;
108
+ let sandboxProxyServer: http.Server | null = null;
109
+ let sandboxProxyUrl: string | null = null;
110
+ let closeTimer: NodeJS.Timeout | null = null;
111
+ let listenersClosed = false;
102
112
 
103
113
  // Track messages from UI for retrieval
104
114
  const sessionMessages: UiSessionMessages = {
@@ -266,6 +276,32 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
266
276
  watchdog = null;
267
277
  };
268
278
 
279
+ const closeListeners = () => {
280
+ if (listenersClosed) return;
281
+ listenersClosed = true;
282
+ stopWatchdog();
283
+ if (closeTimer) {
284
+ clearTimeout(closeTimer);
285
+ closeTimer = null;
286
+ }
287
+ try {
288
+ server?.close();
289
+ } catch {}
290
+ try {
291
+ sandboxProxyServer?.close();
292
+ } catch {}
293
+ closeSse();
294
+ };
295
+
296
+ const scheduleListenerClose = () => {
297
+ if (closeTimer || listenersClosed) return;
298
+ closeTimer = setTimeout(() => {
299
+ closeTimer = null;
300
+ closeListeners();
301
+ }, 20);
302
+ closeTimer.unref();
303
+ };
304
+
269
305
  const markCompleted = (reason: string) => {
270
306
  if (completed) return;
271
307
  log.debug("Session completed", { reason });
@@ -273,9 +309,10 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
273
309
  completed = true;
274
310
  stopWatchdog();
275
311
  options.onComplete?.(reason);
312
+ scheduleListenerClose();
276
313
  };
277
314
 
278
- const server = http.createServer(async (req, res) => {
315
+ const hostServer = http.createServer(async (req, res) => {
279
316
  try {
280
317
  const method = req.method || "GET";
281
318
  const hostHeader = req.headers.host;
@@ -302,6 +339,10 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
302
339
  }
303
340
  if (!validateTokenQuery(url, sessionToken, res)) return;
304
341
  touchHeartbeat();
342
+ if (!sandboxProxyUrl) {
343
+ sendText(res, 503, "Sandbox proxy is not ready");
344
+ return;
345
+ }
305
346
 
306
347
  const html = buildHostHtmlTemplate({
307
348
  sessionToken,
@@ -314,6 +355,7 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
314
355
  requireToolConsent: options.consentManager.requiresPrompt(options.serverName),
315
356
  cacheToolConsent: options.consentManager.shouldCacheConsent(),
316
357
  hostContext,
358
+ sandboxProxyUrl,
317
359
  });
318
360
 
319
361
  res.writeHead(200, {
@@ -498,9 +540,15 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
498
540
  ...(options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}),
499
541
  },
500
542
  options.serverName,
501
- (conn) => conn.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName)),
543
+ async (conn) => {
544
+ await options.manager.ensureListen?.(options.serverName, conn);
545
+ return conn.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName));
546
+ },
502
547
  )
503
- : await connection.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName));
548
+ : await (async () => {
549
+ await options.manager.ensureListen?.(options.serverName, connection);
550
+ return connection.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName));
551
+ })();
504
552
  sendJson(res, 200, { ok: true, result });
505
553
  } finally {
506
554
  options.manager.decrementInFlight(options.serverName);
@@ -614,12 +662,6 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
614
662
  : "done";
615
663
  markCompleted(reason);
616
664
  sendJson(res, 200, { ok: true, result: {} });
617
- setTimeout(() => {
618
- try {
619
- server.close();
620
- } catch {}
621
- closeSse();
622
- }, 20).unref();
623
665
  return;
624
666
  }
625
667
 
@@ -641,6 +683,7 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
641
683
  sendJson(res, status, { ok: false, error: wrapped.message });
642
684
  }
643
685
  });
686
+ server = hostServer;
644
687
 
645
688
  if (options.initialResultPromise) {
646
689
  options.initialResultPromise.then(
@@ -656,10 +699,6 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
656
699
  if (completed) return;
657
700
  if (Date.now() - lastHeartbeatAt <= ABANDONED_GRACE_MS) return;
658
701
  markCompleted("stale");
659
- try {
660
- server.close();
661
- } catch {}
662
- closeSse();
663
702
  }, WATCHDOG_INTERVAL_MS);
664
703
  watchdog.unref();
665
704
 
@@ -667,13 +706,79 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
667
706
  const candidates = resolvePortCandidates(options.port);
668
707
  let candidateIndex = 0;
669
708
 
709
+ const startSandboxProxy = (parentOrigin: string): Promise<number> => {
710
+ const proxy = http.createServer((req, res) => {
711
+ try {
712
+ const method = req.method || "GET";
713
+ const hostHeader = req.headers.host;
714
+ const url = new URL(req.url || "/", `http://${hostHeader || "127.0.0.1"}`);
715
+ if (hostHeader !== undefined && !isAllowedHost(url.hostname)) {
716
+ sendText(res, 403, "Invalid host");
717
+ return;
718
+ }
719
+
720
+ if (method === "HEAD" && url.pathname === SANDBOX_PROXY_PATH) {
721
+ res.writeHead(200, {
722
+ "Content-Type": "text/html; charset=utf-8",
723
+ "Cache-Control": "no-store",
724
+ "Content-Security-Policy": buildSandboxProxyCsp(),
725
+ });
726
+ res.end();
727
+ return;
728
+ }
729
+
730
+ if (method === "GET" && url.pathname === SANDBOX_PROXY_PATH) {
731
+ res.writeHead(200, {
732
+ "Content-Type": "text/html; charset=utf-8",
733
+ "Cache-Control": "no-store",
734
+ "Content-Security-Policy": buildSandboxProxyCsp(),
735
+ "Referrer-Policy": "no-referrer",
736
+ "X-Content-Type-Options": "nosniff",
737
+ });
738
+ res.end(buildSandboxProxyHtml({ parentOrigin }));
739
+ return;
740
+ }
741
+
742
+ sendJson(res, 404, { ok: false, error: "Not found" });
743
+ } catch (error) {
744
+ sendJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) });
745
+ }
746
+ });
747
+ sandboxProxyServer = proxy;
748
+
749
+ return new Promise((resolveProxy, rejectProxy) => {
750
+ const onError = (error: NodeJS.ErrnoException) => {
751
+ proxy.off("listening", onListening);
752
+ rejectProxy(error);
753
+ };
754
+ const onListening = () => {
755
+ proxy.off("error", onError);
756
+ const address = proxy.address();
757
+ if (!address || typeof address === "string") {
758
+ rejectProxy(new ServerError("invalid sandbox proxy address"));
759
+ return;
760
+ }
761
+ resolveProxy(address.port);
762
+ };
763
+ proxy.once("error", onError);
764
+ proxy.listen(0, "127.0.0.1", onListening);
765
+ });
766
+ };
767
+
670
768
  const listen = () => {
671
- server.once("error", onError);
672
- server.listen(candidates[candidateIndex], "127.0.0.1", onListening);
769
+ const candidate = candidates[candidateIndex];
770
+ if (candidate === undefined) {
771
+ const error = new ServerError("no UI server port candidates available");
772
+ closeListeners();
773
+ reject(error);
774
+ return;
775
+ }
776
+ hostServer.once("error", onError);
777
+ hostServer.listen(candidate, "127.0.0.1", onListening);
673
778
  };
674
779
 
675
780
  const onError = (error: NodeJS.ErrnoException) => {
676
- server.off("listening", onListening);
781
+ hostServer.off("listening", onListening);
677
782
  if (error.code === "EADDRINUSE" && candidateIndex < candidates.length - 1) {
678
783
  candidateIndex += 1;
679
784
  listen();
@@ -681,6 +786,7 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
681
786
  }
682
787
  log.error("Failed to start server", error);
683
788
  const port = candidates[candidateIndex];
789
+ closeListeners();
684
790
  reject(new ServerError(error.message, {
685
791
  ...(port !== undefined ? { port } : {}),
686
792
  cause: error,
@@ -688,52 +794,71 @@ export async function startUiServer(options: UiServerOptions): Promise<UiServerH
688
794
  };
689
795
 
690
796
  const onListening = () => {
691
- server.off("error", onError);
692
- const address = server.address();
797
+ hostServer.off("error", onError);
798
+ const address = hostServer.address();
693
799
  if (!address || typeof address === "string") {
694
800
  const err = new ServerError("invalid address");
695
801
  log.error("Invalid server address", err);
802
+ closeListeners();
696
803
  reject(err);
697
804
  return;
698
805
  }
699
806
 
700
- log.debug("Server started", { port: address.port });
701
- rememberMoshiDiscoveryPort(address.port);
702
-
703
- const handle: UiServerHandle = {
704
- url: `http://localhost:${address.port}/?session=${sessionToken}`,
705
- port: address.port,
706
- sessionToken,
707
- serverName: options.serverName,
708
- toolName: options.toolName,
709
- close: (reason?: string) => {
710
- markCompleted(reason ?? "closed");
711
- try {
712
- server.close();
713
- } catch {}
714
- closeSse();
715
- },
716
- sendToolInput: (args: Record<string, unknown>) => {
717
- pushEvent("tool-input", { arguments: args });
718
- },
719
- sendToolResult: (result: CallToolResult) => {
720
- pushEvent("tool-result", result);
721
- },
722
- sendResultPatch: (result: CallToolResult) => {
723
- pushEvent("result-patch", result);
724
- },
725
- sendToolCancelled: (reason: string) => {
726
- pushEvent("tool-cancelled", { reason });
727
- },
728
- sendHostContext: (context: UiHostContext) => {
729
- Object.assign(hostContext, context);
730
- pushEvent("host-context", context);
731
- },
732
- getSessionMessages: () => ({ ...sessionMessages }),
733
- getStreamSummary: () => streamSummary ? { ...streamSummary, phases: [...streamSummary.phases] } : undefined,
734
- };
807
+ const parentOrigin = `http://localhost:${address.port}`;
808
+ void startSandboxProxy(parentOrigin).then((proxyPort) => {
809
+ if (completed || listenersClosed) {
810
+ closeListeners();
811
+ reject(new ServerError("UI session completed before sandbox proxy was ready"));
812
+ return;
813
+ }
814
+ sandboxProxyUrl = `http://localhost:${proxyPort}${SANDBOX_PROXY_PATH}`;
815
+ log.debug("Servers started", { port: address.port, proxyPort });
816
+ rememberMoshiDiscoveryPort(address.port);
817
+
818
+ const handle: UiServerHandle = {
819
+ url: `http://localhost:${address.port}/?session=${sessionToken}`,
820
+ port: address.port,
821
+ proxyUrl: sandboxProxyUrl,
822
+ proxyPort,
823
+ sessionToken,
824
+ serverName: options.serverName,
825
+ toolName: options.toolName,
826
+ close: (reason?: string) => {
827
+ markCompleted(reason ?? "closed");
828
+ closeListeners();
829
+ },
830
+ sendToolInput: (args: Record<string, unknown>) => {
831
+ pushEvent("tool-input", { arguments: args });
832
+ },
833
+ sendToolResult: (result: CallToolResult) => {
834
+ pushEvent("tool-result", result);
835
+ },
836
+ sendResultPatch: (result: CallToolResult) => {
837
+ pushEvent("result-patch", result);
838
+ },
839
+ sendToolCancelled: (reason: string) => {
840
+ pushEvent("tool-cancelled", { reason });
841
+ },
842
+ sendResourceUpdated: (uri: string) => {
843
+ pushEvent("resource-updated", { uri });
844
+ },
845
+ sendHostContext: (context: UiHostContext) => {
846
+ Object.assign(hostContext, context);
847
+ pushEvent("host-context", context);
848
+ },
849
+ getSessionMessages: () => ({ ...sessionMessages }),
850
+ getStreamSummary: () => streamSummary ? { ...streamSummary, phases: [...streamSummary.phases] } : undefined,
851
+ };
735
852
 
736
- resolve(handle);
853
+ resolve(handle);
854
+ }).catch((error) => {
855
+ log.error("Failed to start sandbox proxy", error instanceof Error ? error : undefined);
856
+ closeListeners();
857
+ const wrapped = error instanceof ServerError
858
+ ? error
859
+ : new ServerError(error instanceof Error ? error.message : String(error), { cause: error });
860
+ reject(wrapped);
861
+ });
737
862
  };
738
863
 
739
864
  listen();
@@ -21,6 +21,7 @@ import { isGlimpseAvailable, openGlimpseWindow } from "./glimpse-ui.ts";
21
21
  import type { SessionRecoveryDeps } from "./session-recovery.ts";
22
22
  import { combineAbortSignals, isAbortError } from "./runtime-owner.ts";
23
23
  import { throwIfAborted } from "./abort.ts";
24
+ import { InputRequiredNeedsUiError } from "./errors.ts";
24
25
 
25
26
  let activeGlimpseWindow: { close(): void } | null = null;
26
27
 
@@ -172,7 +173,7 @@ function probeMoshiGateway(): Promise<boolean> {
172
173
  });
173
174
  }
174
175
 
175
- function remoteAccessHint(opts: { url: string; port: number; moshi: boolean; openError: string | null; openedOnHost?: boolean }): string {
176
+ function remoteAccessHint(opts: { url: string; port: number; proxyPort: number; moshi: boolean; openError: string | null; openedOnHost?: boolean }): string {
176
177
  const lines = [
177
178
  opts.openError !== null
178
179
  ? "Couldn't open MCP UI here. Open it from your own device:"
@@ -185,10 +186,12 @@ function remoteAccessHint(opts: { url: string; port: number; moshi: boolean; ope
185
186
  lines.push(`Browser launch failed: ${opts.openError}`);
186
187
  }
187
188
  if (opts.moshi) {
188
- lines.push("Moshi: tap the preview button in the terminal title bar and pick this MCP UI server.");
189
+ lines.push(
190
+ `Moshi: tap the preview button in the terminal title bar and pick this MCP UI server (it must reach ports ${opts.port} and ${opts.proxyPort}).`,
191
+ );
189
192
  }
190
193
  lines.push(
191
- `SSH: run \`ssh -L ${opts.port}:127.0.0.1:${opts.port} <this-host>\` on your local machine, then open the URL above.`,
194
+ `SSH: run \`ssh -L ${opts.port}:127.0.0.1:${opts.port} -L ${opts.proxyPort}:127.0.0.1:${opts.proxyPort} <this-host>\` on your local machine, then open the URL above.`,
192
195
  "mosh can't forward ports - run that ssh command in a separate terminal.",
193
196
  );
194
197
  return lines.join("\n");
@@ -314,11 +317,13 @@ export async function maybeStartUiSession(
314
317
  let active = true;
315
318
  let nextStreamSequence = 0;
316
319
  let handle: UiServerHandle;
320
+ const resourceListenerToken = randomUUID();
317
321
 
318
- const cleanupStreamListener = () => {
322
+ const cleanupListeners = () => {
319
323
  if (streamToken) {
320
324
  state.manager.removeUiStreamListener(streamToken);
321
325
  }
326
+ state.manager.removeResourceUpdatedListener?.(resourceListenerToken);
322
327
  };
323
328
 
324
329
  handle = await startUiServer({
@@ -394,7 +399,7 @@ export async function maybeStartUiSession(
394
399
 
395
400
  onComplete: (reason: string) => {
396
401
  active = false;
397
- cleanupStreamListener();
402
+ cleanupListeners();
398
403
 
399
404
  if (state.uiServer === handle) {
400
405
  const messages = handle.getSessionMessages();
@@ -454,6 +459,16 @@ export async function maybeStartUiSession(
454
459
  });
455
460
  }
456
461
 
462
+ state.manager.registerResourceUpdatedListener?.(
463
+ resourceListenerToken,
464
+ request.serverName,
465
+ request.uiResourceUri,
466
+ (_serverName, uri) => {
467
+ if (!active || state.uiServer !== handle) return;
468
+ handle.sendResourceUpdated(uri);
469
+ },
470
+ );
471
+
457
472
  state.uiServer = handle;
458
473
 
459
474
  const viewerPref = process.env.MCP_UI_VIEWER?.toLowerCase();
@@ -466,7 +481,11 @@ export async function maybeStartUiSession(
466
481
  if (uiSuppressed) {
467
482
  viewer = "suppressed";
468
483
  windowOpen = false;
469
- state.ui?.notify(`MCP UI window suppressed (MCP_UI_VIEWER=${viewerPref}). Open manually: ${handle.url}`, "info");
484
+ state.ui?.notify(
485
+ `MCP UI window suppressed (MCP_UI_VIEWER=${viewerPref}). Open manually: ${handle.url}\n` +
486
+ `If this session is remote, run ssh -L ${handle.port}:127.0.0.1:${handle.port} -L ${handle.proxyPort}:127.0.0.1:${handle.proxyPort} <this-host> first.`,
487
+ "info",
488
+ );
470
489
  log.info("Suppressing MCP UI window (MCP_UI_VIEWER=" + viewerPref + ")", { url: handle.url });
471
490
  } else {
472
491
  const remoteLikely = remoteByEnv || await hasActiveRemoteLogin();
@@ -474,6 +493,7 @@ export async function maybeStartUiSession(
474
493
  state.ui?.notify(remoteAccessHint({
475
494
  url: handle.url,
476
495
  port: handle.port,
496
+ proxyPort: handle.proxyPort,
477
497
  moshi: await probeMoshiGateway(),
478
498
  openError,
479
499
  openedOnHost,
@@ -554,12 +574,12 @@ export async function maybeStartUiSession(
554
574
  },
555
575
  close: (reason?: string) => {
556
576
  active = false;
557
- cleanupStreamListener();
577
+ cleanupListeners();
558
578
  handle.close(reason);
559
579
  },
560
580
  };
561
581
  } catch (error) {
562
- if (error instanceof UrlElicitationRequiredError || isAbortError(error, runtimeSignal)) throw error;
582
+ if (error instanceof UrlElicitationRequiredError || error instanceof InputRequiredNeedsUiError || isAbortError(error, runtimeSignal)) throw error;
563
583
  const message = error instanceof Error ? error.message : String(error);
564
584
  log.error("Failed to start UI session", error instanceof Error ? error : undefined);
565
585
  state.ui?.notify(
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.27.0] - 2026-08-28
8
+
9
+ ### Highlights
10
+
11
+ - `fetch_content` now lets you tune direct HTTP and Jina Reader timeouts from config.
12
+ - Answer mode can use a configured default model while still allowing per-call overrides.
13
+ - HTML fallback parsing is quieter for pages with relative canonical links.
14
+ - GitHub repository fetching is safer when more than one Pi process is running.
15
+
16
+ ### Added
17
+
18
+ - Added opt-in `fetch.timeout` configuration in seconds for the direct HTTP and Jina Reader `fetch_content` paths, with per-call timeout overrides taking precedence. Thanks to [@linuxtextadventurer](https://github.com/linuxtextadventurer) for PR #327.
19
+ - Added opt-in `fetch.answerProvider` and `fetch.answerModel` defaults for `fetch_content` answer mode, with per-call `answerModel` overrides taking precedence. Thanks to [@linuxtextadventurer](https://github.com/linuxtextadventurer) for PR #328.
20
+
21
+ ### Fixed
22
+
23
+ - Set the Defuddle fallback document URL before parsing pages with relative canonical links, preventing `ERR_INVALID_URL` warnings. Thanks to [@bin115885](https://github.com/bin115885) for issue #322.
24
+ - Isolated GitHub clone workdirs per extension runtime so cleanup in one process cannot delete another process's clone. Thanks to [@MDGChamomile](https://github.com/MDGChamomile) for PR #323.
25
+
26
+
7
27
  ## [0.26.0] - 2026-08-28
8
28
 
9
29
  ### Highlights
@@ -159,11 +159,15 @@ fetch_content({ url: "https://example.com/diagram.png" })
159
159
  | `url` / `urls` | Single URL/path or multiple URLs |
160
160
  | `prompt` | Question for video analysis, or the page-local question required by `mode: "answer"` |
161
161
  | `mode` | `readable` (default), `raw` for exact textual HTTP bodies, or `answer` for a grounded answer from fetched content |
162
- | `answerModel` | Optional `provider/model-id` override for answer mode; defaults to the current enabled Pi model |
162
+ | `answerModel` | Optional `provider/model-id` override for answer mode; defaults to the configured `fetch.answerProvider` + `fetch.answerModel` pair, or the current enabled Pi model when no pair is configured |
163
163
  | `timestamp` | Extract frame(s) — single (`"23:41"`), range (`"23:41-25:00"`), or seconds (`"85"`) |
164
164
  | `frames` | Number of frames to extract (max 12) |
165
165
  | `forceClone` | Clone GitHub repos that exceed the 350MB size threshold |
166
166
 
167
+ For a standing answer-mode model, set both `fetch.answerProvider` and `fetch.answerModel` in `web-search.json`; a per-call `answerModel` takes precedence. Configured answer defaults are opt-in and can send fetched page text to a different provider/model, which may change privacy and cost behavior.
168
+
169
+ Thanks to [@linuxtextadventurer](https://github.com/linuxtextadventurer) for PR #328.
170
+
167
171
  ### get_search_content
168
172
 
169
173
  Retrieve stored content from previous searches or fetches. Fetched URL content is stored in full in a private `web-search-cache` directory under the Pi config directory, not in the session JSONL. This includes `fetch_content` answer mode, which stores the original page content. The cache has a one-hour lifetime and fixed limits of 128 entries and 128 MiB; when either limit is reached, the oldest entries are removed first. On macOS and Linux the cache directory and files are kept at permissions `0700` and `0600`, respectively. Use `findText` to locate bounded matching passages without paging through a large page, or use `offset` and `limit` to retrieve slices intentionally.
@@ -403,6 +407,11 @@ Config defaults to `~/.pi/web-search.json`, or `web-search.json` under `PI_CODIN
403
407
  "providers": ["http", "firecrawl", "jina", "tinyfish", "search1api", "querit", "kagi", "ollama", "parallel", "brightdata", "gemini"],
404
408
  "allowRemoteHostedProviders": false
405
409
  },
410
+ "fetch": {
411
+ "timeout": 30,
412
+ "answerProvider": "openai",
413
+ "answerModel": "gpt-5.6"
414
+ },
406
415
  "webSearch": {
407
416
  "enabled": true
408
417
  },
@@ -502,6 +511,10 @@ Set `braveBaseUrl`, `exaBaseUrl`, or `tavilyBaseUrl` to route those providers th
502
511
 
503
512
  `fetchContent.domainPolicy` is an optional hostname allow/deny policy for `fetch_content` target URLs. It is off when omitted. Each bare hostname matches itself and its subdomains; `deny` wins when a hostname matches both lists. The policy is checked before HTTP(S) target handling and before each redirect followed by this extension's own fetch path. Local file paths and non-HTTP sources are not subject to this policy. It is an additional restriction: the existing SSRF guard still blocks private and internal destinations. Remote extraction services can still perform their own DNS, redirects, and egress after this extension preflights the submitted target URL, so third-party hosted HTTP(S) fallbacks stay disabled unless `fetchRouting.allowRemoteHostedProviders` is enabled for separately isolated provider deployments.
504
513
 
514
+ `fetch.timeout` is an optional positive finite number of seconds for direct HTTP fetches and the Jina Reader fallback. When omitted, both use a 30-second budget. Fractional values are supported and rounded up to at least 1 millisecond; values that cannot be converted to a finite safe integer delay from 1 through Node's 2,147,483,647 ms timer maximum are rejected. An invalid declared value fails closed with an error naming `web-search.json`. An internal/per-call `timeoutMs` override takes precedence over this setting. Other remote extraction fallbacks keep their own documented budgets.
515
+
516
+ `fetch.answerProvider` and `fetch.answerModel` are an optional pair that selects the model used by `fetch_content` answer mode when no per-call `answerModel` is supplied. Both values must be non-empty strings and must identify an enabled text-capable model available in Pi's model registry; invalid or partial configuration fails closed. This is opt-in: answering with the configured provider/model can send fetched page text outside the current session and may incur that provider's costs. A per-call `answerModel` override is resolved first and remains usable even when these configured defaults are malformed.
517
+
505
518
  Set `searxngBaseUrl` or `SEARXNG_BASE_URL` to use a self-hosted SearXNG JSON API. A configured endpoint is preferred first in `auto` mode for local/private search. Its base URL and redirects remain subject to the SSRF guard; add only the narrowest self-hosted range to `ssrf.allowRanges` when it resolves to a private or synthetic range. Optional `searxngHeaders` merges extra HTTP headers into each SearXNG request (string values only; invalid header names are ignored), which is useful for reverse-proxy or Zero Trust auth such as Cloudflare Access service tokens (`CF-Access-Client-Id` / `CF-Access-Client-Secret`). Configured headers override the default `Accept: application/json` when the same name is supplied. Thanks to Marcos A. Núñez (@marnunez) for PR #107 and Avinash Kanaujiya (@avinashkanaujiya) for issue #105.
506
519
 
507
520
  **DuckDuckGo.** DuckDuckGo HTML search is keyless and explicit-only. Select it with `provider: "duckduckgo"` or place it in `searchRouting`; it is never chosen by `auto` and never participates in `provider: "all"`. Domain filters are enforced locally after DuckDuckGo redirect URLs are decoded. `recencyFilter` is not guaranteed because the HTML endpoint has no documented stable time parameter. A 200 page with no parseable results is reported as an invalid response, so routing can continue when `fallbackOn` includes `"invalid-response"`.
@@ -868,7 +881,7 @@ Values use the same format as pi keybindings (e.g. `ctrl+s`, `ctrl+shift+s`, `al
868
881
 
869
882
  Set `"enabled": false` under `tools`, `commands`, `image`, or `pdf` to disable that feature. Tool-specific settings override the legacy `webSearch.enabled` shorthand; without an override, it still disables `web_search` and `source_check`. `image.enabled: false` blocks direct image fetches and video frame extraction, and prevents video thumbnails. `pdf.enabled: false` blocks PDF extraction. For GitHub specifically, `githubClone.enabled: false` only skips clone/API specialization, and `githubPrIssue.enabled: false` only skips PR/issue specialization; neither setting unregisters `fetch_content` or blocks generic URL extraction. Pi restart is required for tool and command registration changes.
870
883
 
871
- Rate limits: Perplexity is capped at 10 requests/minute (client-side). Jina Search, TinyFish, Search1API, and Searchinfinity apply the plan limits documented by their APIs. Querit Search and Contents subscriptions are independent. Content fetches run 3 concurrent with a 30s timeout for the direct HTTP fetch of each URL. Remote extraction fallbacks carry their own budgets and are not covered by that number: Jina Reader 30s, Firecrawl 60s, Kagi Extract 60s, Ollama Web Fetch 60s, Bright Data Web Unlocker 60s, TinyFish up to 150s, Gemini 120s, Datalab 120s (capped at 300s, rate-limited to 25 requests/minute on the free tier). `pdf.maxSizeMB` defaults to 20 and is capped at 50. `pdf.maxPages` defaults to 100 and limits every PDF provider to the first N pages.
884
+ Rate limits: Perplexity is capped at 10 requests/minute (client-side). Jina Search, TinyFish, Search1API, and Searchinfinity apply the plan limits documented by their APIs. Querit Search and Contents subscriptions are independent. Content fetches run 3 concurrent; direct HTTP fetches and Jina Reader use a 30s timeout by default, configurable together with `fetch.timeout` in seconds. Remote extraction fallbacks carry their own budgets and are not covered by that setting: Firecrawl 60s, Kagi Extract 60s, Ollama Web Fetch 60s, Bright Data Web Unlocker 60s, TinyFish up to 150s, Gemini 120s, Datalab 120s (capped at 300s, rate-limited to 25 requests/minute on the free tier). `pdf.maxSizeMB` defaults to 20 and is capped at 50. `pdf.maxPages` defaults to 100 and limits every PDF provider to the first N pages.
872
885
 
873
886
  ## Limitations
874
887