@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
@@ -1,32 +1,702 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as nodeFs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { isPathIgnoredByProject } from "./file-utils.js";
5
+ import { tokenizeShellCommand } from "./bash-file-access.js";
6
+ import { normalizeMapKey } from "./path-utils.js";
7
+ import { logLatency } from "./latency-logger.js";
8
+ const MAX_AFFECTED_FILES = 256;
9
+ function resolveGuardPath(filePath, cwd) {
10
+ return path.resolve(cwd, filePath);
11
+ }
12
+ function guardPathKey(filePath, cwd) {
13
+ return normalizeMapKey(resolveGuardPath(filePath, cwd));
14
+ }
15
+ function fileFingerprint(filePath) {
16
+ try {
17
+ return createHash("sha256").update(nodeFs.readFileSync(filePath)).digest("hex");
18
+ }
19
+ catch (err) {
20
+ return `unreadable:${err.code ?? "unknown"}`;
21
+ }
22
+ }
23
+ function currentFileFingerprint(filePath) {
24
+ try {
25
+ nodeFs.statSync(filePath);
26
+ return fileFingerprint(filePath);
27
+ }
28
+ catch (err) {
29
+ return err.code === "ENOENT"
30
+ ? "missing"
31
+ : `unreadable:${err.code ?? "unknown"}`;
32
+ }
33
+ }
34
+ function snapshotFileHashes(files, cwd) {
35
+ const hashes = {};
36
+ for (const file of files) {
37
+ const resolved = resolveGuardPath(file, cwd);
38
+ hashes[guardPathKey(resolved, cwd)] = currentFileFingerprint(resolved);
39
+ }
40
+ return hashes;
41
+ }
42
+ function capAffectedFiles(files, cwd) {
43
+ const unique = [...new Set(files.map((file) => resolveGuardPath(file, cwd)))];
44
+ return {
45
+ files: unique.slice(0, MAX_AFFECTED_FILES),
46
+ truncated: unique.length > MAX_AFFECTED_FILES,
47
+ };
48
+ }
49
+ /** Recovery is safe only when blocker content and its file provenance agree. */
50
+ function hasCompleteBlockingProvenance(blockerContent, blockingFiles, cwd) {
51
+ if (typeof blockerContent !== "string" || blockerContent.length === 0)
52
+ return false;
53
+ if (!Array.isArray(blockingFiles) || blockingFiles.length === 0)
54
+ return false;
55
+ if (blockingFiles.some((file) => typeof file !== "string" || file.trim().length === 0)) {
56
+ return false;
57
+ }
58
+ const provenanceKeys = blockingFiles.map((file) => guardPathKey(file, cwd));
59
+ if (new Set(provenanceKeys).size !== provenanceKeys.length)
60
+ return false;
61
+ const blockerKeys = blockerContent.split("\n").map((line) => {
62
+ const separator = line.indexOf(": ");
63
+ if (separator <= 0)
64
+ return undefined;
65
+ const file = line.slice(0, separator).trim();
66
+ return file.length > 0 ? guardPathKey(file, cwd) : undefined;
67
+ });
68
+ if (blockerKeys.some((key) => key === undefined))
69
+ return false;
70
+ const uniqueBlockerKeys = new Set(blockerKeys);
71
+ return (uniqueBlockerKeys.size === blockerKeys.length &&
72
+ uniqueBlockerKeys.size === provenanceKeys.length &&
73
+ [...uniqueBlockerKeys].every((key) => provenanceKeys.includes(key)));
74
+ }
1
75
  function getShellCommand(input) {
2
76
  if (!input || typeof input !== "object")
3
77
  return "";
4
78
  const raw = input;
5
- if (typeof raw.command === "string")
79
+ if (typeof raw.command === "string" && raw.command.trim())
6
80
  return raw.command;
7
- if (typeof raw.cmd === "string")
81
+ if (typeof raw.cmd === "string" && raw.cmd.trim())
8
82
  return raw.cmd;
83
+ if (typeof raw.command === "string")
84
+ return raw.command;
9
85
  return "";
10
86
  }
87
+ function executableName(value) {
88
+ const normalized = value.replace(/\\/g, "/");
89
+ let name = (normalized.slice(normalized.lastIndexOf("/") + 1) ?? "").toLowerCase();
90
+ // Shell launchers are commonly supplied as resolved Windows paths or as
91
+ // PATHEXT-qualified names. Guard classification must happen after the same
92
+ // basename/extension normalization for every wrapper family.
93
+ return name.replace(/\.(?:exe|com|bat|cmd)$/i, "");
94
+ }
95
+ function isShellWrapper(value) {
96
+ return new Set([
97
+ "sh",
98
+ "bash",
99
+ "dash",
100
+ "zsh",
101
+ "ash",
102
+ "cmd",
103
+ "pwsh",
104
+ "powershell",
105
+ ]).has(executableName(value));
106
+ }
107
+ function isGitExecutable(value) {
108
+ return new Set(["git", "git.exe", "git.cmd", "git.bat"]).has(executableName(value));
109
+ }
110
+ const COMMAND_STRING_WRAPPERS = new Set(["busybox", "toybox", "nix-shell"]);
111
+ function isCommandStringWrapper(value) {
112
+ return COMMAND_STRING_WRAPPERS.has(executableName(value));
113
+ }
114
+ /**
115
+ * Canonicalize shell parameter separators before command classification. The
116
+ * quote-aware pass deliberately leaves literal arguments alone; the lexer
117
+ * then supplies the command-position boundaries used by the guard.
118
+ */
119
+ function canonicalizeGuardCommand(command) {
120
+ let result = "";
121
+ let quote;
122
+ let escaped = false;
123
+ for (let i = 0; i < command.length; i++) {
124
+ const ch = command[i];
125
+ if (quote === "single") {
126
+ result += ch;
127
+ if (ch === "'")
128
+ quote = undefined;
129
+ continue;
130
+ }
131
+ if (quote === "double") {
132
+ result += ch;
133
+ if (escaped)
134
+ escaped = false;
135
+ else if (ch === "\\")
136
+ escaped = true;
137
+ else if (ch === '"')
138
+ quote = undefined;
139
+ continue;
140
+ }
141
+ if (ch === "'" || ch === '"') {
142
+ quote = ch === "'" ? "single" : "double";
143
+ result += ch;
144
+ continue;
145
+ }
146
+ const parameter = command.slice(i).match(/^\$\{IFS[^}]*\}/)?.[0];
147
+ const positional = command.slice(i).match(/^\$IFS(?:\$[0-9]+)?/)?.[0];
148
+ if (parameter || positional) {
149
+ result += " ";
150
+ i += (parameter ?? positional ?? "").length - 1;
151
+ continue;
152
+ }
153
+ result += ch;
154
+ }
155
+ let collapsed = "";
156
+ let pendingSpace = false;
157
+ quote = undefined;
158
+ for (const ch of result) {
159
+ if (!quote && /\s/.test(ch)) {
160
+ pendingSpace = collapsed.length > 0;
161
+ continue;
162
+ }
163
+ if (pendingSpace)
164
+ collapsed += " ";
165
+ pendingSpace = false;
166
+ collapsed += ch;
167
+ if (!quote && (ch === "'" || ch === '"')) {
168
+ quote = ch === "'" ? "single" : "double";
169
+ }
170
+ else if ((quote === "single" && ch === "'") || (quote === "double" && ch === '"')) {
171
+ quote = undefined;
172
+ }
173
+ }
174
+ return collapsed.trim();
175
+ }
176
+ /** Normalize only a command-position token; never apply this to path args. */
177
+ function normalizeGuardVerbToken(value) {
178
+ return value
179
+ .replace(/\\(?=[A-Za-z0-9_])/g, "")
180
+ .replace(/`(?=.)/g, "")
181
+ .replace(/\^(?=.)/g, "");
182
+ }
183
+ function expandGuardVerbToken(value) {
184
+ return normalizeGuardVerbToken(value).trim().split(/\s+/).filter(Boolean);
185
+ }
186
+ function delimitedSubstitutionBody(command, start, opener, closer) {
187
+ let depth = 1;
188
+ let quote;
189
+ let escaped = false;
190
+ for (let i = start + opener.length; i < command.length; i++) {
191
+ const ch = command[i];
192
+ if (quote === "single") {
193
+ if (ch === "'")
194
+ quote = undefined;
195
+ continue;
196
+ }
197
+ if (quote === "double" && ch === '"') {
198
+ quote = undefined;
199
+ continue;
200
+ }
201
+ if (escaped) {
202
+ escaped = false;
203
+ continue;
204
+ }
205
+ if (ch === "\\") {
206
+ escaped = true;
207
+ continue;
208
+ }
209
+ if (ch === "'" || ch === '"') {
210
+ quote = ch === "'" ? "single" : "double";
211
+ continue;
212
+ }
213
+ if (command.startsWith(opener, i)) {
214
+ depth += 1;
215
+ i += opener.length - 1;
216
+ continue;
217
+ }
218
+ if (command.startsWith(closer, i) && --depth === 0) {
219
+ return { body: command.slice(start + opener.length, i), end: i };
220
+ }
221
+ }
222
+ return undefined;
223
+ }
224
+ function containsGuardedSubstitution(command, depth) {
225
+ if (depth > 3)
226
+ return false;
227
+ let quote;
228
+ let escaped = false;
229
+ for (let i = 0; i < command.length; i++) {
230
+ const ch = command[i];
231
+ if (quote === "single") {
232
+ if (ch === "'")
233
+ quote = undefined;
234
+ continue;
235
+ }
236
+ if (quote === "double" && ch === '"') {
237
+ quote = undefined;
238
+ continue;
239
+ }
240
+ if (escaped) {
241
+ escaped = false;
242
+ continue;
243
+ }
244
+ if (ch === "\\") {
245
+ escaped = true;
246
+ continue;
247
+ }
248
+ if (ch === '"' || ch === "'") {
249
+ quote = ch === "'" ? "single" : "double";
250
+ continue;
251
+ }
252
+ const substitution = command.startsWith("$(", i)
253
+ ? delimitedSubstitutionBody(command, i, "$(", ")")
254
+ : command.startsWith("<(", i)
255
+ ? delimitedSubstitutionBody(command, i, "<(", ")")
256
+ : command.startsWith(">(", i)
257
+ ? delimitedSubstitutionBody(command, i, ">(", ")")
258
+ : ch === "`"
259
+ ? { body: command.slice(i + 1, command.indexOf("`", i + 1)), end: command.indexOf("`", i + 1) }
260
+ : undefined;
261
+ if (substitution && substitution.end >= 0) {
262
+ const nested = canonicalizeGuardCommand(substitution.body);
263
+ if (containsGuardedSubstitution(nested, depth + 1) ||
264
+ tokenizeShellCommand(nested).some((segment) => containsCommitOrPush(segment.tokens, depth + 1))) {
265
+ return true;
266
+ }
267
+ i = substitution.end;
268
+ }
269
+ }
270
+ return false;
271
+ }
272
+ function containsCommitOrPush(tokens, depth) {
273
+ if (depth > 3 || tokens.length === 0)
274
+ return false;
275
+ let commandTokens = tokens;
276
+ while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(commandTokens[0] ?? "")) {
277
+ commandTokens = commandTokens.slice(1);
278
+ }
279
+ if (commandTokens.length === 0)
280
+ return false;
281
+ const expandedHead = expandGuardVerbToken(commandTokens[0] ?? "");
282
+ if (expandedHead.length > 1) {
283
+ commandTokens = [...expandedHead, ...commandTokens.slice(1)];
284
+ }
285
+ if (isCommandStringWrapper(commandTokens[0] ?? "")) {
286
+ const lower = commandTokens.slice(1).map((token) => token.toLowerCase());
287
+ const runIndex = lower.findIndex((token) => token === "--run");
288
+ if (runIndex >= 0 && runIndex + 1 < commandTokens.length) {
289
+ const nestedCommand = commandTokens.slice(runIndex + 2).join(" ");
290
+ return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) => containsCommitOrPush(segment.tokens, depth + 1));
291
+ }
292
+ }
293
+ // These commands consume their following words as text/patterns; a bare
294
+ // git token in their arguments is not an indirect executable invocation.
295
+ // `$(git push)` is execution and must block; `"git push"` as literal text
296
+ // is allowed. Substitutions are screened before this text-consumer escape.
297
+ if (["echo", "printf", "grep"].includes(executableName(commandTokens[0] ?? ""))) {
298
+ return false;
299
+ }
300
+ const gitIndex = commandTokens.findIndex((token) => isGitExecutable(token));
301
+ if (gitIndex >= 0) {
302
+ // Any non-leading git invocation is indirect. Do not maintain a wrapper
303
+ // or flag allowlist: unknown launchers are the security boundary here.
304
+ if (gitIndex > 0)
305
+ return true;
306
+ const gitTokens = commandTokens.slice(gitIndex);
307
+ let i = 1;
308
+ const takesValue = new Set([
309
+ "-C",
310
+ "-c",
311
+ "--config-env",
312
+ "--git-dir",
313
+ "--work-tree",
314
+ "--exec-path",
315
+ "--namespace",
316
+ ]);
317
+ while (i < gitTokens.length && gitTokens[i].startsWith("-")) {
318
+ const option = gitTokens[i];
319
+ if (["--help", "-h", "--version", "-v", "-V"].includes(option))
320
+ return false;
321
+ if (option === "--")
322
+ return gitTokens[i + 1] === "commit" || gitTokens[i + 1] === "push";
323
+ if (["-C", "-c"].some((prefix) => option.startsWith(prefix) && option.length > prefix.length)) {
324
+ i += 1;
325
+ continue;
326
+ }
327
+ if (["--config-env", "--git-dir", "--work-tree", "--exec-path", "--namespace"].some((prefix) => option.startsWith(`${prefix}=`))) {
328
+ i += 1;
329
+ continue;
330
+ }
331
+ i += takesValue.has(option) ? 2 : 1;
332
+ }
333
+ const verbs = expandGuardVerbToken(gitTokens[i] ?? "");
334
+ return verbs.length === 1 && (verbs[0] === "commit" || verbs[0] === "push");
335
+ }
336
+ const leadingExecutable = commandTokens[0] ?? "";
337
+ const knownCommandStringWrapper = isShellWrapper(leadingExecutable) || isCommandStringWrapper(leadingExecutable);
338
+ if (!knownCommandStringWrapper) {
339
+ // Unknown launchers are a fail-closed boundary only when they explicitly
340
+ // accept a command string. Re-tokenizing the value keeps literal mentions
341
+ // such as `myprog -c "echo git push"` out of the guarded-command path.
342
+ const unknownSwitchIndex = commandTokens.slice(1).findIndex((token) => (token.startsWith("-") || token.startsWith("/")) && token.length > 1);
343
+ if (unknownSwitchIndex < 0)
344
+ return false;
345
+ const commandIndex = unknownSwitchIndex + 2;
346
+ if (commandIndex >= commandTokens.length)
347
+ return false;
348
+ const nestedCommand = commandTokens.slice(commandIndex).join(" ");
349
+ return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) => containsCommitOrPush(segment.tokens, depth + 1));
350
+ }
351
+ const lower = commandTokens.slice(1).map((token) => token.toLowerCase());
352
+ const switchIndex = lower.findIndex((token) => token === "-c" ||
353
+ token === "--run" ||
354
+ token === "-lc" ||
355
+ (/^-[^-]*c$/.test(token) && !token.startsWith("--")) ||
356
+ token === "/c" ||
357
+ token === "-command" ||
358
+ token === "-command:" ||
359
+ token === "-encodedcommand");
360
+ if (switchIndex < 0 || switchIndex + 1 >= commandTokens.length)
361
+ return false;
362
+ // Encoded PowerShell is intentionally unsupported: decoding it here would
363
+ // be a second shell/parser and could turn an ambiguous command into a false
364
+ // allow. Plain -Command/-c is safely handed back to the shared lexer.
365
+ if (lower[switchIndex] === "-encodedcommand")
366
+ return false;
367
+ // switchIndex is relative to commandTokens.slice(1), so +2 addresses the
368
+ // command token after the switch for both cmd and PowerShell. This also
369
+ // handles cmd options preceding /C (for example `/S /C`).
370
+ let commandIndex = switchIndex + 2;
371
+ if (commandTokens[commandIndex] === "--")
372
+ commandIndex += 1;
373
+ const nestedCommand = commandTokens.slice(commandIndex).join(" ");
374
+ return tokenizeShellCommand(canonicalizeGuardCommand(nestedCommand)).some((segment) => containsCommitOrPush(segment.tokens, depth + 1));
375
+ }
376
+ /** Analyze actual executable invocations, not substrings in shell text. */
11
377
  export function isGitCommitOrPushAttempt(toolName, input) {
12
378
  if (toolName !== "bash")
13
379
  return false;
14
- const cmd = getShellCommand(input).toLowerCase();
15
- if (!cmd)
380
+ const command = getShellCommand(input);
381
+ if (!command)
382
+ return false;
383
+ const canonical = canonicalizeGuardCommand(command);
384
+ if (containsGuardedSubstitution(canonical, 0))
385
+ return true;
386
+ return tokenizeShellCommand(canonical).some((segment) => containsCommitOrPush(segment.tokens, 0));
387
+ }
388
+ function isTurnEndFindingsCache(value) {
389
+ if (!value || typeof value !== "object")
390
+ return false;
391
+ const record = value;
392
+ return (typeof record.content === "string" &&
393
+ typeof record.hasBlockers === "boolean" &&
394
+ Array.isArray(record.affectedFiles) &&
395
+ record.affectedFiles.every((file) => typeof file === "string") &&
396
+ typeof record.sessionId === "string" &&
397
+ typeof record.projectSeqStart === "number" &&
398
+ typeof record.projectSeqEnd === "number" &&
399
+ Number.isFinite(record.projectSeqStart) &&
400
+ Number.isFinite(record.projectSeqEnd) &&
401
+ !!record.fileSeqByPath &&
402
+ typeof record.fileSeqByPath === "object" &&
403
+ Object.values(record.fileSeqByPath).every((seq) => typeof seq === "number" && Number.isFinite(seq)) &&
404
+ !!record.fileContentHashes &&
405
+ typeof record.fileContentHashes === "object" &&
406
+ Object.values(record.fileContentHashes).every((hash) => typeof hash === "string") &&
407
+ (record.affectedFilesTruncated === undefined || typeof record.affectedFilesTruncated === "boolean") &&
408
+ (record.blockingFiles === undefined || Array.isArray(record.blockingFiles)));
409
+ }
410
+ function cacheRecord(cacheManager, cwd) {
411
+ const entry = cacheManager.readCache("turn-end-findings", cwd);
412
+ return entry && isTurnEndFindingsCache(entry.data) ? entry.data : undefined;
413
+ }
414
+ function markCacheUnknown(runtime, reason) {
415
+ runtime.markGitGuardCacheUnknown(reason);
416
+ }
417
+ /** Persist a complete, bounded, content-bound guard record. */
418
+ export function writeGitGuardRecord(cacheManager, runtime, cwd, record) {
419
+ const capped = capAffectedFiles(Array.isArray(record.affectedFiles) ? record.affectedFiles : [], cwd);
420
+ const fileSeqByPath = { ...(record.fileSeqByPath ?? {}) };
421
+ for (const file of capped.files) {
422
+ const key = guardPathKey(file, cwd);
423
+ if (fileSeqByPath[key] === undefined) {
424
+ fileSeqByPath[key] = runtime.getFileSeq(resolveGuardPath(file, cwd));
425
+ }
426
+ }
427
+ const data = {
428
+ ...record,
429
+ affectedFiles: capped.files,
430
+ affectedFilesTruncated: capped.truncated,
431
+ fileSeqByPath,
432
+ blockingFiles: Array.isArray(record.blockingFiles)
433
+ ? capAffectedFiles(record.blockingFiles, cwd).files
434
+ : undefined,
435
+ fileContentHashes: snapshotFileHashes(capped.files, cwd),
436
+ };
437
+ try {
438
+ cacheManager.writeCache("turn-end-findings", data, cwd);
439
+ runtime.clearGitGuardCacheUnknown();
440
+ return true;
441
+ }
442
+ catch {
443
+ markCacheUnknown(runtime, "cache_write_failed");
16
444
  return false;
17
- return /(^|\s|&&|;|\|)git\s+(commit|push)(\s|$)/.test(cmd);
445
+ }
446
+ }
447
+ function logDecision(cwd, decision, reasonCategory, metadata = {}) {
448
+ logLatency({
449
+ type: "phase",
450
+ toolName: "git-guard",
451
+ filePath: cwd,
452
+ phase: "decision",
453
+ durationMs: 0,
454
+ result: decision,
455
+ metadata: { decision, reasonCategory, ...metadata },
456
+ });
457
+ }
458
+ function unknown(cwd, reasonCategory, metadata = {}) {
459
+ logDecision(cwd, "unknown", reasonCategory, metadata);
460
+ return {
461
+ block: true,
462
+ unknown: true,
463
+ reason: `🔴 COMMIT BLOCKED (--lens-guard): blocker state is unknown (${reasonCategory}). Re-run pi-lens checks or start a fresh session, then retry.`,
464
+ };
465
+ }
466
+ /**
467
+ * Reconcile the one persisted record after a per-file dispatch. This is called
468
+ * on tool_result, never tool_call, and therefore does not add disk I/O to the
469
+ * edit preflight path.
470
+ */
471
+ export function syncGitGuardRecord(runtime, cacheManager, cwd, editedFilePath) {
472
+ const entries = runtime.getInlineBlockersSnapshot?.() ?? [];
473
+ const inspection = cacheManager.inspectCache("turn-end-findings", cwd);
474
+ const existing = cacheRecord(cacheManager, cwd);
475
+ if (existing &&
476
+ existing.sessionId !== runtime.telemetrySessionId &&
477
+ entries.length === 0) {
478
+ markCacheUnknown(runtime, "session_mismatch");
479
+ return;
480
+ }
481
+ if (!existing && inspection !== "missing" && entries.length === 0) {
482
+ markCacheUnknown(runtime, `cache_${inspection}`);
483
+ return;
484
+ }
485
+ const fileSeqByPath = {};
486
+ for (const [filePath, seq] of runtime.getFileSeqEntries?.() ?? []) {
487
+ fileSeqByPath[guardPathKey(filePath, cwd)] = seq;
488
+ }
489
+ const inlineFiles = entries.map((entry) => resolveGuardPath(entry.filePath, cwd));
490
+ const existingBlockingFiles = existing?.blockingFiles ?? [];
491
+ const provenanceComplete = existing?.blockerContent
492
+ ? hasCompleteBlockingProvenance(existing.blockerContent, existingBlockingFiles, cwd)
493
+ : true;
494
+ if (existing?.blockerContent && !provenanceComplete) {
495
+ markCacheUnknown(runtime, "blocking_provenance_untrusted");
496
+ return;
497
+ }
498
+ const editedKey = editedFilePath ? guardPathKey(editedFilePath, cwd) : undefined;
499
+ const testFiles = existing?.testFailureFiles ?? [];
500
+ const remainingBlockingFiles = editedKey && existingBlockingFiles.length > 0
501
+ ? existingBlockingFiles.filter((file) => guardPathKey(file, cwd) !== editedKey)
502
+ : existingBlockingFiles;
503
+ let affectedFiles = [...(existing?.affectedFiles ?? []), ...inlineFiles];
504
+ if (!entries.length && editedKey && existing) {
505
+ const isStillTestFailure = testFiles.some((file) => guardPathKey(file, cwd) === editedKey);
506
+ const isUnknownBlockingPath = existingBlockingFiles.length === 0 && !!existing.blockerContent;
507
+ if (!isStillTestFailure && !isUnknownBlockingPath) {
508
+ affectedFiles = affectedFiles.filter((file) => guardPathKey(file, cwd) !== editedKey);
509
+ }
510
+ }
511
+ // A clean per-file dispatch is authoritative for that file. When the
512
+ // persisted record has explicit blocking-file provenance and the last such
513
+ // file just reconciled clean, retaining blockerContent would resurrect a
514
+ // stale blocker on every later git-guard lookup. Records without that
515
+ // provenance remain fail-closed: they cannot be safely cleared here.
516
+ const clearedLastKnownBlocker = !entries.length &&
517
+ !!editedKey &&
518
+ provenanceComplete &&
519
+ existingBlockingFiles.length > 0 &&
520
+ remainingBlockingFiles.length === 0;
521
+ const blockerContent = entries.length > 0
522
+ ? entries.map((entry) => `${entry.filePath}: ${entry.summary}`).join("\n")
523
+ : clearedLastKnownBlocker
524
+ ? undefined
525
+ : existing?.blockerContent;
526
+ const hasTestFailures = existing?.testFailures === true;
527
+ const hasBlockers = !!blockerContent || hasTestFailures;
528
+ const content = [blockerContent, hasTestFailures ? existing?.testFailureContent : undefined]
529
+ .filter((value) => !!value)
530
+ .join("\n\n");
531
+ if (!hasBlockers && !content) {
532
+ cacheManager.clearCache("turn-end-findings", cwd);
533
+ return;
534
+ }
535
+ writeGitGuardRecord(cacheManager, runtime, cwd, {
536
+ content: content || existing?.content || "",
537
+ blockerContent,
538
+ blockingFiles: entries.length > 0 ? inlineFiles : remainingBlockingFiles,
539
+ hasBlockers,
540
+ affectedFiles,
541
+ sessionId: runtime.telemetrySessionId,
542
+ projectSeqStart: runtime.turnStartProjectSeq,
543
+ projectSeqEnd: runtime.projectSeq,
544
+ fileSeqByPath,
545
+ fileContentHashes: {},
546
+ consumed: false,
547
+ testFailures: existing?.testFailures,
548
+ testFailureContent: existing?.testFailureContent,
549
+ testFailureFiles: existing?.testFailureFiles,
550
+ });
551
+ }
552
+ /** Add blocking test failures to the same turn-end record. */
553
+ export function mergeGitGuardTestFailure(cacheManager, cwd, runtime, content, files) {
554
+ const existing = cacheRecord(cacheManager, cwd);
555
+ const fileSeqByPath = {};
556
+ for (const [filePath, seq] of runtime.getFileSeqEntries?.() ?? []) {
557
+ fileSeqByPath[guardPathKey(filePath, cwd)] = seq;
558
+ }
559
+ const failedFiles = files.map((file) => resolveGuardPath(file, cwd));
560
+ const blockerContent = existing?.blockerContent;
561
+ const testFailureFiles = [
562
+ ...(existing?.testFailureFiles ?? []),
563
+ ...failedFiles,
564
+ ].filter((file, index, all) => all.findIndex((candidate) => guardPathKey(candidate, cwd) === guardPathKey(file, cwd)) === index);
565
+ writeGitGuardRecord(cacheManager, runtime, cwd, {
566
+ content: [blockerContent, content].filter(Boolean).join("\n\n"),
567
+ blockerContent,
568
+ blockingFiles: existing?.blockingFiles ?? [],
569
+ testFailureContent: content,
570
+ testFailureFiles: testFailureFiles,
571
+ hasBlockers: true,
572
+ affectedFiles: [...(existing?.affectedFiles ?? []), ...failedFiles],
573
+ sessionId: runtime.telemetrySessionId,
574
+ projectSeqStart: runtime.turnStartProjectSeq,
575
+ projectSeqEnd: runtime.projectSeq,
576
+ fileSeqByPath,
577
+ fileContentHashes: {},
578
+ consumed: false,
579
+ testFailures: true,
580
+ });
581
+ }
582
+ /** A passing test run resolves only its own previous test failures. */
583
+ export function clearGitGuardTestFailure(cacheManager, cwd, runtime, passedFiles = []) {
584
+ const existing = cacheRecord(cacheManager, cwd);
585
+ if (!existing?.testFailures)
586
+ return;
587
+ const passedKeys = new Set((passedFiles.length > 0 ? passedFiles : existing.testFailureFiles ?? []).map((file) => guardPathKey(file, cwd)));
588
+ const remainingFiles = (existing.testFailureFiles ?? []).filter((file) => !passedKeys.has(guardPathKey(file, cwd)));
589
+ const blockerContent = existing.blockerContent ?? "";
590
+ if (!blockerContent && remainingFiles.length === 0) {
591
+ cacheManager.clearCache("turn-end-findings", cwd);
592
+ return;
593
+ }
594
+ const blockingKeys = new Set((existing.blockingFiles ?? []).map((file) => guardPathKey(file, cwd)));
595
+ const affectedFiles = existing.affectedFiles.filter((file) => blockingKeys.has(guardPathKey(file, cwd)) ||
596
+ remainingFiles.some((testFile) => guardPathKey(testFile, cwd) === guardPathKey(file, cwd)));
597
+ writeGitGuardRecord(cacheManager, runtime, cwd, {
598
+ ...existing,
599
+ content: blockerContent,
600
+ blockerContent,
601
+ affectedFiles,
602
+ testFailures: remainingFiles.length > 0,
603
+ testFailureContent: remainingFiles.length > 0 ? existing.testFailureContent : undefined,
604
+ testFailureFiles: remainingFiles.length > 0 ? remainingFiles : undefined,
605
+ hasBlockers: !!blockerContent || remainingFiles.length > 0,
606
+ sessionId: runtime.telemetrySessionId,
607
+ projectSeqStart: runtime.turnStartProjectSeq,
608
+ projectSeqEnd: runtime.projectSeq,
609
+ fileSeqByPath: Object.fromEntries(runtime.getFileSeqEntries().map(([filePath, seq]) => [guardPathKey(filePath, cwd), seq])),
610
+ fileContentHashes: {},
611
+ });
18
612
  }
19
613
  export function evaluateGitGuard(runtime, cacheManager, cwd) {
614
+ if (runtime.gitGuardHasBlockers) {
615
+ logDecision(cwd, "blocked", "runtime_blockers", {
616
+ projectSeq: runtime.projectSeq,
617
+ });
618
+ const detail = runtime.gitGuardSummary ? `\n${runtime.gitGuardSummary}` : "";
619
+ return {
620
+ block: true,
621
+ reason: `🔴 COMMIT BLOCKED (--lens-guard): unresolved blockers must be fixed before commit/push.${detail}\nRun lens_diagnostics mode=all for full details, then commit again.`,
622
+ };
623
+ }
624
+ if (runtime.gitGuardCacheUnknownReason) {
625
+ return unknown(cwd, runtime.gitGuardCacheUnknownReason);
626
+ }
627
+ const inspection = cacheManager.inspectCache("turn-end-findings", cwd);
628
+ if (inspection === "missing") {
629
+ logDecision(cwd, "allowed", "no_record");
630
+ return { block: false };
631
+ }
632
+ if (inspection !== "fresh")
633
+ return unknown(cwd, `cache_${inspection}`);
20
634
  const pending = cacheManager.readCache("turn-end-findings", cwd);
21
- const turnEndHasBlockers = !!pending?.data?.content;
22
- if (!runtime.gitGuardHasBlockers && !turnEndHasBlockers) {
635
+ if (!pending || !isTurnEndFindingsCache(pending.data)) {
636
+ return unknown(cwd, "cache_malformed");
637
+ }
638
+ const record = pending.data;
639
+ if (!record.hasBlockers) {
640
+ logDecision(cwd, "allowed", "advisory_only");
641
+ return { block: false };
642
+ }
643
+ if (record.affectedFilesTruncated) {
644
+ return unknown(cwd, "affected_files_truncated");
645
+ }
646
+ if (record.sessionId !== runtime.telemetrySessionId) {
647
+ return unknown(cwd, "session_mismatch");
648
+ }
649
+ if (record.projectSeqEnd !== runtime.projectSeq) {
650
+ return unknown(cwd, "project_sequence_mismatch", {
651
+ recordedProjectSeq: record.projectSeqEnd,
652
+ currentProjectSeq: runtime.projectSeq,
653
+ });
654
+ }
655
+ const liveFiles = [];
656
+ for (const file of record.affectedFiles) {
657
+ const resolved = resolveGuardPath(file, cwd);
658
+ const key = guardPathKey(resolved, cwd);
659
+ const recordedSeq = record.fileSeqByPath[key];
660
+ if (recordedSeq === undefined ||
661
+ recordedSeq !== (runtime.getFileSeq?.(resolved) ?? 0)) {
662
+ return unknown(cwd, "file_sequence_mismatch", { file: resolved });
663
+ }
664
+ try {
665
+ if (!isPathIgnoredByProject(resolved, runtime.projectRoot || cwd, false)) {
666
+ const currentHash = currentFileFingerprint(resolved);
667
+ if (currentHash !== "missing") {
668
+ liveFiles.push(resolved);
669
+ const expectedHash = record.fileContentHashes[key];
670
+ if (!expectedHash ||
671
+ expectedHash.startsWith("unreadable:") ||
672
+ currentHash.startsWith("unreadable:") ||
673
+ expectedHash !== currentHash) {
674
+ return unknown(cwd, "file_content_changed", { file: resolved });
675
+ }
676
+ }
677
+ }
678
+ }
679
+ catch {
680
+ return unknown(cwd, "file_unreadable", { file: resolved });
681
+ }
682
+ }
683
+ if (record.hasBlockers && record.affectedFiles.length === 0) {
684
+ return unknown(cwd, "blocker_without_affected_file");
685
+ }
686
+ // A deleted/ignored affected file has been resolved; no stale blocker is
687
+ // allowed to survive solely because its old path remains in the record.
688
+ if (record.hasBlockers && liveFiles.length === 0 && record.affectedFiles.length > 0) {
689
+ logDecision(cwd, "allowed", "affected_files_resolved", {
690
+ projectSeq: record.projectSeqEnd,
691
+ });
23
692
  return { block: false };
24
693
  }
25
- const details = runtime.gitGuardSummary
26
- ? `\n${runtime.gitGuardSummary}`
27
- : "";
694
+ logDecision(cwd, "blocked", "cache_blockers", {
695
+ projectSeq: record.projectSeqEnd,
696
+ affectedFileCount: record.affectedFiles.length,
697
+ });
28
698
  return {
29
699
  block: true,
30
- reason: `🔴 COMMIT BLOCKED (--lens-guard): unresolved blockers must be fixed before commit/push.${details}\nRun lens_diagnostics mode=all for full details, then commit again.`,
700
+ reason: "🔴 COMMIT BLOCKED (--lens-guard): unresolved blockers must be fixed before commit/push.\nRun lens_diagnostics mode=all for full details, then commit again.",
31
701
  };
32
702
  }