pi-lens 3.8.23 → 3.8.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 CHANGED
@@ -4,6 +4,25 @@ All notable changes to pi-lens will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.8.25] - 2026-04-13
8
+
9
+ ### Changed
10
+ - **Go LSP PATH augmentation on Windows** — LSP subprocess PATH now includes common Go install directories (`C:\Program Files\Go\bin`, `C:\Go\bin`) to prevent `gopls` startup/runtime failures when `go` is not in inherited shell PATH.
11
+ - **Similarity runner cold-start behavior** — similarity now skips fast when no cached project index exists and for tiny/trivial files, reducing write/edit pipeline tail latency and eliminating frequent 30s timeout noise in scratch-file workflows.
12
+
13
+ ### Fixed
14
+ - **Non-git workspace commit lookup noise** — metrics snapshot commit detection now pre-checks repository context before invoking Git, preventing `fatal: not a git repository` terminal noise in non-repo folders.
15
+
16
+ ## [3.8.24] - 2026-04-12
17
+
18
+ ### Changed
19
+ - **Lazy bootstrap client loading** — startup now defers heavy client initialization behind a shared bootstrap promise, reducing first-turn startup overhead while preserving tool behavior.
20
+ - **LSP config discovery scope** — `.pi-lens/lsp.json` (and related config paths) are now resolved from the current directory up through parent directories, improving nested-workspace support.
21
+ - **Ruby server fallback chain** — Ruby LSP startup now tries `ruby-lsp`, then `solargraph`, then `rubocop --lsp` for broader environment compatibility.
22
+
23
+ ### Fixed
24
+ - **LSP config activation timing** — LSP server config initialization now runs reliably at `session_start` and before LSP-backed `tool_call` operations, so server enable/disable overrides apply in one-shot and interactive sessions.
25
+
7
26
  ## [3.8.23] - 2026-04-12
8
27
 
9
28
  ### Added
@@ -0,0 +1,106 @@
1
+ import type { AgentBehaviorClient } from "./agent-behavior-client.js";
2
+ import type { ArchitectClient } from "./architect-client.js";
3
+ import type { BiomeClient } from "./biome-client.js";
4
+ import type { ComplexityClient } from "./complexity-client.js";
5
+ import type { DependencyChecker } from "./dependency-checker.js";
6
+ import type { GoClient } from "./go-client.js";
7
+ import type { JscpdClient } from "./jscpd-client.js";
8
+ import type { KnipClient } from "./knip-client.js";
9
+ import type { MetricsClient } from "./metrics-client.js";
10
+ import type { RuffClient } from "./ruff-client.js";
11
+ import type { RustClient } from "./rust-client.js";
12
+ import type { TestRunnerClient } from "./test-runner-client.js";
13
+ import type { TodoScanner } from "./todo-scanner.js";
14
+ import type { TypeCoverageClient } from "./type-coverage-client.js";
15
+
16
+ export interface BootstrapClients {
17
+ ruffClient: RuffClient;
18
+ biomeClient: BiomeClient;
19
+ knipClient: KnipClient;
20
+ todoScanner: TodoScanner;
21
+ jscpdClient: JscpdClient;
22
+ typeCoverageClient: TypeCoverageClient;
23
+ depChecker: DependencyChecker;
24
+ testRunnerClient: TestRunnerClient;
25
+ metricsClient: MetricsClient;
26
+ complexityClient: ComplexityClient;
27
+ architectClient: ArchitectClient;
28
+ goClient: GoClient;
29
+ rustClient: RustClient;
30
+ agentBehaviorClient: AgentBehaviorClient;
31
+ }
32
+
33
+ let bootstrapPromise: Promise<BootstrapClients> | null = null;
34
+
35
+ function createArchitectFallback(): ArchitectClient {
36
+ return {
37
+ loadConfig: () => false,
38
+ isUserDefined: () => false,
39
+ hasConfig: () => false,
40
+ getRulesForFile: () => [],
41
+ checkFile: () => [],
42
+ checkFileSize: () => null,
43
+ getHints: () => [],
44
+ } as unknown as ArchitectClient;
45
+ }
46
+
47
+ export function loadBootstrapClients(): Promise<BootstrapClients> {
48
+ bootstrapPromise ??= (async () => {
49
+ const [
50
+ ruffMod,
51
+ biomeMod,
52
+ knipMod,
53
+ todoMod,
54
+ jscpdMod,
55
+ typeCoverageMod,
56
+ depCheckerMod,
57
+ testRunnerMod,
58
+ metricsMod,
59
+ complexityMod,
60
+ goMod,
61
+ rustMod,
62
+ agentBehaviorMod,
63
+ ] = await Promise.all([
64
+ import("./ruff-client.js"),
65
+ import("./biome-client.js"),
66
+ import("./knip-client.js"),
67
+ import("./todo-scanner.js"),
68
+ import("./jscpd-client.js"),
69
+ import("./type-coverage-client.js"),
70
+ import("./dependency-checker.js"),
71
+ import("./test-runner-client.js"),
72
+ import("./metrics-client.js"),
73
+ import("./complexity-client.js"),
74
+ import("./go-client.js"),
75
+ import("./rust-client.js"),
76
+ import("./agent-behavior-client.js"),
77
+ ]);
78
+
79
+ let architectClient: ArchitectClient;
80
+ try {
81
+ const architectMod = await import("./architect-client.js");
82
+ architectClient = new architectMod.ArchitectClient();
83
+ } catch {
84
+ architectClient = createArchitectFallback();
85
+ }
86
+
87
+ return {
88
+ ruffClient: new ruffMod.RuffClient(),
89
+ biomeClient: new biomeMod.BiomeClient(),
90
+ knipClient: new knipMod.KnipClient(),
91
+ todoScanner: new todoMod.TodoScanner(),
92
+ jscpdClient: new jscpdMod.JscpdClient(),
93
+ typeCoverageClient: new typeCoverageMod.TypeCoverageClient(),
94
+ depChecker: new depCheckerMod.DependencyChecker(),
95
+ testRunnerClient: new testRunnerMod.TestRunnerClient(),
96
+ metricsClient: new metricsMod.MetricsClient(),
97
+ complexityClient: new complexityMod.ComplexityClient(),
98
+ architectClient,
99
+ goClient: new goMod.GoClient(),
100
+ rustClient: new rustMod.RustClient(),
101
+ agentBehaviorClient: new agentBehaviorMod.AgentBehaviorClient(),
102
+ };
103
+ })();
104
+
105
+ return bootstrapPromise;
106
+ }
@@ -9,8 +9,8 @@ import * as nodeFs from "node:fs";
9
9
  import * as fs from "node:fs/promises";
10
10
  import * as path from "node:path";
11
11
  import * as ts from "typescript";
12
- import { EXCLUDED_DIRS } from "../../file-utils.js";
13
12
  import { NativeRustCoreClient } from "../../native-rust-client.js";
13
+ import { collectSourceFiles } from "../../source-filter.js";
14
14
  import {
15
15
  buildProjectIndex,
16
16
  findSimilarFunctions,
@@ -40,6 +40,7 @@ const CONFIG = {
40
40
  SIMILARITY_THRESHOLD: 0.96, // align with booboo: stricter to reduce boilerplate false positives
41
41
  MIN_TRANSITIONS: 40, // stronger signal floor for structural comparisons
42
42
  MIN_FUNCTION_LINES: 8, // Ignore tiny helpers/wrappers
43
+ MIN_FILE_CHARS: 140, // Skip tiny/trivial files early
43
44
  MAX_TRANSITION_RATIO: 1.8, // Skip pairs with highly mismatched complexity/size
44
45
  MAX_SUGGESTIONS: 3, // Max 3 suggestions per file
45
46
  MAX_PER_TARGET_NAME: 1, // Avoid one-to-many spam for the same target utility
@@ -115,12 +116,26 @@ const similarityRunner: RunnerDefinition = {
115
116
  return { status: "skipped", diagnostics: [], semantic: "none" };
116
117
  }
117
118
 
119
+ const lineCount = content.split(/\r?\n/).length;
120
+ if (
121
+ content.trim().length < CONFIG.MIN_FILE_CHARS ||
122
+ lineCount < CONFIG.MIN_FUNCTION_LINES + 2 ||
123
+ !/(\bfunction\b|=>)/.test(content)
124
+ ) {
125
+ return { status: "skipped", diagnostics: [], semantic: "none" };
126
+ }
127
+
118
128
  // Find project root and load index
119
129
  const projectRoot = await findProjectRoot(filePath);
120
130
  if (!projectRoot) {
121
131
  return { status: "skipped", diagnostics: [], semantic: "none" };
122
132
  }
123
133
 
134
+ const cachedIndex = await loadCachedIndex(projectRoot);
135
+ if (!cachedIndex || cachedIndex.entries.size === 0) {
136
+ return { status: "skipped", diagnostics: [], semantic: "none" };
137
+ }
138
+
124
139
  // ── Rust fast-path ─────────────────────────────────────────────────────
125
140
  // Try Rust for file scanning + similarity detection. If the Rust binary
126
141
  // is available, use it. On any failure, fall through to the pure-TS path.
@@ -139,10 +154,7 @@ const similarityRunner: RunnerDefinition = {
139
154
  }
140
155
  // ── TypeScript fallback ─────────────────────────────────────────────────
141
156
 
142
- const index = await loadOrBuildIndex(projectRoot);
143
- if (!index || index.entries.size === 0) {
144
- return { status: "skipped", diagnostics: [], semantic: "none" };
145
- }
157
+ const index = cachedIndex;
146
158
 
147
159
  // Parse the file
148
160
  const sourceFile = ts.createSourceFile(
@@ -512,30 +524,42 @@ async function loadOrBuildIndex(
512
524
  }
513
525
 
514
526
  // Build new index
515
- const { glob } = await import("glob");
516
- // Build ignore patterns from centralized EXCLUDED_DIRS
517
- const ignorePatterns = [
518
- ...EXCLUDED_DIRS.map((d) => `**/${d}/**`),
519
- "**/*.test.ts",
520
- "**/*.spec.ts",
521
- "**/*.poc.test.ts",
522
- ];
523
- const files = await glob("**/*.ts", {
524
- cwd: projectRoot,
525
- ignore: ignorePatterns,
527
+ const absoluteFiles = collectSourceFiles(projectRoot, {
528
+ extensions: [".ts"],
529
+ }).filter((filePath) => {
530
+ const normalized = filePath.replace(/\\/g, "/");
531
+ return (
532
+ !normalized.endsWith(".test.ts") &&
533
+ !normalized.endsWith(".spec.ts") &&
534
+ !normalized.endsWith(".poc.test.ts")
535
+ );
526
536
  });
527
537
 
528
- if (files.length === 0) {
538
+ if (absoluteFiles.length === 0) {
529
539
  return null;
530
540
  }
531
541
 
532
- const absoluteFiles = files.map((f) => path.join(projectRoot, f));
533
542
  const index = await buildProjectIndex(projectRoot, absoluteFiles);
534
543
 
535
544
  indexCache.set(projectRoot, index);
536
545
  return index;
537
546
  }
538
547
 
548
+ async function loadCachedIndex(projectRoot: string): Promise<ProjectIndex | null> {
549
+ const cached = indexCache.get(projectRoot);
550
+ if (cached) {
551
+ return cached;
552
+ }
553
+
554
+ const existing = await loadIndex(projectRoot);
555
+ if (!existing) {
556
+ return null;
557
+ }
558
+
559
+ indexCache.set(projectRoot, existing);
560
+ return existing;
561
+ }
562
+
539
563
  // ============================================================================
540
564
  // Testing Helper
541
565
  // ============================================================================
@@ -55,17 +55,25 @@ const CONFIG_PATHS = [".pi-lens/lsp.json", ".pi-lens.json", "pi-lsp.json"];
55
55
  * Load LSP configuration from file
56
56
  */
57
57
  export async function loadLSPConfig(cwd: string): Promise<LSPConfig> {
58
- for (const configPath of CONFIG_PATHS) {
59
- const fullPath = path.join(cwd, configPath);
60
- try {
61
- const content = await fs.readFile(fullPath, "utf-8");
62
- const config = JSON.parse(content) as LSPConfig;
63
- console.error(`[lsp-config] Loaded config from ${configPath}`);
64
- return config;
65
- } catch {
66
- // File doesn't exist or is invalid, try next
58
+ let dir = path.resolve(cwd);
59
+ while (true) {
60
+ for (const configPath of CONFIG_PATHS) {
61
+ const fullPath = path.join(dir, configPath);
62
+ try {
63
+ const content = await fs.readFile(fullPath, "utf-8");
64
+ const config = JSON.parse(content) as LSPConfig;
65
+ console.error(`[lsp-config] Loaded config from ${fullPath}`);
66
+ return config;
67
+ } catch {
68
+ // File doesn't exist or is invalid, try next
69
+ }
67
70
  }
71
+
72
+ const parent = path.dirname(dir);
73
+ if (parent === dir) break;
74
+ dir = parent;
68
75
  }
76
+
69
77
  return {};
70
78
  }
71
79
 
@@ -35,6 +35,8 @@ function buildAugmentedPath(basePath?: string): string {
35
35
  candidates.push(path.join(userProfile, ".cargo", "bin"));
36
36
  candidates.push(path.join(userProfile, "go", "bin"));
37
37
  }
38
+ candidates.push(path.join("C:\\", "Program Files", "Go", "bin"));
39
+ candidates.push(path.join("C:\\", "Go", "bin"));
38
40
  candidates.push(path.join("C:\\", "Ruby34-x64", "bin"));
39
41
  candidates.push(path.join("C:\\", "Ruby33-x64", "bin"));
40
42
 
@@ -740,7 +740,7 @@ export const RubyServer: LSPServerInfo = {
740
740
  extensions: [".rb", ".rake", ".gemspec", ".ru"],
741
741
  root: PriorityRoot([["Gemfile", ".ruby-version"], [".git"]]),
742
742
  async spawn(root, options) {
743
- // Try ruby-lsp first (prompts to install via gem if missing), fall back to solargraph
743
+ // Try ruby-lsp first, then solargraph, then rubocop --lsp
744
744
  const proc = await spawnWithInteractiveInstall(
745
745
  "ruby",
746
746
  "ruby-lsp",
@@ -763,6 +763,14 @@ export const RubyServer: LSPServerInfo = {
763
763
  }
764
764
  }
765
765
 
766
+ for (const command of ["rubocop", ...rubyBinCandidates("rubocop")]) {
767
+ try {
768
+ return await launchLSP(command, ["--lsp"], { cwd: root });
769
+ } catch {
770
+ // try next rubocop candidate
771
+ }
772
+ }
773
+
766
774
  throw new Error("ENOENT: command not found");
767
775
  },
768
776
  );
@@ -45,10 +45,30 @@ const MAX_HISTORY_PER_FILE = 20;
45
45
 
46
46
  // --- Git Helpers ---
47
47
 
48
+ /**
49
+ * Check whether cwd is inside a Git worktree.
50
+ */
51
+ function isInsideGitRepo(startDir: string): boolean {
52
+ let dir = path.resolve(startDir);
53
+ while (true) {
54
+ if (fs.existsSync(path.join(dir, ".git"))) {
55
+ return true;
56
+ }
57
+ const parent = path.dirname(dir);
58
+ if (parent === dir) break;
59
+ dir = parent;
60
+ }
61
+ return false;
62
+ }
63
+
48
64
  /**
49
65
  * Get current git commit hash (short)
50
66
  */
51
67
  function getCurrentCommit(): string {
68
+ if (!isInsideGitRepo(process.cwd())) {
69
+ return "unknown";
70
+ }
71
+
52
72
  try {
53
73
  return execSync("git rev-parse --short HEAD", {
54
74
  encoding: "utf-8",
@@ -10,13 +10,13 @@ import { getKnipIgnorePatterns } from "./file-utils.js";
10
10
  import type { GoClient } from "./go-client.js";
11
11
  import type { JscpdClient } from "./jscpd-client.js";
12
12
  import type { KnipClient } from "./knip-client.js";
13
+ import { canRunStartupHeavyScans } from "./language-policy.js";
13
14
  import {
14
15
  detectProjectLanguageProfile,
15
16
  getDefaultStartupTools,
16
17
  hasLanguage,
17
18
  isLanguageConfigured,
18
19
  } from "./language-profile.js";
19
- import { canRunStartupHeavyScans } from "./language-policy.js";
20
20
  import type { MetricsClient } from "./metrics-client.js";
21
21
  import {
22
22
  buildProjectIndex,
@@ -64,7 +64,10 @@ interface SessionStartDeps {
64
64
 
65
65
  type StartupMode = "full" | "minimal" | "quick";
66
66
 
67
- function isCommandAvailable(command: string, args: string[] = ["--version"]): boolean {
67
+ function isCommandAvailable(
68
+ command: string,
69
+ args: string[] = ["--version"],
70
+ ): boolean {
68
71
  const result = safeSpawn(command, args, { timeout: 5000 });
69
72
  return !result.error && result.status === 0;
70
73
  }
@@ -97,7 +100,9 @@ function getLanguageInstallHints(
97
100
  };
98
101
 
99
102
  if (hasStrongSignal("go") && !isCommandAvailable("gopls")) {
100
- hints.push("Go detected: install gopls (`go install golang.org/x/tools/gopls@latest`).");
103
+ hints.push(
104
+ "Go detected: install gopls (`go install golang.org/x/tools/gopls@latest`).",
105
+ );
101
106
  }
102
107
  if (hasStrongSignal("rust") && !isCommandAvailable("rust-analyzer")) {
103
108
  hints.push(
@@ -174,6 +179,25 @@ export async function handleSessionStart(
174
179
  delete process.env.PI_LENS_DISABLE_LSP_INSTALL;
175
180
  }
176
181
 
182
+ const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
183
+ const cwd = ctxCwd ?? process.cwd();
184
+ if (quickMode) {
185
+ runtime.projectRoot = cwd;
186
+ const quickTools: string[] = [];
187
+ if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
188
+ quickTools.push("LSP Service");
189
+ }
190
+ log(`Active tools: ${quickTools.join(", ")}`);
191
+ dbg(
192
+ `session_start tools: ${quickTools.join(", ") || "deferred (quick mode)"}`,
193
+ );
194
+ dbg(
195
+ "session_start: quick mode active - skipping slow tool probes, language profiling, preinstall, scans, and error debt baseline",
196
+ );
197
+ dbg(`session_start total: ${Date.now() - sessionStartMs}ms`);
198
+ return;
199
+ }
200
+
177
201
  const tools: string[] = [];
178
202
  if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
179
203
  tools.push("LSP Service");
@@ -200,17 +224,6 @@ export async function handleSessionStart(
200
224
  }
201
225
  }
202
226
 
203
- const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
204
- const cwd = ctxCwd ?? process.cwd();
205
- if (quickMode) {
206
- runtime.projectRoot = cwd;
207
- dbg(
208
- "session_start: quick mode active - skipping language profiling, preinstall, scans, and error debt baseline",
209
- );
210
- dbg(`session_start total: ${Date.now() - sessionStartMs}ms`);
211
- return;
212
- }
213
-
214
227
  const startupScan = resolveStartupScanContext(cwd);
215
228
  const scanRoot = startupScan.projectRoot ?? cwd;
216
229
  const useScanRootForSignals =
@@ -236,18 +249,20 @@ export async function handleSessionStart(
236
249
  }
237
250
 
238
251
  const lensLspEnabled = !!getFlag("lens-lsp") && !getFlag("no-lsp");
239
- const startupDefaults = getDefaultStartupTools(languageProfile).filter((tool) => {
240
- if (
241
- (tool === "typescript-language-server" || tool === "pyright") &&
242
- !lensLspEnabled
243
- ) {
244
- return false;
245
- }
246
- if (tool === "ruff" && getFlag("no-autofix-ruff")) {
247
- return false;
248
- }
249
- return true;
250
- });
252
+ const startupDefaults = getDefaultStartupTools(languageProfile).filter(
253
+ (tool) => {
254
+ if (
255
+ (tool === "typescript-language-server" || tool === "pyright") &&
256
+ !lensLspEnabled
257
+ ) {
258
+ return false;
259
+ }
260
+ if (tool === "ruff" && getFlag("no-autofix-ruff")) {
261
+ return false;
262
+ }
263
+ return true;
264
+ },
265
+ );
251
266
 
252
267
  if (!allowBootstrapTasks) {
253
268
  dbg("session_start: skipping tool preinstall (startup mode)");
@@ -369,7 +384,9 @@ export async function handleSessionStart(
369
384
  runtime.markStartupScanInFlight(name, sessionGeneration);
370
385
  void task()
371
386
  .then(() => {
372
- dbg(`session_start task ${name}: success (${Date.now() - startedAt}ms)`);
387
+ dbg(
388
+ `session_start task ${name}: success (${Date.now() - startedAt}ms)`,
389
+ );
373
390
  })
374
391
  .catch((err) => {
375
392
  dbg(`session_start: ${name} background scan failed: ${err}`);
@@ -391,7 +408,9 @@ export async function handleSessionStart(
391
408
  dbg(
392
409
  `session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`,
393
410
  );
394
- dbg(`session_start: skipping TODO scan (${startupScan.reason ?? "unknown"})`);
411
+ dbg(
412
+ `session_start: skipping TODO scan (${startupScan.reason ?? "unknown"})`,
413
+ );
395
414
  } else {
396
415
  const canRunJsTsHeavyScans = canRunStartupHeavyScans(
397
416
  languageProfile,
@@ -401,9 +420,7 @@ export async function handleSessionStart(
401
420
  if (canRunJsTsHeavyScans) {
402
421
  scanNames.push("knip", "jscpd", "ast-grep exports", "project index");
403
422
  }
404
- dbg(
405
- `session_start: launching background scans (${scanNames.join(", ")})`,
406
- );
423
+ dbg(`session_start: launching background scans (${scanNames.join(", ")})`);
407
424
 
408
425
  runStartupTask("todo", async () => {
409
426
  if (!runtime.isCurrentSession(sessionGeneration)) return;
@@ -458,10 +475,9 @@ export async function handleSessionStart(
458
475
  runStartupTask("jscpd", async () => {
459
476
  if (await jscpdClient.ensureAvailable()) {
460
477
  if (!runtime.isCurrentSession(sessionGeneration)) return;
461
- const cached = cacheManager.readCache<ReturnType<JscpdClient["scan"]>>(
462
- "jscpd",
463
- analysisRoot,
464
- );
478
+ const cached = cacheManager.readCache<
479
+ ReturnType<JscpdClient["scan"]>
480
+ >("jscpd", analysisRoot);
465
481
  if (cached) {
466
482
  if (!runtime.isCurrentSession(sessionGeneration)) return;
467
483
  dbg("session_start jscpd: cache hit");
@@ -527,7 +543,9 @@ export async function handleSessionStart(
527
543
  );
528
544
  } else {
529
545
  if (!runtime.isCurrentSession(sessionGeneration)) return;
530
- dbg(`session_start: skipped project index (${tsFiles.length} files)`);
546
+ dbg(
547
+ `session_start: skipped project index (${tsFiles.length} files)`,
548
+ );
531
549
  }
532
550
  }
533
551
  });
@@ -369,33 +369,13 @@ export async function handleBooboo(
369
369
  // Runner 3: Semantic similarity
370
370
  await tracker.run("semantic similarity (Amain)", async () => {
371
371
  try {
372
- const { glob } = await import("glob");
373
- const sourceFiles = await glob("**/*.ts", {
374
- cwd: targetPath,
375
- ignore: [
376
- "**/node_modules/**",
377
- "**/*.test.ts",
378
- "**/*.test.tsx",
379
- "**/*.spec.ts",
380
- "**/*.spec.tsx",
381
- "**/*.poc.test.ts",
382
- "**/*.poc.test.tsx",
383
- "**/test-utils.ts",
384
- "**/test-*.ts",
385
- "**/__tests__/**",
386
- "**/tests/**",
387
- "**/dist/**",
388
- ],
389
- });
372
+ const absoluteFiles = collectSourceFiles(targetPath, {
373
+ extensions: [".ts"],
374
+ }).filter(shouldIncludeFile);
390
375
 
391
- if (sourceFiles.length === 0) {
376
+ if (absoluteFiles.length === 0) {
392
377
  return { findings: 0, status: "done" };
393
378
  }
394
-
395
- // Filter out test files using centralized exclusion
396
- const absoluteFiles = sourceFiles
397
- .map((f) => path.join(targetPath, f))
398
- .filter(shouldIncludeFile);
399
379
  const index = await buildProjectIndex(targetPath, absoluteFiles);
400
380
  const topPairs = findTopSimilarPairs(index, 10);
401
381
 
package/index.ts CHANGED
@@ -4,31 +4,22 @@ import * as path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
6
  import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
7
- import { AgentBehaviorClient } from "./clients/agent-behavior-client.js";
8
- import { ArchitectClient } from "./clients/architect-client.js";
9
7
  import { AstGrepClient } from "./clients/ast-grep-client.js";
10
- import { BiomeClient } from "./clients/biome-client.js";
11
8
  import { CacheManager } from "./clients/cache-manager.js";
12
- import { ComplexityClient } from "./clients/complexity-client.js";
13
- import { DependencyChecker } from "./clients/dependency-checker.js";
9
+ import { loadBootstrapClients } from "./clients/bootstrap.js";
14
10
  import { getDiagnosticTracker } from "./clients/diagnostic-tracker.js";
15
11
  import {
16
12
  getDispatchSlopScoreLine,
17
13
  getLatencyReports,
18
14
  resetDispatchBaselines,
19
15
  } from "./clients/dispatch/integration.js";
20
- import { extractFunctions } from "./clients/dispatch/runners/similarity.js";
21
16
  import { resetFormatService } from "./clients/format-service.js";
22
17
  import { evaluateGitGuard, isGitCommitOrPushAttempt } from "./clients/git-guard.js";
23
- import { GoClient } from "./clients/go-client.js";
24
18
  import { ensureTool } from "./clients/installer/index.js";
25
- import { JscpdClient } from "./clients/jscpd-client.js";
26
- import { KnipClient } from "./clients/knip-client.js";
19
+ import { initLSPConfig } from "./clients/lsp/config.js";
27
20
  import { getLSPService, resetLSPService } from "./clients/lsp/index.js";
28
- import { MetricsClient } from "./clients/metrics-client.js";
29
21
  import { captureSnapshot } from "./clients/metrics-history.js";
30
22
  import { findSimilarFunctions } from "./clients/project-index.js";
31
- import { RuffClient } from "./clients/ruff-client.js";
32
23
  import { RuntimeCoordinator } from "./clients/runtime-coordinator.js";
33
24
  import {
34
25
  consumeSessionStartGuidance,
@@ -38,11 +29,6 @@ import { handleSessionStart } from "./clients/runtime-session.js";
38
29
  import { handleToolResult } from "./clients/runtime-tool-result.js";
39
30
  import { handleTurnEnd } from "./clients/runtime-turn.js";
40
31
  import { formatRulesForPrompt } from "./clients/rules-scanner.js";
41
- import { RustClient } from "./clients/rust-client.js";
42
- import { TestRunnerClient } from "./clients/test-runner-client.js";
43
- import { TodoScanner } from "./clients/todo-scanner.js";
44
- import { TypeCoverageClient } from "./clients/type-coverage-client.js";
45
- import { TypeScriptClient } from "./clients/typescript-client.js";
46
32
  import { handleBooboo } from "./commands/booboo.js";
47
33
  import { createAstGrepReplaceTool } from "./tools/ast-grep-replace.js";
48
34
  import { createAstGrepSearchTool } from "./tools/ast-grep-search.js";
@@ -72,11 +58,19 @@ function dbg(msg: string) {
72
58
 
73
59
  let _verbose = false;
74
60
  const runtime = new RuntimeCoordinator();
61
+ const _lspConfigInitializedCwds = new Set<string>();
75
62
 
76
63
  function log(msg: string) {
77
64
  if (_verbose) console.error(`[pi-lens] ${msg}`);
78
65
  }
79
66
 
67
+ async function ensureLSPConfigInitialized(cwd: string): Promise<void> {
68
+ const normalizedCwd = path.resolve(cwd);
69
+ if (_lspConfigInitializedCwds.has(normalizedCwd)) return;
70
+ await initLSPConfig(normalizedCwd);
71
+ _lspConfigInitializedCwds.add(normalizedCwd);
72
+ }
73
+
80
74
  function updateRuntimeIdentityFromEvent(event: unknown): void {
81
75
  const raw = event as {
82
76
  provider?: string;
@@ -136,24 +130,10 @@ function cleanStaleTsBuildInfo(cwd: string): string[] {
136
130
  // --- Extension ---
137
131
 
138
132
  export default function (pi: ExtensionAPI) {
139
- const tsClient = new TypeScriptClient();
140
133
  const astGrepClient = new AstGrepClient();
141
- const ruffClient = new RuffClient();
142
- const biomeClient = new BiomeClient();
143
- const knipClient = new KnipClient();
144
- const todoScanner = new TodoScanner();
145
- const jscpdClient = new JscpdClient();
146
- const typeCoverageClient = new TypeCoverageClient();
147
- const depChecker = new DependencyChecker();
148
- const testRunnerClient = new TestRunnerClient();
149
- const metricsClient = new MetricsClient();
150
- const complexityClient = new ComplexityClient();
151
- const architectClient = new ArchitectClient();
152
- const goClient = new GoClient();
153
- const rustClient = new RustClient();
154
- const agentBehaviorClient = new AgentBehaviorClient();
155
134
  const cacheManager = new CacheManager();
156
135
 
136
+
157
137
  // --- Flags ---
158
138
 
159
139
  pi.registerFlag("lens-verbose", {
@@ -299,8 +279,9 @@ export default function (pi: ExtensionAPI) {
299
279
  pi.registerCommand("lens-booboo", {
300
280
  description:
301
281
  "Full codebase review: design smells, complexity, AI slop detection, TODOs, dead code, duplicates, type coverage. Results saved to .pi-lens/reviews/. Usage: /lens-booboo [path]",
302
- handler: (args, ctx) =>
303
- handleBooboo(
282
+ handler: async (args, ctx) => {
283
+ const { complexityClient, todoScanner, knipClient, jscpdClient, typeCoverageClient, depChecker, architectClient } = await loadBootstrapClients();
284
+ return handleBooboo(
304
285
  args,
305
286
  ctx,
306
287
  {
@@ -310,11 +291,12 @@ export default function (pi: ExtensionAPI) {
310
291
  knip: knipClient,
311
292
  jscpd: jscpdClient,
312
293
  typeCoverage: typeCoverageClient,
313
- depChecker: depChecker,
294
+ depChecker,
314
295
  architect: architectClient,
315
296
  },
316
297
  pi,
317
- ),
298
+ );
299
+ },
318
300
  });
319
301
 
320
302
  // DISABLED: lens-booboo-fix command - disabled per user request
@@ -482,7 +464,26 @@ pi.on("session_start", async (event, ctx) => {
482
464
  _verbose = !!pi.getFlag("lens-verbose");
483
465
  dbg("session_start fired");
484
466
  updateRuntimeIdentityFromEvent(event);
467
+ try {
468
+ await ensureLSPConfigInitialized(ctx.cwd ?? process.cwd());
469
+ } catch (cfgErr) {
470
+ dbg(`lsp config init failed: ${cfgErr}`);
471
+ }
485
472
 
473
+ const {
474
+ metricsClient,
475
+ todoScanner,
476
+ biomeClient,
477
+ ruffClient,
478
+ knipClient,
479
+ jscpdClient,
480
+ typeCoverageClient,
481
+ depChecker,
482
+ architectClient,
483
+ testRunnerClient,
484
+ goClient,
485
+ rustClient,
486
+ } = await loadBootstrapClients();
486
487
  await handleSessionStart({
487
488
  ctxCwd: ctx.cwd,
488
489
  getFlag: (name: string) => pi.getFlag(name),
@@ -504,7 +505,8 @@ pi.on("session_start", async (event, ctx) => {
504
505
  testRunnerClient,
505
506
  goClient,
506
507
  rustClient,
507
- ensureTool,
508
+ ensureTool: async (name: string) =>
509
+ (await import("./clients/installer/index.js")).ensureTool(name),
508
510
  cleanStaleTsBuildInfo,
509
511
  resetDispatchBaselines,
510
512
  resetLSPService,
@@ -546,6 +548,17 @@ pi.on("tool_call", async (event, ctx) => {
546
548
  : path.resolve(ctx.cwd ?? runtime.projectRoot, rawFilePath)
547
549
  : undefined;
548
550
 
551
+ if (pi.getFlag("lens-lsp") && !pi.getFlag("no-lsp")) {
552
+ try {
553
+ const configCwd = filePath
554
+ ? path.dirname(filePath)
555
+ : (ctx.cwd ?? runtime.projectRoot ?? process.cwd());
556
+ await ensureLSPConfigInitialized(configCwd);
557
+ } catch (cfgErr) {
558
+ dbg(`lsp config init failed during tool_call: ${cfgErr}`);
559
+ }
560
+ }
561
+
549
562
  if (!filePath) return;
550
563
 
551
564
  dbg(
@@ -571,6 +584,7 @@ pi.on("tool_call", async (event, ctx) => {
571
584
  }
572
585
  }
573
586
 
587
+ const { complexityClient } = await loadBootstrapClients();
574
588
  // Record complexity baseline for historical tracking (booboo/tdi).
575
589
  // Not shown inline — just captured for delta analysis.
576
590
  if (
@@ -580,6 +594,9 @@ pi.on("tool_call", async (event, ctx) => {
580
594
  const baseline = complexityClient.analyzeFile(filePath);
581
595
  if (baseline) {
582
596
  runtime.complexityBaselines.set(filePath, baseline);
597
+ const { captureSnapshot } = await import(
598
+ "./clients/metrics-history.js"
599
+ );
583
600
  captureSnapshot(filePath, {
584
601
  maintainabilityIndex: baseline.maintainabilityIndex,
585
602
  cognitiveComplexity: baseline.cognitiveComplexity,
@@ -645,6 +662,8 @@ pi.on("tool_call", async (event, ctx) => {
645
662
  ts.ScriptTarget.Latest,
646
663
  true,
647
664
  );
665
+ const { extractFunctions } = await import("./clients/dispatch/runners/similarity.js");
666
+ const { findSimilarFunctions } = await import("./clients/project-index.js");
648
667
  const newFunctions = extractFunctions(sourceFile, newContent);
649
668
  const simWarnings: string[] = [];
650
669
  let simHintsTruncated = false;
@@ -711,6 +730,13 @@ pi.on("tool_call", async (event, ctx) => {
711
730
  // biome-ignore lint/suspicious/noExplicitAny: pi.on overload mismatch for tool_result event type
712
731
  (pi as any).on("tool_result", async (event: any) => {
713
732
  updateRuntimeIdentityFromEvent(event);
733
+ const {
734
+ biomeClient,
735
+ ruffClient,
736
+ testRunnerClient,
737
+ metricsClient,
738
+ agentBehaviorClient,
739
+ } = await loadBootstrapClients();
714
740
  return handleToolResult({
715
741
  event: event as any,
716
742
  getFlag: (name: string) => pi.getFlag(name),
@@ -747,6 +773,7 @@ pi.on("turn_start", () => {
747
773
 
748
774
  pi.on("turn_end", async (_event, ctx) => {
749
775
  try {
776
+ const { jscpdClient, knipClient, depChecker } = await loadBootstrapClients();
750
777
  await handleTurnEnd({
751
778
  ctxCwd: ctx.cwd,
752
779
  getFlag: (name: string) => pi.getFlag(name),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-lens",
3
- "version": "3.8.23",
3
+ "version": "3.8.25",
4
4
  "type": "module",
5
5
  "description": "Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo",
6
6
  "repository": {
@@ -75,6 +75,7 @@
75
75
  "dependencies": {
76
76
  "@sinclair/typebox": "^0.34.0",
77
77
  "cross-spawn": "^7.0.6",
78
+ "minimatch": "^10.2.5",
78
79
  "typescript": "^5.0.0",
79
80
  "vscode-jsonrpc": "^8.2.1"
80
81
  },