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.
- package/CHANGELOG.md +104 -0
- package/README.md +74 -48
- package/clients/ast-grep-client.js +340 -0
- package/clients/ast-grep-client.test.js +141 -0
- package/clients/ast-grep-client.ts +132 -362
- package/clients/ast-grep-parser.js +84 -0
- package/clients/ast-grep-parser.ts +130 -0
- package/clients/ast-grep-rule-manager.js +81 -0
- package/clients/ast-grep-rule-manager.ts +95 -0
- package/clients/biome-client.js +323 -0
- package/clients/biome-client.test.js +142 -0
- package/clients/biome-client.ts +5 -3
- package/clients/complexity-client.js +633 -0
- package/clients/complexity-client.test.js +234 -0
- package/clients/complexity-client.ts +1 -1
- package/clients/dependency-checker.js +295 -0
- package/clients/dependency-checker.test.js +60 -0
- package/clients/dependency-checker.ts +1 -1
- package/clients/go-client.js +212 -0
- package/clients/go-client.test.js +127 -0
- package/clients/go-client.ts +3 -1
- package/clients/interviewer.js +201 -0
- package/clients/interviewer.ts +232 -0
- package/clients/jscpd-client.js +145 -0
- package/clients/jscpd-client.test.js +127 -0
- package/clients/jscpd-client.ts +3 -2
- package/clients/knip-client.js +206 -0
- package/clients/knip-client.test.js +112 -0
- package/clients/knip-client.ts +3 -2
- package/clients/metrics-client.js +264 -0
- package/clients/metrics-client.test.js +141 -0
- package/clients/metrics-client.ts +66 -15
- package/clients/ruff-client.js +255 -0
- package/clients/ruff-client.test.js +132 -0
- package/clients/ruff-client.ts +9 -4
- package/clients/rust-client.js +212 -0
- package/clients/rust-client.test.js +108 -0
- package/clients/rust-client.ts +1 -1
- package/clients/scan-architectural-debt.js +130 -0
- package/clients/scan-architectural-debt.ts +135 -0
- package/clients/subprocess-client.js +101 -0
- package/clients/subprocess-client.ts +3 -2
- package/clients/test-runner-client.js +527 -0
- package/clients/test-runner-client.test.js +192 -0
- package/clients/test-runner-client.ts +69 -125
- package/clients/test-utils.js +27 -0
- package/clients/test-utils.ts +3 -5
- package/clients/todo-scanner.js +200 -0
- package/clients/todo-scanner.test.js +301 -0
- package/clients/type-coverage-client.js +131 -0
- package/clients/type-coverage-client.test.js +105 -0
- package/clients/type-coverage-client.ts +1 -1
- package/clients/types.js +11 -0
- package/clients/typescript-client.js +463 -0
- package/clients/typescript-client.test.js +105 -0
- package/clients/typescript-client.ts +85 -101
- package/index.ts +342 -236
- package/package.json +2 -2
- package/rules/ast-grep-rules/rules/empty-catch.yml +1 -0
- package/rules/ast-grep-rules/rules/jsx-boolean-short-circuit.yml +20 -0
- package/rules/ast-grep-rules/rules/jwt-no-verify.yml +14 -0
- package/rules/ast-grep-rules/rules/large-class.yml +1 -0
- package/rules/ast-grep-rules/rules/long-method.yml +8 -2
- package/rules/ast-grep-rules/rules/long-parameter-list.yml +1 -0
- package/rules/ast-grep-rules/rules/missed-concurrency.yml +25 -0
- package/rules/ast-grep-rules/rules/no-await-in-loop.yml +1 -0
- package/rules/ast-grep-rules/rules/no-await-in-promise-all.yml +1 -0
- package/rules/ast-grep-rules/rules/no-console-log.yml +1 -0
- package/rules/ast-grep-rules/rules/no-eval.yml +1 -0
- package/rules/ast-grep-rules/rules/no-hardcoded-secrets.yml +1 -0
- package/rules/ast-grep-rules/rules/redundant-state.yml +16 -0
- package/rules/ast-grep-rules/rules/toctou.yml +112 -0
- 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
|
|
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
|
-
🔴
|
|
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
|
-
|
|
32
|
+
→ Auto-fixable: check the hints above
|
|
34
33
|
|
|
35
34
|
✅ ast-grep: fixed no-console-log (-1)
|
|
36
35
|
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
60
|
+
### Session start (silent caching)
|
|
68
61
|
|
|
69
|
-
On every new session,
|
|
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
|
-
|
|
|
64
|
+
| Scanner | Cached for |
|
|
72
65
|
|---|---|
|
|
73
|
-
| **TODO scanner** |
|
|
74
|
-
| **Knip** |
|
|
75
|
-
| **jscpd** | Duplicate
|
|
76
|
-
| **type-coverage** |
|
|
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-
|
|
107
|
-
| `/lens-
|
|
108
|
-
| `/lens-
|
|
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
|
+
}
|