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 +19 -0
- package/clients/bootstrap.ts +106 -0
- package/clients/dispatch/runners/similarity.ts +42 -18
- package/clients/lsp/config.ts +17 -9
- package/clients/lsp/launch.ts +2 -0
- package/clients/lsp/server.ts +9 -1
- package/clients/metrics-history.ts +20 -0
- package/clients/runtime-session.ts +54 -36
- package/commands/booboo.ts +4 -24
- package/index.ts +63 -36
- package/package.json +2 -1
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 =
|
|
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
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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 (
|
|
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
|
// ============================================================================
|
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/launch.ts
CHANGED
|
@@ -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
|
|
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
|
);
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
240
|
-
|
|
241
|
-
(
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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(
|
|
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(
|
|
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<
|
|
462
|
-
"
|
|
463
|
-
|
|
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(
|
|
546
|
+
dbg(
|
|
547
|
+
`session_start: skipped project index (${tsFiles.length} files)`,
|
|
548
|
+
);
|
|
531
549
|
}
|
|
532
550
|
}
|
|
533
551
|
});
|
package/commands/booboo.ts
CHANGED
|
@@ -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
|
|
373
|
-
|
|
374
|
-
|
|
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 (
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
},
|