@herbertgao/pi-extensions 2026.8.3 → 2026.8.5

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 (408) hide show
  1. package/README.md +45 -4
  2. package/examples/pi-footer.json +275 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
  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 +5 -8
  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 +8 -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 +15 -2
  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/schedule-menu.ts +9 -8
  25. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  31. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  32. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  33. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  34. package/node_modules/pi-footer/CHANGELOG.md +145 -0
  35. package/node_modules/pi-footer/LICENSE +21 -0
  36. package/node_modules/pi-footer/README.md +354 -0
  37. package/node_modules/pi-footer/package.json +68 -0
  38. package/node_modules/pi-footer/src/cache.ts +106 -0
  39. package/node_modules/pi-footer/src/colors.ts +237 -0
  40. package/node_modules/pi-footer/src/config.ts +202 -0
  41. package/node_modules/pi-footer/src/event-widgets.ts +41 -0
  42. package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
  43. package/node_modules/pi-footer/src/git.ts +135 -0
  44. package/node_modules/pi-footer/src/index.ts +268 -0
  45. package/node_modules/pi-footer/src/metrics.ts +141 -0
  46. package/node_modules/pi-footer/src/presets.ts +383 -0
  47. package/node_modules/pi-footer/src/render.ts +114 -0
  48. package/node_modules/pi-footer/src/separators.ts +52 -0
  49. package/node_modules/pi-footer/src/types.ts +122 -0
  50. package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
  51. package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
  52. package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
  53. package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
  54. package/node_modules/pi-footer/src/ui/events.ts +35 -0
  55. package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
  56. package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
  57. package/node_modules/pi-footer/src/ui/fields.ts +252 -0
  58. package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
  59. package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
  60. package/node_modules/pi-footer/src/ui/layout.ts +17 -0
  61. package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
  62. package/node_modules/pi-footer/src/ui/model.ts +64 -0
  63. package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
  64. package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
  65. package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
  66. package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
  67. package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
  68. package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
  69. package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
  70. package/node_modules/pi-footer/src/ui/screen.ts +243 -0
  71. package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
  72. package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
  73. package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
  74. package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
  75. package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
  76. package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
  77. package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
  78. package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
  79. package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
  80. package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
  81. package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
  82. package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
  83. package/node_modules/pi-footer/src/ui/theme.ts +48 -0
  84. package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
  85. package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
  86. package/node_modules/pi-footer/src/ui.ts +66 -0
  87. package/node_modules/pi-footer/src/widgets/context.ts +28 -0
  88. package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
  89. package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
  90. package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
  91. package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
  92. package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
  93. package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
  94. package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
  95. package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
  96. package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
  97. package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
  98. package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
  99. package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
  100. package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
  101. package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
  102. package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
  103. package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
  104. package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
  105. package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
  106. package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
  107. package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
  108. package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
  109. package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
  110. package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
  111. package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
  112. package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
  113. package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
  114. package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
  115. package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
  116. package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
  117. package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
  118. package/node_modules/pi-footer/src/widgets/options.ts +122 -0
  119. package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
  120. package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
  121. package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
  122. package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
  123. package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
  124. package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
  125. package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
  126. package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
  127. package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
  128. package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
  129. package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
  130. package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
  131. package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
  132. package/node_modules/pi-footer/src/widgets/store.ts +26 -0
  133. package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
  134. package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
  135. package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
  136. package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
  137. package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
  138. package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
  139. package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
  140. package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
  141. package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
  142. package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
  143. package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
  144. package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
  145. package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
  146. package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
  147. package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
  148. package/node_modules/pi-footer/src/widgets/types.ts +181 -0
  149. package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
  150. package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
  151. package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
  152. package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
  153. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  154. package/node_modules/pi-lens/README.md +13 -0
  155. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  156. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  157. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  158. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  159. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  160. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  161. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  162. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  163. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  164. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  165. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  166. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  167. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  168. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  169. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  170. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  171. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  172. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  173. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  174. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  175. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  177. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  178. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  179. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  180. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  181. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  182. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  183. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  184. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  185. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  186. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  187. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  188. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  189. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  190. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  191. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  192. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  193. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  194. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  195. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  196. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  197. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  198. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  199. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  200. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  201. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  202. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  203. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  204. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  205. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  206. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  207. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  208. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  209. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  210. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  211. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  212. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  213. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  214. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  215. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  216. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  217. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  218. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  219. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  220. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  221. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  222. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  223. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  224. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  225. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  226. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  227. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  228. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  229. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  230. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  231. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  232. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  233. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  234. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  235. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  236. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  237. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  238. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  239. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  240. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  241. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  242. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  243. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  244. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  245. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  246. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  247. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  248. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  249. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  250. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  251. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  252. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  253. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  254. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  255. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  256. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  257. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  258. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  259. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  260. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  261. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  262. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  263. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  264. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  265. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  266. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  267. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  268. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  269. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  270. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  271. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  272. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  273. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  274. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  275. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  276. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  277. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  278. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  279. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  280. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  281. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  282. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  283. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  284. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  285. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  286. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  287. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  288. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  289. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  290. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  291. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  292. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  293. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  294. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  295. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  296. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  297. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  298. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  299. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  300. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  301. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  302. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  303. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  304. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  305. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  306. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  307. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  308. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  309. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  310. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  311. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  312. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  313. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  314. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  315. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  316. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  317. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  318. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  319. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  320. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  321. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  322. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  323. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  324. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  325. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  326. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  327. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  328. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  329. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  330. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  331. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  332. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  333. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  334. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  335. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  336. package/node_modules/pi-lens/dist/index.js +49361 -36888
  337. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  338. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  339. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  340. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  341. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  342. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  343. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  344. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  345. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  346. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  347. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  348. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  349. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  350. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  351. package/node_modules/pi-lens/docs/fable.md +27 -0
  352. package/node_modules/pi-lens/docs/features.md +31 -2
  353. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  354. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  355. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  356. package/node_modules/pi-lens/docs/mcp.md +37 -4
  357. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  358. package/node_modules/pi-lens/docs/settings.md +12 -1
  359. package/node_modules/pi-lens/docs/tools.md +4 -1
  360. package/node_modules/pi-lens/docs/usage.md +18 -0
  361. package/node_modules/pi-lens/package.json +10 -10
  362. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  363. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  364. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  365. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  366. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  367. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  368. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  369. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  370. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  371. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  372. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  373. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  374. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  375. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  376. package/node_modules/pi-mcp-adapter/README.md +34 -3
  377. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  378. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  379. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  380. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  381. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  382. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  383. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  384. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  385. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  386. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  387. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  388. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  389. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  390. package/node_modules/pi-mcp-adapter/package.json +3 -3
  391. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  392. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  393. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  394. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  395. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  396. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  397. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  398. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  399. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  400. package/package.json +17 -13
  401. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  402. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  403. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  404. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  405. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  406. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  407. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  408. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -12,8 +12,11 @@ import os from "node:os";
12
12
  import path from "node:path";
13
13
  import { isTestMode } from "../env-utils.js";
14
14
  import { getGlobalPiLensDir } from "../file-utils.js";
15
+ import { isFullyQualified } from "../path-utils.js";
15
16
  import { findGlobalBinary } from "../package-manager.js";
16
17
  import { redactSecrets } from "../redact/secrets.js";
18
+ import { classifySpawnFailure, SpawnFailureError, } from "../safe-spawn.js";
19
+ import { getRubyVersionDirNamesAsync } from "./ruby-drive-dirs.js";
17
20
  const isWindows = process.platform === "win32";
18
21
  /**
19
22
  * Whether a resolved command must be spawned through a shell on Windows.
@@ -109,7 +112,7 @@ function getLiveWindowsPath() {
109
112
  }
110
113
  return _liveWindowsPath;
111
114
  }
112
- function buildAugmentedPath(basePath) {
115
+ async function buildAugmentedPath(basePath) {
113
116
  const candidates = [];
114
117
  const nodeDir = path.dirname(process.execPath);
115
118
  if (nodeDir) {
@@ -125,16 +128,12 @@ function buildAugmentedPath(basePath) {
125
128
  candidates.push(PI_LENS_TOOLS_BIN_DIR);
126
129
  candidates.push(path.join(driveRoot, "Program Files", "Go", "bin"));
127
130
  candidates.push(path.join(driveRoot, "Go", "bin"));
128
- // Ruby installer drops versioned dirs (e.g. Ruby34-x64) on the drive root — scan dynamically
129
- try {
130
- for (const entry of fs.readdirSync(driveRoot)) {
131
- if (/^ruby\d/i.test(entry)) {
132
- candidates.push(path.join(driveRoot, entry, "bin"));
133
- }
134
- }
135
- }
136
- catch {
137
- // drive root not readable — skip
131
+ // Ruby installer drops versioned dirs (e.g. Ruby34-x64) on the drive root.
132
+ // Read off the event loop and memoized once per process (#1137): a
133
+ // synchronous drive-root enumeration on every LSP spawn blocked the loop
134
+ // for the whole stall on a slow cloud/network-backed drive root.
135
+ for (const entry of await getRubyVersionDirNamesAsync(driveRoot)) {
136
+ candidates.push(path.join(driveRoot, entry, "bin"));
138
137
  }
139
138
  }
140
139
  // On Windows, merge the live registry PATH so newly installed tools are visible
@@ -339,10 +338,12 @@ function _attachErrorHandler(proc, context, logContext, rejectOnImmediateError)
339
338
  err.message +
340
339
  (stderrPreview ? " stderr=" + compactLogValue(stderrPreview) : ""));
341
340
  }
342
- // If we have a reject function and this is an immediate spawn error, reject
343
- if (rejectOnImmediateError &&
344
- err.code === "ENOENT") {
345
- rejectOnImmediateError(err);
341
+ // Preserve intent at the boundary instead of leaking a raw errno check.
342
+ if (rejectOnImmediateError && logContext) {
343
+ void classifySpawnFailure(err, {
344
+ command: logContext.command,
345
+ cwd: logContext.cwd,
346
+ }).then(rejectOnImmediateError);
346
347
  }
347
348
  });
348
349
  proc.on("close", (code, signal) => {
@@ -402,7 +403,7 @@ function _attachErrorHandler(proc, context, logContext, rejectOnImmediateError)
402
403
  export async function launchLSP(command, args = [], options = {}) {
403
404
  const cwd = String(options.cwd ?? process.cwd());
404
405
  const mergedEnv = { ...process.env, ...options.env };
405
- const augmentedPath = buildAugmentedPath(resolvePathValue(mergedEnv));
406
+ const augmentedPath = await buildAugmentedPath(resolvePathValue(mergedEnv));
406
407
  const env = {
407
408
  ...mergedEnv,
408
409
  PATH: augmentedPath,
@@ -412,10 +413,10 @@ export async function launchLSP(command, args = [], options = {}) {
412
413
  // - If already absolute, use as-is
413
414
  // - If it's a simple command (no path separators), let system find it via PATH
414
415
  // - Otherwise, resolve relative to cwd
415
- const isRelativePath = !path.isAbsolute(command) &&
416
+ const isRelativePath = !isFullyQualified(command) &&
416
417
  (command.includes(path.sep) || command.includes("/"));
417
418
  const explicitCommand = isRelativePath ? path.resolve(cwd, command) : command;
418
- const resolvedCommand = !path.isAbsolute(command) &&
419
+ const resolvedCommand = !isFullyQualified(command) &&
419
420
  !command.includes(path.sep) &&
420
421
  !command.includes("/")
421
422
  ? (findBinaryOnPath(command, env) ?? explicitCommand)
@@ -428,7 +429,7 @@ export async function launchLSP(command, args = [], options = {}) {
428
429
  // If command not found, try npm global as fallback (handles PATH caching after install)
429
430
  let spawnCommand = resolvedCommand;
430
431
  // First, try to find in npm global if it's a simple command name
431
- if (!path.isAbsolute(command) &&
432
+ if (!isFullyQualified(command) &&
432
433
  !command.includes(path.sep) &&
433
434
  !command.includes("/")) {
434
435
  const globalBinPath = await findGlobalBinary(command);
@@ -444,7 +445,8 @@ export async function launchLSP(command, args = [], options = {}) {
444
445
  /\.(cmd|bat)$/i.test(spawnCommand) &&
445
446
  !isCmdShimValid(spawnCommand)) {
446
447
  logSessionStart(`lsp cmd-shim-invalid: ${spawnCommand} target missing — skipping candidate`);
447
- throw new Error(`LSP .cmd shim target not found: ${spawnCommand}. The npm package may not be installed.`);
448
+ const cause = Object.assign(new Error(`LSP .cmd shim target not found: ${spawnCommand}`), { code: "ENOENT" });
449
+ throw await classifySpawnFailure(cause, { command, cwd });
448
450
  }
449
451
  // P0 FIX: Never spawn .ps1 wrappers on Windows — they hang when PowerShell
450
452
  // execution policy is Restricted/AllSigned. Prefer .cmd or direct node.
@@ -466,7 +468,7 @@ export async function launchLSP(command, args = [], options = {}) {
466
468
  }
467
469
  catch (err) {
468
470
  // If spawn failed with simple command, try npm global
469
- if (!path.isAbsolute(command) &&
471
+ if (!isFullyQualified(command) &&
470
472
  !command.includes(path.sep) &&
471
473
  !command.includes("/")) {
472
474
  const globalBinPath = await findGlobalBinary(command);
@@ -476,11 +478,11 @@ export async function launchLSP(command, args = [], options = {}) {
476
478
  proc = trySpawn(globalBinPath, args, cwd, env, needsShellGlobal);
477
479
  }
478
480
  else {
479
- throw err;
481
+ throw await classifySpawnFailure(err, { command, cwd });
480
482
  }
481
483
  }
482
484
  else {
483
- throw err;
485
+ throw await classifySpawnFailure(err, { command, cwd });
484
486
  }
485
487
  }
486
488
  if (!proc.stdin || !proc.stdout || !proc.stderr) {
@@ -512,10 +514,14 @@ export async function launchLSP(command, args = [], options = {}) {
512
514
  let settled = false;
513
515
  // Attach error handler that can reject for immediate errors
514
516
  proc.on("error", (err) => {
515
- if (!settled && (err.code === "ENOENT" || err.code === "EINVAL")) {
517
+ if (!settled) {
516
518
  settled = true;
517
- reject(new Error(`LSP server binary not found: ${command}. ` +
518
- `Install it or check your PATH.${formatStartupStderr(startupStderr)}`));
519
+ void classifySpawnFailure(err, { command, cwd }).then((failure) => {
520
+ const detail = formatStartupStderr(startupStderr);
521
+ reject(detail
522
+ ? new SpawnFailureError(failure.kind, `${failure.message}${detail}`, failure.cause)
523
+ : failure);
524
+ });
519
525
  }
520
526
  });
521
527
  // Also listen for immediate exit
@@ -2,4 +2,4 @@
2
2
  * Re-export from shared path-utils.
3
3
  * Kept as a local module for LSP imports that use relative paths.
4
4
  */
5
- export { isUnderDir, normalizeFilePath, normalizeMapKey, pathsEqual, pathToUri, uriToPath, } from "../path-utils.js";
5
+ export { isUnderDir, normalizeFilePath, normalizeEphemeralMapKey, normalizeMapKey, pathsEqual, pathToUri, uriToDiskPath, uriToPath, } from "../path-utils.js";
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Windows Ruby-installer drive-root version-dir discovery (refs #1137).
3
+ *
4
+ * The Ruby installer drops versioned directories (e.g. `Ruby34-x64`) directly
5
+ * at a drive root, and the drive and version suffix vary — so both the LSP
6
+ * candidate builder (`rubyBinCandidates`) and the spawn PATH augmenter
7
+ * (`buildAugmentedPath`) scan the drive root for `ruby<N>...` directories.
8
+ *
9
+ * Enumerating a drive root **synchronously on every LSP spawn** was a genuine
10
+ * #1137 event-loop offender: a stalled cloud/network-backed drive-root
11
+ * `readdirSync` blocks the Node loop (and pi's TUI) for the entire stall. This
12
+ * module makes that enumeration:
13
+ * 1. **O(1) amortized** — the matching directory names are memoized for the
14
+ * process (the set is static for a session; the previous inline code
15
+ * equally required a restart to notice a new Ruby install on PATH), and
16
+ * 2. **non-blocking on the hot path** — `buildAugmentedPath` (called on every
17
+ * spawn) reads the drive root via `fs.promises.readdir` off the loop.
18
+ *
19
+ * Both readers share ONE cache keyed by `driveRoot`, so at most one drive-root
20
+ * read happens per drive per process regardless of which reader runs first.
21
+ * Callers derive their own paths from the returned directory NAMES (the two
22
+ * readers build different suffixes: `bin/<tool>` vs `bin`), so only the raw
23
+ * `ruby<N>` names are cached.
24
+ */
25
+ import { BoundedLruCache } from "../bounded-cache.js";
26
+ import { normalizeMapKey } from "../path-utils.js";
27
+ import * as fs from "node:fs";
28
+ /** Matches the Ruby installer's `Ruby34-x64`, `ruby3.3`, … drive-root dirs. */
29
+ const RUBY_VERSION_DIR = /^ruby\d/i;
30
+ /** Process-lifetime memo of matching dir names, keyed by drive root (e.g. `C:\`). */
31
+ const rubyDirNamesCache = new BoundedLruCache(16);
32
+ function rubyDriveKey(driveRoot) {
33
+ return normalizeMapKey(driveRoot);
34
+ }
35
+ function filterRubyDirNames(entries) {
36
+ return entries.filter((name) => RUBY_VERSION_DIR.test(name));
37
+ }
38
+ /**
39
+ * Memoized **synchronous** enumeration for sync callers (`rubyBinCandidates`).
40
+ * First call reads the drive root once (fail-open to `[]` on an unreadable
41
+ * root); every later call is a cache hit. If an async caller populated the
42
+ * cache first, this returns that result with no drive-root read at all.
43
+ */
44
+ export function getRubyVersionDirNamesSync(driveRoot) {
45
+ const key = rubyDriveKey(driveRoot);
46
+ const cached = rubyDirNamesCache.get(key);
47
+ if (cached !== undefined)
48
+ return cached;
49
+ let names;
50
+ try {
51
+ names = filterRubyDirNames(fs.readdirSync(driveRoot));
52
+ }
53
+ catch {
54
+ names = [];
55
+ }
56
+ rubyDirNamesCache.set(key, names);
57
+ return names;
58
+ }
59
+ /**
60
+ * Memoized **async** twin for the hot spawn path (`buildAugmentedPath`) — reads
61
+ * the drive root via `fs.promises.readdir` so a slow/stalled cloud-backed drive
62
+ * root never blocks the event loop. Shares the same process cache as the sync
63
+ * reader (identical content, so a benign sync/async populate race is fine).
64
+ */
65
+ export async function getRubyVersionDirNamesAsync(driveRoot) {
66
+ const key = rubyDriveKey(driveRoot);
67
+ const cached = rubyDirNamesCache.get(key);
68
+ if (cached !== undefined)
69
+ return cached;
70
+ let names;
71
+ try {
72
+ names = filterRubyDirNames(await fs.promises.readdir(driveRoot));
73
+ }
74
+ catch {
75
+ names = [];
76
+ }
77
+ rubyDirNamesCache.set(key, names);
78
+ return names;
79
+ }
80
+ /** Test-only: clear the process memo so each case starts cold. */
81
+ export function __resetRubyDriveDirsCacheForTest() {
82
+ rubyDirNamesCache.clear();
83
+ }
@@ -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, } 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,198 @@ 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 relative = path.relative(path.resolve(ancestor), path.resolve(candidate));
82
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
83
+ }
84
+ /** Enforce the session cwd as the hard boundary for LSP client root selection. */
85
+ export function enforceLspRootCeiling(root, sessionCwd, filePath) {
86
+ const resolvedRoot = path.resolve(root);
87
+ const resolvedCwd = path.resolve(sessionCwd);
88
+ // Callers may explicitly inspect an out-of-session file (notably isolated
89
+ // tests and API consumers). The cwd ceiling governs roots for files that are
90
+ // actually inside the declared session project.
91
+ if (filePath && !isSameOrWithin(resolvedCwd, path.resolve(filePath))) {
92
+ return resolvedRoot;
93
+ }
94
+ if (isSameOrWithin(resolvedCwd, resolvedRoot))
95
+ return resolvedRoot;
96
+ const logKey = `${normalizeMapKey(resolvedCwd)}:${normalizeMapKey(resolvedRoot)}`;
97
+ if (!loggedRootCeilingClamps.has(logKey)) {
98
+ loggedRootCeilingClamps.add(logKey);
99
+ logSessionStart(`lsp root clamped to session cwd: candidate=${resolvedRoot} cwd=${resolvedCwd}`);
100
+ }
101
+ return resolvedCwd;
102
+ }
103
+ export async function hasProjectBoundaryMarker(dir) {
104
+ // A nested Git checkout is an independently hosted project even when it has
105
+ // no language manifest. Keep this directory boundary aligned with the shared
106
+ // FALLBACK_PROJECT_MARKERS policy used by nearestNonExcludedFallbackRoot.
107
+ if (await markerExists(dir, ".git"))
108
+ return true;
109
+ for (const marker of PROJECT_BOUNDARY_MARKERS) {
110
+ if (await markerExists(dir, marker))
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+ function pathSegments(dir) {
116
+ const parsed = path.parse(path.resolve(dir));
117
+ return path
118
+ .relative(parsed.root, path.resolve(dir))
119
+ .split(path.sep)
120
+ .filter(Boolean)
121
+ .map((segment) => segment.toLowerCase());
122
+ }
123
+ function hasFixtureConvention(dir) {
124
+ const segments = pathSegments(dir);
125
+ // Go treats any directory named testdata as fixture data by convention. The
126
+ // exclusion is intentionally ancestor-wide so nested fixture projects cannot
127
+ // become independent LSP roots, but the segment match itself stays exact.
128
+ if (segments.some((segment) => FIXTURE_ROOT_SEGMENTS.has(segment)))
129
+ return true;
130
+ return segments.some((segment, index) => segment === "tests" && segments[index + 1] === "fixtures");
131
+ }
132
+ function hasAtomicStageSegment(dir) {
133
+ return pathSegments(dir).some((segment) => STAGE_TMP_PATTERN.test(segment));
134
+ }
135
+ async function findGitBoundary(dir) {
136
+ let current = path.resolve(dir);
137
+ const fsRoot = path.parse(current).root;
138
+ while (true) {
139
+ if (await markerExists(current, ".git"))
140
+ return current;
141
+ if (current === fsRoot)
142
+ return undefined;
143
+ current = path.dirname(current);
144
+ }
145
+ }
146
+ async function isExcludedLspRoot(dir) {
147
+ const candidate = path.resolve(dir);
148
+ if (hasFixtureConvention(candidate) || hasAtomicStageSegment(candidate))
149
+ return true;
150
+ const gitRoot = await findGitBoundary(candidate);
151
+ if (!gitRoot || gitRoot === candidate)
152
+ return false;
153
+ // Avoid constructing the matcher when the project has no positive ignore rules.
154
+ // The matcher remains authoritative (including anchored rules and directory form).
155
+ if (getProjectIgnoreGlobs(gitRoot).length === 0)
156
+ return false;
157
+ return isPathIgnoredByProject(candidate, gitRoot, true);
158
+ }
159
+ async function nearestNonExcludedFallbackRoot(candidate) {
160
+ if (!(await isExcludedLspRoot(candidate)))
161
+ return path.resolve(candidate);
162
+ let current = path.dirname(path.resolve(candidate));
163
+ const fsRoot = path.parse(current).root;
164
+ let nearestAllowed;
165
+ while (true) {
166
+ if (!(await isExcludedLspRoot(current))) {
167
+ nearestAllowed ??= current;
168
+ for (const marker of FALLBACK_PROJECT_MARKERS) {
169
+ if (await markerExists(current, marker))
170
+ return current;
171
+ }
172
+ }
173
+ if (current === fsRoot)
174
+ break;
175
+ current = path.dirname(current);
176
+ }
177
+ // No project marker was available. Keep the file attached to a stable,
178
+ // non-excluded ancestor rather than minting a client inside the fixture/stage.
179
+ return nearestAllowed ?? fsRoot;
180
+ }
30
181
  function isLspInstallDisabled() {
31
182
  return process.env.PI_LENS_DISABLE_LSP_INSTALL === "1";
32
183
  }
33
184
  function canInstall(allowInstall) {
34
185
  return allowInstall !== false && !isLspInstallDisabled();
35
186
  }
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
187
  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
188
  const directLspCommandUnavailableUntil = new Map();
44
189
  const directLspCommandSkipLoggedUntil = new Map();
190
+ function pruneExpiredDirectLspNegativeEntries(now = Date.now()) {
191
+ for (const [command, until] of directLspCommandUnavailableUntil) {
192
+ if (until <= now) {
193
+ directLspCommandUnavailableUntil.delete(command);
194
+ directLspCommandSkipLoggedUntil.delete(command);
195
+ }
196
+ }
197
+ for (const [command, until] of directLspCommandSkipLoggedUntil) {
198
+ if (until <= now)
199
+ directLspCommandSkipLoggedUntil.delete(command);
200
+ }
201
+ }
45
202
  function isSimpleCommand(command) {
46
- return (!path.isAbsolute(command) &&
203
+ return (!isFullyQualified(command) &&
47
204
  !command.includes("/") &&
48
205
  !command.includes("\\"));
49
206
  }
50
207
  export function isDirectLspCommandTemporarilyUnavailable(command) {
208
+ const now = Date.now();
209
+ pruneExpiredDirectLspNegativeEntries(now);
51
210
  const until = directLspCommandUnavailableUntil.get(command);
52
- if (!until || until <= Date.now()) {
211
+ if (!until || until <= now) {
53
212
  directLspCommandUnavailableUntil.delete(command);
54
213
  return false;
55
214
  }
56
215
  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)`);
216
+ if (loggedUntil <= now) {
217
+ logSessionStart(`lsp direct command ${command}: skipped by negative availability cache (${Math.max(0, until - now)}ms remaining)`);
59
218
  directLspCommandSkipLoggedUntil.set(command, until);
60
219
  }
61
220
  return true;
@@ -74,7 +233,7 @@ export async function resolveAndLaunch(spec, allowInstall) {
74
233
  let lastRuntimeFailure;
75
234
  const trackRuntimeFailure = (err) => {
76
235
  const message = err instanceof Error ? err.message : String(err);
77
- if (!isCommandNotFoundError(message)) {
236
+ if (!hasSpawnFailureKind(err, "tool-not-found")) {
78
237
  lastRuntimeFailure = err instanceof Error ? err : new Error(message);
79
238
  }
80
239
  };
@@ -145,6 +304,12 @@ export async function resolveAndLaunch(spec, allowInstall) {
145
304
  logSessionStart(`lsp launch candidate failed tool=${toolLabel} idx=${failure.index} command=${failure.command} error=${failure.message}`);
146
305
  trackRuntimeFailure(failure.err);
147
306
  }
307
+ const hasOnlyRepairableCandidateFailures = candidateFailures.every((failure) => hasSpawnFailureKind(failure.err, "tool-not-found"));
308
+ if (!hasOnlyRepairableCandidateFailures) {
309
+ if (lastRuntimeFailure)
310
+ throw lastRuntimeFailure;
311
+ return undefined;
312
+ }
148
313
  if (!canInstall(allowInstall)) {
149
314
  logSessionStart(`lsp launch install blocked tool=${toolLabel} cwd=${spec.cwd} allowInstall=${allowInstall !== false} globalDisabled=${isLspInstallDisabled()}`);
150
315
  logLatency({
@@ -214,7 +379,7 @@ export async function resolveAndLaunch(spec, allowInstall) {
214
379
  // fails to launch (e.g. broken symlink, missing .dll), nuke the
215
380
  // caches and download a managed copy from the registry.
216
381
  const looksPathResolved = !installed.includes("/") && !installed.includes("\\");
217
- if (looksPathResolved) {
382
+ if (looksPathResolved && hasSpawnFailureKind(err, "tool-not-found")) {
218
383
  logSessionStart(`lsp launch managed retry force-reinstall tool=${spec.managedToolId}`);
219
384
  const reinstalled = await ensureTool(spec.managedToolId, {
220
385
  forceReinstall: true,
@@ -415,19 +580,13 @@ function rubyBinCandidates(baseName) {
415
580
  candidates.push(path.join(home, ".asdf", "installs", "ruby", "bin", `${baseName}${ext}`));
416
581
  if (isWin) {
417
582
  // 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
583
+ // and version suffix vary — scan what's actually present instead of
584
+ // hardcoding. Memoized once per process (#1137): a synchronous drive-root
585
+ // enumeration per LSP spawn was an event-loop offender.
419
586
  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
587
+ for (const entry of getRubyVersionDirNamesSync(driveRoot)) {
588
+ candidates.push(path.join(driveRoot, entry, "bin", `${baseName}.bat`));
589
+ candidates.push(path.join(driveRoot, entry, "bin", baseName));
431
590
  }
432
591
  }
433
592
  return candidates;
@@ -465,7 +624,7 @@ function createInteractiveServer(spec) {
465
624
  return { process: proc, source: "direct", initialization };
466
625
  }
467
626
  catch (err) {
468
- if (isCommandNotFoundError(err)) {
627
+ if (hasSpawnFailureKind(err, "tool-not-found")) {
469
628
  markDirectLspCommandUnavailable(command);
470
629
  }
471
630
  return undefined;
@@ -484,7 +643,10 @@ export function PriorityRoot(markerGroups, excludePatterns, stopDir) {
484
643
  return undefined;
485
644
  };
486
645
  }
487
- export const FileDirRoot = async (file) => path.resolve(path.dirname(file));
646
+ export const FileDirRoot = async (file) => {
647
+ const candidate = path.resolve(path.dirname(file));
648
+ return nearestNonExcludedFallbackRoot(candidate);
649
+ };
488
650
  export function RootWithFallback(primary, fallback = FileDirRoot) {
489
651
  return async (file) => {
490
652
  const primaryRoot = await primary(file);
@@ -597,8 +759,10 @@ export function NearestRoot(includePatterns, excludePatterns, stopDir) {
597
759
  // Check include patterns. Exact marker names stay cheap (`stat`), while
598
760
  // glob markers like `*.csproj` match real project filenames (#201).
599
761
  for (const pattern of includePatterns) {
600
- if (await markerExists(currentDir, pattern))
601
- return currentDir;
762
+ if ((await markerExists(currentDir, pattern)) &&
763
+ !(await isExcludedLspRoot(currentDir))) {
764
+ return enforceLspRootCeiling(currentDir, process.cwd(), file);
765
+ }
602
766
  }
603
767
  if (currentDir === stop || currentDir === fsRoot) {
604
768
  break;
@@ -1,9 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { getProjectDataDir } from "../file-utils.js";
4
+ import { writeFileAtomic } from "../atomic-write.js";
4
5
  import { readJsonCache } from "../json-cache-read.js";
5
6
  import { normalizeMapKey } from "../path-utils.js";
6
7
  import { loadReverseDependencyIndexFromSnapshot } from "../reverse-deps.js";
8
+ import { createDiskBindingCache, } from "./diagnostic-binding.js";
7
9
  /**
8
10
  * #671: per-file cache of the last CONFIRMED `runWorkspaceDiagnostics` sweep
9
11
  * result, so a repeat `lens_diagnostics mode=full` with no intervening edits
@@ -24,7 +26,12 @@ import { loadReverseDependencyIndexFromSnapshot } from "../reverse-deps.js";
24
26
  * `LSPWorkspaceDiagnosticResult.timedOut` in `./index.ts`) into
25
27
  * `WorkspaceDiagnosticsCacheEntry`.
26
28
  */
27
- export const WORKSPACE_DIAGNOSTICS_CACHE_VERSION = 1;
29
+ // v2 (#1095): entries may carry an optional `contentHash` fingerprint of the
30
+ // file bytes the cached diagnostics were computed against, so a `lookup` can
31
+ // surface a content `binding` (boundToCurrentDisk) beyond the mtime proxy.
32
+ // Legacy v1 entries are rejected by the version guard and re-scanned — a
33
+ // deliberately clean break so no entry lacking the field is ever served.
34
+ export const WORKSPACE_DIAGNOSTICS_CACHE_VERSION = 2;
28
35
  const CACHE_FILE = "lsp-workspace-diagnostics.json";
29
36
  function cachePath(cwd) {
30
37
  return path.join(getProjectDataDir(cwd), "cache", CACHE_FILE);
@@ -48,7 +55,11 @@ export function loadWorkspaceDiagnosticsCache(cwd) {
48
55
  export function saveWorkspaceDiagnosticsCache(cwd, cache) {
49
56
  const filePath = cachePath(cwd);
50
57
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
- fs.writeFileSync(filePath, JSON.stringify(cache, null, 2));
58
+ // bestEffort: false — persist()'s catch is the error policy here, and it
59
+ // must observe the failure so `dirty` stays set and the next sweep retries.
60
+ writeFileAtomic(filePath, JSON.stringify(cache, null, 2), {
61
+ bestEffort: false,
62
+ });
52
63
  }
53
64
  /**
54
65
  * True when `filePath`'s cached entry is still trustworthy enough to reuse
@@ -133,6 +144,8 @@ export function createWorkspaceDiagnosticsCacheContext(cwd) {
133
144
  return undefined;
134
145
  return reverseDepsIndex.imports[cacheKeyFor(filePath)] ?? [];
135
146
  };
147
+ // #1095: per-sweep disk-fingerprint memo (per file+mtime) for binding verify.
148
+ const diskBindingCache = createDiskBindingCache();
136
149
  let dirty = false;
137
150
  return {
138
151
  lookup(filePath, scopeKey) {
@@ -141,15 +154,26 @@ export function createWorkspaceDiagnosticsCacheContext(cwd) {
141
154
  return undefined;
142
155
  if (!isEntryFresh(filePath, entry, getImports))
143
156
  return undefined;
144
- return { diagnostics: entry.diagnostics, count: entry.count };
157
+ return {
158
+ diagnostics: entry.diagnostics,
159
+ count: entry.count,
160
+ binding: {
161
+ contentHash: entry.contentHash,
162
+ boundToCurrentDisk: diskBindingCache.boundToCurrentDisk(filePath, {
163
+ contentHash: entry.contentHash,
164
+ }),
165
+ },
166
+ scannedAt: entry.scannedAt,
167
+ };
145
168
  },
146
- record(filePath, scopeKey, diagnostics, mtimeMs) {
169
+ record(filePath, scopeKey, diagnostics, mtimeMs, contentHash) {
147
170
  entries[cacheKeyFor(filePath)] = {
148
171
  diagnostics,
149
172
  count: diagnostics.length,
150
173
  mtimeMs,
151
174
  scannedAt: Date.now(),
152
175
  scopeKey,
176
+ ...(contentHash !== undefined && { contentHash }),
153
177
  };
154
178
  dirty = true;
155
179
  },
@@ -0,0 +1,8 @@
1
+ /** Shared lazy LSP service seam (#1394). */
2
+ let lspPromise;
3
+ export function warmLspService() {
4
+ return (lspPromise ??= import("./lsp/index.js"));
5
+ }
6
+ export function loadLspService() {
7
+ return warmLspService();
8
+ }