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 +10 -0
- package/clients/bootstrap.ts +106 -0
- package/clients/lsp/config.ts +17 -9
- package/clients/lsp/server.ts +9 -1
- package/index.ts +56 -34
- package/package.json +2 -1
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
|
+
}
|
package/clients/lsp/config.ts
CHANGED
|
@@ -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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
package/clients/lsp/server.ts
CHANGED
|
@@ -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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
},
|