@herbertgao/pi-extensions 2026.8.4 → 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 (288) hide show
  1. package/README.md +4 -4
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
  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/index.ts +7 -6
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
  13. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  14. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  15. package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
  16. package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
  17. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
  18. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
  19. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
  20. package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
  21. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
  22. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
  23. package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
  24. package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
  25. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
  26. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
  27. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
  30. package/node_modules/@narumitw/pi-btw/README.md +8 -7
  31. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  32. package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
  33. package/node_modules/pi-lens/CHANGELOG.md +2440 -954
  34. package/node_modules/pi-lens/README.md +13 -0
  35. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
  36. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
  37. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
  38. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
  39. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
  40. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
  41. package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
  42. package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
  43. package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
  44. package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
  45. package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
  46. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
  47. package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
  48. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
  49. package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
  50. package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
  51. package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
  52. package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
  53. package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
  54. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
  55. package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
  56. package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
  57. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
  58. package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
  59. package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
  60. package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
  61. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
  62. package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
  63. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
  64. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
  65. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
  66. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
  67. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
  68. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
  69. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
  70. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
  71. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
  72. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
  73. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
  74. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
  75. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
  76. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
  77. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
  78. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
  79. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
  80. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
  81. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
  82. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
  83. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
  84. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
  85. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
  86. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
  87. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
  88. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
  99. package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
  100. package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
  101. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
  102. package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
  103. package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
  104. package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
  105. package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
  106. package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
  107. package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
  108. package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
  109. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
  110. package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
  111. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
  112. package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
  113. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
  114. package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
  115. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
  116. package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
  117. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
  118. package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
  119. package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
  120. package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
  121. package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
  122. package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
  123. package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
  124. package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
  125. package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
  126. package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
  127. package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
  128. package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
  129. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  130. package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
  131. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
  132. package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
  133. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
  134. package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
  135. package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
  136. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
  137. package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
  138. package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
  139. package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
  140. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
  141. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
  142. package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
  143. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
  144. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
  145. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
  146. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
  147. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
  148. package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
  149. package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
  150. package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
  151. package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
  152. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
  153. package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
  154. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
  155. package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
  156. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
  157. package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
  158. package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
  159. package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
  160. package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
  161. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
  162. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
  163. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
  164. package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
  165. package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
  166. package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
  167. package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
  168. package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
  169. package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
  170. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
  171. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
  172. package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
  173. package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
  174. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
  175. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
  176. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
  177. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
  178. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
  179. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
  180. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
  181. package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
  182. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
  183. package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
  184. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
  185. package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
  186. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
  187. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
  188. package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
  189. package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
  190. package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
  191. package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
  192. package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
  193. package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
  194. package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
  195. package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
  196. package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
  197. package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
  198. package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
  199. package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
  200. package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
  201. package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
  202. package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
  203. package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
  204. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
  205. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
  206. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
  207. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
  208. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
  209. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
  210. package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
  211. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
  212. package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
  213. package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
  214. package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
  215. package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
  216. package/node_modules/pi-lens/dist/index.js +49361 -36888
  217. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
  218. package/node_modules/pi-lens/dist/mcp/server.js +211 -44
  219. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
  220. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
  221. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
  222. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
  223. package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
  224. package/node_modules/pi-lens/docs/agent-guide.md +2 -2
  225. package/node_modules/pi-lens/docs/agent-tools.md +21 -10
  226. package/node_modules/pi-lens/docs/analysisall.md +192 -0
  227. package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
  228. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
  229. package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
  230. package/node_modules/pi-lens/docs/environment-variables.md +44 -0
  231. package/node_modules/pi-lens/docs/fable.md +27 -0
  232. package/node_modules/pi-lens/docs/features.md +31 -2
  233. package/node_modules/pi-lens/docs/globalconfig.md +45 -1
  234. package/node_modules/pi-lens/docs/language-coverage.md +4 -3
  235. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
  236. package/node_modules/pi-lens/docs/mcp.md +37 -4
  237. package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
  238. package/node_modules/pi-lens/docs/settings.md +12 -1
  239. package/node_modules/pi-lens/docs/tools.md +4 -1
  240. package/node_modules/pi-lens/docs/usage.md +18 -0
  241. package/node_modules/pi-lens/package.json +10 -10
  242. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
  243. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
  244. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
  245. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
  246. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
  247. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
  248. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
  249. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
  250. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
  251. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
  252. package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
  253. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
  254. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
  255. package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
  256. package/node_modules/pi-mcp-adapter/README.md +34 -3
  257. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
  258. package/node_modules/pi-mcp-adapter/commands.ts +15 -7
  259. package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
  260. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
  261. package/node_modules/pi-mcp-adapter/index.ts +8 -3
  262. package/node_modules/pi-mcp-adapter/init.ts +29 -5
  263. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
  264. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
  265. package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
  266. package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
  267. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
  268. package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
  269. package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
  270. package/node_modules/pi-mcp-adapter/package.json +3 -3
  271. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
  272. package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
  273. package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
  274. package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
  275. package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
  276. package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
  277. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
  278. package/node_modules/pi-mcp-adapter/types.ts +93 -27
  279. package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
  280. package/package.json +11 -12
  281. package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
  282. package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
  283. package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
  284. package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
  285. package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
  286. package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
  287. package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
  288. package/node_modules/pi-lens/dist/clients/types.js +0 -11
@@ -28,6 +28,11 @@ pi-lens gives AI coding agents fast, language-aware feedback while they write/ed
28
28
  - `/lens-map` — interactive HTML dependency map of the project
29
29
  - Read-guard and edit-autopatch support to reduce bad edits
30
30
  - Background security/dependency scans for opted-in projects
31
+ - Runtime health telemetry (`/lens-health`) including a bounded degradation
32
+ ledger for silently-degraded behavior (LSP breakers, formatter
33
+ skips/failures, idle evictions, timeouts)
34
+ - MCP server (experimental) so Claude Code or any MCP client can drive the
35
+ same diagnostics/read-substitute tools pi-lens exposes to pi
31
36
 
32
37
  ## Install
33
38
 
@@ -70,6 +75,8 @@ pi install git:github.com/apmantza/pi-lens
70
75
  and formatters
71
76
  - [Dependencies](docs/dependencies.md) — auto-install policy and external tools
72
77
  - [Custom rules](docs/custom-rules.md) — project ast-grep and tree-sitter rules
78
+ - [MCP server](docs/mcp.md) — experimental MCP server for Claude Code and
79
+ other MCP clients
73
80
 
74
81
  ## Contributing
75
82
 
@@ -206,6 +213,12 @@ Thanks goes to these wonderful people:
206
213
  </tr>
207
214
  <tr>
208
215
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/pppobear"><img src="https://avatars.githubusercontent.com/u/21952175?v=4" width="100px;" alt=""/><br /><sub><b>pppobear</b></sub></a><br /><a href="#bug-pppobear" title="Bug reports">🐛</a></td>
216
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/3choBoomer"><img src="https://avatars.githubusercontent.com/u/521828?v=4" width="100px;" alt=""/><br /><sub><b>Nathan Cooke</b></sub></a><br /><a href="#code-3choBoomer" title="Code">💻</a></td>
217
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/white-hat"><img src="https://avatars.githubusercontent.com/u/922988?v=4" width="100px;" alt=""/><br /><sub><b>Eli Stark</b></sub></a><br /><a href="#code-white-hat" title="Code">💻</a></td>
218
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/marvtub"><img src="https://avatars.githubusercontent.com/u/33159023?v=4" width="100px;" alt=""/><br /><sub><b>Marvin Aziz</b></sub></a><br /><a href="#code-marvtub" title="Code">💻</a></td>
219
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/mjfaga"><img src="https://avatars.githubusercontent.com/u/7584015?v=4" width="100px;" alt=""/><br /><sub><b>Mark Faga</b></sub></a><br /><a href="#code-mjfaga" title="Code">💻</a></td>
220
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/aeturnal"><img src="https://avatars.githubusercontent.com/u/219271200?v=4" width="100px;" alt=""/><br /><sub><b>aeturnal</b></sub></a><br /><a href="#code-aeturnal" title="Code">💻</a></td>
221
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/floatGray"><img src="https://avatars.githubusercontent.com/u/44295302?v=4" width="100px;" alt=""/><br /><sub><b>floatGray</b></sub></a><br /><a href="#code-floatGray" title="Code">💻</a></td>
209
222
  </tr>
210
223
  </tbody>
211
224
  </table>
@@ -4,9 +4,16 @@ import * as path from "node:path";
4
4
  import { applyWorkspaceEdit } from "./lsp/edits.js";
5
5
  import { getLSPService } from "./lsp/index.js";
6
6
  import { normalizeMapKey } from "./path-utils.js";
7
+ import { recordLspMutationBatch, } from "./lsp-mutation.js";
7
8
  import { toRunnerDisplayPath } from "./dispatch/runner-context.js";
8
9
  import { logActionableWarningsEvent } from "./actionable-warnings-logger.js";
9
10
  import { getProjectDataDir } from "./file-utils.js";
11
+ import { commitDurableStore } from "./durable-store.js";
12
+ let beforeWarningStateLockForTests = null;
13
+ /** Test seam for a sibling process commit immediately before lock acquisition. */
14
+ export function _setBeforeWarningStateLockForTests(hook) {
15
+ beforeWarningStateLockForTests = hook;
16
+ }
10
17
  function normalizeMessage(message) {
11
18
  return message.replace(/\s+/g, " ").trim().toLowerCase();
12
19
  }
@@ -53,10 +60,9 @@ function serializeAction(action) {
53
60
  skipReason: safety.reason,
54
61
  };
55
62
  }
56
- function readSuppressionState(cwd) {
57
- const statePath = path.join(getProjectDataDir(cwd), "cache", "actionable-warning-state.json");
63
+ function deserializeSuppressionState(contents) {
58
64
  try {
59
- const parsed = JSON.parse(fs.readFileSync(statePath, "utf-8"));
65
+ const parsed = JSON.parse(contents ?? "");
60
66
  return parsed && typeof parsed === "object"
61
67
  ? parsed
62
68
  : {};
@@ -65,23 +71,49 @@ function readSuppressionState(cwd) {
65
71
  return {};
66
72
  }
67
73
  }
68
- function updateWarningState(cwd, warnings) {
74
+ function readSuppressionState(cwd) {
69
75
  const statePath = path.join(getProjectDataDir(cwd), "cache", "actionable-warning-state.json");
70
- const now = new Date().toISOString();
71
- const state = readSuppressionState(cwd);
72
- state.warnings ??= {};
73
- for (const warning of warnings) {
74
- const existing = state.warnings[warning.id] ?? {};
75
- state.warnings[warning.id] = {
76
- ...existing,
77
- status: existing.status ?? "active",
78
- firstSeenAt: existing.firstSeenAt ?? now,
79
- lastSeenAt: now,
80
- seenCount: (existing.seenCount ?? 0) + 1,
81
- };
76
+ try {
77
+ return deserializeSuppressionState(fs.readFileSync(statePath, "utf8"));
78
+ }
79
+ catch {
80
+ return {};
82
81
  }
82
+ }
83
+ function updateWarningState(cwd, warnings) {
84
+ const statePath = path.join(getProjectDataDir(cwd), "cache", "actionable-warning-state.json");
83
85
  fs.mkdirSync(path.dirname(statePath), { recursive: true });
84
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
86
+ const hook = beforeWarningStateLockForTests;
87
+ beforeWarningStateLockForTests = null;
88
+ hook?.();
89
+ commitDurableStore({
90
+ path: statePath,
91
+ deserialize: deserializeSuppressionState,
92
+ merge: (state) => {
93
+ const now = new Date().toISOString();
94
+ state.warnings ??= {};
95
+ for (const warning of warnings) {
96
+ const existing = state.warnings[warning.id] ?? {};
97
+ state.warnings[warning.id] = {
98
+ ...existing,
99
+ status: existing.status ?? "active",
100
+ firstSeenAt: existing.firstSeenAt ?? now,
101
+ lastSeenAt: now,
102
+ seenCount: (existing.seenCount ?? 0) + 1,
103
+ };
104
+ }
105
+ return state;
106
+ },
107
+ serialize: (state) => JSON.stringify(state, null, 2),
108
+ waitMs: 2_000,
109
+ retryMs: 10,
110
+ timeoutMessage: "timed out acquiring actionable warning store lock",
111
+ onContention: "skip-log",
112
+ logContention: () => logActionableWarningsEvent({
113
+ event: "warning_state_write_dropped",
114
+ metadata: { reason: "lock_contention" },
115
+ }),
116
+ });
85
117
  }
86
118
  function suppressionFor(cwd, id) {
87
119
  const entry = readSuppressionState(cwd).warnings?.[id];
@@ -452,6 +484,8 @@ export async function applyConservativeActionableWarningFixes(args) {
452
484
  skipped: [],
453
485
  };
454
486
  const changedFiles = new Set();
487
+ const appliedResults = [];
488
+ let failedCount = 0;
455
489
  const lspService = getLSPService();
456
490
  const maxFixes = Math.max(0, args.maxFixes ?? 5);
457
491
  for (const file of args.report.files) {
@@ -504,12 +538,23 @@ export async function applyConservativeActionableWarningFixes(args) {
504
538
  continue;
505
539
  }
506
540
  const edit = selected.edit;
507
- const applied = await applyWorkspaceEdit(edit, args.cwd);
541
+ const applied = await applyWorkspaceEdit(edit, args.cwd, args.mutationContext
542
+ ? { mutationContext: { ...args.mutationContext, emitSummary: false } }
543
+ : undefined);
544
+ appliedResults.push(applied);
508
545
  for (const changedFile of applied.files)
509
546
  changedFiles.add(changedFile);
510
547
  summary.applied++;
511
548
  }
512
549
  catch (err) {
550
+ failedCount++;
551
+ const partial = err
552
+ .appliedWorkspaceEdit;
553
+ if (partial) {
554
+ appliedResults.push(partial);
555
+ for (const changedFile of partial.files)
556
+ changedFiles.add(changedFile);
557
+ }
513
558
  const message = err instanceof Error ? err.message : String(err);
514
559
  args.dbg?.(`actionable_warnings_autofix failed for ${warning.id}: ${message}`);
515
560
  summary.skipped.push({ id: warning.id, reason: "apply_failed" });
@@ -517,6 +562,16 @@ export async function applyConservativeActionableWarningFixes(args) {
517
562
  }
518
563
  }
519
564
  summary.changedFiles = [...changedFiles];
565
+ if (args.mutationContext && (summary.considered > 0 || appliedResults.length > 0)) {
566
+ recordLspMutationBatch(args.mutationContext, {
567
+ results: appliedResults,
568
+ considered: summary.considered,
569
+ completed: summary.applied,
570
+ failedCount,
571
+ status: failedCount > 0 ? "failed" : appliedResults.length > 0 ? "success" : "skipped",
572
+ bookkeep: false,
573
+ });
574
+ }
520
575
  return summary;
521
576
  }
522
577
  export function formatActionableWarningsAdvisory(report) {
@@ -2,7 +2,8 @@
2
2
  * AgentBehaviorClient for pi-lens
3
3
  *
4
4
  * Tracks tool call sequences and flags anti-patterns in real-time:
5
- * - Blind writes: editing or writing without reading first
5
+ * - Blind writes: editing or writing without reading first (a same-session
6
+ * write/edit of the same path counts as file knowledge)
6
7
  * - Thrashing: repeated identical tool calls with no progress
7
8
  *
8
9
  * No external dependencies — purely tracks tool call history.
@@ -23,6 +24,7 @@ export class AgentBehaviorClient {
23
24
  lastToolTimestamp = 0;
24
25
  // Per-file tracking
25
26
  fileEditCount = new Map();
27
+ static FILE_EDIT_IDLE_MS = 30 * 60_000;
26
28
  /**
27
29
  * Record a tool call and return any warnings triggered.
28
30
  * Called from tool_result handler.
@@ -63,7 +65,14 @@ export class AgentBehaviorClient {
63
65
  // Check for blind writes
64
66
  if (WRITE_OPS.has(toolName)) {
65
67
  const recentWindow = this.toolHistory.slice(-BLIND_WRITE_WINDOW);
66
- const hasRecentRead = recentWindow.some((r) => READ_OPS.has(r.toolName));
68
+ // A read in the window proves file knowledge; so does a same-session
69
+ // write/edit of THIS path — the agent authored the content, so the
70
+ // write-then-edit loop on a self-authored file is not a blind write.
71
+ const hasRecentRead = recentWindow.some((r) => READ_OPS.has(r.toolName) ||
72
+ (WRITE_OPS.has(r.toolName) &&
73
+ r.filePath !== undefined &&
74
+ normalizedPath !== null &&
75
+ normalizeMapKey(r.filePath) === normalizedPath));
67
76
  if (!hasRecentRead && recentWindow.length > 0) {
68
77
  // Count how many writes in the window without reads
69
78
  const writesWithoutRead = recentWindow.filter((r) => WRITE_OPS.has(r.toolName)).length;
@@ -82,7 +91,15 @@ export class AgentBehaviorClient {
82
91
  // Track edits per file
83
92
  if (filePath) {
84
93
  const key = normalizeMapKey(filePath);
85
- this.fileEditCount.set(key, (this.fileEditCount.get(key) ?? 0) + 1);
94
+ this.fileEditCount.set(key, {
95
+ count: (this.fileEditCount.get(key)?.count ?? 0) + 1,
96
+ lastUsedAt: now,
97
+ });
98
+ for (const [pathKey, entry] of this.fileEditCount) {
99
+ if (now - entry.lastUsedAt > AgentBehaviorClient.FILE_EDIT_IDLE_MS) {
100
+ this.fileEditCount.delete(pathKey);
101
+ }
102
+ }
86
103
  }
87
104
  }
88
105
  // Add to history (keep last 50 entries)
@@ -104,7 +121,7 @@ export class AgentBehaviorClient {
104
121
  * Get edit count for a file in this session.
105
122
  */
106
123
  getEditCount(filePath) {
107
- return this.fileEditCount.get(filePath) ?? 0;
124
+ return this.fileEditCount.get(normalizeMapKey(filePath))?.count ?? 0;
108
125
  }
109
126
  /**
110
127
  * Reset state (e.g., on session start).
@@ -7,12 +7,13 @@
7
7
  * Requires: npm install -D @ast-grep/cli
8
8
  * Rules: ./rules/ directory
9
9
  */
10
+ import { createSubsystemLogger } from "./extension-log.js";
10
11
  import * as fs from "node:fs";
11
12
  import * as os from "node:os";
12
13
  import * as path from "node:path";
13
14
  import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
14
15
  import { resolvePackagePath } from "./package-root.js";
15
- import { SgRunner } from "./sg-runner.js";
16
+ import { SgRunner, } from "./sg-runner.js";
16
17
  // --- Client ---
17
18
  function extractDebugAst(raw) {
18
19
  const lines = raw.split(/\r?\n/);
@@ -53,7 +54,7 @@ const VALIDATION_SNIPPETS = {
53
54
  html: { ext: "html", source: "<main>pi-lens</main>\n" },
54
55
  java: { ext: "java", source: "class Main { void run() {} }\n" },
55
56
  javascript: { ext: "js", source: "const piLensValidate = 1;\n" },
56
- json: { ext: "json", source: "{\"piLensValidate\":true}\n" },
57
+ json: { ext: "json", source: '{"piLensValidate":true}\n' },
57
58
  kotlin: { ext: "kt", source: "fun main() {}\n" },
58
59
  lua: { ext: "lua", source: "local pi_lens_validate = 1\n" },
59
60
  php: { ext: "php", source: "<?php $piLensValidate = 1;\n" },
@@ -66,7 +67,10 @@ const VALIDATION_SNIPPETS = {
66
67
  };
67
68
  function validationSnippetFor(language) {
68
69
  const key = language.toLowerCase().replace(/^"|"$/g, "");
69
- return VALIDATION_SNIPPETS[key] ?? { ext: key.replace(/[^a-z0-9_-]/gi, "") || "txt", source: "pi_lens_validate\n" };
70
+ return (VALIDATION_SNIPPETS[key] ?? {
71
+ ext: key.replace(/[^a-z0-9_-]/gi, "") || "txt",
72
+ source: "pi_lens_validate\n",
73
+ });
70
74
  }
71
75
  function validateInputShape(value, maxChars, label) {
72
76
  if (value.includes("\0"))
@@ -110,7 +114,7 @@ export class AstGrepClient {
110
114
  ? projectRuleDir
111
115
  : resolvePackagePath(import.meta.url, "rules"));
112
116
  this.log = verbose
113
- ? (msg) => console.error(`[ast-grep] ${msg}`)
117
+ ? createSubsystemLogger("ast-grep")
114
118
  : () => { };
115
119
  this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
116
120
  this.runner = new SgRunner(verbose);
@@ -129,9 +133,18 @@ export class AstGrepClient {
129
133
  const allMatches = [];
130
134
  for (const scanPath of paths) {
131
135
  if (apply) {
132
- // Stale-preview check: dry-run first
133
- const preCheck = await this.runner.tempScanAsync(scanPath, "agent-rule", ruleYaml);
134
- if (preCheck.length === 0) {
136
+ // Stale-preview check: dry-run first. Preserve CLI failures instead of
137
+ // treating an invalid rule as a stale, no-match preview.
138
+ const preCheck = await this.tempScanDetailed(scanPath, ruleYaml);
139
+ if (preCheck.failure || preCheck.error) {
140
+ return {
141
+ matches: allMatches,
142
+ totalMatches: allMatches.length,
143
+ applied: false,
144
+ error: preCheck.error ?? "ast-grep preview failed",
145
+ };
146
+ }
147
+ if (preCheck.matches.length === 0) {
135
148
  return {
136
149
  matches: [],
137
150
  totalMatches: 0,
@@ -162,12 +175,31 @@ export class AstGrepClient {
162
175
  * Routes through sg scan --config rather than sg run -p.
163
176
  * Each path is scanned independently; results are merged.
164
177
  */
165
- async searchWithRule(ruleYaml, paths) {
178
+ async tempScanDetailed(dir, ruleYaml, options = {}) {
179
+ // Keep tests and older embedders that replace the runner with the historic
180
+ // match-only seam working. The production SgRunner always has the detailed
181
+ // method, so failures cannot be mistaken for an empty result there.
182
+ const detailed = this.runner.tempScanDetailedAsync;
183
+ if (detailed) {
184
+ return detailed.call(this.runner, dir, "agent-rule", ruleYaml, 30_000, options);
185
+ }
186
+ const matches = await this.runner.tempScanAsync(dir, "agent-rule", ruleYaml, 30_000, options);
187
+ return { matches, status: 0 };
188
+ }
189
+ async searchWithRule(ruleYaml, paths, options = {}) {
166
190
  const allMatches = [];
167
191
  for (const scanPath of paths) {
168
192
  try {
169
- const results = await this.runner.tempScanAsync(scanPath, "agent-rule", ruleYaml);
170
- allMatches.push(...results);
193
+ const results = await this.tempScanDetailed(scanPath, ruleYaml, options);
194
+ if (results.failure || results.error) {
195
+ return {
196
+ matches: allMatches,
197
+ totalMatches: allMatches.length,
198
+ error: results.error ||
199
+ `ast-grep scan failed (${results.failure ?? "unknown failure"})`,
200
+ };
201
+ }
202
+ allMatches.push(...results.matches);
171
203
  }
172
204
  catch (err) {
173
205
  return {
@@ -227,11 +259,24 @@ export class AstGrepClient {
227
259
  if (options?.strictness)
228
260
  args.push("--strictness", options.strictness);
229
261
  args.push(tmpFile);
230
- const result = await this.runner.execRaw(args);
262
+ const result = await this.runner.execRaw(args, 10_000, options);
231
263
  const stderr = result.stderr.trim();
232
264
  const stdout = result.stdout.trim();
233
265
  if (result.error)
234
266
  return { valid: false, error: result.error };
267
+ if (result.status !== 0 && !(result.status === 1 && !stderr)) {
268
+ return {
269
+ valid: false,
270
+ error: stderr ||
271
+ `ast-grep validation failed with exit code ${result.status}`,
272
+ };
273
+ }
274
+ if (result.outputTruncated) {
275
+ return {
276
+ valid: false,
277
+ error: "ast-grep validation output was truncated",
278
+ };
279
+ }
235
280
  if (stderrHasError(stderr))
236
281
  return { valid: false, error: stderr };
237
282
  const warning = stderr || stdout || undefined;
@@ -249,7 +294,7 @@ export class AstGrepClient {
249
294
  }
250
295
  }
251
296
  }
252
- async validateRule(ruleYaml) {
297
+ async validateRule(ruleYaml, options = {}) {
253
298
  const shapeError = validateInputShape(ruleYaml, MAX_VALIDATE_RULE_CHARS, "rule");
254
299
  if (shapeError)
255
300
  return { valid: false, error: shapeError };
@@ -258,7 +303,14 @@ export class AstGrepClient {
258
303
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lens-sg-rule-"));
259
304
  try {
260
305
  fs.writeFileSync(path.join(tmpDir, `snippet.${snippet.ext}`), snippet.source, "utf-8");
261
- await this.runner.tempScanAsync(tmpDir, "agent-rule", ruleYaml, 10000);
306
+ const result = await this.tempScanDetailed(tmpDir, ruleYaml, options);
307
+ if (result.failure || result.error) {
308
+ return {
309
+ valid: false,
310
+ error: result.error ||
311
+ `ast-grep rule validation failed (${result.failure ?? "unknown failure"})`,
312
+ };
313
+ }
262
314
  return { valid: true };
263
315
  }
264
316
  catch (err) {
@@ -332,7 +384,7 @@ export class AstGrepClient {
332
384
  args.push("--strictness", options.strictness);
333
385
  }
334
386
  args.push(...paths);
335
- const result = await this.runner.exec(args);
387
+ const result = await this.runner.exec(args, options);
336
388
  return {
337
389
  matches: result.matches,
338
390
  totalMatches: result.totalMatches,
@@ -505,8 +557,8 @@ message: found
505
557
  }
506
558
  return exports;
507
559
  }
508
- formatMatches(matches, isDryRun = false, showModeIndicator = false) {
509
- return this.runner.formatMatches(matches, isDryRun, 50, showModeIndicator);
560
+ formatMatches(matches, isDryRun = false, showModeIndicator = false, maxItems = 50) {
561
+ return this.runner.formatMatches(matches, isDryRun, maxItems, showModeIndicator);
510
562
  }
511
563
  /**
512
564
  * Format diagnostics for LLM consumption
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Extracted to prevent circular dependencies between:
5
5
  * - ast-grep-client.ts
6
- * - ast-grep-parser.ts
7
6
  * - ast-grep-rule-manager.ts
8
7
  */
9
8
  export {};
@@ -21,7 +21,12 @@ const NODE_KIND_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
21
21
  * Returns true when at least one structural-intent field is present.
22
22
  */
23
23
  export function hasStructuralIntent(intent) {
24
- return !!(intent.insideKind || intent.hasKind || intent.follows || intent.precedes);
24
+ return !!(intent.nodeKind ||
25
+ intent.insideKind ||
26
+ intent.hasKind ||
27
+ intent.hasDescendantKind ||
28
+ intent.follows ||
29
+ intent.precedes);
25
30
  }
26
31
  /**
27
32
  * Synthesize an ast-grep YAML rule for replace operations.
@@ -33,26 +38,48 @@ export function synthesizeReplaceRule(intent) {
33
38
  return `${base}fix: ${JSON.stringify(intent.rewrite)}\n`;
34
39
  }
35
40
  /**
36
- * Synthesize an ast-grep YAML rule from a pattern and structural constraints.
41
+ * Synthesize an ast-grep YAML rule from a pattern (or node kind) and
42
+ * structural constraints.
37
43
  *
38
- * The generated rule uses `stopBy: end` on `inside` so the search climbs
39
- * all ancestors, not just the immediate parent.
44
+ * The generated rule uses `stopBy: end` on `inside` and
45
+ * `hasDescendantKind`, so those two explicit recursive forms climb all
46
+ * ancestors/descendants. `hasKind` intentionally retains ast-grep's default
47
+ * immediate-child semantics for compatibility.
40
48
  *
41
- * @throws if pattern is empty
49
+ * @throws if neither pattern nor nodeKind is supplied, or both are supplied
42
50
  */
43
51
  export function synthesizeRule(intent) {
44
- assertSafePattern(intent.pattern, "pattern");
52
+ const hasPattern = !!intent.pattern?.trim();
53
+ const hasNodeKind = !!intent.nodeKind?.trim();
54
+ if (hasPattern === hasNodeKind) {
55
+ throw new Error("provide exactly one of pattern or nodeKind");
56
+ }
57
+ const pattern = intent.pattern;
58
+ if (hasPattern && pattern)
59
+ assertSafePattern(pattern, "pattern");
60
+ if (intent.hasKind?.trim() && intent.hasDescendantKind?.trim()) {
61
+ throw new Error("hasKind and hasDescendantKind are mutually exclusive");
62
+ }
45
63
  // Canonical language name for the YAML header (ast-grep is case-sensitive here).
46
64
  const language = canonicalLanguage(intent.lang);
47
- const rule = {
48
- pattern: intent.pattern,
49
- };
65
+ const rule = hasNodeKind
66
+ ? { kind: assertSafeNodeKind(intent.nodeKind ?? "", "nodeKind") }
67
+ : { pattern };
50
68
  if (intent.insideKind) {
51
- rule.inside = { kind: assertSafeNodeKind(intent.insideKind, "insideKind"), stopBy: "end" };
69
+ rule.inside = {
70
+ kind: assertSafeNodeKind(intent.insideKind, "insideKind"),
71
+ stopBy: "end",
72
+ };
52
73
  }
53
74
  if (intent.hasKind) {
54
75
  rule.has = { kind: assertSafeNodeKind(intent.hasKind, "hasKind") };
55
76
  }
77
+ if (intent.hasDescendantKind) {
78
+ rule.has = {
79
+ kind: assertSafeNodeKind(intent.hasDescendantKind, "hasDescendantKind"),
80
+ stopBy: "end",
81
+ };
82
+ }
56
83
  if (intent.follows) {
57
84
  assertSafePattern(intent.follows, "follows");
58
85
  rule.follows = { pattern: intent.follows };
@@ -0,0 +1,63 @@
1
+ /** Owns the generic atomic-write `.tmp-<pid>-<threadId>-<seq>` namespace. */
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { threadId } from "node:worker_threads";
5
+ let stageSeq = 0;
6
+ export const STAGE_TMP_PATTERN = /\.tmp-(0|[1-9]\d*)-(0|[1-9]\d*)-(0|[1-9]\d*)$/;
7
+ const STAGE_TMP_PID_PATTERN = /\.tmp-(0|[1-9]\d*)-(?:0|[1-9]\d*)-(?:0|[1-9]\d*)$/;
8
+ export function stagePathFor(targetPath) {
9
+ return `${targetPath}.tmp-${process.pid}-${threadId}-${stageSeq++}`;
10
+ }
11
+ export function stageOwnerPidFromName(name) {
12
+ if (!STAGE_TMP_PATTERN.test(name))
13
+ return undefined;
14
+ const match = STAGE_TMP_PID_PATTERN.exec(name);
15
+ if (!match)
16
+ return undefined;
17
+ const pid = Number(match[1]);
18
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : undefined;
19
+ }
20
+ export async function sweepOwnStagingFiles(directories, options) {
21
+ const uniqueDirectories = [...new Set(directories)].filter(Boolean);
22
+ const result = { directories: uniqueDirectories.length, scanned: 0, removed: 0, liveSkipped: 0, truncated: false };
23
+ for (const directory of uniqueDirectories) {
24
+ let handle;
25
+ try {
26
+ handle = await fs.promises.opendir(directory);
27
+ }
28
+ catch {
29
+ continue;
30
+ }
31
+ try {
32
+ let reachedEntryCap = true;
33
+ for (let i = 0; i < options.maxEntries; i++) {
34
+ const entry = await handle.read();
35
+ if (entry === null) {
36
+ reachedEntryCap = false;
37
+ break;
38
+ }
39
+ result.scanned++;
40
+ if (!entry.isFile())
41
+ continue;
42
+ const pid = stageOwnerPidFromName(entry.name);
43
+ if (pid === undefined)
44
+ continue;
45
+ if (pid === process.pid || options.isPidAlive(pid)) {
46
+ result.liveSkipped++;
47
+ continue;
48
+ }
49
+ try {
50
+ await fs.promises.rm(path.join(directory, entry.name), { force: true });
51
+ result.removed++;
52
+ }
53
+ catch { /* leave it for the next bounded sweep */ }
54
+ }
55
+ result.truncated ||= reachedEntryCap;
56
+ }
57
+ catch { /* best-effort directory scan */ }
58
+ finally {
59
+ await handle.close().catch(() => { });
60
+ }
61
+ }
62
+ return result;
63
+ }