pi-lens 3.8.23 → 3.8.24

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,16 @@ All notable changes to pi-lens will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [3.8.24] - 2026-04-12
8
+
9
+ ### Changed
10
+ - **Lazy bootstrap client loading** — startup now defers heavy client initialization behind a shared bootstrap promise, reducing first-turn startup overhead while preserving tool behavior.
11
+ - **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.
12
+ - **Ruby server fallback chain** — Ruby LSP startup now tries `ruby-lsp`, then `solargraph`, then `rubocop --lsp` for broader environment compatibility.
13
+
14
+ ### Fixed
15
+ - **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.
16
+
7
17
  ## [3.8.23] - 2026-04-12
8
18
 
9
19
  ### 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
+ }
@@ -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
 
@@ -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
  );
package/index.ts CHANGED
@@ -4,13 +4,9 @@ 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,
@@ -20,15 +16,11 @@ import {
20
16
  import { extractFunctions } from "./clients/dispatch/runners/similarity.js";
21
17
  import { resetFormatService } from "./clients/format-service.js";
22
18
  import { evaluateGitGuard, isGitCommitOrPushAttempt } from "./clients/git-guard.js";
23
- import { GoClient } from "./clients/go-client.js";
24
19
  import { ensureTool } from "./clients/installer/index.js";
25
- import { JscpdClient } from "./clients/jscpd-client.js";
26
- import { KnipClient } from "./clients/knip-client.js";
20
+ import { initLSPConfig } from "./clients/lsp/config.js";
27
21
  import { getLSPService, resetLSPService } from "./clients/lsp/index.js";
28
- import { MetricsClient } from "./clients/metrics-client.js";
29
22
  import { captureSnapshot } from "./clients/metrics-history.js";
30
23
  import { findSimilarFunctions } from "./clients/project-index.js";
31
- import { RuffClient } from "./clients/ruff-client.js";
32
24
  import { RuntimeCoordinator } from "./clients/runtime-coordinator.js";
33
25
  import {
34
26
  consumeSessionStartGuidance,
@@ -38,11 +30,6 @@ import { handleSessionStart } from "./clients/runtime-session.js";
38
30
  import { handleToolResult } from "./clients/runtime-tool-result.js";
39
31
  import { handleTurnEnd } from "./clients/runtime-turn.js";
40
32
  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
33
  import { handleBooboo } from "./commands/booboo.js";
47
34
  import { createAstGrepReplaceTool } from "./tools/ast-grep-replace.js";
48
35
  import { createAstGrepSearchTool } from "./tools/ast-grep-search.js";
@@ -72,11 +59,19 @@ function dbg(msg: string) {
72
59
 
73
60
  let _verbose = false;
74
61
  const runtime = new RuntimeCoordinator();
62
+ const _lspConfigInitializedCwds = new Set<string>();
75
63
 
76
64
  function log(msg: string) {
77
65
  if (_verbose) console.error(`[pi-lens] ${msg}`);
78
66
  }
79
67
 
68
+ async function ensureLSPConfigInitialized(cwd: string): Promise<void> {
69
+ const normalizedCwd = path.resolve(cwd);
70
+ if (_lspConfigInitializedCwds.has(normalizedCwd)) return;
71
+ await initLSPConfig(normalizedCwd);
72
+ _lspConfigInitializedCwds.add(normalizedCwd);
73
+ }
74
+
80
75
  function updateRuntimeIdentityFromEvent(event: unknown): void {
81
76
  const raw = event as {
82
77
  provider?: string;
@@ -136,24 +131,10 @@ function cleanStaleTsBuildInfo(cwd: string): string[] {
136
131
  // --- Extension ---
137
132
 
138
133
  export default function (pi: ExtensionAPI) {
139
- const tsClient = new TypeScriptClient();
140
134
  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
135
  const cacheManager = new CacheManager();
156
136
 
137
+
157
138
  // --- Flags ---
158
139
 
159
140
  pi.registerFlag("lens-verbose", {
@@ -299,8 +280,9 @@ export default function (pi: ExtensionAPI) {
299
280
  pi.registerCommand("lens-booboo", {
300
281
  description:
301
282
  "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(
283
+ handler: async (args, ctx) => {
284
+ const { complexityClient, todoScanner, knipClient, jscpdClient, typeCoverageClient, depChecker, architectClient } = await loadBootstrapClients();
285
+ return handleBooboo(
304
286
  args,
305
287
  ctx,
306
288
  {
@@ -310,11 +292,12 @@ export default function (pi: ExtensionAPI) {
310
292
  knip: knipClient,
311
293
  jscpd: jscpdClient,
312
294
  typeCoverage: typeCoverageClient,
313
- depChecker: depChecker,
295
+ depChecker,
314
296
  architect: architectClient,
315
297
  },
316
298
  pi,
317
- ),
299
+ );
300
+ },
318
301
  });
319
302
 
320
303
  // DISABLED: lens-booboo-fix command - disabled per user request
@@ -482,7 +465,26 @@ pi.on("session_start", async (event, ctx) => {
482
465
  _verbose = !!pi.getFlag("lens-verbose");
483
466
  dbg("session_start fired");
484
467
  updateRuntimeIdentityFromEvent(event);
468
+ try {
469
+ await ensureLSPConfigInitialized(ctx.cwd ?? process.cwd());
470
+ } catch (cfgErr) {
471
+ dbg(`lsp config init failed: ${cfgErr}`);
472
+ }
485
473
 
474
+ const {
475
+ metricsClient,
476
+ todoScanner,
477
+ biomeClient,
478
+ ruffClient,
479
+ knipClient,
480
+ jscpdClient,
481
+ typeCoverageClient,
482
+ depChecker,
483
+ architectClient,
484
+ testRunnerClient,
485
+ goClient,
486
+ rustClient,
487
+ } = await loadBootstrapClients();
486
488
  await handleSessionStart({
487
489
  ctxCwd: ctx.cwd,
488
490
  getFlag: (name: string) => pi.getFlag(name),
@@ -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 (
@@ -711,6 +725,13 @@ pi.on("tool_call", async (event, ctx) => {
711
725
  // biome-ignore lint/suspicious/noExplicitAny: pi.on overload mismatch for tool_result event type
712
726
  (pi as any).on("tool_result", async (event: any) => {
713
727
  updateRuntimeIdentityFromEvent(event);
728
+ const {
729
+ biomeClient,
730
+ ruffClient,
731
+ testRunnerClient,
732
+ metricsClient,
733
+ agentBehaviorClient,
734
+ } = await loadBootstrapClients();
714
735
  return handleToolResult({
715
736
  event: event as any,
716
737
  getFlag: (name: string) => pi.getFlag(name),
@@ -747,6 +768,7 @@ pi.on("turn_start", () => {
747
768
 
748
769
  pi.on("turn_end", async (_event, ctx) => {
749
770
  try {
771
+ const { jscpdClient, knipClient, depChecker } = await loadBootstrapClients();
750
772
  await handleTurnEnd({
751
773
  ctxCwd: ctx.cwd,
752
774
  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.24",
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
  },