pi-lens 3.8.63 → 3.8.64

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 (49) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -9
  3. package/dist/clients/deadline-utils.js +23 -0
  4. package/dist/clients/installer/index.js +18 -27
  5. package/dist/clients/log-cleanup.js +36 -13
  6. package/dist/clients/lsp/client.js +44 -0
  7. package/dist/clients/lsp/index.js +208 -33
  8. package/dist/clients/lsp/interactive-install.js +14 -4
  9. package/dist/clients/lsp/launch.js +29 -100
  10. package/dist/clients/mcp/review.js +7 -4
  11. package/dist/clients/package-manager.js +223 -0
  12. package/dist/clients/pipeline.js +55 -2
  13. package/dist/clients/project-metadata.js +3 -23
  14. package/dist/clients/safe-spawn.js +9 -0
  15. package/dist/clients/tree-sitter-client.js +72 -14
  16. package/dist/index.js +16 -0
  17. package/dist/mcp/server.js +4 -2
  18. package/dist/tools/ast-grep-search.js +10 -6
  19. package/dist/tools/lens-diagnostics.js +43 -10
  20. package/dist/tools/lsp-diagnostics.js +23 -4
  21. package/dist/tools/scan-progress.js +38 -0
  22. package/grammars/tree-sitter-bash.wasm +0 -0
  23. package/grammars/tree-sitter-bash.wasm.json +5 -0
  24. package/grammars/tree-sitter-css.wasm +0 -0
  25. package/grammars/tree-sitter-css.wasm.json +5 -0
  26. package/grammars/tree-sitter-go.wasm +0 -0
  27. package/grammars/tree-sitter-go.wasm.json +5 -0
  28. package/grammars/tree-sitter-html.wasm +0 -0
  29. package/grammars/tree-sitter-html.wasm.json +5 -0
  30. package/grammars/tree-sitter-java.wasm +0 -0
  31. package/grammars/tree-sitter-java.wasm.json +5 -0
  32. package/grammars/tree-sitter-javascript.wasm +0 -0
  33. package/grammars/tree-sitter-javascript.wasm.json +5 -0
  34. package/grammars/tree-sitter-json.wasm +0 -0
  35. package/grammars/tree-sitter-json.wasm.json +5 -0
  36. package/grammars/tree-sitter-python.wasm +0 -0
  37. package/grammars/tree-sitter-python.wasm.json +5 -0
  38. package/grammars/tree-sitter-rust.wasm +0 -0
  39. package/grammars/tree-sitter-rust.wasm.json +5 -0
  40. package/grammars/tree-sitter-tsx.wasm +0 -0
  41. package/grammars/tree-sitter-tsx.wasm.json +5 -0
  42. package/grammars/tree-sitter-typescript.wasm +0 -0
  43. package/grammars/tree-sitter-typescript.wasm.json +5 -0
  44. package/grammars/tree-sitter-yaml.wasm +0 -0
  45. package/grammars/tree-sitter-yaml.wasm.json +5 -0
  46. package/package.json +7 -5
  47. package/scripts/analyze-pi-lens-logs.mjs +101 -3
  48. package/scripts/download-grammars.js +171 -30
  49. package/scripts/grammars.lock.json +32 -0
package/CHANGELOG.md CHANGED
@@ -10,6 +10,28 @@ All notable changes to pi-lens will be documented in this file.
10
10
 
11
11
  ### Fixed
12
12
 
13
+ ## [3.8.64] - 2026-07-04
14
+
15
+ ### Added
16
+
17
+ - **Opt-in `workspace/diagnostic` pull for the full scan — one request per server instead of N file opens (#387 Item 2)** — where a language server advertises `workspace/diagnostic` (e.g. TypeScript), `lens_diagnostics mode=full` can now issue a single project-wide pull per server instead of opening every file, detected via a new `workspaceDiagnostics` capability flag (distinct from per-document pull) and a `requestWorkspaceDiagnostics` client method. Gated behind `PI_LENS_LSP_WORKSPACE_PULL=1` (default off) and used per server-group only when the server advertises it and no file in the group has an auxiliary scanner; **any** miss (unsupported / dead / timeout / auxiliary present) falls back to the per-server-serial per-file path from #387 Item 1. Off by default because a **cold** server can answer a workspace pull with an empty/partial report that would read as a false "all clean", and the pull covers only the primary server — so it stays opt-in pending real-server validation before becoming the default. Completes the capability side of #387 (Item 1 shipped in #388).
18
+ - **Progress bar for the long full-mode diagnostic scans (`lens_diagnostics mode=full`, `lsp_diagnostics` batch/directory)** — these scans can run for seconds to minutes and were previously opaque until they returned. They now stream a throttled progress bar (`Scanning… [████░░░░░░] 45/123 (37%)`) to the tool's `onUpdate` callback — at most ~4×/s plus a guaranteed final tick — so the agent/user sees movement. The data already existed (the sweep's per-file completion count); this just surfaces it. Shared `tools/scan-progress.ts` (`renderScanProgress` + `makeProgressReporter`); `runWorkspaceDiagnostics` and `mapWithConcurrency` gained an optional `onProgress(completed, total)`.
19
+
20
+ - **Tree-sitter grammar provenance — sha256 sidecars + a committed manifest + a CI guard (#177)** — the grammar downloader now verifies every fetched `.wasm` against a committed provenance manifest (`scripts/grammars.lock.json`: package, version, per-grammar sha256) and writes a `<grammar>.wasm.json` sidecar recording what was installed. Two integrity gaps close: (1) fetched bytes are checked against the pinned hash — a grammar whose bytes don't match is never written (guards CDN corruption/tampering); (2) the old **skip-if-exists** behavior is replaced by **skip-if-verified** — on a version bump or hash mismatch the stale grammar is re-downloaded instead of silently persisting, so an ABI-mismatched `.wasm` can't survive against the deliberately pinned `web-tree-sitter`. A new `npm run check:grammars` (`scripts/check-grammar-provenance.mjs`, wired into CI like `check:lockfile`) re-hashes the installed grammars' bytes and fails on any drift from the manifest. Regenerate on a deliberate tree-sitter-wasms bump with `node scripts/download-grammars.js --write-manifest`.
21
+ - **Package-manager detection — pi-lens no longer hardcodes npm/npx (#374)** — new `clients/package-manager.ts` is the single source of truth for *which* Node package manager to use (npm/pnpm/yarn/bun) and *how* to spell each command (run-script / install / global-install / exec / global-bin). Resolution: the project's lockfile or corepack `packageManager` field when that manager is installed, else the first installed by preference (npm → pnpm → yarn → bun), else npm. Routed through it: tool auto-install and global-bin discovery (`clients/installer`), LSP global binary lookup (`clients/lsp/launch.ts`), the interactive LSP-server global install (`clients/lsp/interactive-install.ts`), the MCP `pilens_rebuild` (`runRebuild` reports which manager it used), and project run-command hints. This makes pi-lens work on hosts that ship bun/pnpm/yarn instead of npm.
22
+
23
+ ### Changed
24
+
25
+ - **Tree-sitter grammars: bundle a core set + fix cross-manager install** — grammars were fetched only by the `postinstall` script, which **npm** runs but **pnpm/bun block by default** (and **yarn** couldn't install at all — see below), so non-npm users depended entirely on a runtime CDN fetch and got *no* tree-sitter offline. Now the 12 core grammars (ts, tsx, js, python, go, rust, json, yaml, bash, html, css, java) are **downloaded at `prepare` time and shipped in the tarball** (`grammars/`, in `files[]`; ~+1 MB to the `.tgz` since wasm gzips well, +9 MB unpacked), so the common languages parse **offline on every package manager**. The long-tail grammars still lazy-fetch on first use, and a failed fetch now emits a **visible, actionable warning** instead of a silent debug line. Also removed the `tree-sitter-wasms: "npm:null@^0.11.0"` optional-dependency sentinel — npm/pnpm/bun skipped it as a failing optional but **yarn classic hard-errored** on it, so `yarn add pi-lens` failed outright; it's now installable under yarn.
26
+
27
+ ### Fixed
28
+
29
+ - **The LSP notify write is now bounded at the source, so a wedged server can't ride an edit to the 30 s dispatcher timeout** — the pre-dispatch sync fix bounded one call site, but *every* `touchFile` caller (the dispatch LSP runner and the workspace sweep too) `await`ed `notify.open`'s `didChange`/`didOpen` write, which backpressures indefinitely on a server whose stdin isn't drained. So edits on a wedged server still took ~31 s — the dispatch LSP runner hung on that write until the coarse 30 s per-runner ceiling killed it (`Runner lsp timed out after 30000ms`). The write is now bounded inside `touchFile` itself (`PI_LENS_LSP_NOTIFY_BUDGET_MS`, default 2 s): on a wedged server it degrades to "no fresh diagnostics" (logged as `lsp_notify_timeout` / `notifyWriteTimedOut`) instead of hanging, for **all** callers.
30
+ - **`lens_diagnostics`/`lsp_diagnostics` full sweep no longer floods a single-threaded server (#387)** — the sweep ran a flat 8-wide worker pool that was server-agnostic, so on a single-language repo all 8 concurrent touches hit one `tsserver`. It's single-threaded per project: they don't parallelize, they queue — inflating the working set (each `didOpen` can force a project recheck) and **cascading per-file-budget timeouts by queue position** (observed: 51 of 123 files "timed out" purely from being behind others, the count climbing 0→51 as the queue deepened). The sweep now **groups files by their primary server, serializes touches within a server (one in flight), and parallelizes across distinct servers** — real parallelism in a mixed TS+Python repo, no flooding in a single-language one. This uses the universal per-file `didOpen` path, so it works for every server regardless of `workspace/diagnostic` support (a single-request workspace pull for servers that advertise it remains the #387 follow-up).
31
+ - **An edit could hang indefinitely (and ignore Escape) on a wedged language server — the pre-dispatch LSP sync was unbounded** — after an edit, pi-lens syncs the new content to the language server (a `didChange`/`didOpen`) before dispatching lint. Client *acquisition* was capped, but the notify *write* was not: when the server's stdin isn't being drained (a CPU-bound/wedged server — e.g. TypeScript mid-recheck), that write backpressures forever, hanging the whole edit with **no per-call bound and no log at all** (the first instrumented phase, `read_file`, sits *after* the sync, so the stall left zero trace). Observed live: an edit wedged 8+ minutes with the server timing out on every request. Now the sync is raced against a hard budget (`PI_LENS_LSP_SYNC_BUDGET_MS`, default 3 s) **and** the turn's abort signal, so Escape cancels it and a slow server can't park the pipeline — the edit proceeds (the dispatch LSP runner, with its own 30 s cap, still tries). Escape now reaches this path because the turn signal is exposed for in-process LSP awaits (`getAmbientAbortSignal`), not just child-process spawns. Two observability gaps closed too: an abandoned sync logs an `lsp_sync_abandoned` phase (timeout vs aborted), and a new `tool_result_received` marker fires the instant pi-lens receives an edit — so a future stall is localizable (present-then-silent = in-pipeline; absent = upstream) instead of invisible.
32
+ - **Log retention was silently broken — rotated backups never got deleted, and three logs never rotated** — the 10 MB rotation worked, but the 7-day retention sweep that's supposed to reap the backups used a pattern (`/\.log\./`) that only matched the *legacy* `name.log.<ts>` shape, never the *current* `name.<ts>.log` that rotation actually produces — so every backup ever rotated accumulated forever (observed: ~200 MB of `~/.pi-lens` backups dating back 2.5 months against a 7-day policy). Separately, three of the eight global logs — `actionable-warnings.log`, `ast-grep-tools.log`, `dead-code.log` — were absent from the rotation list and grew unbounded. Both are fixed: the deletion pattern now matches both backup shapes (and never an active log), and all eight logs are managed from one shared `MANAGED_LOG_FILES` list so rotation and the storage summary can't drift apart again. Because `runLogCleanup` already runs unconditionally on every session start, the corrected sweep **self-heals existing backlogs** — the next launch after upgrade reaps each user's accumulated `>7 day` backups with no migration step.
33
+ - **`lens_diagnostics mode=full` could hang indefinitely and didn't cancel on Escape** — an unattended session was observed wedged for ~8 hours on a small repo. Two root causes, both fixed: **(1) Escape didn't cancel** — the tool honored only the tool-call `signal` positional, not `ctx.signal` (the turn-wired abort Escape fires for a registered extension tool), so the sweep ran on. It now combines both (`combineAbortSignals`, shared in `deadline-utils`) and threads the result through the LSP sweep and project-runner scan. **(2) It could hang forever** — per-file diagnostic waits were bounded, but *client acquisition* in the sweep was not, so a language server hanging on spawn/`initialize` parked a worker permanently. Now (a) each file gets a per-file wall-clock budget (`PI_LENS_LSP_WORKSPACE_PER_FILE_MS`, default 15 s) so a worker always returns to its abort check, and (b) the whole scan has a hard wall-clock ceiling (`PI_LENS_LENS_DIAGNOSTICS_FULL_TIMEOUT_MS`, default 3 min) that aborts it to partial results rather than never returning. The sweep also now emits **start + periodic heartbeat** latency logs (with `completed/total`, `timedOutFiles`) so a future stall is debuggable instead of silent, and the `scripts/analyze-pi-lens-logs.mjs` smell report now consumes them — surfacing sweeps that started but never completed (the hang/kill signature, with the last heartbeat's `completed X/Y`), sweeps whose files hit the per-file budget, and any `*_timeout` phases. The same pass repairs the analyzer's `slow-background-tasks` detector, whose regex had drifted from the runtime's `session_start task … success runMs=<n>` format and was silently matching **zero** of ~2k rows — it now flags the real multi-second startup tasks (`call-graph`, `knip`, `project-index`) again, and accepts both the current `runMs=` and the older `(<n>ms)` shapes. The same `ctx.signal` cancellation gap is fixed in the sibling tools `lsp_diagnostics` (batch/directory scans) and `ast_grep_search` (which was already abort-aware but checked the wrong signal).
34
+
13
35
  ## [3.8.63] - 2026-07-01
14
36
 
15
37
  ### Added
package/README.md CHANGED
@@ -71,7 +71,7 @@ Thanks goes to these wonderful people:
71
71
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/anh-chu"><img src="https://avatars.githubusercontent.com/u/34973633?v=4" width="100px;" alt=""/><br /><sub><b>Anh Chu</b></sub></a><br /><a href="#code-anh-chu" title="Code">💻</a></td>
72
72
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/Istar-Eldritch"><img src="https://avatars.githubusercontent.com/u/3746468?v=4" width="100px;" alt=""/><br /><sub><b>Ruben Paz</b></sub></a><br /><a href="#code-Istar-Eldritch" title="Code">💻</a> <a href="#bug-Istar-Eldritch" title="Bug reports">🐛</a></td>
73
73
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/tifandotme"><img src="https://avatars.githubusercontent.com/u/33323177?v=4" width="100px;" alt=""/><br /><sub><b>Tifan Dwi Avianto</b></sub></a><br /><a href="#code-tifandotme" title="Code">💻</a></td>
74
- </tr>
74
+ </tr><br />
75
75
  <tr>
76
76
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/ricardoraposo"><img src="https://avatars.githubusercontent.com/u/50217712?v=4" width="100px;" alt=""/><br /><sub><b>Ricardo Raposo</b></sub></a><br /><a href="#code-ricardoraposo" title="Code">💻</a></td>
77
77
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/cjunxiang"><img src="https://avatars.githubusercontent.com/u/26619858?v=4" width="100px;" alt=""/><br /><sub><b>C.Junxiang</b></sub></a><br /><a href="#code-cjunxiang" title="Code">💻</a> <a href="#bug-cjunxiang" title="Bug reports">🐛</a></td>
@@ -80,7 +80,7 @@ Thanks goes to these wonderful people:
80
80
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/fractiunate"><img src="https://avatars.githubusercontent.com/u/78024279?v=4" width="100px;" alt=""/><br /><sub><b>Fractiunate // David Rahäuser</b></sub></a><br /><a href="#code-fractiunate" title="Code">💻</a></td>
81
81
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/bloodf"><img src="https://avatars.githubusercontent.com/u/1626923?v=4" width="100px;" alt=""/><br /><sub><b>Heitor Ramon Ribeiro</b></sub></a><br /><a href="#code-bloodf" title="Code">💻</a></td>
82
82
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/JayceFreeman"><img src="https://avatars.githubusercontent.com/u/92962110?v=4" width="100px;" alt=""/><br /><sub><b>JayceFreeman</b></sub></a><br /><a href="#code-JayceFreeman" title="Code">💻</a></td>
83
- </tr>
83
+ </tr><br />
84
84
  <tr>
85
85
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/shaDmx"><img src="https://avatars.githubusercontent.com/u/91132641?v=4" width="100px;" alt=""/><br /><sub><b>Max L.</b></sub></a><br /><a href="#code-shaDmx" title="Code">💻</a></td>
86
86
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/Roman-Galeev"><img src="https://avatars.githubusercontent.com/u/40388226?v=4" width="100px;" alt=""/><br /><sub><b>Roman Galeev</b></sub></a><br /><a href="#code-Roman-Galeev" title="Code">💻</a></td>
@@ -89,7 +89,7 @@ Thanks goes to these wonderful people:
89
89
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/flowing-abyss"><img src="https://avatars.githubusercontent.com/u/98622217?v=4" width="100px;" alt=""/><br /><sub><b>flowing-abyss</b></sub></a><br /><a href="#code-flowing-abyss" title="Code">💻</a></td>
90
90
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/jerryfan"><img src="https://avatars.githubusercontent.com/u/2540814?v=4" width="100px;" alt=""/><br /><sub><b>jerryfan</b></sub></a><br /><a href="#code-jerryfan" title="Code">💻</a></td>
91
91
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/loss-and-quick"><img src="https://avatars.githubusercontent.com/u/39405619?v=4" width="100px;" alt=""/><br /><sub><b>minicx</b></sub></a><br /><a href="#code-loss-and-quick" title="Code">💻</a></td>
92
- </tr>
92
+ </tr><br />
93
93
  <tr>
94
94
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/spyrosbazios"><img src="https://avatars.githubusercontent.com/u/37960233?v=4" width="100px;" alt=""/><br /><sub><b>spyrosbazios</b></sub></a><br /><a href="#code-spyrosbazios" title="Code">💻</a></td>
95
95
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/trvon"><img src="https://avatars.githubusercontent.com/u/6031322?v=4" width="100px;" alt=""/><br /><sub><b>Trevon</b></sub></a><br /><a href="#code-trvon" title="Code">💻</a></td>
@@ -98,7 +98,7 @@ Thanks goes to these wonderful people:
98
98
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/mdbooth"><img src="https://avatars.githubusercontent.com/u/1318691?v=4" width="100px;" alt=""/><br /><sub><b>Matthew Booth</b></sub></a><br /><a href="#bug-mdbooth" title="Bug reports">🐛</a></td>
99
99
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/Wiedzmin89"><img src="https://avatars.githubusercontent.com/u/61706855?v=4" width="100px;" alt=""/><br /><sub><b>Wiedzmin89</b></sub></a><br /><a href="#ideas-Wiedzmin89" title="Ideas & Planning">🤔</a></td>
100
100
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/grodingo"><img src="https://avatars.githubusercontent.com/u/244184972?v=4" width="100px;" alt=""/><br /><sub><b>Virgile</b></sub></a><br /><a href="#bug-grodingo" title="Bug reports">🐛</a></td>
101
- </tr>
101
+ </tr><br />
102
102
  <tr>
103
103
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/finnvyrn"><img src="https://avatars.githubusercontent.com/u/90801772?v=4" width="100px;" alt=""/><br /><sub><b>Finn</b></sub></a><br /><a href="#ideas-finnvyrn" title="Ideas & Planning">🤔</a></td>
104
104
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/ywh555hhh"><img src="https://avatars.githubusercontent.com/u/121592812?v=4" width="100px;" alt=""/><br /><sub><b>Wayne E</b></sub></a><br /><a href="#bug-ywh555hhh" title="Bug reports">🐛</a></td>
@@ -107,7 +107,7 @@ Thanks goes to these wonderful people:
107
107
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/pvtri96"><img src="https://avatars.githubusercontent.com/u/28696888?v=4" width="100px;" alt=""/><br /><sub><b>Tri Van Pham</b></sub></a><br /><a href="#bug-pvtri96" title="Bug reports">🐛</a></td>
108
108
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/BingWuJ"><img src="https://avatars.githubusercontent.com/u/117666511?v=4" width="100px;" alt=""/><br /><sub><b>BingWuJ</b></sub></a><br /><a href="#bug-BingWuJ" title="Bug reports">🐛</a></td>
109
109
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/tackleberrybey"><img src="https://avatars.githubusercontent.com/u/156954032?v=4" width="100px;" alt=""/><br /><sub><b>tackleberrybey</b></sub></a><br /><a href="#bug-tackleberrybey" title="Bug reports">🐛</a></td>
110
- </tr>
110
+ </tr><br />
111
111
  <tr>
112
112
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/TaterDoge"><img src="https://avatars.githubusercontent.com/u/121467933?v=4" width="100px;" alt=""/><br /><sub><b>Mariann Abshire</b></sub></a><br /><a href="#bug-TaterDoge" title="Bug reports">🐛</a></td>
113
113
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/ProbabilityEngineer"><img src="https://avatars.githubusercontent.com/u/38498804?v=4" width="100px;" alt=""/><br /><sub><b>ProbabilityEngineer</b></sub></a><br /><a href="#ideas-ProbabilityEngineer" title="Ideas & Planning">🤔</a></td>
@@ -116,7 +116,7 @@ Thanks goes to these wonderful people:
116
116
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/rstacruz"><img src="https://avatars.githubusercontent.com/u/74385?v=4" width="100px;" alt=""/><br /><sub><b>Rico Sta. Cruz</b></sub></a><br /><a href="#ideas-rstacruz" title="Ideas & Planning">🤔</a></td>
117
117
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/acidnik"><img src="https://avatars.githubusercontent.com/u/1227955?v=4" width="100px;" alt=""/><br /><sub><b>Nikita Bilous</b></sub></a><br /><a href="#bug-acidnik" title="Bug reports">🐛</a></td>
118
118
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/av1155"><img src="https://avatars.githubusercontent.com/u/117413846?v=4" width="100px;" alt=""/><br /><sub><b>Andrea Arturo Venti Fuentes</b></sub></a><br /><a href="#bug-av1155" title="Bug reports">🐛</a></td>
119
- </tr>
119
+ </tr><br />
120
120
  <tr>
121
121
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/dacec354"><img src="https://avatars.githubusercontent.com/u/90093629?v=4" width="100px;" alt=""/><br /><sub><b>dacec354</b></sub></a><br /><a href="#bug-dacec354" title="Bug reports">🐛</a></td>
122
122
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/xuli500177"><img src="https://avatars.githubusercontent.com/u/62830942?v=4" width="100px;" alt=""/><br /><sub><b>Xu Yili</b></sub></a><br /><a href="#bug-xuli500177" title="Bug reports">🐛</a></td>
@@ -125,7 +125,7 @@ Thanks goes to these wonderful people:
125
125
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/asolopovas"><img src="https://avatars.githubusercontent.com/u/6893216?v=4" width="100px;" alt=""/><br /><sub><b>Andrius Solopovas</b></sub></a><br /><a href="#bug-asolopovas" title="Bug reports">🐛</a></td>
126
126
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/GrahamJenkins"><img src="https://avatars.githubusercontent.com/u/6607975?v=4" width="100px;" alt=""/><br /><sub><b>Graham Jenkins</b></sub></a><br /><a href="#bug-GrahamJenkins" title="Bug reports">🐛</a></td>
127
127
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/wings1848"><img src="https://avatars.githubusercontent.com/u/120104016?v=4" width="100px;" alt=""/><br /><sub><b>Wings Butterfly</b></sub></a><br /><a href="#bug-wings1848" title="Bug reports">🐛</a></td>
128
- </tr>
128
+ </tr><br />
129
129
  <tr>
130
130
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/JohannesKlauss"><img src="https://avatars.githubusercontent.com/u/6214415?v=4" width="100px;" alt=""/><br /><sub><b>Johannes Klauss</b></sub></a><br /><a href="#ideas-JohannesKlauss" title="Ideas & Planning">🤔</a></td>
131
131
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/tan-yong-sheng"><img src="https://avatars.githubusercontent.com/u/64836390?v=4" width="100px;" alt=""/><br /><sub><b>Tan Yong Sheng</b></sub></a><br /><a href="#ideas-tan-yong-sheng" title="Ideas & Planning">🤔</a></td>
@@ -134,7 +134,7 @@ Thanks goes to these wonderful people:
134
134
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/Viterkim"><img src="https://avatars.githubusercontent.com/u/17838985?v=4" width="100px;" alt=""/><br /><sub><b>Viktor</b></sub></a><br /><a href="#bug-Viterkim" title="Bug reports">🐛</a></td>
135
135
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/ortonomy"><img src="https://avatars.githubusercontent.com/u/6688676?v=4" width="100px;" alt=""/><br /><sub><b>Gregory Orton</b></sub></a><br /><a href="#bug-ortonomy" title="Bug reports">🐛</a></td>
136
136
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/jimallen"><img src="https://avatars.githubusercontent.com/u/868773?v=4" width="100px;" alt=""/><br /><sub><b>Jim Allen</b></sub></a><br /><a href="#bug-jimallen" title="Bug reports">🐛</a></td>
137
- </tr>
137
+ </tr><br />
138
138
  <tr>
139
139
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/quantfiction"><img src="https://avatars.githubusercontent.com/u/49965454?v=4" width="100px;" alt=""/><br /><sub><b>quantfiction</b></sub></a><br /><a href="#bug-quantfiction" title="Bug reports">🐛</a></td>
140
140
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/Artawower"><img src="https://avatars.githubusercontent.com/u/16963833?v=4" width="100px;" alt=""/><br /><sub><b>Art</b></sub></a><br /><a href="#bug-Artawower" title="Bug reports">🐛</a></td>
@@ -143,10 +143,13 @@ Thanks goes to these wonderful people:
143
143
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/stark-bit"><img src="https://avatars.githubusercontent.com/u/44064758?v=4" width="100px;" alt=""/><br /><sub><b>Rei Starks</b></sub></a><br /><a href="#bug-stark-bit" title="Bug reports">🐛</a></td>
144
144
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/robertoecf"><img src="https://avatars.githubusercontent.com/u/54923863?v=4" width="100px;" alt=""/><br /><sub><b>Roberto Freitas</b></sub></a><br /><a href="#bug-robertoecf" title="Bug reports">🐛</a></td>
145
145
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/tmttodd"><img src="https://avatars.githubusercontent.com/u/160077416?v=4" width="100px;" alt=""/><br /><sub><b>tmttodd</b></sub></a><br /><a href="#bug-tmttodd" title="Bug reports">🐛</a></td>
146
- </tr>
146
+ </tr><br />
147
147
  <tr>
148
148
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/smaharnav"><img src="https://avatars.githubusercontent.com/u/263328627?v=4" width="100px;" alt=""/><br /><sub><b>smaharnav</b></sub></a><br /><a href="#bug-smaharnav" title="Bug reports">🐛</a></td>
149
149
  <td align="center" valign="top" width="14.28%"><a href="https://github.com/MzaGuille"><img src="https://avatars.githubusercontent.com/u/151482698?v=4" width="100px;" alt=""/><br /><sub><b>Mza.Guille</b></sub></a><br /><a href="#bug-MzaGuille" title="Bug reports">🐛</a></td>
150
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/cmptr"><img src="https://avatars.githubusercontent.com/u/32746889?v=4" width="100px;" alt=""/><br /><sub><b>Aaron Bell</b></sub></a><br /><a href="#code-cmptr" title="Code">💻</a> <a href="#bug-cmptr" title="Bug reports">🐛</a></td>
151
+ <td align="center" valign="top" width="14.28%"><a href="http://jasonrimmer.com/"><img src="https://avatars.githubusercontent.com/u/629?v=4" width="100px;" alt=""/><br /><sub><b>Jason Rimmer</b></sub></a><br /><a href="#bug-jrimmer" title="Bug reports">🐛</a></td>
152
+ <td align="center" valign="top" width="14.28%"><a href="https://github.com/moj02090"><img src="https://avatars.githubusercontent.com/u/57255166?v=4" width="100px;" alt=""/><br /><sub><b>moj02090</b></sub></a><br /><a href="#code-moj02090" title="Code">💻</a></td>
150
153
  </tr>
151
154
  </tbody>
152
155
  </table>
@@ -10,6 +10,29 @@
10
10
  * promise's late rejection (an unhandled rejection if the timer won first), and
11
11
  * `withinRemaining` never cleared its timer.
12
12
  */
13
+ /**
14
+ * Combine multiple abort signals into one that aborts when ANY of them does.
15
+ * Returns the single signal unchanged when only one is live, and `undefined`
16
+ * when none are — so callers can pass it straight through. Used so a tool honors
17
+ * both its tool-call `signal` positional and the turn-wired `ctx.signal` (Escape),
18
+ * and to fold in a wall-clock ceiling via `AbortSignal.timeout`.
19
+ */
20
+ export function combineAbortSignals(...signals) {
21
+ const live = signals.filter((s) => s !== undefined);
22
+ if (live.length <= 1)
23
+ return live[0];
24
+ if (typeof AbortSignal.any === "function")
25
+ return AbortSignal.any(live);
26
+ const controller = new AbortController();
27
+ for (const s of live) {
28
+ if (s.aborted) {
29
+ controller.abort(s.reason);
30
+ break;
31
+ }
32
+ s.addEventListener("abort", () => controller.abort(s.reason), { once: true });
33
+ }
34
+ return controller.signal;
35
+ }
13
36
  export function withDeadline(promise, options) {
14
37
  const onTimeout = options.onTimeout ?? "reject";
15
38
  const onReject = options.onReject ?? "propagate";
@@ -56,6 +56,7 @@ const _installerRequire = createRequire(import.meta.url);
56
56
  import { createGunzip } from "node:zlib";
57
57
  import { isTestMode } from "../env-utils.js";
58
58
  import { getGlobalPiLensDir } from "../file-utils.js";
59
+ import { allAvailableGlobalBinDirs, installArgs, pmBinary, resolveNodePackageManager, } from "../package-manager.js";
59
60
  // Global installation directory for pi-lens tools
60
61
  const TOOLS_DIR = path.join(getGlobalPiLensDir(), "tools");
61
62
  // Directory for GitHub-downloaded binaries
@@ -1639,19 +1640,10 @@ async function getNpmGlobalBinCandidates() {
1639
1640
  else {
1640
1641
  add(path.join(os.homedir(), ".npm-global", "bin"));
1641
1642
  }
1642
- const pm = process.platform === "win32" ? "npm.cmd" : "npm";
1643
- const prefix = await new Promise((resolve) => {
1644
- const proc = spawn(pm, ["config", "get", "prefix"], {
1645
- stdio: ["ignore", "pipe", "pipe"],
1646
- shell: process.platform === "win32",
1647
- });
1648
- let stdout = "";
1649
- proc.stdout?.on("data", (data) => (stdout += data));
1650
- proc.on("exit", (code) => resolve(code === 0 ? stdout.trim() : ""));
1651
- proc.on("error", () => resolve(""));
1652
- });
1653
- if (prefix) {
1654
- add(process.platform === "win32" ? prefix : path.join(prefix, "bin"));
1643
+ // Global bin dirs for every installed manager (npm/pnpm/yarn/bun) a tool
1644
+ // may have been installed globally via any of them.
1645
+ for (const dir of await allAvailableGlobalBinDirs()) {
1646
+ add(dir);
1655
1647
  }
1656
1648
  return dirs;
1657
1649
  }
@@ -2227,21 +2219,19 @@ async function installNpmTool(packageName, binaryName) {
2227
2219
  catch {
2228
2220
  await fs.writeFile(packageJsonPath, JSON.stringify({ name: "pi-lens-tools", version: "1.0.0" }, null, 2));
2229
2221
  }
2230
- // Install via npm or bun (use .cmd on Windows)
2222
+ // Resolve the package manager for the tools dir and build install args.
2231
2223
  const isWindows = process.platform === "win32";
2232
- let pm = isWindows ? "npm.cmd" : "npm";
2233
- if (process.env.BUN_INSTALL) {
2234
- pm = isWindows ? "bun.exe" : "bun";
2235
- }
2224
+ const pm = await resolveNodePackageManager(TOOLS_DIR);
2225
+ const pmCommand = pmBinary(pm);
2236
2226
  // Use --ignore-scripts unless the package explicitly needs postinstall
2237
2227
  // (e.g. biome downloads a platform-specific native binary via postinstall).
2238
2228
  const needsScripts = NEEDS_POSTINSTALL.has(packageName);
2239
- const baseInstallArgs = needsScripts
2240
- ? ["install", packageName]
2241
- : ["install", "--ignore-scripts", packageName];
2229
+ const baseInstallArgs = installArgs(pm, packageName, {
2230
+ ignoreScripts: !needsScripts,
2231
+ });
2242
2232
  const INSTALL_TIMEOUT_MS = 120_000;
2243
2233
  const runInstallAttempt = async (args) => new Promise((resolve) => {
2244
- const proc = spawn(pm, args, {
2234
+ const proc = spawn(pmCommand, args, {
2245
2235
  cwd: TOOLS_DIR,
2246
2236
  stdio: ["ignore", "pipe", "pipe"],
2247
2237
  shell: isWindows, // Required for .cmd files on Windows
@@ -2265,13 +2255,14 @@ async function installNpmTool(packageName, binaryName) {
2265
2255
  });
2266
2256
  });
2267
2257
  let outcome = await runInstallAttempt(baseInstallArgs);
2268
- const isNpm = pm === "npm" || pm === "npm.cmd";
2258
+ // --legacy-peer-deps is npm-only; retry just npm's ERESOLVE failures.
2269
2259
  const erResolve = outcome.ok === false &&
2270
2260
  /npm\s+error\s+ERESOLVE|\bERESOLVE\b|could not resolve/i.test(outcome.stderr);
2271
- if (isNpm && erResolve) {
2272
- const retryArgs = needsScripts
2273
- ? ["install", "--legacy-peer-deps", packageName]
2274
- : ["install", "--ignore-scripts", "--legacy-peer-deps", packageName];
2261
+ if (pm === "npm" && erResolve) {
2262
+ const retryArgs = installArgs(pm, packageName, {
2263
+ ignoreScripts: !needsScripts,
2264
+ legacyPeerDeps: true,
2265
+ });
2275
2266
  logSessionStart(`auto-install npm ${packageName}: retry with --legacy-peer-deps after ERESOLVE`);
2276
2267
  outcome = await runInstallAttempt(retryArgs);
2277
2268
  }
@@ -6,9 +6,11 @@
6
6
  * PI_LENS_MAX_LOG_SIZE_MB - Max size before rotation (default: 10)
7
7
  *
8
8
  * Scope:
9
- * - ~/.pi-lens/*.log (latency.log, sessionstart.log, tree-sitter.log)
9
+ * - ~/.pi-lens/*.log (all 8 global logs — see MANAGED_LOG_FILES: latency,
10
+ * sessionstart, tree-sitter, cascade, read-guard, actionable-warnings,
11
+ * ast-grep-tools, dead-code)
10
12
  * - ~/.pi-lens/logs/*.jsonl (daily diagnostic logs)
11
- * - ~/.pi-lens/*.log.* (rotated backups)
13
+ * - ~/.pi-lens/<name>.<timestamp>.log (rotated backups, and legacy .log.<ts>)
12
14
  *
13
15
  * Excluded (intentionally NOT cleaned - project-scoped or persistent):
14
16
  * - <project-data>/worklog.jsonl - Agent fixable diagnostics
@@ -25,6 +27,30 @@ import * as path from "node:path";
25
27
  import { getGlobalPiLensDir } from "./file-utils.js";
26
28
  const LOG_DIR = getGlobalPiLensDir();
27
29
  const LOGS_SUBDIR = path.join(LOG_DIR, "logs");
30
+ /**
31
+ * Every global `.log` pi-lens writes under ~/.pi-lens. Kept as one list so
32
+ * rotation and the storage summary can't drift out of sync — that drift is
33
+ * exactly what left actionable-warnings/ast-grep-tools/dead-code unrotated and
34
+ * growing unbounded. New global log file? Add it here.
35
+ */
36
+ export const MANAGED_LOG_FILES = [
37
+ "latency.log",
38
+ "sessionstart.log",
39
+ "tree-sitter.log",
40
+ "cascade.log",
41
+ "read-guard.log",
42
+ "actionable-warnings.log",
43
+ "ast-grep-tools.log",
44
+ "dead-code.log",
45
+ ];
46
+ /**
47
+ * Matches a rotated backup, never an active log. Rotation writes
48
+ * `name.<ISO-timestamp>.log` (timestamp before the extension); an older shape
49
+ * was `name.log.<timestamp>`. Match both so every backup — whichever version
50
+ * produced it — is reaped by the retention sweep. The active `name.log` has no
51
+ * timestamp segment and no trailing suffix after `.log`, so it never matches.
52
+ */
53
+ export const ROTATED_BACKUP_RE = /(\.\d{4}-\d{2}-\d{2}T.*\.log|\.log\..+)$/;
28
54
  function getConfig() {
29
55
  return {
30
56
  retentionDays: Math.max(1, Number.parseInt(process.env.PI_LENS_LOG_RETENTION_DAYS ?? "7", 10) || 7),
@@ -123,17 +149,15 @@ export function runLogCleanup(dbg) {
123
149
  // Cleanup old daily diagnostic logs (*.jsonl)
124
150
  const dailyLogs = cleanupOldLogs(LOGS_SUBDIR, /\.jsonl$/, config.retentionDays);
125
151
  results.cleaned += dailyLogs.deleted.length;
126
- // Cleanup old rotated log backups (*.log.*)
127
- const rotatedLogs = cleanupOldLogs(LOG_DIR, /\.log\./, config.retentionDays);
152
+ // Cleanup old rotated log backups. This sweep runs unconditionally on every
153
+ // session start, so correcting the pattern self-heals any pre-existing
154
+ // backlog on the next launch — no separate migration needed. (The prior
155
+ // `/\.log\./` only matched the legacy `name.log.<ts>` shape, never the
156
+ // current `name.<ts>.log`, so backups accumulated indefinitely.)
157
+ const rotatedLogs = cleanupOldLogs(LOG_DIR, ROTATED_BACKUP_RE, config.retentionDays);
128
158
  results.cleaned += rotatedLogs.deleted.length;
129
159
  // Check main logs for rotation
130
- const mainLogs = [
131
- path.join(LOG_DIR, "latency.log"),
132
- path.join(LOG_DIR, "sessionstart.log"),
133
- path.join(LOG_DIR, "tree-sitter.log"),
134
- path.join(LOG_DIR, "cascade.log"),
135
- path.join(LOG_DIR, "read-guard.log"),
136
- ];
160
+ const mainLogs = MANAGED_LOG_FILES.map((name) => path.join(LOG_DIR, name));
137
161
  for (const logFile of mainLogs) {
138
162
  const rotation = rotateLogIfNeeded(logFile, config.maxSizeMB);
139
163
  if (rotation.rotated) {
@@ -171,8 +195,7 @@ export function getLogStorageSummary() {
171
195
  const files = [];
172
196
  let totalMB = 0;
173
197
  // Main logs
174
- const mainLogs = ["latency.log", "sessionstart.log", "tree-sitter.log"];
175
- for (const name of mainLogs) {
198
+ for (const name of MANAGED_LOG_FILES) {
176
199
  const filePath = path.join(LOG_DIR, name);
177
200
  if (fs.existsSync(filePath)) {
178
201
  const sizeMB = getFileSizeMB(filePath);
@@ -323,6 +323,7 @@ export function applyDynamicCapabilities(state) {
323
323
  state.workspaceDiagnosticsSupport = {
324
324
  advertised: true,
325
325
  mode: "pull",
326
+ workspaceDiagnostics: registeredMethods.has("workspace/diagnostic"),
326
327
  diagnosticProviderKind: "dynamic",
327
328
  };
328
329
  }
@@ -332,6 +333,7 @@ export function applyDynamicCapabilities(state) {
332
333
  state.workspaceDiagnosticsSupport = {
333
334
  advertised: false,
334
335
  mode: "push-only",
336
+ workspaceDiagnostics: false,
335
337
  diagnosticProviderKind: "none",
336
338
  };
337
339
  }
@@ -506,6 +508,39 @@ async function clientRequestPullDiagnostics(state, filePath, budgetMs = PULL_REQ
506
508
  return { status: "unavailable" };
507
509
  }
508
510
  }
511
+ /**
512
+ * One project-wide `workspace/diagnostic` pull — a single request that returns
513
+ * diagnostics for every document the server knows, instead of opening N files.
514
+ * Returns per-file reports, or `undefined` on unsupported/dead/timeout/malformed
515
+ * (caller falls back to the per-file path). `unchanged`-kind items carry no
516
+ * diagnostics and are skipped, so a file absent from the result is "clean".
517
+ */
518
+ export async function clientRequestWorkspaceDiagnostics(state, budgetMs) {
519
+ if (!isClientAlive(state))
520
+ return undefined;
521
+ if (!state.workspaceDiagnosticsSupport.workspaceDiagnostics)
522
+ return undefined;
523
+ try {
524
+ const report = await withTimeout(safeSendRequest(state.connection, "workspace/diagnostic", { previousResultIds: [] }), Math.max(1, budgetMs));
525
+ if (!report || !Array.isArray(report.items))
526
+ return undefined;
527
+ const out = [];
528
+ for (const item of report.items) {
529
+ // Only "full" reports carry items; "unchanged" means "same as last pull"
530
+ // (none, since previousResultIds is empty on this one-shot request).
531
+ if (!item?.uri || item.kind !== "full")
532
+ continue;
533
+ out.push({
534
+ filePath: uriToPath(item.uri),
535
+ diagnostics: normalizeLspDiagnostics(item.items ?? []),
536
+ });
537
+ }
538
+ return out;
539
+ }
540
+ catch {
541
+ return undefined;
542
+ }
543
+ }
509
544
  export async function clientWaitForDiagnostics(state, filePath, timeoutMs, options = {}) {
510
545
  const normalizedPath = normalizeMapKey(filePath);
511
546
  const minVersion = options.minVersion;
@@ -1079,6 +1114,9 @@ export async function createLSPClient(options) {
1079
1114
  getWorkspaceDiagnosticsSupport() {
1080
1115
  return state.workspaceDiagnosticsSupport;
1081
1116
  },
1117
+ requestWorkspaceDiagnostics(budgetMs) {
1118
+ return clientRequestWorkspaceDiagnostics(state, budgetMs);
1119
+ },
1082
1120
  getOperationSupport() {
1083
1121
  return state.operationSupport;
1084
1122
  },
@@ -1310,6 +1348,7 @@ function detectWorkspaceDiagnosticsSupport(initResult) {
1310
1348
  return {
1311
1349
  advertised: false,
1312
1350
  mode: "push-only",
1351
+ workspaceDiagnostics: false,
1313
1352
  diagnosticProviderKind: "none",
1314
1353
  };
1315
1354
  }
@@ -1317,6 +1356,8 @@ function detectWorkspaceDiagnosticsSupport(initResult) {
1317
1356
  return {
1318
1357
  advertised: diagnosticProvider,
1319
1358
  mode: diagnosticProvider ? "pull" : "push-only",
1359
+ // The boolean form of diagnosticProvider only signals document pull.
1360
+ workspaceDiagnostics: false,
1320
1361
  diagnosticProviderKind: "boolean",
1321
1362
  };
1322
1363
  }
@@ -1324,12 +1365,15 @@ function detectWorkspaceDiagnosticsSupport(initResult) {
1324
1365
  return {
1325
1366
  advertised: true,
1326
1367
  mode: "pull",
1368
+ workspaceDiagnostics: diagnosticProvider
1369
+ .workspaceDiagnostics === true,
1327
1370
  diagnosticProviderKind: "object",
1328
1371
  };
1329
1372
  }
1330
1373
  return {
1331
1374
  advertised: false,
1332
1375
  mode: "push-only",
1376
+ workspaceDiagnostics: false,
1333
1377
  diagnosticProviderKind: typeof diagnosticProvider,
1334
1378
  };
1335
1379
  }