pi-lens 2.0.9 → 2.0.25

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 (73) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +74 -48
  3. package/clients/ast-grep-client.js +340 -0
  4. package/clients/ast-grep-client.test.js +141 -0
  5. package/clients/ast-grep-client.ts +132 -362
  6. package/clients/ast-grep-parser.js +84 -0
  7. package/clients/ast-grep-parser.ts +130 -0
  8. package/clients/ast-grep-rule-manager.js +81 -0
  9. package/clients/ast-grep-rule-manager.ts +95 -0
  10. package/clients/biome-client.js +323 -0
  11. package/clients/biome-client.test.js +142 -0
  12. package/clients/biome-client.ts +5 -3
  13. package/clients/complexity-client.js +633 -0
  14. package/clients/complexity-client.test.js +234 -0
  15. package/clients/complexity-client.ts +1 -1
  16. package/clients/dependency-checker.js +295 -0
  17. package/clients/dependency-checker.test.js +60 -0
  18. package/clients/dependency-checker.ts +1 -1
  19. package/clients/go-client.js +212 -0
  20. package/clients/go-client.test.js +127 -0
  21. package/clients/go-client.ts +3 -1
  22. package/clients/interviewer.js +201 -0
  23. package/clients/interviewer.ts +232 -0
  24. package/clients/jscpd-client.js +145 -0
  25. package/clients/jscpd-client.test.js +127 -0
  26. package/clients/jscpd-client.ts +3 -2
  27. package/clients/knip-client.js +206 -0
  28. package/clients/knip-client.test.js +112 -0
  29. package/clients/knip-client.ts +3 -2
  30. package/clients/metrics-client.js +264 -0
  31. package/clients/metrics-client.test.js +141 -0
  32. package/clients/metrics-client.ts +66 -15
  33. package/clients/ruff-client.js +255 -0
  34. package/clients/ruff-client.test.js +132 -0
  35. package/clients/ruff-client.ts +9 -4
  36. package/clients/rust-client.js +212 -0
  37. package/clients/rust-client.test.js +108 -0
  38. package/clients/rust-client.ts +1 -1
  39. package/clients/scan-architectural-debt.js +130 -0
  40. package/clients/scan-architectural-debt.ts +135 -0
  41. package/clients/subprocess-client.js +101 -0
  42. package/clients/subprocess-client.ts +3 -2
  43. package/clients/test-runner-client.js +527 -0
  44. package/clients/test-runner-client.test.js +192 -0
  45. package/clients/test-runner-client.ts +69 -125
  46. package/clients/test-utils.js +27 -0
  47. package/clients/test-utils.ts +3 -5
  48. package/clients/todo-scanner.js +200 -0
  49. package/clients/todo-scanner.test.js +301 -0
  50. package/clients/type-coverage-client.js +131 -0
  51. package/clients/type-coverage-client.test.js +105 -0
  52. package/clients/type-coverage-client.ts +1 -1
  53. package/clients/types.js +11 -0
  54. package/clients/typescript-client.js +463 -0
  55. package/clients/typescript-client.test.js +105 -0
  56. package/clients/typescript-client.ts +85 -101
  57. package/index.ts +342 -236
  58. package/package.json +2 -2
  59. package/rules/ast-grep-rules/rules/empty-catch.yml +1 -0
  60. package/rules/ast-grep-rules/rules/jsx-boolean-short-circuit.yml +20 -0
  61. package/rules/ast-grep-rules/rules/jwt-no-verify.yml +14 -0
  62. package/rules/ast-grep-rules/rules/large-class.yml +1 -0
  63. package/rules/ast-grep-rules/rules/long-method.yml +8 -2
  64. package/rules/ast-grep-rules/rules/long-parameter-list.yml +1 -0
  65. package/rules/ast-grep-rules/rules/missed-concurrency.yml +25 -0
  66. package/rules/ast-grep-rules/rules/no-await-in-loop.yml +1 -0
  67. package/rules/ast-grep-rules/rules/no-await-in-promise-all.yml +1 -0
  68. package/rules/ast-grep-rules/rules/no-console-log.yml +1 -0
  69. package/rules/ast-grep-rules/rules/no-eval.yml +1 -0
  70. package/rules/ast-grep-rules/rules/no-hardcoded-secrets.yml +1 -0
  71. package/rules/ast-grep-rules/rules/redundant-state.yml +16 -0
  72. package/rules/ast-grep-rules/rules/toctou.yml +112 -0
  73. package/rules/ast-grep-rules/rules/weak-rsa-key.yml +15 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,110 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
+ ## [2.0.24] - 2026-03-26
6
+
7
+ ### Changed
8
+ - **Simplified `/lens-booboo-refactor` confirmation flow**: Post-change report instead of pre-change gate. Agent implements first, then shows what was changed (git diff + metrics delta). User reviews and can request refinements via chat. No more temp files or dry-run diffs.
9
+ - **Confirmation screen**: "✅ Looks good — move to next offender" / "💬 Request changes" (chat textarea). Diff display is optional.
10
+
11
+ ## [2.0.23] - 2026-03-26
12
+
13
+ ### Changed
14
+ - **Extracted interviewer and scan modules from `index.ts`**: `index.ts` reduced by 460 lines.
15
+ - `clients/interviewer.ts` — all browser interview infrastructure (HTML generation, HTTP server, browser launch, option selection, diff confirmation screen)
16
+ - `clients/scan-architectural-debt.ts` — shared scanning utilities (`scanSkipViolations`, `scanComplexityMetrics`, `scoreFiles`, `extractCodeSnippet`)
17
+ - **`/lens-booboo-refactor`** now uses imported scan functions instead of duplicated inline code.
18
+
19
+ ## [2.0.22] - 2026-03-26
20
+
21
+ ### Added
22
+ - **Impact metrics in interview options**: Each option now supports an `impact` object (`linesReduced`, `miProjection`, `cognitiveProjection`) rendered as colored badges in the browser form. Agent estimates impact when presenting refactoring options.
23
+ - **Iterative confirmation loop**: Confirmation screen now includes "🔄 Describe a different approach" option with free-text textarea. Agent regenerates plan+diff based on feedback, re-opens confirmation. Repeat until user confirms or cancels.
24
+ - **Auto-close on confirm**: Browser tab closes automatically after user submits.
25
+
26
+ ## [2.0.21] - 2026-03-26
27
+
28
+ ### Added
29
+ - **Two-step confirmation for `/lens-booboo-refactor`**: Agent implements changes, then calls `interviewer` with `confirmationMode=true` to show plan (markdown) + unified diff (green/red line coloring) + line counts at the top. User can Confirm, Cancel, or describe a different approach.
30
+ - **Plan + diff confirmation screen**: Plan rendered as styled markdown, diff rendered with syntax-colored `+`/`-` lines. Line counts (`+N / −N`) shown in diff header.
31
+
32
+ ## [2.0.20] - 2026-03-26
33
+
34
+ ### Added
35
+ - **Impact metrics in interview options**: Structured `impact` field per option with `linesReduced`, `miProjection`, `cognitiveProjection`. Rendered as colored badges (green for lines reduced, blue for metric projections) inside each option card.
36
+
37
+ ## [2.0.19] - 2026-03-26
38
+
39
+ ### Changed
40
+ - **`/lens-booboo-fix` jscpd filter**: Only within-file duplicates shown in actionable section. Cross-file duplicates are architectural — shown in skip section only.
41
+ - **AI slop filter tightened**: Require 2+ signals per file (was 1+). Single-issue flags on small files are noise — skip them.
42
+
43
+ ## [2.0.18] - 2026-03-26
44
+
45
+ ### Fixed
46
+ - **`/lens-booboo-fix` max iterations**: Session file auto-deletes when hitting max iterations. Previously blocked with a manual "delete .pi-lens/fix-session.json" message.
47
+
48
+ ## [2.0.17] - 2026-03-26
49
+
50
+ ### Changed
51
+ - **Agent-driven option generation**: `/lens-booboo-refactor` no longer hardcodes refactoring options per violation type. The command scans and presents the problem + code to the agent; the agent analyzes the actual code and generates 3-5 contextual options with rationale and impact estimates. Calls the `interviewer` tool to present them.
52
+ - **`interviewer` tool**: Generic, reusable browser-based interview mechanism. Accepts `question`, `options` (with `value`, `label`, `context`, `recommended`, `impact`), and `confirmationMode`. Zero dependencies — Node's built-in `http` module + platform CLI `open`/`start`/`xdg-open`.
53
+
54
+ ## [2.0.16] - 2026-03-26
55
+
56
+ ### Added
57
+ - **`/lens-booboo-refactor`**: Interactive architectural refactor session. Scans for worst offender by combined debt score (ast-grep skip violations + complexity metrics). Opens a browser interview with the problem, code context, and AI-generated options. Steers the agent to propose a plan and wait for user confirmation before making changes.
58
+
59
+ ### Changed
60
+ - **Inline tool_result suppresses skip-category rules**: `long-method`, `large-class`, `long-parameter-list`, `no-shadow`, `no-as-any`, `no-non-null-assertion`, `no-star-imports` no longer show as hard stops in real-time feedback. They are architectural — handled by `/lens-booboo-refactor` instead.
61
+
62
+ ## [2.0.15] - 2026-03-26
63
+
64
+ ### Removed
65
+ - **Complexity metrics from real-time feedback**: MI, cognitive complexity, nesting depth, try/catch counts, and entropy scores removed from tool_result output. These were always noise — the agent never acted on "MI dropped to 5.6" mid-task. Metrics still available via `/lens-metrics` and `/lens-booboo`.
66
+ - **Session summary injection**: The `[Session Start]` block (TODOs, dead code, jscpd, type-coverage) is no longer injected into the first tool result. Scans still run for caching purposes (exports, clones, baselines). Data surfaced on-demand via explicit commands.
67
+ - **`/lens-todos`**: Removed (covered by `/lens-booboo`).
68
+ - **`/lens-dead-code`**: Removed (covered by `/lens-booboo`).
69
+ - **`/lens-deps`**: Removed — circular dep scan added to `/lens-booboo` as Part 8.
70
+
71
+ ### Changed
72
+ - **Hardened stop signals**: New violations (ast-grep, Biome, jscpd, duplicate exports) now all use `🔴 STOP` framing. The agent is instructed to fix these before continuing.
73
+ - **`/lens-booboo` now includes circular dependencies**: Added as Part 8 (after type coverage) using `depChecker.scanProject`.
74
+
75
+ ## [2.0.14] - 2026-03-26
76
+
77
+ ### Fixed
78
+ - **`/lens-booboo-fix` excludes `.js` compiled output**: Detects `tsconfig.json` and excludes `*.js` from jscpd, ast-grep, and complexity scans. Prevents double-counting of the same code in `.ts` and `.js` forms.
79
+ - **`raw-strings` rule added to skip list**: 230 false positives in CLI/tooling codebases.
80
+ - **`typescript-client.ts` duplication**: Extracted `resolvePosition()`, `resolveTree()`, and `toLocations()` helpers, deduplicating 6+ LSP methods.
81
+ - **All clients**: `console.log` → `console.error` in verbose loggers (stderr for debug, stdout for data).
82
+
83
+ ## [2.0.13] - 2026-03-26
84
+
85
+ ### Removed
86
+ - **`raw-strings` ast-grep rule**: Not an AI-specific pattern. Humans write magic strings too. Biome handles style. Generated 230 false positives on first real run.
87
+
88
+ ## [2.0.12] - 2026-03-26
89
+
90
+ ### Fixed
91
+ - **`/lens-booboo-fix` sequential scan order**: Reordered to Biome/Ruff → jscpd (duplicates) → knip (dead code) → ast-grep → AI slop → remaining Biome. Duplicates should be fixed before violations (fixing one fixes both). Dead code should be deleted before fixing violations in it.
92
+
93
+ ### Changed
94
+ - **Remaining Biome section rephrased**: "These couldn't be auto-fixed even with `--unsafe` — fix each manually."
95
+
96
+ ## [2.0.11] - 2026-03-26
97
+
98
+ ### Added
99
+ - **Circular dependency scan to `/lens-booboo`**: Added as Part 8, using `depChecker.scanProject()` to detect circular chains across the codebase.
100
+
101
+ ### Removed
102
+ - **`/lens-todos`**, **`/lens-dead-code`**, **`/lens-deps`**: Removed standalone commands — all covered by `/lens-booboo`.
103
+
104
+ ## [2.0.10] - 2026-03-26
105
+
106
+ ### Changed
107
+ - **Session summary injection removed**: The `[Session Start]` block is no longer injected into the first tool result. Scans still run silently for caching (exports for duplicate detection, clones for jscpd, complexity baselines for deltas).
108
+
5
109
  ## [2.0.1] - 2026-03-25
6
110
 
7
111
  ### Fixed
package/README.md CHANGED
@@ -15,42 +15,35 @@ Real-time code quality feedback for [pi](https://github.com/mariozechner/pi-codi
15
15
  | **Biome** | Lint + format for JS/TS/JSX/TSX/CSS/JSON. Auto-fix disabled by default, use `/lens-format` to apply |
16
16
  | **Ruff** | Lint + format for Python. Auto-fixes on every write by default |
17
17
  | **Test Runner** | Runs corresponding test file when you edit source code (vitest, jest, pytest). Silent if no test file exists. |
18
- | **Complexity Metrics** | AST-based analysis: Maintainability Index, Cyclomatic/Cognitive Complexity, Halstead Volume, nesting depth, function length, code entropy. AI slop indicators: emoji comments, try/catch density, over-abstraction, long parameter lists. |
19
18
  | **jscpd** | Code duplication detection. Warns when editing a file that has duplicates with other files in the project. |
20
19
  | **Duplicate Exports** | Detects when you redefine a function that already exists elsewhere in the codebase. |
21
20
 
22
- ### Delta-mode feedback (new in 2.0)
21
+ ### Delta-mode feedback
23
22
 
24
- ast-grep and Biome run in **delta mode** — only violations *introduced by the current edit* are shown. Pre-existing issues are silent. Fixed violations are acknowledged.
23
+ ast-grep and Biome run in **delta mode** — only violations *introduced by the current edit* are shown. Pre-existing issues are silent. Fixed violations are acknowledged. Skipped rules (`long-method`, `large-class`, etc.) are suppressed — they're architectural and handled by `/lens-booboo-refactor`.
25
24
 
26
25
  ```
27
26
  🔴 Fix 2 TypeScript error(s) — these must be resolved:
28
27
  L10: Type 'string' is not assignable to type 'number'
29
28
 
30
- 🔴 You introduced 1 new structural violation(s) fix before moving on:
29
+ 🔴 STOP — you introduced 1 new structural violation(s). Fix before continuing:
31
30
  no-var: Use 'const' or 'let' instead of 'var' (L23)
32
31
  → var has function scope and can lead to unexpected hoisting behavior.
33
- (18 total remaining)
32
+ Auto-fixable: check the hints above
34
33
 
35
34
  ✅ ast-grep: fixed no-console-log (-1)
36
35
 
37
- 🟠 You introduced 1 new Biome violation(s) fix before moving on:
36
+ 🔴 STOP — you introduced 1 new Biome violation(s). Fix before continuing:
38
37
  L23:5 [style/useConst] This let declares a variable that is only assigned once.
39
38
  → Auto-fixable: `npx @biomejs/biome check --write utils.ts`
40
- (4 total remaining)
41
39
 
42
- 🟠 This file has 1 duplicate block(s) extract to shared utilities:
40
+ 🔴 STOP — this file has 1 duplicate block(s). Extract to a shared utility before adding more code:
43
41
  15 lines duplicated with helpers.ts:20
44
42
 
45
43
  🔴 Do not redefine — 1 function(s) already exist elsewhere:
46
44
  formatDate (already in helpers.ts)
47
45
  → Import the existing function instead
48
46
 
49
- 🟡 Complexity issues — refactor when you get a chance:
50
- ⚠ Maintainability dropped to 55 — extract logic into helper functions
51
- ⚠ AI-style comments (6) — remove hand-holding comments
52
- ⚠ Many try/catch blocks (7) — consolidate error handling
53
-
54
47
  [Tests] ✗ 1/3 failed, 2 passed
55
48
  ✗ should format date
56
49
  → Fix failing tests before proceeding
@@ -64,50 +57,26 @@ Before every write or edit, the agent is warned about blocking TypeScript errors
64
57
  ⚠ Pre-write: file already has 5 TypeScript error(s) — fix before adding more
65
58
  ```
66
59
 
67
- ### Session start summary (injected into first tool result)
60
+ ### Session start (silent caching)
68
61
 
69
- On every new session, the following scans run against the whole project and are delivered once into the first tool result:
62
+ On every new session, scans run silently in the background. Data is cached for real-time feedback during the session and surfaced on-demand via explicit commands:
70
63
 
71
- | Tool | What it reports |
64
+ | Scanner | Cached for |
72
65
  |---|---|
73
- | **TODO scanner** | All TODO / FIXME / HACK / BUG / DEPRECATED annotations, sorted by severity |
74
- | **Knip** | Unused exports, types, and unlisted dependencies |
75
- | **jscpd** | Duplicate code blocks file, line, size, percentage of codebase |
76
- | **type-coverage** | Percentage of identifiers properly typed; lists exact locations of `any` |
77
-
78
- Example:
79
-
80
- ```
81
- [Session Start]
82
- [TODOs] 3 annotation(s) found (2 FIXME, 1 TODO):
83
- 🔴 src/auth.ts:42 — FIXME: token refresh not implemented
84
- 🟠 src/parser.ts:17 — HACK: bypassing validation
85
- 📝 src/api.ts:88 — TODO: add rate limiting
86
-
87
- [Knip] 2 issue(s) — 2 unused export(s):
88
- Unused exports:
89
- - legacyFormat (utils.ts)
90
- - oldParser (parser.ts)
91
-
92
- [jscpd] 2 duplicate block(s) — 1.2% of codebase (47/3920 lines):
93
- 16 lines — openrouter.ts:183 ↔ openrouter.ts:135
94
- 11 lines — cline-auth.ts:51 ↔ kilo-auth.ts:9
95
-
96
- [type-coverage] ⚠ 94.3% typed (3870/4107 identifiers):
97
- auth.ts:138:44 — undefined as any
98
- config.ts:52:12 — err
99
- ... and 12 more
100
- ```
66
+ | **TODO scanner** | `/lens-booboo` reports |
67
+ | **Knip** | Dead code detection in `/lens-booboo` and `/lens-booboo-fix` |
68
+ | **jscpd** | Duplicate detection on write; `/lens-booboo` reports |
69
+ | **type-coverage** | `/lens-booboo` reports |
70
+ | **Complexity baselines** | Regressed/improved delta tracking via `/lens-metrics` |
101
71
 
102
72
  ### On-demand commands
103
73
 
104
74
  | Command | Description |
105
75
  |---|---|
106
- | `/lens-todos [path]` | Scan for TODO/FIXME/HACK annotations |
107
- | `/lens-dead-code` | Find unused exports/files/dependencies (requires knip) |
108
- | `/lens-deps` | Circular dependency scan (requires madge) |
76
+ | `/lens-booboo [path]` | Full code review: TODOs, dead code, duplicates, type coverage, circular dependencies. Saves full report to `.pi-lens/reviews/` |
77
+ | `/lens-booboo-fix [path]` | Iterative automated fix loop. Runs Biome/Ruff autofix, then scans for fixable issues (ast-grep agent rules, dead code). Generates a fix plan for the agent to execute. Re-run for up to 3 iterations, then reset. |
78
+ | `/lens-booboo-refactor [path]` | Interactive architectural refactor. Scans for worst offender by combined debt score (ast-grep skip rules + complexity metrics). Opens a browser interview with impact metrics — agent proposes refactoring options with rationale, user picks one, agent implements and shows a post-change report. |
109
79
  | `/lens-format [file\|--all]` | Apply Biome formatting |
110
- | `/lens-booboo [path]` | Full code review: design smells, complexity, AI slop, TODOs, dead code, duplicates, type coverage. Saves full report to `.pi-lens/reviews/` |
111
80
  | `/lens-metrics [path]` | Measure complexity metrics for all files. Exports `report.md` with grades (A-F), summary stats, and top 10 worst files |
112
81
 
113
82
  ### On-demand tools
@@ -157,6 +126,63 @@ pip install ruff
157
126
 
158
127
  ---
159
128
 
129
+ ## Fix loop commands
130
+
131
+ ### `/lens-booboo-fix` — automated mechanical fixes
132
+
133
+ Iterative loop that auto-fixes what it can, then generates a fix plan for the agent. Scan order:
134
+
135
+ 1. **Biome + Ruff** — auto-fix lint/format issues silently
136
+ 2. **jscpd** — within-file duplicate blocks (extract to shared utilities)
137
+ 3. **Knip** — dead code (delete unused exports/files)
138
+ 4. **ast-grep** — structural violations on surviving code (agent fixes)
139
+ 5. **AI slop** — files with 2+ complexity signals
140
+ 6. **Remaining Biome** — issues that couldn't be auto-fixed even with `--unsafe`
141
+
142
+ Run up to 3 iterations per session. Session auto-resets after hitting max — just run again.
143
+
144
+ ```
145
+ 📋 BOOBOO FIX PLAN — Iteration 1/3 (44 fixable items remaining)
146
+ ✅ Fixed 249 issues since last iteration.
147
+
148
+ ⚡ Auto-fixed: Biome --write --unsafe, Ruff --fix + format already ran.
149
+
150
+ ## 🔨 Fix these [12 items]
151
+
152
+ ### no-console-log (14)
153
+ → Remove or replace with class logger method
154
+ - `clients/ruff-client.ts:47`
155
+ - `clients/biome-client.ts:48`
156
+ ...
157
+
158
+ ## ⏭️ Skip [109 items — architectural]
159
+ - **long-method** (79): Extraction requires understanding the function's purpose.
160
+ - **large-class** (16): Splitting a class requires architectural decisions.
161
+ ```
162
+
163
+ ### `/lens-booboo-refactor` — interactive architectural refactoring
164
+
165
+ Surfaces the worst offender in the codebase by combined debt score (ast-grep skip rules + complexity metrics). The agent analyzes the code, generates refactoring options with impact estimates, and presents them in a browser interview.
166
+
167
+ **Two-step flow:**
168
+ 1. **Option selection** — browser opens with numbered radio cards, each showing rationale + impact metrics (`linesReduced`, `miProjection`, `cognitiveProjection`). One option is recommended.
169
+ 2. **Post-change report** — after implementing, agent shows what changed (git diff + line counts) and how metrics evolved. User can say "looks good" or request changes via chat.
170
+
171
+ ```
172
+ 🏗️ BOOBOO REFACTOR — worst offender identified
173
+
174
+ File: index.ts (debt score: 35)
175
+ Complexity: MI: 2.7, Cognitive: 1590, Nesting: 10
176
+
177
+ Violations:
178
+ - long-method (×18)
179
+ - long-parameter-list (×6)
180
+ ```
181
+
182
+ The agent then calls the built-in `interviewer` tool, which opens a browser form with the generated options. Zero dependencies — Node's built-in `http` module + platform CLI (`start`/`open`/`xdg-open`).
183
+
184
+ ---
185
+
160
186
  ## ast-grep rules
161
187
 
162
188
  Rules live in `rules/ast-grep-rules/rules/`. All rules are YAML files you can edit or extend.
@@ -0,0 +1,340 @@
1
+ /**
2
+ * AstGrep Client for pi-lens
3
+ *
4
+ * Structural code analysis using ast-grep CLI.
5
+ * Scans files against YAML rule definitions.
6
+ *
7
+ * Requires: npm install -D @ast-grep/cli
8
+ * Rules: ./rules/ directory
9
+ */
10
+ import { spawn, spawnSync } from "node:child_process";
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import { AstGrepParser } from "./ast-grep-parser.js";
14
+ import { AstGrepRuleManager } from "./ast-grep-rule-manager.js";
15
+ // --- Client ---
16
+ export class AstGrepClient {
17
+ constructor(ruleDir, verbose = false) {
18
+ this.available = null;
19
+ this.ruleDir =
20
+ ruleDir ||
21
+ path.join(typeof __dirname !== "undefined" ? __dirname : ".", "..", "rules");
22
+ this.log = verbose
23
+ ? (msg) => console.error(`[ast-grep] ${msg}`)
24
+ : () => { };
25
+ this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
26
+ }
27
+ /**
28
+ * Check if ast-grep CLI is available
29
+ */
30
+ isAvailable() {
31
+ if (this.available !== null)
32
+ return this.available;
33
+ const result = spawnSync("npx", ["sg", "--version"], {
34
+ encoding: "utf-8",
35
+ timeout: 10000,
36
+ shell: true,
37
+ });
38
+ this.available = !result.error && result.status === 0;
39
+ if (this.available) {
40
+ this.log("ast-grep available");
41
+ }
42
+ return this.available;
43
+ }
44
+ /**
45
+ * Search for AST patterns in files
46
+ */
47
+ async search(pattern, lang, paths) {
48
+ return this.runSg([
49
+ "run",
50
+ "-p",
51
+ pattern,
52
+ "--lang",
53
+ lang,
54
+ "--json=compact",
55
+ ...paths,
56
+ ]);
57
+ }
58
+ /**
59
+ * Search and replace AST patterns
60
+ */
61
+ async replace(pattern, rewrite, lang, paths, apply = false) {
62
+ const args = [
63
+ "run",
64
+ "-p",
65
+ pattern,
66
+ "-r",
67
+ rewrite,
68
+ "--lang",
69
+ lang,
70
+ "--json=compact",
71
+ ];
72
+ if (apply)
73
+ args.push("--update-all");
74
+ args.push(...paths);
75
+ const result = await this.runSg(args);
76
+ return { matches: result.matches, applied: apply, error: result.error };
77
+ }
78
+ /**
79
+ * Run a one-off scan with a temporary rule and configuration
80
+ */
81
+ runTempScan(dir, ruleId, ruleYaml, timeout = 30000) {
82
+ if (!this.isAvailable())
83
+ return [];
84
+ const tmpDir = require("node:os").tmpdir();
85
+ const ts = Date.now();
86
+ const sessionDir = path.join(tmpDir, `pi-lens-temp-${ruleId}-${ts}`);
87
+ const rulesSubdir = path.join(sessionDir, "rules");
88
+ const ruleFile = path.join(rulesSubdir, `${ruleId}.yml`);
89
+ const configFile = path.join(sessionDir, ".sgconfig.yml");
90
+ try {
91
+ fs.mkdirSync(rulesSubdir, { recursive: true });
92
+ fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
93
+ fs.writeFileSync(ruleFile, ruleYaml);
94
+ const result = spawnSync("npx", ["sg", "scan", "--config", configFile, "--json", dir], {
95
+ encoding: "utf-8",
96
+ timeout,
97
+ shell: true,
98
+ });
99
+ const output = result.stdout || result.stderr || "";
100
+ if (!output.trim())
101
+ return [];
102
+ const items = JSON.parse(output);
103
+ return Array.isArray(items) ? items : [items];
104
+ }
105
+ catch (err) {
106
+ void err;
107
+ return [];
108
+ }
109
+ finally {
110
+ try {
111
+ fs.rmSync(sessionDir, { recursive: true, force: true });
112
+ }
113
+ catch (err) {
114
+ void err;
115
+ }
116
+ }
117
+ }
118
+ /**
119
+ * Find similar functions by comparing normalized AST structure
120
+ */
121
+ async findSimilarFunctions(dir, lang = "typescript") {
122
+ const ruleYaml = `id: find-functions
123
+ language: ${lang}
124
+ rule:
125
+ kind: function_declaration
126
+ severity: info
127
+ message: found
128
+ `;
129
+ const matches = this.runTempScan(dir, "find-functions", ruleYaml);
130
+ if (matches.length === 0)
131
+ return [];
132
+ return this.groupSimilarFunctions(matches);
133
+ }
134
+ groupSimilarFunctions(matches) {
135
+ const normalized = new Map();
136
+ for (const item of matches) {
137
+ const text = item.text || "";
138
+ const nameMatch = text.match(/function\s+(\w+)/);
139
+ if (!nameMatch?.[1])
140
+ continue;
141
+ const signature = this.normalizeFunction(text);
142
+ if (!normalized.has(signature)) {
143
+ normalized.set(signature, []);
144
+ }
145
+ const line = item.range?.start?.line || item.labels?.[0]?.range?.start?.line || 0;
146
+ normalized.get(signature)?.push({
147
+ name: nameMatch[1],
148
+ file: item.file,
149
+ line: line + 1,
150
+ });
151
+ }
152
+ const result_groups = [];
153
+ for (const [pattern, functions] of normalized) {
154
+ if (functions.length > 1) {
155
+ result_groups.push({ pattern, functions });
156
+ }
157
+ }
158
+ return result_groups;
159
+ }
160
+ normalizeFunction(text) {
161
+ const normalizedText = text
162
+ .replace(/function\s+\w+/, "function FN")
163
+ .replace(/\bconst\b|\blet\b|\bvar\b/g, "VAR")
164
+ .replace(/["'].*?["']/g, "STR")
165
+ .replace(/`[^`]*`/g, "TMPL")
166
+ .replace(/\b\d+\b/g, "NUM")
167
+ .replace(/\btrue\b|\bfalse\b/g, "BOOL")
168
+ .replace(/\/\/.*/g, "")
169
+ .replace(/\/\*[\s\S]*?\*\//g, "")
170
+ .replace(/\s+/g, " ")
171
+ .trim();
172
+ // Extract just the body structure
173
+ const bodyMatch = normalizedText.match(/\{(.*)\}/);
174
+ const body = bodyMatch ? bodyMatch[1].trim() : normalizedText;
175
+ // Use first 200 chars as signature
176
+ return body.slice(0, 200);
177
+ }
178
+ /**
179
+ * Scan for exported function names in a directory
180
+ */
181
+ async scanExports(dir, lang = "typescript") {
182
+ const exports = new Map();
183
+ const ruleYaml = `id: find-functions
184
+ language: ${lang}
185
+ rule:
186
+ kind: function_declaration
187
+ severity: info
188
+ message: found
189
+ `;
190
+ const matches = this.runTempScan(dir, "find-functions", ruleYaml, 15000);
191
+ this.log(`scanExports output length: ${matches.length}`);
192
+ for (const item of matches) {
193
+ const text = item.text || "";
194
+ const nameMatch = text.match(/function\s+(\w+)/);
195
+ if (nameMatch?.[1]) {
196
+ this.log(`scanExports found: ${nameMatch[1]} in ${item.file}`);
197
+ exports.set(nameMatch[1], item.file);
198
+ }
199
+ }
200
+ return exports;
201
+ }
202
+ runSg(args) {
203
+ return new Promise((resolve) => {
204
+ const proc = spawn("npx", ["sg", ...args], {
205
+ stdio: ["ignore", "pipe", "pipe"],
206
+ shell: true,
207
+ });
208
+ let stdout = "";
209
+ let stderr = "";
210
+ proc.stdout.on("data", (data) => (stdout += data.toString()));
211
+ proc.stderr.on("data", (data) => (stderr += data.toString()));
212
+ proc.on("error", (err) => {
213
+ if (err.message.includes("ENOENT")) {
214
+ resolve({
215
+ matches: [],
216
+ error: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli",
217
+ });
218
+ }
219
+ else {
220
+ resolve({ matches: [], error: err.message });
221
+ }
222
+ });
223
+ proc.on("close", (code) => {
224
+ if (code !== 0 && !stdout.trim()) {
225
+ resolve({
226
+ matches: [],
227
+ error: stderr.includes("No files found")
228
+ ? undefined
229
+ : stderr.trim() || `Exit code ${code}`,
230
+ });
231
+ return;
232
+ }
233
+ if (!stdout.trim()) {
234
+ resolve({ matches: [] });
235
+ return;
236
+ }
237
+ try {
238
+ const parsed = JSON.parse(stdout);
239
+ const matches = Array.isArray(parsed) ? parsed : [parsed];
240
+ resolve({ matches });
241
+ }
242
+ catch (err) {
243
+ void err;
244
+ resolve({ matches: [], error: "Failed to parse output" });
245
+ }
246
+ });
247
+ });
248
+ }
249
+ formatMatches(matches, isDryRun = false) {
250
+ if (matches.length === 0)
251
+ return "No matches found";
252
+ const MAX = 50;
253
+ const shown = matches.slice(0, MAX);
254
+ const lines = shown.map((m) => {
255
+ const loc = `${m.file}:${m.range.start.line + 1}:${m.range.start.column + 1}`;
256
+ const text = m.text.length > 100 ? `${m.text.slice(0, 100)}...` : m.text;
257
+ return isDryRun && m.replacement
258
+ ? `${loc}\n - ${text}\n + ${m.replacement}`
259
+ : `${loc}: ${text}`;
260
+ });
261
+ if (matches.length > MAX)
262
+ lines.unshift(`Found ${matches.length} matches (showing first ${MAX}):`);
263
+ return lines.join("\n");
264
+ }
265
+ /**
266
+ * Scan a file against all rules
267
+ */
268
+ scanFile(filePath) {
269
+ if (!this.isAvailable())
270
+ return [];
271
+ const absolutePath = path.resolve(filePath);
272
+ if (!fs.existsSync(absolutePath))
273
+ return [];
274
+ const configPath = path.join(this.ruleDir, ".sgconfig.yml");
275
+ try {
276
+ const result = spawnSync("npx", ["sg", "scan", "--config", configPath, "--json", absolutePath], {
277
+ encoding: "utf-8",
278
+ timeout: 15000,
279
+ shell: true,
280
+ });
281
+ // ast-grep exits 1 when it finds issues
282
+ const output = result.stdout || result.stderr || "";
283
+ if (!output.trim())
284
+ return [];
285
+ const parser = new AstGrepParser((id) => this.getRuleDescription(id), (sev) => this.mapSeverity(sev));
286
+ return parser.parseOutput(output, absolutePath);
287
+ }
288
+ catch (err) {
289
+ this.log(`Scan error: ${err.message}`);
290
+ return [];
291
+ }
292
+ }
293
+ /**
294
+ * Format diagnostics for LLM consumption
295
+ */
296
+ formatDiagnostics(diags) {
297
+ if (diags.length === 0)
298
+ return "";
299
+ const errors = diags.filter((d) => d.severity === "error");
300
+ const warnings = diags.filter((d) => d.severity === "warning");
301
+ const hints = diags.filter((d) => d.severity === "hint");
302
+ let output = `[ast-grep] ${diags.length} structural issue(s)`;
303
+ if (errors.length)
304
+ output += ` — ${errors.length} error(s)`;
305
+ if (warnings.length)
306
+ output += ` — ${warnings.length} warning(s)`;
307
+ if (hints.length)
308
+ output += ` — ${hints.length} hint(s)`;
309
+ output += ":\n";
310
+ for (const d of diags.slice(0, 10)) {
311
+ const loc = d.line === d.endLine ? `L${d.line}` : `L${d.line}-${d.endLine}`;
312
+ const ruleInfo = d.ruleDescription
313
+ ? `${d.rule}: ${d.ruleDescription.message}`
314
+ : d.rule;
315
+ const fix = d.fix || d.ruleDescription?.note ? " [fixable]" : "";
316
+ output += ` ${ruleInfo} (${loc})${fix}\n`;
317
+ if (d.ruleDescription?.note) {
318
+ const shortNote = d.ruleDescription.note.split("\n")[0];
319
+ output += ` → ${shortNote}\n`;
320
+ }
321
+ }
322
+ if (diags.length > 10) {
323
+ output += ` ... and ${diags.length - 10} more\n`;
324
+ }
325
+ return output;
326
+ }
327
+ getRuleDescription(ruleId) {
328
+ return this.ruleManager.loadRuleDescriptions().get(ruleId);
329
+ }
330
+ mapSeverity(severity) {
331
+ const lower = severity.toLowerCase();
332
+ if (lower === "error")
333
+ return "error";
334
+ if (lower === "warning")
335
+ return "warning";
336
+ if (lower === "info")
337
+ return "info";
338
+ return "hint";
339
+ }
340
+ }