@herbertgao/pi-extensions 2026.8.4 → 2026.8.6

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 (360) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +11 -3
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +74 -5
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +53 -14
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  14. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  15. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +18 -0
  16. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +20 -4
  21. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  22. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +15 -6
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  26. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  27. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +9 -5
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -0
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  36. package/node_modules/@narumitw/pi-btw/README.md +32 -24
  37. package/node_modules/@narumitw/pi-btw/package.json +6 -6
  38. package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
  39. package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
  40. package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
  41. package/node_modules/pi-lens/CHANGELOG.md +2535 -954
  42. package/node_modules/pi-lens/README.md +13 -0
  43. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  44. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +314 -0
  45. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  46. package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
  47. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  48. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  49. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  50. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  51. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  52. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  53. package/node_modules/pi-lens/dist/clients/biome-client.js +136 -24
  54. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  55. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  56. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  57. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +62 -6
  58. package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
  59. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  60. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  61. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  62. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  63. package/node_modules/pi-lens/dist/clients/cascade-format.js +57 -2
  64. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  65. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  66. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  67. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  68. package/node_modules/pi-lens/dist/clients/console-guard-install.js +26 -0
  69. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  70. package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
  71. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  72. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  73. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  74. package/node_modules/pi-lens/dist/clients/dependency-checker.js +361 -77
  75. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
  76. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
  77. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
  78. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  79. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  80. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  81. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  82. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  83. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1172 -683
  84. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  86. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  87. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +47 -6
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +226 -0
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +473 -42
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -0
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  114. package/node_modules/pi-lens/dist/clients/disposition-publish.js +18 -4
  115. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  116. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
  117. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  118. package/node_modules/pi-lens/dist/clients/extension-log.js +432 -0
  119. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  120. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  121. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  122. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  123. package/node_modules/pi-lens/dist/clients/fix-worklog.js +5 -1
  124. package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
  125. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  126. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  127. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  128. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  129. package/node_modules/pi-lens/dist/clients/git-guard.js +678 -11
  130. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  131. package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
  132. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
  133. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  134. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  135. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  136. package/node_modules/pi-lens/dist/clients/installer/index.js +395 -100
  137. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  138. package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
  139. package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
  140. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  141. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  142. package/node_modules/pi-lens/dist/clients/latency-logger.js +72 -1
  143. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  144. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  145. package/node_modules/pi-lens/dist/clients/lens-events.js +63 -9
  146. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
  147. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  148. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
  149. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  150. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
  151. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
  152. package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
  153. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  154. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
  155. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  156. package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
  157. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  158. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  159. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  160. package/node_modules/pi-lens/dist/clients/lsp/server.js +437 -86
  161. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
  162. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
  163. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
  164. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  165. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  166. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  167. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +127 -32
  168. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  169. package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
  170. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  171. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  172. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  173. package/node_modules/pi-lens/dist/clients/model-provider.js +53 -0
  174. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  175. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  176. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  177. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  178. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  179. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  180. package/node_modules/pi-lens/dist/clients/pipeline.js +76 -15
  181. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  182. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  183. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  184. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +1 -1
  185. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  186. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +23 -2
  187. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  188. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  189. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  190. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  191. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  192. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  193. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  194. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  195. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  196. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  197. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  198. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  199. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  201. package/node_modules/pi-lens/dist/clients/review-graph/query.js +24 -0
  202. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  203. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  204. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  205. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  206. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  207. package/node_modules/pi-lens/dist/clients/run-duration.js +55 -0
  208. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
  209. package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
  210. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
  211. package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -117
  212. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  213. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +342 -33
  214. package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
  215. package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
  216. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  217. package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
  218. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  219. package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
  220. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  221. package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -0
  222. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  223. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  224. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  225. package/node_modules/pi-lens/dist/clients/startup-timing.js +7 -1
  226. package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
  227. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  228. package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
  229. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  230. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -0
  231. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  232. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  233. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  234. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  235. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  236. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  237. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  238. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  239. package/node_modules/pi-lens/dist/clients/warm-attach.js +50 -13
  240. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  241. package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
  242. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  243. package/node_modules/pi-lens/dist/index.js +51785 -36118
  244. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  245. package/node_modules/pi-lens/dist/mcp/server.js +219 -46
  246. package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
  247. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
  248. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  249. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
  250. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
  251. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
  252. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  253. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  254. package/node_modules/pi-lens/dist/tools/symbol-search.js +1 -1
  255. package/node_modules/pi-lens/docs/agent-guide.md +40 -15
  256. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  257. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  258. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  259. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +14 -3
  260. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  261. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  262. package/node_modules/pi-lens/docs/fable.md +27 -0
  263. package/node_modules/pi-lens/docs/features.md +45 -3
  264. package/node_modules/pi-lens/docs/globalconfig.md +56 -1
  265. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  266. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  267. package/node_modules/pi-lens/docs/mcp.md +37 -4
  268. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  269. package/node_modules/pi-lens/docs/settings.md +18 -1
  270. package/node_modules/pi-lens/docs/tools.md +4 -1
  271. package/node_modules/pi-lens/docs/usage.md +41 -5
  272. package/node_modules/pi-lens/docs/word-index.md +35 -0
  273. package/node_modules/pi-lens/package.json +10 -10
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +8 -0
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -0
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +7 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  288. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  289. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +21 -0
  290. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
  291. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -0
  292. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  293. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  294. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +9 -0
  295. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
  296. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
  297. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
  298. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -0
  299. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  300. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  301. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  302. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +134 -2
  303. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
  304. package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
  305. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
  306. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
  307. package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
  308. package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
  309. package/node_modules/pi-mcp-adapter/README.md +38 -4
  310. package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
  311. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  312. package/node_modules/pi-mcp-adapter/cli.js +25 -4
  313. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  314. package/node_modules/pi-mcp-adapter/config.ts +4 -4
  315. package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
  316. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  317. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  318. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  319. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  320. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  321. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  322. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  323. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  324. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +2 -1
  325. package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
  326. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  327. package/node_modules/pi-mcp-adapter/package.json +4 -3
  328. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  329. package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
  330. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  331. package/node_modules/pi-mcp-adapter/server-manager.ts +41 -7
  332. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  333. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  334. package/node_modules/pi-mcp-adapter/tool-metadata.ts +62 -4
  335. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  336. package/node_modules/pi-mcp-adapter/types.ts +176 -27
  337. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  338. package/node_modules/pi-web-access/CHANGELOG.md +14 -0
  339. package/node_modules/pi-web-access/README.md +18 -12
  340. package/node_modules/pi-web-access/auth-fetch.ts +148 -0
  341. package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
  342. package/node_modules/pi-web-access/curator-page.ts +5 -3
  343. package/node_modules/pi-web-access/curator-server.ts +2 -1
  344. package/node_modules/pi-web-access/extract.ts +106 -34
  345. package/node_modules/pi-web-access/fetch-params.ts +17 -3
  346. package/node_modules/pi-web-access/firecrawl.ts +172 -12
  347. package/node_modules/pi-web-access/gemini-search.ts +18 -4
  348. package/node_modules/pi-web-access/index.ts +120 -48
  349. package/node_modules/pi-web-access/package.json +2 -2
  350. package/node_modules/pi-web-access/summary-review.ts +11 -5
  351. package/node_modules/pi-web-access/youtube-extract.ts +2 -2
  352. package/package.json +12 -13
  353. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  354. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  355. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  356. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  357. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  358. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  359. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  360. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -6,13 +6,14 @@
6
6
  * - Auto-installation strategies
7
7
  * - Platform-specific handling
8
8
  */
9
- import { existsSync, mkdirSync, readdirSync } from "node:fs";
9
+ import { existsSync, mkdirSync } from "node:fs";
10
10
  import { access, readFile, readdir, stat, } from "node:fs/promises";
11
11
  import os from "node:os";
12
12
  import path from "node:path";
13
- import { getGlobalPiLensDir } from "../file-utils.js";
13
+ import { getGlobalPiLensDir, getProjectIgnoreGlobs, isPathIgnoredByProject, } from "../file-utils.js";
14
+ import { STAGE_TMP_PATTERN } from "../atomic-write-staging.js";
14
15
  import { DOTNET_CSHARP_ROOT_MARKERS, DOTNET_FSHARP_ROOT_MARKERS, KIND_EXTENSIONS, } from "../file-kinds.js";
15
- import { direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, } from "../path-utils.js";
16
+ import { direntsHaveMarkerGlobMatch, isAtOrAboveHomeDir, isFullyQualified, isWindowsPath, } from "../path-utils.js";
16
17
  import { ensureTool, getToolEnvironment, getToolPath, } from "../installer/index.js";
17
18
  import { resolveOpengrepConfig } from "../opengrep-config.js";
18
19
  import { isZizmorAuditTarget, resolveZizmorGitHubToken } from "../zizmor-config.js";
@@ -22,40 +23,203 @@ import { findLocalSgconfig, resolveBaselineSgconfig } from "../sgconfig.js";
22
23
  import { findLocalTyposConfig } from "../typos-config.js";
23
24
  import { resolvePackagePath } from "../package-root.js";
24
25
  import { resolveAstGrepNativeExe } from "./wait-policy/index.js";
25
- import { isCommandAvailableAsync, safeSpawnAsync } from "../safe-spawn.js";
26
+ import { hasSpawnFailureKind, isCommandAvailableAsync, safeSpawnAsync, } from "../safe-spawn.js";
26
27
  import { launchLSP } from "./launch.js";
27
28
  import { createLombokJdtlsArgs } from "./lombok.js";
28
29
  import { resolveJavaRuntimeEnv } from "./jvm-runtime.js";
29
30
  import { normalizeMapKey } from "./path-utils.js";
31
+ import { getRubyVersionDirNamesSync } from "./ruby-drive-dirs.js";
32
+ const FIXTURE_ROOT_SEGMENTS = new Set(["__fixtures__", "testdata"]);
33
+ const FALLBACK_PROJECT_MARKERS = [
34
+ ".git",
35
+ "package.json",
36
+ "go.work",
37
+ "go.mod",
38
+ "Cargo.toml",
39
+ "pyproject.toml",
40
+ "pom.xml",
41
+ "build.gradle",
42
+ "build.gradle.kts",
43
+ "mix.exs",
44
+ "pubspec.yaml",
45
+ "Package.swift",
46
+ ];
47
+ /**
48
+ * Markers that make a nested root an independently hosted project rather than
49
+ * a config-only directory that should share an ancestor's LSP client.
50
+ */
51
+ const PROJECT_BOUNDARY_MARKERS = [
52
+ "package.json",
53
+ "package-lock.json",
54
+ "pnpm-lock.yaml",
55
+ "yarn.lock",
56
+ "bun.lock",
57
+ "bun.lockb",
58
+ "go.work",
59
+ "go.mod",
60
+ "Cargo.toml",
61
+ "Cargo.lock",
62
+ "pyproject.toml",
63
+ "uv.lock",
64
+ "poetry.lock",
65
+ "Pipfile",
66
+ "Pipfile.lock",
67
+ "pom.xml",
68
+ "build.gradle",
69
+ "build.gradle.kts",
70
+ "settings.gradle",
71
+ "settings.gradle.kts",
72
+ "mix.exs",
73
+ "pubspec.yaml",
74
+ "Package.swift",
75
+ ];
76
+ // This is process-global state, so each candidate pair is logged once per
77
+ // process (not once per session). Keep the root-boundary marker below aligned
78
+ // with FALLBACK_PROJECT_MARKERS, the shared fallback root-policy marker set.
79
+ const loggedRootCeilingClamps = new Set();
80
+ function isSameOrWithin(ancestor, candidate) {
81
+ const windowsShaped = isWindowsPath(ancestor) || isWindowsPath(candidate);
82
+ const pathApi = windowsShaped ? path.win32 : path;
83
+ const relative = pathApi.relative(pathApi.resolve(ancestor), pathApi.resolve(candidate));
84
+ return (relative === "" ||
85
+ (!relative.startsWith("..") && !pathApi.isAbsolute(relative)));
86
+ }
87
+ /** Enforce the session cwd as the hard boundary for LSP client root selection. */
88
+ export function enforceLspRootCeiling(root, sessionCwd, filePath) {
89
+ const windowsShaped = isWindowsPath(root) || isWindowsPath(sessionCwd);
90
+ const pathApi = windowsShaped ? path.win32 : path;
91
+ const resolvedRoot = pathApi.resolve(root);
92
+ const resolvedCwd = pathApi.resolve(sessionCwd);
93
+ // Callers may explicitly inspect an out-of-session file (notably isolated
94
+ // tests and API consumers). The cwd ceiling governs roots for files that are
95
+ // actually inside the declared session project.
96
+ if (filePath && !isSameOrWithin(resolvedCwd, pathApi.resolve(filePath))) {
97
+ return resolvedRoot;
98
+ }
99
+ if (isSameOrWithin(resolvedCwd, resolvedRoot))
100
+ return resolvedRoot;
101
+ const logKey = `${normalizeMapKey(resolvedCwd)}:${normalizeMapKey(resolvedRoot)}`;
102
+ if (!loggedRootCeilingClamps.has(logKey)) {
103
+ loggedRootCeilingClamps.add(logKey);
104
+ logSessionStart(`lsp root clamped to session cwd: candidate=${resolvedRoot} cwd=${resolvedCwd}`);
105
+ }
106
+ return resolvedCwd;
107
+ }
108
+ export async function hasProjectBoundaryMarker(dir) {
109
+ // A nested Git checkout is an independently hosted project even when it has
110
+ // no language manifest. Keep this directory boundary aligned with the shared
111
+ // FALLBACK_PROJECT_MARKERS policy used by nearestNonExcludedFallbackRoot.
112
+ if (await markerExists(dir, ".git"))
113
+ return true;
114
+ for (const marker of PROJECT_BOUNDARY_MARKERS) {
115
+ if (await markerExists(dir, marker))
116
+ return true;
117
+ }
118
+ return false;
119
+ }
120
+ function pathSegments(dir) {
121
+ const parsed = path.parse(path.resolve(dir));
122
+ return path
123
+ .relative(parsed.root, path.resolve(dir))
124
+ .split(path.sep)
125
+ .filter(Boolean)
126
+ .map((segment) => segment.toLowerCase());
127
+ }
128
+ function hasFixtureConvention(dir) {
129
+ const segments = pathSegments(dir);
130
+ // Go treats any directory named testdata as fixture data by convention. The
131
+ // exclusion is intentionally ancestor-wide so nested fixture projects cannot
132
+ // become independent LSP roots, but the segment match itself stays exact.
133
+ if (segments.some((segment) => FIXTURE_ROOT_SEGMENTS.has(segment)))
134
+ return true;
135
+ return segments.some((segment, index) => segment === "tests" && segments[index + 1] === "fixtures");
136
+ }
137
+ function hasAtomicStageSegment(dir) {
138
+ return pathSegments(dir).some((segment) => STAGE_TMP_PATTERN.test(segment));
139
+ }
140
+ async function findGitBoundary(dir) {
141
+ let current = path.resolve(dir);
142
+ const fsRoot = path.parse(current).root;
143
+ while (true) {
144
+ if (await markerExists(current, ".git"))
145
+ return current;
146
+ if (current === fsRoot)
147
+ return undefined;
148
+ current = path.dirname(current);
149
+ }
150
+ }
151
+ async function isExcludedLspRoot(dir) {
152
+ const candidate = path.resolve(dir);
153
+ if (hasFixtureConvention(candidate) || hasAtomicStageSegment(candidate))
154
+ return true;
155
+ const gitRoot = await findGitBoundary(candidate);
156
+ if (!gitRoot || gitRoot === candidate)
157
+ return false;
158
+ // Avoid constructing the matcher when the project has no positive ignore rules.
159
+ // The matcher remains authoritative (including anchored rules and directory form).
160
+ if (getProjectIgnoreGlobs(gitRoot).length === 0)
161
+ return false;
162
+ return isPathIgnoredByProject(candidate, gitRoot, true);
163
+ }
164
+ async function nearestNonExcludedFallbackRoot(candidate) {
165
+ if (!(await isExcludedLspRoot(candidate)))
166
+ return path.resolve(candidate);
167
+ let current = path.dirname(path.resolve(candidate));
168
+ const fsRoot = path.parse(current).root;
169
+ let nearestAllowed;
170
+ while (true) {
171
+ if (!(await isExcludedLspRoot(current))) {
172
+ nearestAllowed ??= current;
173
+ for (const marker of FALLBACK_PROJECT_MARKERS) {
174
+ if (await markerExists(current, marker))
175
+ return current;
176
+ }
177
+ }
178
+ if (current === fsRoot)
179
+ break;
180
+ current = path.dirname(current);
181
+ }
182
+ // No project marker was available. Keep the file attached to a stable,
183
+ // non-excluded ancestor rather than minting a client inside the fixture/stage.
184
+ return nearestAllowed ?? fsRoot;
185
+ }
30
186
  function isLspInstallDisabled() {
31
187
  return process.env.PI_LENS_DISABLE_LSP_INSTALL === "1";
32
188
  }
33
189
  function canInstall(allowInstall) {
34
190
  return allowInstall !== false && !isLspInstallDisabled();
35
191
  }
36
- function isCommandNotFoundError(error) {
37
- const msg = String(error);
38
- return (msg.includes("not found") ||
39
- msg.includes("ENOENT") ||
40
- msg.includes("not recognized"));
41
- }
42
192
  const DIRECT_LSP_NEGATIVE_TTL_MS = Math.max(30_000, Number.parseInt(process.env.PI_LENS_DIRECT_LSP_NEGATIVE_TTL_MS ?? "600000", 10) || 600_000);
43
193
  const directLspCommandUnavailableUntil = new Map();
44
194
  const directLspCommandSkipLoggedUntil = new Map();
195
+ function pruneExpiredDirectLspNegativeEntries(now = Date.now()) {
196
+ for (const [command, until] of directLspCommandUnavailableUntil) {
197
+ if (until <= now) {
198
+ directLspCommandUnavailableUntil.delete(command);
199
+ directLspCommandSkipLoggedUntil.delete(command);
200
+ }
201
+ }
202
+ for (const [command, until] of directLspCommandSkipLoggedUntil) {
203
+ if (until <= now)
204
+ directLspCommandSkipLoggedUntil.delete(command);
205
+ }
206
+ }
45
207
  function isSimpleCommand(command) {
46
- return (!path.isAbsolute(command) &&
208
+ return (!isFullyQualified(command) &&
47
209
  !command.includes("/") &&
48
210
  !command.includes("\\"));
49
211
  }
50
212
  export function isDirectLspCommandTemporarilyUnavailable(command) {
213
+ const now = Date.now();
214
+ pruneExpiredDirectLspNegativeEntries(now);
51
215
  const until = directLspCommandUnavailableUntil.get(command);
52
- if (!until || until <= Date.now()) {
216
+ if (!until || until <= now) {
53
217
  directLspCommandUnavailableUntil.delete(command);
54
218
  return false;
55
219
  }
56
220
  const loggedUntil = directLspCommandSkipLoggedUntil.get(command) ?? 0;
57
- if (loggedUntil <= Date.now()) {
58
- logSessionStart(`lsp direct command ${command}: skipped by negative availability cache (${Math.max(0, until - Date.now())}ms remaining)`);
221
+ if (loggedUntil <= now) {
222
+ logSessionStart(`lsp direct command ${command}: skipped by negative availability cache (${Math.max(0, until - now)}ms remaining)`);
59
223
  directLspCommandSkipLoggedUntil.set(command, until);
60
224
  }
61
225
  return true;
@@ -74,7 +238,7 @@ export async function resolveAndLaunch(spec, allowInstall) {
74
238
  let lastRuntimeFailure;
75
239
  const trackRuntimeFailure = (err) => {
76
240
  const message = err instanceof Error ? err.message : String(err);
77
- if (!isCommandNotFoundError(message)) {
241
+ if (!hasSpawnFailureKind(err, "tool-not-found")) {
78
242
  lastRuntimeFailure = err instanceof Error ? err : new Error(message);
79
243
  }
80
244
  };
@@ -145,6 +309,12 @@ export async function resolveAndLaunch(spec, allowInstall) {
145
309
  logSessionStart(`lsp launch candidate failed tool=${toolLabel} idx=${failure.index} command=${failure.command} error=${failure.message}`);
146
310
  trackRuntimeFailure(failure.err);
147
311
  }
312
+ const hasOnlyRepairableCandidateFailures = candidateFailures.every((failure) => hasSpawnFailureKind(failure.err, "tool-not-found"));
313
+ if (!hasOnlyRepairableCandidateFailures) {
314
+ if (lastRuntimeFailure)
315
+ throw lastRuntimeFailure;
316
+ return undefined;
317
+ }
148
318
  if (!canInstall(allowInstall)) {
149
319
  logSessionStart(`lsp launch install blocked tool=${toolLabel} cwd=${spec.cwd} allowInstall=${allowInstall !== false} globalDisabled=${isLspInstallDisabled()}`);
150
320
  logLatency({
@@ -214,7 +384,7 @@ export async function resolveAndLaunch(spec, allowInstall) {
214
384
  // fails to launch (e.g. broken symlink, missing .dll), nuke the
215
385
  // caches and download a managed copy from the registry.
216
386
  const looksPathResolved = !installed.includes("/") && !installed.includes("\\");
217
- if (looksPathResolved) {
387
+ if (looksPathResolved && hasSpawnFailureKind(err, "tool-not-found")) {
218
388
  logSessionStart(`lsp launch managed retry force-reinstall tool=${spec.managedToolId}`);
219
389
  const reinstalled = await ensureTool(spec.managedToolId, {
220
390
  forceReinstall: true,
@@ -415,19 +585,13 @@ function rubyBinCandidates(baseName) {
415
585
  candidates.push(path.join(home, ".asdf", "installs", "ruby", "bin", `${baseName}${ext}`));
416
586
  if (isWin) {
417
587
  // Ruby installer drops versioned dirs on C: by convention, but the drive
418
- // and version suffix vary — scan what's actually present instead of hardcoding
588
+ // and version suffix vary — scan what's actually present instead of
589
+ // hardcoding. Memoized once per process (#1137): a synchronous drive-root
590
+ // enumeration per LSP spawn was an event-loop offender.
419
591
  const driveRoot = path.parse(home).root; // e.g. "C:\"
420
- try {
421
- const entries = readdirSync(driveRoot);
422
- for (const entry of entries) {
423
- if (/^ruby\d/i.test(entry)) {
424
- candidates.push(path.join(driveRoot, entry, "bin", `${baseName}.bat`));
425
- candidates.push(path.join(driveRoot, entry, "bin", baseName));
426
- }
427
- }
428
- }
429
- catch {
430
- // drive root not readable — skip
592
+ for (const entry of getRubyVersionDirNamesSync(driveRoot)) {
593
+ candidates.push(path.join(driveRoot, entry, "bin", `${baseName}.bat`));
594
+ candidates.push(path.join(driveRoot, entry, "bin", baseName));
431
595
  }
432
596
  }
433
597
  return candidates;
@@ -465,7 +629,7 @@ function createInteractiveServer(spec) {
465
629
  return { process: proc, source: "direct", initialization };
466
630
  }
467
631
  catch (err) {
468
- if (isCommandNotFoundError(err)) {
632
+ if (hasSpawnFailureKind(err, "tool-not-found")) {
469
633
  markDirectLspCommandUnavailable(command);
470
634
  }
471
635
  return undefined;
@@ -484,7 +648,10 @@ export function PriorityRoot(markerGroups, excludePatterns, stopDir) {
484
648
  return undefined;
485
649
  };
486
650
  }
487
- export const FileDirRoot = async (file) => path.resolve(path.dirname(file));
651
+ export const FileDirRoot = async (file) => {
652
+ const candidate = path.resolve(path.dirname(file));
653
+ return nearestNonExcludedFallbackRoot(candidate);
654
+ };
488
655
  export function RootWithFallback(primary, fallback = FileDirRoot) {
489
656
  return async (file) => {
490
657
  const primaryRoot = await primary(file);
@@ -555,6 +722,12 @@ export function NearestRoot(includePatterns, excludePatterns, stopDir) {
555
722
  // different servers (e.g. TypeScript vs Go) with different marker sets never
556
723
  // share entries. vi.resetModules() in tests resets module state between cases.
557
724
  const cache = new Map();
725
+ // Only cache successful hits. Undefined results are NOT cached so that a
726
+ // newly-created root marker (e.g. package.json or tsconfig.json scaffolded
727
+ // mid-session by the agent) is detected on the next call — the absent →
728
+ // present transition must work without a process restart. The uncached
729
+ // re-walk cost for configless repos is a known trade-off; bounding the
730
+ // walk with stopDir for in-cwd files is the tracked optimization (#1412).
558
731
  const inFlight = new Map();
559
732
  return async (file) => {
560
733
  // Cache key is the resolved directory — all files in the same dir share a root.
@@ -597,8 +770,10 @@ export function NearestRoot(includePatterns, excludePatterns, stopDir) {
597
770
  // Check include patterns. Exact marker names stay cheap (`stat`), while
598
771
  // glob markers like `*.csproj` match real project filenames (#201).
599
772
  for (const pattern of includePatterns) {
600
- if (await markerExists(currentDir, pattern))
601
- return currentDir;
773
+ if ((await markerExists(currentDir, pattern)) &&
774
+ !(await isExcludedLspRoot(currentDir))) {
775
+ return enforceLspRootCeiling(currentDir, process.cwd(), file);
776
+ }
602
777
  }
603
778
  if (currentDir === stop || currentDir === fsRoot) {
604
779
  break;
@@ -610,9 +785,6 @@ export function NearestRoot(includePatterns, excludePatterns, stopDir) {
610
785
  inFlight.set(dirKey, promise);
611
786
  try {
612
787
  const result = await promise;
613
- // Only cache successful hits. Undefined results are not cached so that
614
- // a newly-created root marker (e.g. package.json added mid-session) is
615
- // detected on the next call.
616
788
  if (result !== undefined)
617
789
  cache.set(dirKey, result);
618
790
  return result;
@@ -665,44 +837,173 @@ export async function tryDotnetToolInstall(tool) {
665
837
  return !updateResult.error && updateResult.status === 0;
666
838
  }
667
839
  /**
668
- * Locate tsserver.js tries local project, then pi-lens managed TypeScript.
669
- * Returns the path to tsserver.js, or undefined if not found.
840
+ * #1412 M1: walk up from `startDir` (inclusive) looking for a file at
841
+ * `startDir/<relativeSegments>`, `dirname(startDir)/<relativeSegments>`, and so
842
+ * on — the same ancestor walk `findNativeTypeScriptLsp` uses, bounded the same
843
+ * way (`isAtOrAboveHomeDir`). A nested config root (e.g. a `cypress/tsconfig.json`
844
+ * LSP root inside a repo whose `node_modules` only exists at the repo root) must
845
+ * still resolve tooling installed at an ancestor, not just directly under the
846
+ * LSP root — mirrors how node module resolution itself walks up.
670
847
  */
671
- async function findTsserverPath(root, allowInstall) {
848
+ async function findAncestorFile(startDir, ...relativeSegments) {
849
+ return findAncestorFileAmong(startDir, [relativeSegments]);
850
+ }
851
+ /**
852
+ * Same ancestor walk as `findAncestorFile`, but checks every candidate
853
+ * relative-path in `candidateSegmentLists` AT EACH LEVEL before moving up —
854
+ * so the nearest ancestor wins regardless of which candidate name matched
855
+ * there, matching normal node_modules resolution priority (nearest install
856
+ * shadows a further one, never the reverse).
857
+ */
858
+ async function findAncestorFileAmong(startDir, candidateSegmentLists) {
672
859
  const fs = await import("node:fs/promises");
673
- const candidates = [
674
- path.join(root, "node_modules", "typescript", "lib", "tsserver.js"),
675
- path.join(process.cwd(), "node_modules", "typescript", "lib", "tsserver.js"),
860
+ let currentDir = path.resolve(startDir);
861
+ while (!isAtOrAboveHomeDir(currentDir)) {
862
+ for (const segments of candidateSegmentLists) {
863
+ const candidate = path.join(currentDir, ...segments);
864
+ try {
865
+ await fs.access(candidate);
866
+ return candidate;
867
+ }
868
+ catch {
869
+ /* not found at this level */
870
+ }
871
+ }
872
+ const parent = path.dirname(currentDir);
873
+ if (parent === currentDir)
874
+ break;
875
+ currentDir = parent;
876
+ }
877
+ return undefined;
878
+ }
879
+ /**
880
+ * A failed classic-compiler repair must not repeat. `ensureTool` caches
881
+ * successful installs, so a repair that works short-circuits later calls on
882
+ * its own. A repair that fails leaves nothing behind, and `findTsserverPath`
883
+ * has three call sites (TypeScript, Vue, Svelte). Without this guard an
884
+ * offline or partial install re-runs a 120 s forced reinstall on every spawn.
885
+ */
886
+ let classicTsRepairAttempted = false;
887
+ /** Test hook — clears the per-process classic-repair guard. */
888
+ export function _resetClassicTsRepairForTests() {
889
+ classicTsRepairAttempted = false;
890
+ }
891
+ /**
892
+ * Directories that may hold the TypeScript package next to a resolved `tsc`
893
+ * binary: `<bin>/../typescript` (npm-global layout) and
894
+ * `<bin>/../../typescript` (managed `node_modules/.bin` layout).
895
+ */
896
+ function typescriptDirsForTsc(tscPath) {
897
+ const binDir = path.dirname(tscPath);
898
+ return [
899
+ path.join(binDir, "..", "typescript"),
900
+ path.join(binDir, "..", "..", "typescript"),
676
901
  ];
677
- for (const p of candidates) {
902
+ }
903
+ /**
904
+ * Read the major version of the TypeScript package that backs a resolved
905
+ * `tsc` binary. Returns undefined when the version is unknowable: `ensureTool`
906
+ * returns the bare string `"tsc"` for a PATH hit, and `path.dirname("tsc")` is
907
+ * `"."`, so the candidates would go cwd-relative. Callers must not repair on
908
+ * an unknown version — a healthy global TypeScript 5.x would be reinstalled
909
+ * for nothing.
910
+ */
911
+ async function typescriptVersionForTsc(tscPath) {
912
+ if (!path.isAbsolute(tscPath))
913
+ return undefined;
914
+ for (const dir of typescriptDirsForTsc(tscPath)) {
915
+ let manifest;
678
916
  try {
679
- await fs.access(p);
680
- return p;
917
+ manifest = await readFile(path.join(dir, "package.json"), "utf8");
681
918
  }
682
919
  catch {
683
- /* not found */
920
+ continue;
684
921
  }
922
+ let version;
923
+ try {
924
+ const parsed = JSON.parse(manifest);
925
+ if (typeof parsed !== "object" ||
926
+ parsed === null ||
927
+ !("version" in parsed) ||
928
+ typeof parsed.version !== "string") {
929
+ return undefined;
930
+ }
931
+ version = parsed.version;
932
+ }
933
+ catch {
934
+ return undefined;
935
+ }
936
+ const majorText = version.split(".", 1)[0] ?? "";
937
+ const major = /^\d+$/.test(majorText) ? Number(majorText) : Number.NaN;
938
+ if (!Number.isFinite(major))
939
+ return undefined;
940
+ return { version, major };
685
941
  }
686
- // Discover the typescript install (PATH / npm-global) even when install is
687
- // disabled; only the download is gated by allowInstall.
688
- const tscPath = await ensureTool("typescript", {
689
- allowInstall: canInstall(allowInstall),
690
- });
691
- if (tscPath) {
692
- for (const p of [
693
- path.join(path.dirname(tscPath), "..", "typescript", "lib", "tsserver.js"),
694
- path.join(path.dirname(tscPath), "..", "..", "typescript", "lib", "tsserver.js"),
695
- ]) {
942
+ return undefined;
943
+ }
944
+ /**
945
+ * Locate tsserver.js — tries local project (walking up from root, #1412 M1),
946
+ * then process.cwd() as a last-resort fallback, then pi-lens managed
947
+ * TypeScript. Returns the path to tsserver.js, or undefined if not found.
948
+ */
949
+ async function findTsserverPath(root, allowInstall) {
950
+ const fs = await import("node:fs/promises");
951
+ const ancestorHit = await findAncestorFile(root, "node_modules", "typescript", "lib", "tsserver.js");
952
+ if (ancestorHit)
953
+ return ancestorHit;
954
+ const cwdCandidate = path.join(process.cwd(), "node_modules", "typescript", "lib", "tsserver.js");
955
+ try {
956
+ await fs.access(cwdCandidate);
957
+ return cwdCandidate;
958
+ }
959
+ catch {
960
+ /* not found */
961
+ }
962
+ const tsserverForTsc = async (tscPath) => {
963
+ if (!tscPath)
964
+ return undefined;
965
+ for (const dir of typescriptDirsForTsc(tscPath)) {
966
+ const candidate = path.join(dir, "lib", "tsserver.js");
696
967
  try {
697
- await fs.access(p);
698
- return p;
968
+ await fs.access(candidate);
969
+ return candidate;
699
970
  }
700
971
  catch {
701
972
  /* not found */
702
973
  }
703
974
  }
975
+ return undefined;
976
+ };
977
+ // Discover the TypeScript install (PATH / npm-global) even when installation
978
+ // is disabled; only the download is gated by allowInstall.
979
+ const installAllowed = canInstall(allowInstall);
980
+ const discoveredTsc = await ensureTool("typescript", {
981
+ allowInstall: installAllowed,
982
+ });
983
+ const discoveredTsserver = await tsserverForTsc(discoveredTsc);
984
+ if (discoveredTsserver ||
985
+ !discoveredTsc ||
986
+ !installAllowed ||
987
+ classicTsRepairAttempted) {
988
+ return discoveredTsserver;
704
989
  }
705
- return undefined;
990
+ // Repair only a compiler we can prove is TypeScript 7+. TypeScript 7 dropped
991
+ // lib/tsserver.js, so the classic wrapper cannot start against it. Any other
992
+ // version — or a version we cannot read, such as a bare PATH `tsc` — is left
993
+ // alone rather than force-reinstalled.
994
+ const discoveredVersion = await typescriptVersionForTsc(discoveredTsc);
995
+ if (!discoveredVersion || discoveredVersion.major < 7)
996
+ return undefined;
997
+ // An older managed tree took `latest` before the registry pinned the classic
998
+ // compiler. Reinstall the pinned version once so that tree self-heals,
999
+ // without deleting user or project-local TypeScript installations.
1000
+ classicTsRepairAttempted = true;
1001
+ logSessionStart(`lsp typescript: managed compiler resolved to TypeScript ${discoveredVersion.version}, which ships no tsserver.js; reinstalling pinned classic fallback`);
1002
+ const repairedTsc = await ensureTool("typescript", {
1003
+ allowInstall: true,
1004
+ forceReinstall: true,
1005
+ });
1006
+ return tsserverForTsc(repairedTsc);
706
1007
  }
707
1008
  /**
708
1009
  * TypeScript 7+ ships the native typescript-go language server through the
@@ -882,10 +1183,17 @@ export async function detectPythonVenv(root) {
882
1183
  }
883
1184
  // --- Server Definitions ---
884
1185
  const JS_TS_LSP_EXTENSIONS = KIND_EXTENSIONS["jsts"].filter((ext) => ext !== ".svelte" && ext !== ".vue");
885
- // Marker set used for both the unbounded TypeScriptProjectRoot walk and the
886
- // extension-bounded walk below. Kept in one place so both code paths look
887
- // for the same project signals.
888
- const TS_PROJECT_MARKERS = [
1186
+ // TypeScript identity and tooling discovery deliberately use separate marker
1187
+ // families. A governing config wins even when a package directory supplies
1188
+ // hoisted binaries. Keep configs out of PROJECT_BOUNDARY_MARKERS: #1373 still
1189
+ // coalesces a config-only nested root when an ancestor client was hosted first
1190
+ // (nested-config-first remains intentionally open-order-sensitive). #1412
1191
+ // accepted risk (M2, not fixed here): honoring nested config roots at all
1192
+ // enlarges the population of directories that can independently coalesce or
1193
+ // diverge under #1373's open-order sensitivity — the same pre-existing
1194
+ // blast-radius, just triggered by more roots than before.
1195
+ const TS_CONFIG_MARKERS = ["tsconfig.json", "jsconfig.json"];
1196
+ const TS_TOOLING_MARKERS = [
889
1197
  "package-lock.json",
890
1198
  "bun.lockb",
891
1199
  "bun.lock",
@@ -893,21 +1201,71 @@ const TS_PROJECT_MARKERS = [
893
1201
  "yarn.lock",
894
1202
  "package.json",
895
1203
  ];
896
- const TypeScriptProjectRoot = IgnoreHomeRoot(createRootDetector([...TS_PROJECT_MARKERS]));
1204
+ // #1412 M3: tsserver associates jsconfig.json with JS files only (its identity
1205
+ // probe reports a jsconfig-governed .ts file as unassociated) — so a TS-family
1206
+ // file under a jsconfig-only directory must NOT root there; keep walking up
1207
+ // for a real tsconfig.json. A JS-family file accepts either: tsconfig also
1208
+ // governs plain JS via `allowJs`, so accepting tsconfig for a .js file is
1209
+ // correct, and jsconfig obviously is too.
1210
+ const TS_FAMILY_EXTENSIONS = new Set([".ts", ".tsx", ".mts", ".cts"]);
1211
+ function isTsFamilyFile(file) {
1212
+ return TS_FAMILY_EXTENSIONS.has(path.extname(file).toLowerCase());
1213
+ }
1214
+ function tsConfigMarkersForFile(file) {
1215
+ return isTsFamilyFile(file) ? ["tsconfig.json"] : TS_CONFIG_MARKERS;
1216
+ }
1217
+ // Two detector instances (not one parameterized by file) so each keeps its own
1218
+ // per-directory NearestRoot cache valid for its fixed marker set — a shared
1219
+ // cache keyed only by directory would conflate the TS-only and either-config
1220
+ // answers for the same directory.
1221
+ const TypeScriptConfigRootTsOnly = IgnoreHomeRoot(createRootDetector(["tsconfig.json"]));
1222
+ const TypeScriptConfigRootEither = IgnoreHomeRoot(createRootDetector([...TS_CONFIG_MARKERS]));
1223
+ const TypeScriptToolingRoot = IgnoreHomeRoot(createRootDetector([...TS_TOOLING_MARKERS]));
1224
+ async function findTypeScriptProjectRoot(file) {
1225
+ const configDetector = isTsFamilyFile(file)
1226
+ ? TypeScriptConfigRootTsOnly
1227
+ : TypeScriptConfigRootEither;
1228
+ const [configRoot, toolingRoot] = await Promise.all([
1229
+ configDetector(file),
1230
+ TypeScriptToolingRoot(file),
1231
+ ]);
1232
+ if (!configRoot)
1233
+ return toolingRoot;
1234
+ if (!toolingRoot)
1235
+ return configRoot;
1236
+ // A config inside (or beside) the nearest package governs its files. A
1237
+ // config above a nearer package must not erase that topology boundary.
1238
+ return isSameOrWithin(toolingRoot, configRoot) ? configRoot : toolingRoot;
1239
+ }
897
1240
  /**
898
- * Walk up from the file's directory looking for a TypeScript project marker,
899
- * but stop at `extensionRootKey` so we never escape the .pi/agent/extensions
900
- * boundary into a higher-up project (e.g. ~/.pi/agent/package.json which
901
- * would pull every extension in the directory into one LSP workspace).
1241
+ * Walk up from the file's directory looking for a TypeScript project marker
1242
+ * (a governing config first, per-directory, per #1412 M3's extension-family
1243
+ * filter; else a tooling/lockfile marker), but stop at `extensionRootKey` so
1244
+ * we never escape the .pi/agent/extensions boundary into a higher-up project
1245
+ * (e.g. ~/.pi/agent/package.json which would pull every extension in the
1246
+ * directory into one LSP workspace).
902
1247
  *
903
- * Returns the nearest directory containing a marker, or undefined if none
904
- * is found between the file and the extensions root inclusive.
1248
+ * #1412 L4: each directory returns immediately on its first match (config or
1249
+ * tooling) there is no cross-level "nearest tooling root" to carry forward,
1250
+ * since a match always wins on the spot. Returns the nearest directory
1251
+ * containing a marker, or undefined if none is found between the file and the
1252
+ * extensions root inclusive.
905
1253
  */
906
1254
  async function findExtensionBoundedRoot(file, extensionRootKey) {
907
1255
  const startDir = path.resolve(path.dirname(file));
908
1256
  let currentDir = startDir;
1257
+ const configMarkers = tsConfigMarkersForFile(file);
909
1258
  while (true) {
910
- for (const pattern of TS_PROJECT_MARKERS) {
1259
+ for (const pattern of configMarkers) {
1260
+ try {
1261
+ await stat(path.join(currentDir, pattern));
1262
+ return currentDir;
1263
+ }
1264
+ catch {
1265
+ /* not found, try next marker */
1266
+ }
1267
+ }
1268
+ for (const pattern of TS_TOOLING_MARKERS) {
911
1269
  try {
912
1270
  await stat(path.join(currentDir, pattern));
913
1271
  return currentDir;
@@ -938,7 +1296,7 @@ async function hasAgentLevelProjectMarker(extensionRootKey) {
938
1296
  const agentDir = path.dirname(extensionRootKey);
939
1297
  if (!agentDir || agentDir === extensionRootKey)
940
1298
  return false;
941
- for (const pattern of TS_PROJECT_MARKERS) {
1299
+ for (const pattern of [...TS_CONFIG_MARKERS, ...TS_TOOLING_MARKERS]) {
942
1300
  try {
943
1301
  await stat(path.join(agentDir, pattern));
944
1302
  return true;
@@ -969,7 +1327,7 @@ const TypeScriptRoot = DenoExcludeRoot(async (file) => {
969
1327
  // analyze a lone .ts file with no package.json above or below).
970
1328
  return undefined;
971
1329
  }
972
- const projectRoot = await TypeScriptProjectRoot(file);
1330
+ const projectRoot = await findTypeScriptProjectRoot(file);
973
1331
  if (projectRoot)
974
1332
  return projectRoot;
975
1333
  return FileDirRoot(file);
@@ -995,20 +1353,13 @@ export const TypeScriptServer = {
995
1353
  let source = "direct";
996
1354
  // TypeScript <=6 uses typescript-language-server + tsserver.js. Prefer a
997
1355
  // project-local wrapper, then fall back to discovered/managed tooling.
998
- let lspPath;
999
- const localLsp = path.join(root, "node_modules", ".bin", "typescript-language-server");
1000
- const localLspCmd = path.join(root, "node_modules", ".bin", "typescript-language-server.cmd");
1001
- // Check for local version first (Windows .cmd first, then Unix)
1002
- for (const checkPath of [localLspCmd, localLsp]) {
1003
- try {
1004
- await fs.access(checkPath);
1005
- lspPath = checkPath;
1006
- break;
1007
- }
1008
- catch {
1009
- /* not found */
1010
- }
1011
- }
1356
+ // #1412 M1: walk up from root (Windows .cmd first, then Unix at each
1357
+ // level) a nested config root's node_modules/.bin lives at an ancestor,
1358
+ // not necessarily directly under the LSP root.
1359
+ let lspPath = await findAncestorFileAmong(root, [
1360
+ ["node_modules", ".bin", "typescript-language-server.cmd"],
1361
+ ["node_modules", ".bin", "typescript-language-server"],
1362
+ ]);
1012
1363
  // Fall back to a discovered or managed install. ensureTool() runs PATH /
1013
1364
  // npm-global discovery even when install is disabled (only the download is
1014
1365
  // gated by canInstall), so a globally-installed typescript-language-server