pi-lens 3.6.0 → 3.6.2

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 CHANGED
@@ -2,6 +2,21 @@
2
2
 
3
3
  All notable changes to pi-lens will be documented in this file.
4
4
 
5
+ ## [3.6.1] - 2026-04-02
6
+
7
+ ### Added
8
+ - **Condensed skill auto-loading** — Injects 70-token tool selection guidance at session start:
9
+ - Quick reference for when to use lsp_navigation vs ast_grep_search vs grep
10
+ - References full skills for lazy loading (ast-grep, lsp-navigation)
11
+ - Prevents common tool selection errors without loading full skill content
12
+
13
+ ### Changed
14
+ - **Updated package description** — More concise: "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo"
15
+
16
+ ### Repository
17
+ - **AGENTS.md is now local-only** — Removed from git repo and added to `.gitignore` so it stays local to each developer's environment
18
+ - **Cleaned up debug files** — Removed old test files (`_debug-*.ts`, `_trigger-test.ts`, `_test-*.ts`) from repo
19
+
5
20
  ## [3.6.0] - 2026-04-02
6
21
 
7
22
  ### Added
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  3. **Scans for secrets** — Blocks on hardcoded API keys, tokens, passwords
11
11
  4. **Runs linters** — Biome (TS/JS), Ruff (Python), plus structural analysis
12
12
  5. **Tree-sitter analysis** — Deep structural patterns (empty catch, eval, deep nesting, mixed async styles)
13
- 6. **Auto-installs** — TypeScript, Python, Biome, Ruff tools install automatically on first use
13
+ 6. **Auto-installs** — TypeScript, Python, Biome, Ruff, and analysis tools auto-install on first use
14
14
  7. **Only shows NEW issues** — Delta-mode tracks baselines and filters pre-existing problems
15
15
 
16
16
  **🔴 Blockers** (type errors, secrets, empty catches) appear inline and stop the agent until fixed.
@@ -28,10 +28,7 @@ pi
28
28
  # Disable auto-formatting if needed
29
29
  pi --no-autoformat
30
30
 
31
- # Full LSP mode (31 language servers)
32
- pi --lens-lsp
33
-
34
- # LSP mode (recommended for large projects)
31
+ # Full LSP mode (31 language servers) — recommended for large/multi-language projects
35
32
  pi --lens-lsp
36
33
  ```
37
34
 
@@ -131,7 +128,7 @@ Enable full Language Server Protocol support with `--lens-lsp`:
131
128
  | **Config** | YAML, JSON, Prisma |
132
129
  | **Web** | Vue, Svelte, CSS/SCSS/Sass/Less |
133
130
 
134
- **Auto-installation (4 core tools):** TypeScript, Python, and formatting tools auto-install on first use to `.pi-lens/tools/`. Other LSP servers are launched via `npx` when available or require manual installation.
131
+ **Auto-installation (8 tools):** TypeScript, Python, Biome, Ruff, and analysis tools (Madge, jscpd, ast-grep, Knip) auto-install on first use to `.pi-lens/tools/`. Other LSP servers are launched via `npx` when available or require manual installation.
135
132
 
136
133
  **Usage:**
137
134
  ```bash
@@ -156,14 +153,6 @@ See [docs/LSP_CONFIG.md](docs/LSP_CONFIG.md) for configuration options.
156
153
 
157
154
  ---
158
155
 
159
- ### Execution Modes
160
-
161
- | Mode | Flag | Description |
162
- |------|------|-------------|
163
- | **Sequential** | (default) | Runners execute one at a time |
164
-
165
- ---
166
-
167
156
  ### On every write / edit
168
157
 
169
158
  Every file write/edit triggers multiple analysis phases:
@@ -294,7 +283,7 @@ message: "Remove console statements before production"
294
283
  severity: warning
295
284
  ```
296
285
 
297
- See [docs/ast-grep-rules.md](docs/ast-grep-rules.md) for full guide.
286
+ See [AST_GREP_RULES.md](AST_GREP_RULES.md) for full guide.
298
287
 
299
288
  ---
300
289
 
@@ -393,125 +382,20 @@ Running the full suite on every edit would be too slow. Targeted testing gives i
393
382
 
394
383
  ### Complexity Metrics
395
384
 
396
- pi-lens calculates comprehensive code quality metrics for every source file:
397
-
398
- | Metric | Range | Description | Thresholds |
399
- |--------|-------|-------------|------------|
400
- | **Maintainability Index (MI)** | 0-100 | Composite score combining complexity, size, and structure | <20: 🔴 Unmaintainable, 20-40: 🟡 Poor, >60: ✅ Good |
401
- | **Cognitive Complexity** | 0+ | Human mental effort to understand code (nesting penalties) | >20: 🟡 Hard to understand, >50: 🔴 Very complex |
402
- | **Cyclomatic Complexity** | 1+ | Independent code paths (branch points + 1) | >10: 🟡 Complex function, >20: 🔴 Highly complex |
403
- | **Max Cyclomatic** | 1+ | Worst function in file | >10 flagged |
404
- | **Nesting Depth** | 0+ | Maximum block nesting level | >4: 🟡 Deep nesting, >6: 🔴 Excessive |
405
- | **Code Entropy** | 0-8+ bits | Shannon entropy — unpredictability of code patterns | >4.0: 🟡 Risky, >7.0: 🔴 Very unpredictable |
406
- | **Halstead Volume** | 0+ | Vocabulary × length — unique ops/operands | High = many different operations |
385
+ pi-lens tracks code quality metrics for every file:
407
386
 
408
- #### Metric Calculations
387
+ | Metric | Description | Threshold |
388
+ |--------|-------------|-----------|
389
+ | **Maintainability Index** | 0-100 composite score | >60 ✅ <20 🔴 |
390
+ | **Cognitive Complexity** | Mental effort to understand | >20 🟡 >50 🔴 |
391
+ | **Cyclomatic Complexity** | Independent code paths | >10 🟡 >20 🔴 |
392
+ | **Code Entropy** | Shannon entropy in bits | >4.0 🟡 >7.0 🔴 |
409
393
 
410
- **Maintainability Index (Microsoft's Formula)**
411
- ```
412
- MI = max(0, (171 - 5.2*ln(Halstead) - 0.23*Cyclomatic - 16.2*ln(LOC)) * 100/171) + commentBonus
413
-
414
- Where:
415
- - Halstead = Halstead Volume (vocabulary-based complexity)
416
- - Cyclomatic = Average cyclomatic complexity across functions
417
- - LOC = Lines of code
418
- - commentBonus = up to +10% for well-commented code
419
- ```
420
-
421
- **Cognitive Complexity (SonarSource Spec)**
422
- - +1 for each structural node (if, for, while, case, catch, switch)
423
- - +1 for each level of nesting (nested structures add nesting penalty)
424
- - +1 for each && and || in binary expressions (logical operators)
425
- - Exception: else-if chains don't add nesting
426
-
427
- **Cyclomatic Complexity (McCabe's Formula)**
428
- ```
429
- M = E - N + 2P
394
+ **Commands:**
395
+ - `/lens-tdi` — Technical Debt Index (0-100) with grades A-F
396
+ - `/lens-booboo` Full complexity table for all files
430
397
 
431
- Where:
432
- - E = Number of edges in control flow graph
433
- - N = Number of nodes
434
- - P = Number of connected components (usually 1)
435
-
436
- Simplified: Count branch points (if, while, for, case, &&, ||) + 1
437
- ```
438
-
439
- **Max Cyclomatic**
440
- - Single value: the highest cyclomatic complexity of any function in the file
441
- - Catches "worst offender" functions that average metrics hide
442
-
443
- **Code Entropy (Shannon Entropy in Bits)**
444
- ```
445
- H = -Σ(p(i) * log2(p(i)))
446
-
447
- Where:
448
- - p(i) = frequency of token i / total tokens
449
- - Measures unpredictability/vocabulary richness
450
-
451
- Thresholds:
452
- - ≤4.0 bits: Predictable, conventional code ✅
453
- - 4.0-7.0 bits: Moderate complexity 🟡
454
- - ≥7.0 bits: Unpredictable, hard to maintain 🔴
455
- ```
456
-
457
- **Halstead Volume**
458
- ```
459
- V = N * log2(n)
460
-
461
- Where:
462
- - N = total operators + operands (program length)
463
- - n = unique operators + operands (vocabulary size)
464
-
465
- Measures: How much information the reader must absorb
466
- ```
467
-
468
- ---
469
-
470
- ### Technical Debt Index (TDI)
471
-
472
- The TDI provides a single score (0-100) representing overall codebase health. Lower is better.
473
-
474
- **TDI Formula (5-Factor Weighted)**
475
- ```
476
- TDI = MI-debt(45%) + cognitive(30%) + nesting(10%) + maxCyc(10%) + entropy(5%)
477
- ```
478
-
479
- **Debt Calculation for Each Factor:**
480
-
481
- | Factor | Debt Formula | Good | Bad |
482
- |--------|---------------|------|-----|
483
- | **MI** | `(100 - MI) / 100` | 100 | 0 |
484
- | **Cognitive** | `min(1, cognitive / 200)` | 0 | ≥500 |
485
- | **Nesting** | `max(0, nesting - 3) / 7` | ≤3 | ≥10 |
486
- | **Max Cyclomatic** | `max(0, maxCyc - 10) / 20` | ≤10 | ≥30 |
487
- | **Entropy** | `max(0, entropy - 4.0) / 3.0` | ≤4.0 | ≥7.0 |
488
-
489
- **Grades:**
490
- - **A** (0-15%): Excellent codebase health
491
- - **B** (16-30%): Good, minor improvements possible
492
- - **C** (31-50%): Moderate debt, consider refactoring
493
- - **D** (51-70%): Significant debt, plan refactoring
494
- - **F** (71%+): High debt, immediate attention needed
495
-
496
- **Usage:**
497
- ```bash
498
- /lens-tdi # Display TDI score with breakdown by category
499
- ```
500
-
501
- ---
502
-
503
- ### AI Slop Indicators
504
-
505
- Metrics that suggest potentially AI-generated low-quality code:
506
-
507
- - **Low MI + high cognitive + high entropy** = potential spaghetti code
508
- - **Excessive comments (>40%) + low MI** = hand-holding anti-patterns
509
- - **Single-use helpers with high entropy** = over-abstraction
510
- - **Many small functions with high cyclomatic** = fragmented complexity
511
-
512
- **Usage:**
513
- - `/lens-booboo` — Shows complexity table for all files
514
- - `tool_result` — Complexity tracked per file, AI slop warnings inline
398
+ See [docs/COMPLEXITY_METRICS.md](docs/COMPLEXITY_METRICS.md) for formulas and detailed calculations.
515
399
 
516
400
  ---
517
401
 
@@ -528,7 +412,7 @@ pi-lens works out of the box for TypeScript/JavaScript. For full language suppor
528
412
  | `knip` | `npm i -D knip` | Dead code / unused exports |
529
413
  | `jscpd` | `npm i -D jscpd` | Copy-paste detection |
530
414
  | `type-coverage` | `npm i -D type-coverage` | TypeScript `any` coverage % |
531
- | `@ast-grep/napi` | `npm i -D @ast-grep/napi` | Fast structural analysis (TS/JS) — currently disabled in realtime |
415
+ | `@ast-grep/napi` | `npm i -D @ast-grep/napi` | Fast structural analysis (TS/JS) — security rules inline, slop in booboo |
532
416
  | `@ast-grep/cli` | `npm i -D @ast-grep/cli` | Structural pattern matching (all languages) |
533
417
  | `typos-cli` | `cargo install typos-cli` | Spellcheck for Markdown |
534
418
 
@@ -564,7 +448,6 @@ pi-lens works out of the box for TypeScript/JavaScript. For full language suppor
564
448
  | Command | Description |
565
449
  |---------|-------------|
566
450
  | `/lens-booboo` | Full codebase review (10 analysis runners) |
567
- | `/lens-format` | Apply Biome formatting |
568
451
  | `/lens-tdi` | Technical Debt Index and trends |
569
452
 
570
453
  ---
@@ -606,141 +489,7 @@ pi --lens-lsp # LSP type-checking (31 languages)
606
489
 
607
490
  ## TypeScript LSP — tsconfig detection
608
491
 
609
- The LSP walks up from the edited file's directory until it finds a `tsconfig.json`. If found, it uses that project's exact `compilerOptions` (paths, strict settings, lib, etc.). If not found, it falls back to sensible defaults:
610
-
611
- - `target: ES2020`
612
- - `lib: ["es2020", "dom", "dom.iterable"]`
613
- - `moduleResolution: bundler`
614
- - `strict: true`
615
-
616
- The compiler options are refreshed automatically when you switch between projects within a session.
617
-
618
- ---
619
-
620
- ## Exclusion Criteria
621
-
622
- pi-lens automatically excludes certain files from analysis to reduce noise and focus on production code.
623
-
624
- ### Test Files
625
-
626
- All runners respect test file exclusions — both in the dispatch system (`skipTestFiles: true`) and the `/lens-booboo` command.
627
-
628
- **Excluded patterns:**
629
- ```
630
- **/*.test.ts **/*.test.tsx **/*.test.js **/*.test.jsx
631
- **/*.spec.ts **/*.spec.tsx **/*.spec.js **/*.spec.jsx
632
- **/*.poc.test.ts **/*.poc.test.tsx
633
- **/test-utils.ts **/test-*.ts
634
- **/__tests__/** **/tests/** **/test/**
635
- ```
636
-
637
- **Why:** Test files intentionally duplicate patterns (test fixtures, mock setups) and have different complexity standards. Including them creates false positives.
638
-
639
- ### Build Artifacts (TypeScript Projects)
640
-
641
- In TypeScript projects (detected by `tsconfig.json` presence), compiled `.js` files are excluded:
642
-
643
- ```
644
- **/*.js **/*.jsx (when corresponding .ts/.tsx exists)
645
- ```
646
-
647
- **Why:** In TS projects, `.js` files are build artifacts. Analyzing them duplicates every issue (once in source `.ts`, once in compiled `.js`).
648
-
649
- **Note:** In pure JavaScript projects (no `tsconfig.json`), `.js` files are **included** as they are the source files.
650
-
651
- ### Excluded Directories
652
-
653
- | Directory | Reason |
654
- |-----------|--------|
655
- | `node_modules/` | Third-party dependencies |
656
- | `.git/` | Version control metadata |
657
- | `dist/`, `build/` | Build outputs |
658
- | `.pi-lens/`, `.pi/` | pi agent internal files |
659
- | `.next/`, `.ruff_cache/` | Framework/build caches |
660
- | `coverage/` | Test coverage reports |
661
-
662
- ### Per-Runner Exclusion Summary
663
-
664
- | Runner | Test Files | Build Artifacts | Directories |
665
- |--------|-----------|-----------------|-------------|
666
- | **dispatch runners** | ✅ `skipTestFiles` | ✅ `.js` excluded in TS | ✅ `EXCLUDED_DIRS` |
667
- | **booboo /lens-booboo** | ✅ `shouldIncludeFile()` | ✅ `isTsProject` check | ✅ `EXCLUDED_DIRS` |
668
- | **Secrets scan** | ❌ No exclusion (security) | ❌ No exclusion | ✅ Dirs excluded |
669
-
670
- ---
671
-
672
- ## Caching Architecture
673
-
674
- pi-lens uses a multi-layer caching strategy to avoid redundant work:
675
-
676
- ### 1. Tool Availability Cache
677
-
678
- **Location:** `clients/tool-availability.ts`
679
-
680
- ```
681
- ┌─────────────────────────────────────────┐
682
- │ TOOL AVAILABILITY CACHE │
683
- │ Map<toolName, {available, version}> │
684
- │ • Persisted for session lifetime │
685
- │ • Refreshed on extension restart │
686
- └─────────────────────────────────────────┘
687
- ```
688
-
689
- Avoids repeated `which`/`where` calls to check if `biome`, `ruff`, `pyright`, etc. are installed.
690
-
691
- ### 2. Dispatch Baselines (Delta Mode)
692
-
693
- **Location:** `clients/dispatch/dispatcher.ts`
694
-
695
- ```
696
- ┌─────────────────────────────────────────┐
697
- │ DISPATCH BASELINES │
698
- │ Map<filePath, Diagnostic[]> │
699
- │ • Cleared at turn start │
700
- │ • Updated after each runner execution │
701
- │ • Filters: only NEW issues shown │
702
- └─────────────────────────────────────────┘
703
- ```
704
-
705
- Delta mode tracking: first edit shows all issues, subsequent edits only show issues that weren't there before.
706
-
707
- ### 3. Client-Level Caches
708
-
709
- | Client | Cache | TTL | Purpose |
710
- |--------|-------|-----|---------|
711
- | **Knip** | `clients/cache-manager.ts` | 5 min | Dead code analysis (slow) |
712
- | **jscpd** | `clients/cache-manager.ts` | 5 min | Duplicate detection (slow) |
713
- | **Type Coverage** | In-memory | Session | `any` type percentage |
714
- | **Complexity** | In-memory | File-level | MI, cognitive complexity per file |
715
-
716
- ### 4. Session Turn State
717
-
718
- **Location:** `clients/cache-manager.ts`
719
-
720
- ```
721
- ┌─────────────────────────────────────────┐
722
- │ TURN STATE TRACKING │
723
- │ • Modified files this turn │
724
- │ • Modified line ranges per file │
725
- │ • Import changes detected │
726
- │ • Turn cycle counter (max 10) │
727
- └─────────────────────────────────────────┘
728
- ```
729
-
730
- Tracks which files were edited in the current agent turn for:
731
- - jscpd: Only re-scan modified files
732
- - Madge: Only check deps if imports changed
733
- - Cycle detection: Prevents infinite fix loops
734
-
735
- ### 5. Runner Internal Caches
736
-
737
- | Runner | Cache | Notes |
738
- |--------|-------|-------|
739
- | `tree-sitter` | Compiled query cache | `.wasm-cache` files with mtime-based invalidation. 10× faster startup. |
740
- | `ast-grep-napi` | Rule descriptions | Loaded once per session |
741
- | `biome` | Tool availability | Checked once, cached |
742
- | `pyright` | Command path | Venv lookup cached |
743
- | `ruff` | Command path | Venv lookup cached |
492
+ The LSP walks up from edited files to find `tsconfig.json`, using its `compilerOptions` (paths, strict settings, etc.). Falls back to sensible defaults if not found.
744
493
 
745
494
  ---
746
495
 
@@ -748,49 +497,17 @@ Tracks which files were edited in the current agent turn for:
748
497
 
749
498
  ```
750
499
  pi-lens/
751
- ├── clients/ # Lint tool wrappers and utilities
752
- ├── bus/ # Event bus system (Phase 1)
753
- │ │ ├── bus.ts
754
- │ │ ├── events.ts
755
- │ │ └── integration.ts
756
- ├── cache/ # Rule compilation cache
757
- │ │ └── rule-cache.ts # Disk-backed cache with mtime invalidation
758
- │ ├── dispatch/ # Dispatcher and runners
759
- │ │ ├── dispatcher.ts
760
- │ │ └── runners/ # Individual runners
761
- │ │ ├── ast-grep-napi.ts # Security rules inline, warnings in booboo
762
- │ │ ├── python-slop.ts # Python slop detection
763
- │ │ ├── ts-lsp.ts # TS type checking
764
- │ │ ├── biome.ts
765
- │ │ ├── ruff.ts
766
- │ │ ├── pyright.ts
767
- │ │ ├── go-vet.ts
768
- │ │ └── rust-clippy.ts
769
- │ ├── lsp/ # LSP client system (Phase 3)
770
- │ │ ├── client.ts
771
- │ │ ├── server.ts # 31 LSP server definitions
772
- │ │ ├── language.ts
773
- │ │ ├── launch.ts
774
- │ │ └── config.ts # Custom LSP configuration
775
- │ ├── installer/ # Auto-installation (Phase 4)
776
- │ │ └── index.ts
777
- │ ├── services/ # Effect-TS services (Phase 2)
778
- │ │ ├── runner-service.ts
779
- │ │ └── effect-integration.ts
780
- │ ├── complexity-client.ts
781
- │ ├── type-safety-client.ts
782
- │ └── secrets-scanner.ts
783
- ├── commands/ # pi commands
784
- │ ├── booboo.ts
785
- │ └── fix-simplified.ts
786
- ├── docs/ # Documentation
787
- │ └── LSP_CONFIG.md # LSP configuration guide
788
- ├── rules/ # AST-grep rules
789
- │ └── ast-grep-rules/ # General structural rules
790
- ├── index.ts # Main entry point
500
+ ├── clients/ # Lint tools, LSP clients, formatters
501
+ ├── commands/ # /lens-booboo, /lens-format commands
502
+ ├── docs/ # Documentation
503
+ ├── rules/ # AST-grep rules
504
+ ├── skills/ # Built-in pi skills
505
+ ├── index.ts # Main extension entry point
791
506
  └── package.json
792
507
  ```
793
508
 
509
+ See source for detailed structure.
510
+
794
511
  ---
795
512
 
796
513
  ## Skills
@@ -862,7 +579,7 @@ See [CHANGELOG.md](CHANGELOG.md) for full history.
862
579
 
863
580
  - **Tree-sitter Query Cache:** Compiled query cache with mtime-based invalidation — 10× faster structural analysis startup
864
581
  - **LSP Support:** 31 Language Server Protocol clients (4 core auto-installed, others via npx or manual)
865
- - **NAPI Runner:** 100x faster TypeScript/JavaScript structural analysis (~9ms vs ~1200ms) — currently disabled in realtime due to stability
582
+ - **NAPI Runner:** 100x faster TypeScript/JavaScript structural analysis (~9ms vs ~1200ms) — security rules fire inline
866
583
  - **Slop Detection:** 33+ TypeScript and 40+ Python patterns for AI-generated code quality issues
867
584
 
868
585
  ---
@@ -73,6 +73,34 @@ export class BiomeClient {
73
73
  }
74
74
  return this.biomeAvailable;
75
75
  }
76
+ /**
77
+ * Ensure Biome is available, auto-installing if necessary.
78
+ * Prefer this over isAvailable() for auto-install behavior.
79
+ */
80
+ async ensureAvailable() {
81
+ if (this.biomeAvailable !== null)
82
+ return this.biomeAvailable;
83
+ // Check if already available
84
+ const result = this.spawnBiome(["--version"], 10000);
85
+ if (!result.error && result.status === 0) {
86
+ this.biomeAvailable = true;
87
+ return true;
88
+ }
89
+ // Auto-install via pi-lens installer
90
+ this.log("Biome not found, attempting auto-install...");
91
+ const { ensureTool } = await import("./installer/index.js");
92
+ const installedPath = await ensureTool("biome");
93
+ if (installedPath) {
94
+ this.log(`Biome auto-installed: ${installedPath}`);
95
+ // Set the installed path as local binary to avoid npx overhead
96
+ this.localBinaryPath = installedPath;
97
+ this.biomeAvailable = true;
98
+ return true;
99
+ }
100
+ this.log("Biome auto-install failed");
101
+ this.biomeAvailable = false;
102
+ return false;
103
+ }
76
104
  /**
77
105
  * Check if a file is supported by Biome
78
106
  */
@@ -110,6 +110,38 @@ export class BiomeClient {
110
110
  return this.biomeAvailable;
111
111
  }
112
112
 
113
+ /**
114
+ * Ensure Biome is available, auto-installing if necessary.
115
+ * Prefer this over isAvailable() for auto-install behavior.
116
+ */
117
+ async ensureAvailable(): Promise<boolean> {
118
+ if (this.biomeAvailable !== null) return this.biomeAvailable;
119
+
120
+ // Check if already available
121
+ const result = this.spawnBiome(["--version"], 10000);
122
+ if (!result.error && result.status === 0) {
123
+ this.biomeAvailable = true;
124
+ return true;
125
+ }
126
+
127
+ // Auto-install via pi-lens installer
128
+ this.log("Biome not found, attempting auto-install...");
129
+ const { ensureTool } = await import("./installer/index.js");
130
+ const installedPath = await ensureTool("biome");
131
+
132
+ if (installedPath) {
133
+ this.log(`Biome auto-installed: ${installedPath}`);
134
+ // Set the installed path as local binary to avoid npx overhead
135
+ this.localBinaryPath = installedPath;
136
+ this.biomeAvailable = true;
137
+ return true;
138
+ }
139
+
140
+ this.log("Biome auto-install failed");
141
+ this.biomeAvailable = false;
142
+ return false;
143
+ }
144
+
113
145
  /**
114
146
  * Check if a file is supported by Biome
115
147
  */
@@ -31,8 +31,7 @@ const biomeRunner = {
31
31
  // IMPORTANT: Never use --write in dispatch runner to prevent infinite loops.
32
32
  // Writing to the file would trigger another tool_result event, which would
33
33
  // call dispatchLint again, creating a feedback loop.
34
- // Use /lens-format command for explicit formatting, or autofix flags on
35
- // the write/edit tools directly.
34
+ // Auto-format handles formatting on write; this runner only checks.
36
35
  const args = useNpx
37
36
  ? ["biome", "check", ctx.filePath]
38
37
  : ["check", ctx.filePath];
@@ -40,8 +40,7 @@ const biomeRunner: RunnerDefinition = {
40
40
  // IMPORTANT: Never use --write in dispatch runner to prevent infinite loops.
41
41
  // Writing to the file would trigger another tool_result event, which would
42
42
  // call dispatchLint again, creating a feedback loop.
43
- // Use /lens-format command for explicit formatting, or autofix flags on
44
- // the write/edit tools directly.
43
+ // Auto-format handles formatting on write; this runner only checks.
45
44
  const args = useNpx
46
45
  ? ["biome", "check", ctx.filePath]
47
46
  : ["check", ctx.filePath];
@@ -113,27 +113,6 @@ const TOOLS = [
113
113
  packageName: "knip",
114
114
  binaryName: "knip",
115
115
  },
116
- // GitHub release LSP servers
117
- {
118
- id: "clangd",
119
- name: "clangd",
120
- checkCommand: "clangd",
121
- checkArgs: ["--version"],
122
- installStrategy: "github",
123
- binaryName: process.platform === "win32" ? "clangd.exe" : "clangd",
124
- githubRepo: "clangd/clangd",
125
- },
126
- {
127
- id: "lua-language-server",
128
- name: "Lua Language Server",
129
- checkCommand: "lua-language-server",
130
- checkArgs: ["--version"],
131
- installStrategy: "github",
132
- binaryName: process.platform === "win32"
133
- ? "bin/lua-language-server.exe"
134
- : "bin/lua-language-server",
135
- githubRepo: "LuaLS/lua-language-server",
136
- },
137
116
  ];
138
117
  // --- Check Functions ---
139
118
  /**
@@ -131,28 +131,6 @@ const TOOLS: ToolDefinition[] = [
131
131
  packageName: "knip",
132
132
  binaryName: "knip",
133
133
  },
134
- // GitHub release LSP servers
135
- {
136
- id: "clangd",
137
- name: "clangd",
138
- checkCommand: "clangd",
139
- checkArgs: ["--version"],
140
- installStrategy: "github",
141
- binaryName: process.platform === "win32" ? "clangd.exe" : "clangd",
142
- githubRepo: "clangd/clangd",
143
- },
144
- {
145
- id: "lua-language-server",
146
- name: "Lua Language Server",
147
- checkCommand: "lua-language-server",
148
- checkArgs: ["--version"],
149
- installStrategy: "github",
150
- binaryName:
151
- process.platform === "win32"
152
- ? "bin/lua-language-server.exe"
153
- : "bin/lua-language-server",
154
- githubRepo: "LuaLS/lua-language-server",
155
- },
156
134
  ];
157
135
 
158
136
  // --- Check Functions ---
@@ -195,11 +195,74 @@ export const PythonServer = {
195
195
  "poetry.lock",
196
196
  ]),
197
197
  async spawn(root) {
198
+ const path = await import("node:path");
199
+ const fs = await import("node:fs/promises");
198
200
  const env = await getToolEnvironment();
199
- const proc = await launchViaPackageManager("pyright-langserver", ["--stdio"], {
200
- cwd: root,
201
- env,
202
- });
201
+ // Strategy 1: Find pyright - prefer local project version
202
+ let pyrightPath;
203
+ const localPyright = path.join(root, "node_modules", ".bin", "pyright");
204
+ const localPyrightCmd = path.join(root, "node_modules", ".bin", "pyright.cmd");
205
+ // Check for local version first (Windows .cmd first, then Unix)
206
+ for (const checkPath of [localPyrightCmd, localPyright]) {
207
+ try {
208
+ await fs.access(checkPath);
209
+ pyrightPath = checkPath;
210
+ break;
211
+ }
212
+ catch {
213
+ /* not found */
214
+ }
215
+ }
216
+ // Strategy 2: Fall back to auto-installed version
217
+ if (!pyrightPath) {
218
+ pyrightPath = await ensureTool("pyright");
219
+ if (!pyrightPath) {
220
+ console.error("[lsp] pyright not found, falling back to npx");
221
+ }
222
+ }
223
+ // Strategy 3: Use found pyright to derive pyright-langserver path
224
+ let langserverPath;
225
+ if (pyrightPath) {
226
+ // Derive langserver from pyright binary location
227
+ // Both are in the same .bin directory
228
+ const binDir = path.dirname(pyrightPath);
229
+ const isWindows = process.platform === "win32";
230
+ const candidates = isWindows
231
+ ? [
232
+ path.join(binDir, "pyright-langserver.cmd"),
233
+ path.join(binDir, "pyright-langserver.ps1"),
234
+ path.join(binDir, "pyright-langserver"),
235
+ ]
236
+ : [path.join(binDir, "pyright-langserver")];
237
+ for (const candidate of candidates) {
238
+ try {
239
+ await fs.access(candidate);
240
+ langserverPath = candidate;
241
+ console.error(`[lsp] Found pyright-langserver: ${candidate}`);
242
+ break;
243
+ }
244
+ catch {
245
+ /* not found */
246
+ }
247
+ }
248
+ }
249
+ // Spawn the LSP server
250
+ let proc;
251
+ if (langserverPath) {
252
+ // Use resolved langserver path
253
+ proc = await launchLSP(langserverPath, ["--stdio"], {
254
+ cwd: root,
255
+ env,
256
+ });
257
+ }
258
+ else {
259
+ // Fallback to npx for auto-download
260
+ console.error("[lsp] Falling back to npx for pyright-langserver");
261
+ proc = await launchViaPackageManager("pyright-langserver", ["--stdio"], {
262
+ cwd: root,
263
+ env,
264
+ });
265
+ }
203
266
  // Detect virtual environment
204
267
  const initialization = {};
205
268
  const venvPaths = [
@@ -214,7 +277,7 @@ export const PythonServer = {
214
277
  const pythonPath = process.platform === "win32"
215
278
  ? path.join(venv, "Scripts", "python.exe")
216
279
  : path.join(venv, "bin", "python");
217
- await import("node:fs/promises").then((fs) => fs.access(pythonPath));
280
+ await fs.access(pythonPath);
218
281
  // Pyright expects pythonPath at top level, not nested
219
282
  initialization.pythonPath = pythonPath;
220
283
  break;
@@ -274,15 +274,83 @@ export const PythonServer: LSPServerInfo = {
274
274
  "poetry.lock",
275
275
  ]),
276
276
  async spawn(root) {
277
+ const path = await import("node:path");
278
+ const fs = await import("node:fs/promises");
277
279
  const env = await getToolEnvironment();
278
- const proc = await launchViaPackageManager(
279
- "pyright-langserver",
280
- ["--stdio"],
281
- {
280
+
281
+ // Strategy 1: Find pyright - prefer local project version
282
+ let pyrightPath: string | undefined;
283
+ const localPyright = path.join(root, "node_modules", ".bin", "pyright");
284
+ const localPyrightCmd = path.join(
285
+ root,
286
+ "node_modules",
287
+ ".bin",
288
+ "pyright.cmd",
289
+ );
290
+
291
+ // Check for local version first (Windows .cmd first, then Unix)
292
+ for (const checkPath of [localPyrightCmd, localPyright]) {
293
+ try {
294
+ await fs.access(checkPath);
295
+ pyrightPath = checkPath;
296
+ break;
297
+ } catch {
298
+ /* not found */
299
+ }
300
+ }
301
+
302
+ // Strategy 2: Fall back to auto-installed version
303
+ if (!pyrightPath) {
304
+ pyrightPath = await ensureTool("pyright");
305
+ if (!pyrightPath) {
306
+ console.error("[lsp] pyright not found, falling back to npx");
307
+ }
308
+ }
309
+
310
+ // Strategy 3: Use found pyright to derive pyright-langserver path
311
+ let langserverPath: string | undefined;
312
+ if (pyrightPath) {
313
+ // Derive langserver from pyright binary location
314
+ // Both are in the same .bin directory
315
+ const binDir = path.dirname(pyrightPath);
316
+ const isWindows = process.platform === "win32";
317
+
318
+ const candidates = isWindows
319
+ ? [
320
+ path.join(binDir, "pyright-langserver.cmd"),
321
+ path.join(binDir, "pyright-langserver.ps1"),
322
+ path.join(binDir, "pyright-langserver"),
323
+ ]
324
+ : [path.join(binDir, "pyright-langserver")];
325
+
326
+ for (const candidate of candidates) {
327
+ try {
328
+ await fs.access(candidate);
329
+ langserverPath = candidate;
330
+ console.error(`[lsp] Found pyright-langserver: ${candidate}`);
331
+ break;
332
+ } catch {
333
+ /* not found */
334
+ }
335
+ }
336
+ }
337
+
338
+ // Spawn the LSP server
339
+ let proc;
340
+ if (langserverPath) {
341
+ // Use resolved langserver path
342
+ proc = await launchLSP(langserverPath, ["--stdio"], {
282
343
  cwd: root,
283
344
  env,
284
- },
285
- );
345
+ });
346
+ } else {
347
+ // Fallback to npx for auto-download
348
+ console.error("[lsp] Falling back to npx for pyright-langserver");
349
+ proc = await launchViaPackageManager("pyright-langserver", ["--stdio"], {
350
+ cwd: root,
351
+ env,
352
+ });
353
+ }
286
354
 
287
355
  // Detect virtual environment
288
356
  const initialization: Record<string, unknown> = {};
@@ -300,7 +368,7 @@ export const PythonServer: LSPServerInfo = {
300
368
  ? path.join(venv, "Scripts", "python.exe")
301
369
  : path.join(venv, "bin", "python");
302
370
 
303
- await import("node:fs/promises").then((fs) => fs.access(pythonPath));
371
+ await fs.access(pythonPath);
304
372
  // Pyright expects pythonPath at top level, not nested
305
373
  initialization.pythonPath = pythonPath;
306
374
  break;
@@ -135,7 +135,7 @@ export async function runPipeline(ctx, deps) {
135
135
  }
136
136
  }
137
137
  if (!noAutofixBiome &&
138
- biomeClient.isAvailable() &&
138
+ (await biomeClient.ensureAvailable()) &&
139
139
  biomeClient.isSupportedFile(filePath)) {
140
140
  const result = biomeClient.fixFile(filePath);
141
141
  if (result.success && result.fixed > 0) {
@@ -208,7 +208,7 @@ export async function runPipeline(
208
208
 
209
209
  if (
210
210
  !noAutofixBiome &&
211
- biomeClient.isAvailable() &&
211
+ (await biomeClient.ensureAvailable()) &&
212
212
  biomeClient.isSupportedFile(filePath)
213
213
  ) {
214
214
  const result = biomeClient.fixFile(filePath);
package/index.ts CHANGED
@@ -374,59 +374,6 @@ export default function (pi: ExtensionAPI) {
374
374
  },
375
375
  });
376
376
 
377
- pi.registerCommand("lens-format", {
378
- description:
379
- "Apply Biome formatting to files. Usage: /lens-format [file-path] or /lens-format --all",
380
- handler: async (args, ctx) => {
381
- if (!biomeClient.isAvailable()) {
382
- ctx.ui.notify(
383
- "Biome not installed. Run: npm install -D @biomejs/biome",
384
- "error",
385
- );
386
- return;
387
- }
388
-
389
- const arg = args.trim();
390
-
391
- if (!arg || arg === "--all") {
392
- ctx.ui.notify("🔍 Formatting all files...", "info");
393
-
394
- let formatted = 0;
395
- let skipped = 0;
396
-
397
- const targetPath = ctx.cwd || process.cwd();
398
- const isTsProject = nodeFs.existsSync(
399
- path.join(targetPath, "tsconfig.json"),
400
- );
401
- const files = getSourceFiles(targetPath, isTsProject);
402
-
403
- for (const fullPath of files) {
404
- if (/\.(ts|tsx|js|jsx|json|css)$/.test(fullPath)) {
405
- const result = biomeClient.formatFile(fullPath);
406
- if (result.changed) formatted++;
407
- else if (result.success) skipped++;
408
- }
409
- }
410
- ctx.ui.notify(
411
- `✓ Formatted ${formatted} file(s), ${skipped} already clean`,
412
- "info",
413
- );
414
- return;
415
- }
416
-
417
- const filePath = path.resolve(arg);
418
- const result = biomeClient.formatFile(filePath);
419
-
420
- if (result.success && result.changed) {
421
- ctx.ui.notify(`✓ Formatted ${path.basename(filePath)}`, "info");
422
- } else if (result.success) {
423
- ctx.ui.notify(`✓ ${path.basename(filePath)} already clean`, "info");
424
- } else {
425
- ctx.ui.notify(`⚠️ Format failed: ${result.error}`, "error");
426
- }
427
- },
428
- });
429
-
430
377
  // --- Tools ---
431
378
 
432
379
  const LANGUAGES = [
@@ -933,7 +880,7 @@ export default function (pi: ExtensionAPI) {
933
880
  // Log available tools
934
881
  const tools: string[] = [];
935
882
  tools.push("TypeScript LSP"); // Always available
936
- if (biomeClient.isAvailable()) tools.push("Biome");
883
+ if (await biomeClient.ensureAvailable()) tools.push("Biome");
937
884
  if (astGrepClient.isAvailable()) tools.push("ast-grep");
938
885
  if (ruffClient.isAvailable()) tools.push("Ruff");
939
886
  if (knipClient.isAvailable()) tools.push("Knip");
@@ -997,6 +944,11 @@ export default function (pi: ExtensionAPI) {
997
944
  log(`Active tools: ${tools.join(", ")}`);
998
945
  dbg(`session_start tools: ${tools.join(", ")}`);
999
946
 
947
+ // --- Condensed skill guidance for efficient tool selection ---
948
+ // Full skills available at skills/ast-grep/SKILL.md and skills/lsp-navigation/SKILL.md
949
+ const condensedGuidance =
950
+ "## Code Tool Selection\n- Navigation (definitions, references): lsp_navigation\n- Pattern search (functions, imports): ast_grep_search\n- Text/TODOs: grep\n- Full guides: skills/ast-grep, skills/lsp-navigation";
951
+
1000
952
  const parts: string[] = [];
1001
953
 
1002
954
  // --- Error ownership reminder ---
@@ -1005,6 +957,9 @@ export default function (pi: ExtensionAPI) {
1005
957
  "📌 Remember: If you find ANY errors (test failures, compile errors, lint issues) in this codebase, fix them — even if you didn't cause them. Don't skip errors as 'not my fault'.",
1006
958
  );
1007
959
 
960
+ // Add condensed skill guidance to system message
961
+ parts.push(condensedGuidance);
962
+
1008
963
  // Scan for project-specific rules (.claude/rules, .agents/rules, CLAUDE.md, etc.)
1009
964
  projectRulesScan = scanProjectRules(cwd);
1010
965
  if (projectRulesScan.hasCustomRules) {
@@ -1022,13 +977,12 @@ export default function (pi: ExtensionAPI) {
1022
977
  dbg("session_start: no project rules found");
1023
978
  }
1024
979
 
1025
- // TODO/FIXME scan — fast, no deps
980
+ // TODO/FIXME scan — fast, no deps (cached for on-demand reporting)
1026
981
  const todoResult = todoScanner.scanDirectory(cwd);
1027
- const todoReport = todoScanner.formatResult(todoResult);
1028
982
  dbg(`session_start TODO scan: ${todoResult.items.length} items`);
1029
- if (todoReport) parts.push(todoReport);
983
+ // Note: TODOs not shown at session start — use /lens-booboo to see them
1030
984
 
1031
- // Dead code scan — use cache if fresh, auto-install if needed
985
+ // Dead code scan — use cache if fresh, auto-install if needed (cached for on-demand reporting)
1032
986
  if (await knipClient.ensureAvailable()) {
1033
987
  const cached = cacheManager.readCache<
1034
988
  ReturnType<KnipClient["analyze"]>
@@ -1037,23 +991,20 @@ export default function (pi: ExtensionAPI) {
1037
991
  dbg(
1038
992
  `session_start Knip: cache hit (${Math.round((Date.now() - new Date(cached.meta.timestamp).getTime()) / 1000)}s ago)`,
1039
993
  );
1040
- const knipReport = knipClient.formatResult(cached.data);
1041
- if (knipReport) parts.push(knipReport);
1042
994
  } else {
1043
995
  const startMs = Date.now();
1044
996
  const knipResult = knipClient.analyze(cwd);
1045
997
  cacheManager.writeCache("knip", knipResult, cwd, {
1046
998
  scanDurationMs: Date.now() - startMs,
1047
999
  });
1048
- const knipReport = knipClient.formatResult(knipResult);
1049
1000
  dbg(`session_start Knip scan done`);
1050
- if (knipReport) parts.push(knipReport);
1051
1001
  }
1052
1002
  } else {
1053
1003
  dbg(`session_start Knip: not available`);
1054
1004
  }
1005
+ // Note: Knip results not shown at session start — use /lens-booboo to see dead code
1055
1006
 
1056
- // Duplicate code detection — use cache if fresh, auto-install if needed
1007
+ // Duplicate code detection — use cache if fresh, auto-install if needed (cached for on-demand reporting)
1057
1008
  if (await jscpdClient.ensureAvailable()) {
1058
1009
  const cached = cacheManager.readCache<ReturnType<JscpdClient["scan"]>>(
1059
1010
  "jscpd",
@@ -1062,8 +1013,6 @@ export default function (pi: ExtensionAPI) {
1062
1013
  if (cached) {
1063
1014
  dbg(`session_start jscpd: cache hit`);
1064
1015
  _cachedJscpdClones = cached.data.clones;
1065
- const jscpdReport = jscpdClient.formatResult(cached.data);
1066
- if (jscpdReport) parts.push(jscpdReport);
1067
1016
  } else {
1068
1017
  const startMs = Date.now();
1069
1018
  const jscpdResult = jscpdClient.scan(cwd);
@@ -1071,13 +1020,12 @@ export default function (pi: ExtensionAPI) {
1071
1020
  cacheManager.writeCache("jscpd", jscpdResult, cwd, {
1072
1021
  scanDurationMs: Date.now() - startMs,
1073
1022
  });
1074
- const jscpdReport = jscpdClient.formatResult(jscpdResult);
1075
1023
  dbg(`session_start jscpd scan done`);
1076
- if (jscpdReport) parts.push(jscpdReport);
1077
1024
  }
1078
1025
  } else {
1079
1026
  dbg(`session_start jscpd: not available`);
1080
1027
  }
1028
+ // Note: jscpd results not shown at session start — use /lens-booboo to see duplicates
1081
1029
 
1082
1030
  // Note: type-coverage runs on-demand via /lens-booboo only (not at session_start)
1083
1031
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "3.6.0",
3
+ "version": "3.6.2",
4
4
  "type": "module",
5
- "description": "pi extension for real-time code quality31 LSP servers, tree-sitter structural analysis, AST pattern matching, auto-install for TypeScript/Python tooling, duplicate detection, complexity metrics, and inline blockers with comprehensive /lens-booboo reports",
5
+ "description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/apmantza/pi-lens.git"
@@ -143,6 +143,12 @@ pattern: "console.log($$$ARGS)"
143
143
 
144
144
  **Error: "Multiple AST nodes detected"** → Your pattern has multiple code fragments. Use metavariables like `$TEST` instead of literal text in quotes.
145
145
 
146
+ **No matches found?** → Debug efficiently (don't `read` large files):
147
+ 1. Use `ast_grep_search` with same pattern to preview matches - costs same as `read` for small result sets
148
+ 2. Simplify pattern: remove constraints, test basic match first
149
+ 3. Check metavariables capture what you expect (`$PARAM` = whole parameter including name and type)
150
+ 4. Ensure quotes/parentheses balance in pattern
151
+
146
152
  **Fallback:** If pattern fails twice → `grep -rn "pattern" src/`
147
153
 
148
154
  **Debug:** https://ast-grep.github.io/playground.html
@@ -1,78 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import { convertDiagnosticsToTDREntries, } from "./metrics-client.js";
3
- describe("TDR conversion", () => {
4
- test("converts type errors to TDR entries", () => {
5
- const diagnostics = [
6
- {
7
- id: "ts-lsp:TS2345:10",
8
- message: "Argument of type 'string' is not assignable",
9
- filePath: "/test/file.ts",
10
- line: 10,
11
- column: 5,
12
- severity: "error",
13
- semantic: "blocking",
14
- tool: "ts-lsp",
15
- rule: "TS2345",
16
- tdrCategory: "type_errors",
17
- },
18
- ];
19
- const entries = convertDiagnosticsToTDREntries(diagnostics);
20
- expect(entries).toHaveLength(1);
21
- expect(entries[0]).toEqual({
22
- category: "type_errors",
23
- count: 1,
24
- severity: "error",
25
- });
26
- });
27
- test("groups multiple diagnostics by category", () => {
28
- const diagnostics = [
29
- {
30
- id: "1",
31
- message: "Type error 1",
32
- filePath: "/test.ts",
33
- severity: "error",
34
- semantic: "blocking",
35
- tool: "ts-lsp",
36
- tdrCategory: "type_errors",
37
- },
38
- {
39
- id: "2",
40
- message: "Type error 2",
41
- filePath: "/test.ts",
42
- severity: "error",
43
- semantic: "blocking",
44
- tool: "ts-lsp",
45
- tdrCategory: "type_errors",
46
- },
47
- {
48
- id: "3",
49
- message: "Security issue",
50
- filePath: "/test.ts",
51
- severity: "error",
52
- semantic: "blocking",
53
- tool: "ast-grep-napi",
54
- tdrCategory: "security",
55
- },
56
- ];
57
- const entries = convertDiagnosticsToTDREntries(diagnostics);
58
- expect(entries).toHaveLength(2);
59
- expect(entries.find((e) => e.category === "type_errors")?.count).toBe(2);
60
- expect(entries.find((e) => e.category === "security")?.count).toBe(1);
61
- });
62
- test("auto-categorizes diagnostics without tdrCategory", () => {
63
- const diagnostics = [
64
- {
65
- id: "1",
66
- message: "Unused variable",
67
- filePath: "/test.ts",
68
- severity: "warning",
69
- semantic: "warning",
70
- tool: "biome",
71
- rule: "no-unused",
72
- },
73
- ];
74
- const entries = convertDiagnosticsToTDREntries(diagnostics);
75
- expect(entries).toHaveLength(1);
76
- expect(entries[0].category).toBe("dead_code");
77
- });
78
- });