@herbertgao/pi-extensions 2026.8.13 → 2026.8.15

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 (263) hide show
  1. package/README.md +19 -14
  2. package/THIRD_PARTY_NOTICES.md +0 -1
  3. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +19 -0
  4. package/node_modules/@czottmann/pi-automode/README.md +5 -1
  5. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +126 -13
  6. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +7 -3
  7. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +2 -0
  8. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +134 -23
  9. package/node_modules/@czottmann/pi-automode/package.json +1 -1
  10. package/node_modules/@herbertgao/{pi-stash → pi-bark}/LICENSE +4 -5
  11. package/node_modules/@herbertgao/pi-bark/README.md +44 -0
  12. package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
  13. package/node_modules/@herbertgao/pi-bark/package.json +53 -0
  14. package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
  15. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  16. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  17. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  18. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  19. package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
  20. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +8 -1
  21. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  22. package/node_modules/@narumitw/pi-btw/README.md +125 -88
  23. package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
  24. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
  25. package/node_modules/@narumitw/pi-btw/package.json +5 -8
  26. package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
  27. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
  28. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
  29. package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
  30. package/node_modules/@tifan/pi-handoff/README.md +36 -13
  31. package/node_modules/@tifan/pi-handoff/package.json +2 -2
  32. package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
  33. package/node_modules/@tifan/pi-preferred-thinking/README.md +1 -1
  34. package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
  35. package/node_modules/@tifan/pi-preferred-thinking/src/index.ts +15 -2
  36. package/node_modules/@tifan/pi-recap/README.md +1 -1
  37. package/node_modules/@tifan/pi-recap/package.json +2 -2
  38. package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
  39. package/node_modules/@tifan/pi-rename/README.md +42 -21
  40. package/node_modules/@tifan/pi-rename/package.json +1 -1
  41. package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
  42. package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
  43. package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
  44. package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
  45. package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
  46. package/node_modules/pi-lens/CHANGELOG.md +320 -0
  47. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
  48. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
  49. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
  50. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
  51. package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
  52. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
  53. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
  54. package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
  55. package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
  56. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
  57. package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
  58. package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
  59. package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
  60. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
  61. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
  63. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
  64. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
  66. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
  67. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
  68. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
  69. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
  70. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
  71. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
  72. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
  73. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
  74. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
  75. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
  85. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
  86. package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
  87. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
  88. package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
  89. package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
  90. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
  91. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
  92. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
  93. package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
  94. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
  95. package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
  96. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
  97. package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
  98. package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
  99. package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
  100. package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
  101. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  102. package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
  103. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
  104. package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
  105. package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
  106. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
  107. package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
  108. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
  109. package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
  110. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
  111. package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
  112. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
  113. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
  114. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  115. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
  116. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
  117. package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
  118. package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
  119. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
  120. package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
  121. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
  122. package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
  123. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
  124. package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
  125. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
  126. package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
  127. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  128. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
  129. package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
  130. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
  131. package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
  132. package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
  133. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
  134. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
  135. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
  136. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
  137. package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
  138. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
  139. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
  140. package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
  141. package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
  142. package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
  143. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
  144. package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
  145. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
  146. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
  147. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
  148. package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
  149. package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
  150. package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
  151. package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
  152. package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
  153. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
  154. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
  155. package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
  156. package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
  157. package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
  158. package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
  159. package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
  160. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
  161. package/node_modules/pi-lens/dist/index.js +11243 -6651
  162. package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
  163. package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
  164. package/node_modules/pi-lens/dist/mcp/server.js +6 -1
  165. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
  166. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
  167. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
  168. package/node_modules/pi-lens/docs/agent-guide.md +4 -2
  169. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
  170. package/node_modules/pi-lens/docs/astplayground.md +26 -17
  171. package/node_modules/pi-lens/package.json +5 -1
  172. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  173. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  174. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  175. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  176. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  177. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  178. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  179. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  180. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  181. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  182. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  183. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  184. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  185. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  186. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  187. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  188. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  189. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  190. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  191. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  192. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  193. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  194. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  195. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  196. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  197. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  198. package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  199. package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  200. package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  201. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  202. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  203. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  204. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  205. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  206. package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  207. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  208. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  209. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  210. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  211. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  212. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  213. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  214. package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  215. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
  216. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  217. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  218. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  219. package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  220. package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  221. package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  222. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  223. package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  224. package/node_modules/pi-mcp-adapter/CHANGELOG.md +35 -0
  225. package/node_modules/pi-mcp-adapter/README.md +5 -2
  226. package/node_modules/pi-mcp-adapter/cli.js +4 -4
  227. package/node_modules/pi-mcp-adapter/commands.ts +1 -1
  228. package/node_modules/pi-mcp-adapter/config.ts +1 -0
  229. package/node_modules/pi-mcp-adapter/dist/config.js +1 -0
  230. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  231. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +24 -0
  232. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +336 -0
  233. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -0
  234. package/node_modules/pi-mcp-adapter/dist/types.d.ts +3 -1
  235. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  236. package/node_modules/pi-mcp-adapter/index.ts +90 -6
  237. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +124 -32
  238. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -2
  239. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +89 -0
  240. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -1
  241. package/node_modules/pi-mcp-adapter/package.json +1 -1
  242. package/node_modules/pi-mcp-adapter/proxy-modes.ts +62 -22
  243. package/node_modules/pi-mcp-adapter/request-headers-command.ts +1 -1
  244. package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
  245. package/node_modules/pi-mcp-adapter/types.ts +3 -1
  246. package/node_modules/pi-web-access/CHANGELOG.md +42 -0
  247. package/node_modules/pi-web-access/README.md +26 -10
  248. package/node_modules/pi-web-access/curator-page.ts +12 -3
  249. package/node_modules/pi-web-access/curator-server.ts +3 -1
  250. package/node_modules/pi-web-access/extract.ts +88 -13
  251. package/node_modules/pi-web-access/gemini-search.ts +10 -5
  252. package/node_modules/pi-web-access/github-extract.ts +47 -3
  253. package/node_modules/pi-web-access/index.ts +40 -40
  254. package/node_modules/pi-web-access/package.json +1 -1
  255. package/node_modules/pi-web-access/page-query.ts +61 -8
  256. package/node_modules/pi-web-access/xcrawl.ts +264 -0
  257. package/package.json +19 -22
  258. package/node_modules/@herbertgao/pi-stash/README.md +0 -34
  259. package/node_modules/@herbertgao/pi-stash/package.json +0 -51
  260. package/node_modules/@herbertgao/pi-stash/src/index.ts +0 -118
  261. package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
  262. package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
  263. package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
@@ -75,6 +75,8 @@ type PendingAuth = {
75
75
  authProvider: McpOAuthProvider
76
76
  serverUrl: string
77
77
  authorizationUrl: string
78
+ manualRedirect: boolean
79
+ manualCompletionController?: AbortController
78
80
  discovery: AuthDiscovery
79
81
  authStorageOptions: AuthStorageOptions
80
82
  }
@@ -238,6 +240,25 @@ export function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
238
240
  }
239
241
  config.logoUri = logoUri
240
242
  }
243
+ if (definition.oauth?.authServerMetadataUrl !== undefined) {
244
+ if (typeof definition.oauth.authServerMetadataUrl !== "string") {
245
+ throw new Error("OAuth authServerMetadataUrl must be a string")
246
+ }
247
+ const authServerMetadataUrl = interpolateEnvVars(definition.oauth.authServerMetadataUrl).trim()
248
+ if (!authServerMetadataUrl) {
249
+ throw new Error("OAuth authServerMetadataUrl must not be empty")
250
+ }
251
+ let parsed: URL
252
+ try {
253
+ parsed = new URL(authServerMetadataUrl)
254
+ } catch {
255
+ throw new Error("OAuth authServerMetadataUrl must be an absolute https:// URL")
256
+ }
257
+ if (parsed.protocol !== "https:") {
258
+ throw new Error("OAuth authServerMetadataUrl must be an absolute https:// URL")
259
+ }
260
+ config.authServerMetadataUrl = authServerMetadataUrl
261
+ }
241
262
  if (definition.oauth?.skipIssuerMetadataValidation !== undefined) {
242
263
  if (typeof definition.oauth.skipIssuerMetadataValidation !== "boolean") {
243
264
  throw new Error("OAuth skipIssuerMetadataValidation must be a boolean")
@@ -288,7 +309,11 @@ async function probeAuthDiscovery(serverUrl: string, definition?: ServerEntry, s
288
309
  }
289
310
  }
290
311
 
291
- function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHost: string; callbackPath: string } {
312
+ type OAuthRedirectTarget =
313
+ | { mode: "local"; port: number; callbackHost: string; callbackPath: string }
314
+ | { mode: "manual" }
315
+
316
+ function parseOAuthRedirectUri(redirectUri: string): OAuthRedirectTarget {
292
317
  let url: URL
293
318
  try {
294
319
  url = new URL(redirectUri)
@@ -296,12 +321,6 @@ function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHos
296
321
  throw new Error(`Invalid OAuth redirectUri: ${redirectUri}`, { cause: error })
297
322
  }
298
323
 
299
- const hostname = url.hostname.toLowerCase()
300
- const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
301
- if (url.protocol !== "http:" || !isLocalhost) {
302
- throw new Error("OAuth redirectUri must be an http:// localhost or loopback URI")
303
- }
304
-
305
324
  if (url.username || url.password) {
306
325
  throw new Error("OAuth redirectUri must not include username or password")
307
326
  }
@@ -310,17 +329,32 @@ function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHos
310
329
  throw new Error("OAuth redirectUri must not include a fragment")
311
330
  }
312
331
 
332
+ const hostname = url.hostname.toLowerCase()
333
+ const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
334
+ if (url.port) {
335
+ const parsedPort = Number.parseInt(url.port, 10)
336
+ if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535) {
337
+ throw new Error("OAuth redirectUri port must be a positive numeric port")
338
+ }
339
+ }
340
+ if (url.protocol === "https:" && !isLocalhost) {
341
+ return { mode: "manual" }
342
+ }
343
+ if (url.protocol !== "http:" || !isLocalhost) {
344
+ throw new Error("OAuth redirectUri must be an https:// URI or an http:// localhost or loopback URI")
345
+ }
346
+
313
347
  if (!url.port) {
314
- throw new Error("OAuth redirectUri must include an explicit numeric port")
348
+ throw new Error("OAuth localhost redirectUri must include an explicit numeric port")
315
349
  }
316
350
 
317
351
  const port = Number.parseInt(url.port, 10)
318
352
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
319
- throw new Error("OAuth redirectUri must include an explicit numeric port")
353
+ throw new Error("OAuth localhost redirectUri must include an explicit numeric port")
320
354
  }
321
355
 
322
356
  const callbackHost = hostname === "[::1]" ? "::1" : hostname
323
- return { port, callbackHost, callbackPath: url.pathname }
357
+ return { mode: "local", port, callbackHost, callbackPath: url.pathname }
324
358
  }
325
359
 
326
360
  /**
@@ -374,25 +408,30 @@ export async function startAuth(
374
408
  return { authorizationUrl: existingPendingAuth.authorizationUrl }
375
409
  }
376
410
 
377
- const redirectCallback = config.redirectUri !== undefined ? parseOAuthRedirectUri(config.redirectUri) : undefined
411
+ const redirectTarget = config.redirectUri !== undefined ? parseOAuthRedirectUri(config.redirectUri) : undefined
412
+ const manualRedirect = redirectTarget?.mode === "manual"
378
413
  const oauthState = generateState()
379
414
 
380
- try {
381
- await ensureCallbackServer({
382
- strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
383
- oauthState,
384
- reserveState: true,
385
- ...(redirectCallback ? { port: redirectCallback.port, callbackHost: redirectCallback.callbackHost, callbackPath: redirectCallback.callbackPath } : {}),
386
- })
387
- throwIfAborted(signal)
388
- } catch (error) {
389
- releaseCallbackServer(oauthState)
415
+ if (!manualRedirect) {
390
416
  try {
391
- await cleanupAndReleaseCallbackServerIfIdle(() => clearOAuthState(serverName, authStorageOptions))
392
- } catch (cleanupError) {
393
- throw new AggregateError([error, cleanupError], "OAuth startup cleanup failed")
417
+ await ensureCallbackServer({
418
+ strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
419
+ oauthState,
420
+ reserveState: true,
421
+ ...(redirectTarget?.mode === "local"
422
+ ? { port: redirectTarget.port, callbackHost: redirectTarget.callbackHost, callbackPath: redirectTarget.callbackPath }
423
+ : {}),
424
+ })
425
+ throwIfAborted(signal)
426
+ } catch (error) {
427
+ releaseCallbackServer(oauthState)
428
+ try {
429
+ await cleanupAndReleaseCallbackServerIfIdle(() => clearOAuthState(serverName, authStorageOptions))
430
+ } catch (cleanupError) {
431
+ throw new AggregateError([error, cleanupError], "OAuth startup cleanup failed")
432
+ }
433
+ throw error
394
434
  }
395
- throw error
396
435
  }
397
436
 
398
437
  let capturedUrl: URL | undefined
@@ -436,7 +475,16 @@ export async function startAuth(
436
475
  if (!capturedUrl) {
437
476
  throw new UnauthorizedError("OAuth authorization URL was not provided")
438
477
  }
439
- await setPendingAuth(runtime, serverName, { serverName, authProvider, serverUrl, authorizationUrl: capturedUrl.toString(), discovery, authStorageOptions }, oauthState, signal, generation)
478
+ await setPendingAuth(runtime, serverName, {
479
+ serverName,
480
+ authProvider,
481
+ serverUrl,
482
+ authorizationUrl: capturedUrl.toString(),
483
+ manualRedirect,
484
+ ...(manualRedirect ? { manualCompletionController: new AbortController() } : {}),
485
+ discovery,
486
+ authStorageOptions,
487
+ }, oauthState, signal, generation)
440
488
  return { authorizationUrl: capturedUrl.toString() }
441
489
  } catch (error) {
442
490
  authProvider.deactivate()
@@ -465,7 +513,13 @@ async function setPendingAuth(
465
513
  state.pendingAuths.set(key, pendingAuth)
466
514
  state.pendingAuthStates.set(key, oauthState)
467
515
  const cleanupTimer = setTimeout(() => {
468
- void clearPendingAuthAndReleaseIfIdle(runtime, serverName, oauthState, pendingAuth.authStorageOptions).catch(error => {
516
+ void clearPendingAuthAndReleaseIfIdle(
517
+ runtime,
518
+ serverName,
519
+ oauthState,
520
+ pendingAuth.authStorageOptions,
521
+ new Error("OAuth authorization timeout - authorization took too long"),
522
+ ).catch(error => {
469
523
  console.error(`MCP Auth: Timed-out flow cleanup failed: ${formatTerminalError(error)}`)
470
524
  })
471
525
  }, MANUAL_AUTH_TIMEOUT_MS)
@@ -473,7 +527,13 @@ async function setPendingAuth(
473
527
  state.pendingAuthCleanupTimers.set(key, cleanupTimer)
474
528
  }
475
529
 
476
- async function clearPendingAuth(runtime: McpOAuthRuntime, serverName: string, oauthState?: string, fallbackStorageOptions: AuthStorageOptions = {}): Promise<void> {
530
+ async function clearPendingAuth(
531
+ runtime: McpOAuthRuntime,
532
+ serverName: string,
533
+ oauthState?: string,
534
+ fallbackStorageOptions: AuthStorageOptions = {},
535
+ reason: Error = new Error("Authorization cancelled"),
536
+ ): Promise<void> {
477
537
  const state = getRuntimeState(runtime)
478
538
  const key = getPendingAuthKey(serverName, fallbackStorageOptions)
479
539
  const pendingAuth = state.pendingAuths.get(key)
@@ -487,6 +547,7 @@ async function clearPendingAuth(runtime: McpOAuthRuntime, serverName: string, oa
487
547
  state.pendingAuthCleanupTimers.delete(key)
488
548
  }
489
549
 
550
+ pendingAuth?.manualCompletionController?.abort(reason)
490
551
  pendingAuth?.authProvider.deactivate()
491
552
  state.pendingAuths.delete(key)
492
553
  state.pendingAuthStates.delete(key)
@@ -505,9 +566,10 @@ async function clearPendingAuthAndReleaseIfIdle(
505
566
  serverName: string,
506
567
  oauthState: string | undefined,
507
568
  fallbackStorageOptions: AuthStorageOptions = {},
569
+ reason?: Error,
508
570
  ): Promise<void> {
509
571
  await cleanupAndReleaseCallbackServerIfIdle(
510
- () => clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions),
572
+ () => clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions, reason),
511
573
  )
512
574
  }
513
575
 
@@ -610,6 +672,19 @@ type AuthorizationResponse = {
610
672
  source: "callback" | "manual"
611
673
  }
612
674
 
675
+ function waitForManualCompletionCancellation(signal: AbortSignal): Promise<AuthorizationCodeInput> {
676
+ return new Promise((_, reject) => {
677
+ const rejectFromSignal = () => {
678
+ reject(signal.reason instanceof Error ? signal.reason : new Error("Authorization cancelled"))
679
+ }
680
+ if (signal.aborted) {
681
+ rejectFromSignal()
682
+ return
683
+ }
684
+ signal.addEventListener("abort", rejectFromSignal, { once: true })
685
+ })
686
+ }
687
+
613
688
  /**
614
689
  * Wait for either the localhost callback or a manually pasted redirect URL.
615
690
  * The manual input prompt is dismissed as soon as either path finishes.
@@ -667,7 +742,11 @@ export async function completeAuthFromInput(
667
742
  throwIfAborted(signal)
668
743
  const key = getPendingAuthKey(serverName, fallbackAuthStorageOptions)
669
744
  const oauthState = runtimeState.pendingAuthStates.get(key)
745
+ const pendingAuth = runtimeState.pendingAuths.get(key)
670
746
  throwIfAborted(signal)
747
+ if (pendingAuth?.manualRedirect && !getSearchParamsFromInput(input.trim())) {
748
+ throw new Error("Paste the full OAuth callback URL, including its code and state parameters")
749
+ }
671
750
  const parsed = parseAuthorizationRedirectInput(input, oauthState)
672
751
  return completeAuth(serverName, parsed, options)
673
752
  }
@@ -786,15 +865,28 @@ export async function authenticate(
786
865
  try {
787
866
  // Get the state that was already generated and stored in startAuth().
788
867
  // Keep this lookup and its abort check inside the cleanup boundary because
789
- // startAuth has already reserved callback state at this point.
868
+ // startAuth already owns the pending flow at this point.
790
869
  oauthState = runtimeState.pendingAuthStates.get(getPendingAuthKey(serverName, authStorageOptions))
791
870
  throwIfAborted(signal)
792
871
  if (!oauthState) {
793
872
  throw new Error("OAuth state not found - this should not happen")
794
873
  }
795
874
 
796
- // Register the callback BEFORE opening the browser.
797
- const callbackPromise = waitForCallback(oauthState)
875
+ const pendingAuth = runtimeState.pendingAuths.get(getPendingAuthKey(serverName, authStorageOptions))
876
+ if (!pendingAuth) {
877
+ throw new Error(`No pending OAuth flow for server: ${serverName}`)
878
+ }
879
+ if (pendingAuth.manualRedirect && !options.onAuthorizationInput) {
880
+ throw new Error(
881
+ `OAuth for ${serverName} uses a remote redirect URI. Complete it with auth-start/auth-complete or /mcp-auth.`,
882
+ )
883
+ }
884
+
885
+ // Register the localhost callback before opening the browser. Remote
886
+ // pre-registered callbacks are completed by pasting their full URL.
887
+ const callbackPromise: Promise<AuthorizationCodeInput> = pendingAuth.manualRedirect
888
+ ? waitForManualCompletionCancellation(pendingAuth.manualCompletionController!.signal)
889
+ : waitForCallback(oauthState)
798
890
  void callbackPromise.catch(() => {})
799
891
 
800
892
  // Open browser. Always surface the URL first so remote/headless users can copy it
@@ -43,8 +43,6 @@ export class BearerCredentialStoreError extends Error {
43
43
  readonly code = "BEARER_CREDENTIAL_STORE_UNAVAILABLE";
44
44
  readonly operation: "read" | "write" | "remove";
45
45
 
46
- // No parameter properties: cli.js loads this module under plain Node
47
- // type stripping, which rejects non-erasable TypeScript syntax.
48
46
  constructor(message: string, operation: "read" | "write" | "remove", cause: unknown) {
49
47
  super(message, { cause });
50
48
  this.name = "BearerCredentialStoreError";
@@ -29,6 +29,7 @@ import {
29
29
  type StoredTokens,
30
30
  type StoredClientInfo,
31
31
  } from "./mcp-auth.ts"
32
+ import { OAuthMetadataSchema, OpenIdProviderDiscoveryMetadataSchema } from "@modelcontextprotocol/core"
32
33
  import { resolveCommandSecret } from "./utils.ts"
33
34
  import { getAppClientUri, getAppName } from "./agent-dir.ts"
34
35
 
@@ -130,6 +131,7 @@ export interface McpOAuthConfig {
130
131
  clientName?: string
131
132
  clientUri?: string
132
133
  logoUri?: string
134
+ authServerMetadataUrl?: string
133
135
  skipIssuerMetadataValidation?: boolean
134
136
  }
135
137
 
@@ -161,6 +163,85 @@ export interface McpOAuthCallbacks {
161
163
  onRedirect: (url: URL) => void | Promise<void>
162
164
  }
163
165
 
166
+ function inferIssuerFromMetadataUrl(metadataUrl: string): string | undefined {
167
+ const url = new URL(metadataUrl)
168
+ const pathname = url.pathname.replace(/\/+$/, "") || "/"
169
+ const oauthPrefix = "/.well-known/oauth-authorization-server"
170
+ if (pathname === oauthPrefix || pathname.startsWith(`${oauthPrefix}/`)) {
171
+ const issuerPath = pathname.slice(oauthPrefix.length) || "/"
172
+ return new URL(issuerPath, url.origin).toString()
173
+ }
174
+
175
+ const oidcPath = "/.well-known/openid-configuration"
176
+ if (pathname === oidcPath || pathname.startsWith(`${oidcPath}/`)) {
177
+ const issuerPath = pathname.slice(oidcPath.length) || "/"
178
+ return new URL(issuerPath, url.origin).toString()
179
+ }
180
+ if (pathname.endsWith(oidcPath)) {
181
+ const issuerPath = pathname.slice(0, -oidcPath.length) || "/"
182
+ return new URL(issuerPath, url.origin).toString()
183
+ }
184
+ return undefined
185
+ }
186
+
187
+ function validateConfiguredIssuer(metadataUrl: string, issuer: string, skipIssuerValidation: boolean): void {
188
+ let parsedIssuer: URL
189
+ try {
190
+ parsedIssuer = new URL(issuer)
191
+ } catch (error) {
192
+ throw new Error("OAuth authorization-server metadata issuer must be an absolute URL", { cause: error })
193
+ }
194
+ if (parsedIssuer.protocol !== "http:" && parsedIssuer.protocol !== "https:") {
195
+ throw new Error("OAuth authorization-server metadata issuer must use http:// or https://")
196
+ }
197
+
198
+ const expectedIssuer = inferIssuerFromMetadataUrl(metadataUrl)
199
+ if (skipIssuerValidation) return
200
+ if (expectedIssuer !== undefined && !issuersMatch(expectedIssuer, issuer)) {
201
+ throw new Error(
202
+ `OAuth authorization-server metadata issuer does not match authServerMetadataUrl: expected ${expectedIssuer}`,
203
+ )
204
+ }
205
+ if (expectedIssuer === undefined && !issuersMatch(new URL(metadataUrl).origin, issuer)) {
206
+ throw new Error(
207
+ `OAuth authorization-server metadata issuer does not match authServerMetadataUrl origin: expected ${new URL(metadataUrl).origin}`,
208
+ )
209
+ }
210
+ }
211
+
212
+ async function loadConfiguredDiscoveryState(
213
+ metadataUrl: string,
214
+ serverUrl: string,
215
+ skipIssuerValidation: boolean,
216
+ signal?: AbortSignal,
217
+ ): Promise<OAuthDiscoveryState> {
218
+ const response = await fetch(metadataUrl, {
219
+ headers: { accept: "application/json" },
220
+ ...(signal !== undefined ? { signal } : {}),
221
+ })
222
+ if (!response.ok) {
223
+ await response.text().catch(() => {})
224
+ throw new Error(`OAuth authServerMetadataUrl request failed with HTTP ${response.status}`)
225
+ }
226
+
227
+ const payload = await response.json()
228
+ const oauthResult = OAuthMetadataSchema.safeParse(payload)
229
+ const metadata = oauthResult.success
230
+ ? oauthResult.data
231
+ : OpenIdProviderDiscoveryMetadataSchema.parse(payload)
232
+ validateConfiguredIssuer(metadataUrl, metadata.issuer, skipIssuerValidation)
233
+
234
+ const resource = new URL(serverUrl)
235
+ resource.hash = ""
236
+ return {
237
+ authorizationServerUrl: metadata.issuer,
238
+ authorizationServerMetadata: metadata,
239
+ // The configured AS document is authoritative, so avoid a second PRM
240
+ // lookup while still binding the token request to the configured resource.
241
+ resourceMetadata: { resource: resource.toString() },
242
+ }
243
+ }
244
+
164
245
  /**
165
246
  * OAuth provider implementation for MCP servers.
166
247
  * Implements the OAuthClientProvider interface from the MCP SDK.
@@ -525,6 +606,14 @@ export class McpOAuthProvider implements OAuthClientProvider {
525
606
 
526
607
  async discoveryState(): Promise<OAuthDiscoveryState | undefined> {
527
608
  this.throwIfInactive()
609
+ if (!this.flowDiscoveryState && this.config.authServerMetadataUrl !== undefined) {
610
+ this.flowDiscoveryState = await loadConfiguredDiscoveryState(
611
+ this.config.authServerMetadataUrl,
612
+ this.serverUrl,
613
+ this.config.skipIssuerMetadataValidation === true,
614
+ this.runtimeSignal,
615
+ )
616
+ }
528
617
  return this.flowDiscoveryState ? structuredClone(this.flowDiscoveryState) : undefined
529
618
  }
530
619
 
@@ -32,9 +32,17 @@ type DirectNameEntry = { name: string; originalName: string };
32
32
  type CachedServer = { serverName: string; definition: ServerEntry; entry: ServerCacheEntry; prefix: ToolPrefix };
33
33
 
34
34
  const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
35
+ const ENCODED_NAMESPACE_MARKER = "_mcpns_";
36
+
37
+ function namespaceServerPart(serverName: string): string {
38
+ const normalized = serverName.replace(/-/g, "_");
39
+ if (normalized === "" || (/^[A-Za-z0-9_]+$/.test(normalized) && !normalized.startsWith(ENCODED_NAMESPACE_MARKER))) return normalized;
40
+ const codePoints = Array.from(normalized, char => char.codePointAt(0)!.toString(16)).join("_");
41
+ return `${ENCODED_NAMESPACE_MARKER}${codePoints}`;
42
+ }
35
43
 
36
44
  export function namespaceProxyName(serverName: string): string {
37
- return `mcp__${serverName.replace(/-/g, "_")}`;
45
+ return `mcp__${namespaceServerPart(serverName)}`;
38
46
  }
39
47
 
40
48
  export function parseMcpReference(raw: string): ParsedMcpReference {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mcp-adapter",
3
- "version": "2.29.0",
3
+ "version": "2.31.0",
4
4
  "description": "MCP (Model Context Protocol) adapter extension for Pi coding agent",
5
5
  "type": "module",
6
6
  "types": "./index.ts",
@@ -93,6 +93,24 @@ function getSingleToolMatch(metadata: ToolMetadata[] | undefined, toolName: stri
93
93
  return matches.length > 1 ? "ambiguous" : matches[0];
94
94
  }
95
95
 
96
+ type ServerScopedToolMatch = { tool: ToolMetadata; precedence: number } | "ambiguous";
97
+
98
+ function getServerScopedToolMatch(metadata: ToolMetadata[] | undefined, toolName: string): ServerScopedToolMatch | undefined {
99
+ if (!metadata) return undefined;
100
+ const normalizedName = toolName.replace(/-/g, "_");
101
+ const matchesByPrecedence = [
102
+ metadata.filter((tool) => tool.name === toolName),
103
+ metadata.filter((tool) => tool.originalName === toolName),
104
+ metadata.filter((tool) => tool.name.replace(/-/g, "_") === normalizedName),
105
+ metadata.filter((tool) => tool.originalName.replace(/-/g, "_") === normalizedName),
106
+ ];
107
+ for (const [precedence, matches] of matchesByPrecedence.entries()) {
108
+ if (matches.length > 1) return "ambiguous";
109
+ if (matches.length === 1) return { tool: matches[0]!, precedence };
110
+ }
111
+ return undefined;
112
+ }
113
+
96
114
  function ambiguousToolResult(mode: "call" | "describe", toolName: string): ProxyToolResult {
97
115
  const message = `Tool "${toolName}" matches multiple servers. Specify a server.`;
98
116
  return {
@@ -125,22 +143,30 @@ function getAuthFailedMessage(state: McpExtensionState, serverName: string, mess
125
143
  return `OAuth authentication failed for "${serverName}": ${message}. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
126
144
  }
127
145
 
128
- function getRedirectPort(authorizationUrl: string): number | undefined {
146
+ function getRedirectDetails(authorizationUrl: string): { port?: number; remote: boolean } {
129
147
  try {
130
148
  const redirectUri = new URL(authorizationUrl).searchParams.get("redirect_uri");
131
- if (!redirectUri) return undefined;
132
- const port = Number.parseInt(new URL(redirectUri).port, 10);
133
- return Number.isInteger(port) ? port : undefined;
149
+ if (!redirectUri) return { remote: false };
150
+ const redirect = new URL(redirectUri);
151
+ const hostname = redirect.hostname.toLowerCase();
152
+ const local = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
153
+ const port = Number.parseInt(redirect.port, 10);
154
+ return {
155
+ ...(Number.isInteger(port) ? { port } : {}),
156
+ remote: !local,
157
+ };
134
158
  } catch {
135
- return undefined;
159
+ return { remote: false };
136
160
  }
137
161
  }
138
162
 
139
163
  function formatManualAuthInstructions(serverName: string, authorizationUrl: string): string {
140
- const port = getRedirectPort(authorizationUrl);
141
- const portNote = port
142
- ? `\nThe redirect URL will use local port ${port}. On a remote server it is expected for that localhost page to fail locally; copy the address bar URL anyway.`
143
- : "";
164
+ const redirect = getRedirectDetails(authorizationUrl);
165
+ const redirectNote = redirect.remote
166
+ ? "The provider uses a pre-registered HTTPS callback. Copy its full URL from the browser address bar, even if the destination page reports an error."
167
+ : redirect.port
168
+ ? `The redirect URL will use local port ${redirect.port}. On a remote server it is expected for that localhost page to fail locally; copy the address bar URL anyway.`
169
+ : "";
144
170
 
145
171
  return [
146
172
  `MCP OAuth required for "${serverName}".`,
@@ -149,11 +175,13 @@ function formatManualAuthInstructions(serverName: string, authorizationUrl: stri
149
175
  "",
150
176
  authorizationUrl,
151
177
  "",
152
- "After approving, copy the full redirected localhost URL from your browser address bar and send it back with:",
178
+ "After approving, copy the full callback URL from your browser address bar and send it back with:",
153
179
  `mcp({ action: "auth-complete", server: "${serverName}", args: { redirectUrl: "PASTE_REDIRECT_URL_HERE" } })`,
154
180
  "",
155
- 'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`. JSON-string args remain supported.',
156
- portNote.trimEnd(),
181
+ redirect.remote
182
+ ? "Remote HTTPS callbacks must include the full callback URL so the OAuth state can be checked. JSON-string args remain supported."
183
+ : 'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`. JSON-string args remain supported.',
184
+ redirectNote,
157
185
  ].filter(Boolean).join("\n");
158
186
  }
159
187
 
@@ -894,9 +922,9 @@ export async function executeCall(
894
922
  };
895
923
  }
896
924
  if (serverName) {
897
- const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
925
+ const match = getServerScopedToolMatch(state.toolMetadata.get(serverName), toolName);
898
926
  if (match === "ambiguous") return ambiguousToolResult("call", toolName);
899
- toolMeta = match;
927
+ toolMeta = match?.tool;
900
928
  if (isServerDisabled(state.config.mcpServers[serverName])) {
901
929
  return disabledCallResult(serverName, toolMeta);
902
930
  }
@@ -938,9 +966,15 @@ export async function executeCall(
938
966
  if (serverName && !toolMeta) {
939
967
  const connected = await lazyConnect(state, serverName, ownedSignal);
940
968
  if (connected) {
941
- const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
942
- if (match === "ambiguous") return ambiguousToolResult("call", toolName);
943
- toolMeta = match;
969
+ if (serverOverride) {
970
+ const match = getServerScopedToolMatch(state.toolMetadata.get(serverName), toolName);
971
+ if (match === "ambiguous") return ambiguousToolResult("call", toolName);
972
+ toolMeta = match?.tool;
973
+ } else {
974
+ const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
975
+ if (match === "ambiguous") return ambiguousToolResult("call", toolName);
976
+ toolMeta = match;
977
+ }
944
978
  } else {
945
979
  const needsAuthConnection = state.manager.getConnection(serverName);
946
980
  if (needsAuthConnection?.status === "needs-auth") {
@@ -958,9 +992,9 @@ export async function executeCall(
958
992
  clearFailure(state, serverName);
959
993
  const connectedAfterAuth = await lazyConnect(state, serverName, ownedSignal);
960
994
  if (connectedAfterAuth) {
961
- const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
995
+ const match = getServerScopedToolMatch(state.toolMetadata.get(serverName), toolName);
962
996
  if (match === "ambiguous") return ambiguousToolResult("call", toolName);
963
- toolMeta = match;
997
+ toolMeta = match?.tool;
964
998
  if (!toolMeta) {
965
999
  const suggestions = rankSuggestions(state, toolName, 5);
966
1000
  const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}` : "";
@@ -1157,9 +1191,15 @@ export async function executeCall(
1157
1191
  if (!restored) notifyToolMetadataUpdated(state, serverName, "proxy-call-reconnect");
1158
1192
  markKeepAliveAfterConnect(state, serverName);
1159
1193
  updateStatusBar(state);
1160
- const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
1161
- if (match === "ambiguous") return ambiguousToolResult("call", toolName);
1162
- toolMeta = match;
1194
+ if (serverOverride) {
1195
+ const match = getServerScopedToolMatch(state.toolMetadata.get(serverName), toolName);
1196
+ if (match === "ambiguous") return ambiguousToolResult("call", toolName);
1197
+ toolMeta = match?.tool;
1198
+ } else {
1199
+ const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
1200
+ if (match === "ambiguous") return ambiguousToolResult("call", toolName);
1201
+ toolMeta = match;
1202
+ }
1163
1203
  if (!toolMeta) {
1164
1204
  const available = getToolNames(state, serverName);
1165
1205
  const hint = available.length > 0
@@ -92,7 +92,7 @@ function killRequestHeadersCommand(child: ChildProcess, trackedPosixDescendantPi
92
92
  encoding: "utf8",
93
93
  windowsHide: true,
94
94
  });
95
- if (result.status === 0 || isTaskkillNoSuchProcess(result)) return;
95
+ if (result.status === 0 || result.status === 128 || isTaskkillNoSuchProcess(result)) return;
96
96
  throw new Error(`HTTP request headers command cleanup failed: taskkill exited with code ${result.status ?? "unknown"}`);
97
97
  }
98
98
 
@@ -34,6 +34,7 @@ import {
34
34
  import { resolveNpxBinary } from "./npx-resolver.ts";
35
35
  import { createJsonSchemaValidator } from "./json-schema-validator.ts";
36
36
  import { logger } from "./logger.ts";
37
+ import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.ts";
37
38
  import { McpOAuthProvider } from "./mcp-oauth-provider.ts";
38
39
  import { extractOAuthConfig, supportsOAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
39
40
  import { invalidateAuthEntryCache, type AuthStorageOptions } from "./mcp-auth.ts";
@@ -725,6 +726,11 @@ export class McpServerManager {
725
726
 
726
727
  private buildClientCapabilities() {
727
728
  return {
729
+ extensions: {
730
+ "io.modelcontextprotocol/ui": {
731
+ mimeTypes: [RESOURCE_MIME_TYPE],
732
+ },
733
+ },
728
734
  ...(this.samplingConfig ? { sampling: {} } : {}),
729
735
  ...(this.elicitationConfig
730
736
  ? {
@@ -378,7 +378,7 @@ export interface OAuthConfig {
378
378
  scope?: string;
379
379
  /** Extra authorization URL parameters for provider-specific extensions. Flow-owned parameters cannot be overridden. */
380
380
  authorizationParams?: Record<string, string>;
381
- /** Exact authorization-code redirect URI for pre-registered clients */
381
+ /** Exact authorization-code redirect URI for pre-registered clients. HTTPS redirects use manual callback URL completion. */
382
382
  redirectUri?: string;
383
383
  /** Client display name for dynamic registration */
384
384
  clientName?: string;
@@ -386,6 +386,8 @@ export interface OAuthConfig {
386
386
  clientUri?: string;
387
387
  /** Client logo URL for dynamic registration; shown on consent screens */
388
388
  logoUri?: string;
389
+ /** HTTPS URL for an authorization-server metadata document used instead of MCP discovery */
390
+ authServerMetadataUrl?: string;
389
391
  /** Security-weakening escape hatch for known-misconfigured authorization servers. */
390
392
  skipIssuerMetadataValidation?: boolean;
391
393
  }
@@ -4,6 +4,48 @@ 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
+
27
+ ## [0.26.0] - 2026-08-28
28
+
29
+ ### Highlights
30
+ - `web_search` now includes XCrawl as an explicit search provider.
31
+ - XCrawl results now produce safer clickable links, including relative redirect links from the API.
32
+ - Local models can send multiple web-search queries as a JSON string and still get separate searches.
33
+ - HTML fallback extraction is quieter and more accurate when Defuddle cannot process a page.
34
+
35
+ ### Added
36
+
37
+ - Added XCrawl as an explicit-only search provider (`provider: "xcrawl"`) with `xcrawlApiKey` / `XCRAWL_API_KEY` credentials, client-side domain filtering, fallback titles, and retriable provider-timeout errors. Thanks to [@zeroicey](https://github.com/zeroicey) for #312 and #313.
38
+
39
+ ### Changed
40
+
41
+ - Refined the XCrawl provider docs, parser shape, availability metadata, and focused tests.
42
+
43
+ ### Fixed
44
+
45
+ - Expanded JSON-array strings supplied in the singular `web_search` `query` field into independent searches, with a clear no-query error for empty arrays. Thanks to [@alex-rs](https://github.com/alex-rs) for #317 and [@zeroicey](https://github.com/zeroicey) for PR #319.
46
+ - Kept Defuddle selector-processing failures out of the Pi console and stopped treating the raw page body as a successful extraction. Thanks to [@riabiy](https://github.com/riabiy) for #315.
47
+ - Resolved API-origin-relative XCrawl result links to absolute URLs and rejected blank result links instead of emitting fabricated source URLs. Thanks to [@zeroicey](https://github.com/zeroicey) for #318.
48
+
7
49
  ## [0.25.0] - 2026-08-25
8
50
 
9
51
  ### Highlights