pi-lens 3.1.2 ā 3.2.0
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 +55 -0
- package/README.md +16 -12
- package/clients/ast-grep-client.js +8 -1
- package/clients/ast-grep-client.ts +9 -1
- package/clients/biome-client.js +51 -38
- package/clients/biome-client.ts +60 -58
- package/clients/dependency-checker.js +30 -1
- package/clients/dependency-checker.ts +35 -1
- package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
- package/clients/dispatch/bus-dispatcher.js +15 -14
- package/clients/dispatch/bus-dispatcher.ts +32 -25
- package/clients/dispatch/dispatcher.js +18 -25
- package/clients/dispatch/dispatcher.test.ts +2 -1
- package/clients/dispatch/dispatcher.ts +17 -28
- package/clients/dispatch/plan.js +77 -32
- package/clients/dispatch/plan.ts +78 -32
- package/clients/dispatch/runners/ast-grep-napi.js +36 -376
- package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
- package/clients/dispatch/runners/index.js +8 -4
- package/clients/dispatch/runners/index.ts +8 -4
- package/clients/dispatch/runners/lsp.js +65 -0
- package/clients/dispatch/runners/lsp.ts +125 -0
- package/clients/dispatch/runners/oxlint.js +2 -2
- package/clients/dispatch/runners/oxlint.ts +2 -2
- package/clients/dispatch/runners/pyright.js +24 -8
- package/clients/dispatch/runners/pyright.ts +28 -14
- package/clients/dispatch/runners/rust-clippy.js +2 -2
- package/clients/dispatch/runners/rust-clippy.ts +2 -4
- package/clients/dispatch/runners/tree-sitter.js +14 -2
- package/clients/dispatch/runners/tree-sitter.ts +15 -2
- package/clients/dispatch/runners/ts-lsp.js +3 -3
- package/clients/dispatch/runners/ts-lsp.ts +8 -5
- package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
- package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
- package/clients/dispatch/types.js +3 -0
- package/clients/dispatch/types.ts +3 -0
- package/clients/formatters.js +67 -14
- package/clients/formatters.ts +68 -15
- package/clients/installer/index.js +78 -10
- package/clients/installer/index.ts +519 -426
- package/clients/jscpd-client.js +28 -0
- package/clients/jscpd-client.ts +41 -3
- package/clients/knip-client.js +30 -1
- package/clients/knip-client.ts +34 -2
- package/clients/lsp/__tests__/client.test.ts +64 -41
- package/clients/lsp/__tests__/config.test.ts +25 -17
- package/clients/lsp/__tests__/launch.test.ts +108 -43
- package/clients/lsp/__tests__/service.test.ts +76 -48
- package/clients/lsp/client.js +87 -2
- package/clients/lsp/client.ts +150 -6
- package/clients/lsp/config.js +8 -11
- package/clients/lsp/config.ts +24 -21
- package/clients/lsp/index.js +69 -0
- package/clients/lsp/index.ts +82 -0
- package/clients/lsp/interactive-install.js +19 -8
- package/clients/lsp/interactive-install.ts +52 -27
- package/clients/lsp/launch.js +182 -32
- package/clients/lsp/launch.ts +241 -38
- package/clients/lsp/path-utils.js +3 -46
- package/clients/lsp/path-utils.ts +11 -51
- package/clients/lsp/server.js +93 -71
- package/clients/lsp/server.ts +173 -131
- package/clients/path-utils.js +142 -0
- package/clients/path-utils.ts +153 -0
- package/clients/ruff-client.js +33 -4
- package/clients/ruff-client.ts +44 -13
- package/clients/safe-spawn.js +3 -1
- package/clients/safe-spawn.ts +3 -1
- package/clients/services/effect-integration.js +11 -7
- package/clients/services/effect-integration.ts +34 -26
- package/clients/sg-runner.js +51 -9
- package/clients/sg-runner.ts +58 -15
- package/clients/tree-sitter-client.js +12 -0
- package/clients/tree-sitter-client.ts +12 -0
- package/clients/typescript-client.js +6 -2
- package/clients/typescript-client.ts +9 -2
- package/commands/booboo.js +2 -4
- package/commands/booboo.ts +2 -4
- package/index.ts +377 -93
- package/package.json +2 -1
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
- package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
- package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
- package/tsconfig.json +1 -1
- package/clients/__tests__/file-time.test.js +0 -216
- package/clients/__tests__/format-service.test.js +0 -245
- package/clients/__tests__/formatters.test.js +0 -271
- package/clients/agent-behavior-client.test.js +0 -94
- package/clients/ast-grep-client.test.js +0 -129
- package/clients/ast-grep-client.test.ts +0 -155
- package/clients/biome-client.test.js +0 -144
- package/clients/cache-manager.test.js +0 -197
- package/clients/complexity-client.test.js +0 -234
- package/clients/dependency-checker.test.js +0 -60
- package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
- package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
- package/clients/dispatch/dispatcher.edge.test.js +0 -82
- package/clients/dispatch/dispatcher.format.test.js +0 -46
- package/clients/dispatch/dispatcher.inline.test.js +0 -74
- package/clients/dispatch/dispatcher.test.js +0 -115
- package/clients/dispatch/runners/architect.test.js +0 -138
- package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
- package/clients/dispatch/runners/oxlint.test.js +0 -230
- package/clients/dispatch/runners/pyright.test.js +0 -98
- package/clients/dispatch/runners/python-slop.test.js +0 -203
- package/clients/dispatch/runners/scan_codebase.test.js +0 -89
- package/clients/dispatch/runners/shellcheck.test.js +0 -98
- package/clients/dispatch/runners/spellcheck.test.js +0 -158
- package/clients/dispatch/runners/ts-slop.test.js +0 -180
- package/clients/dispatch/runners/ts-slop.test.ts +0 -230
- package/clients/dogfood.test.js +0 -201
- package/clients/file-kinds.test.js +0 -169
- package/clients/go-client.test.js +0 -127
- package/clients/jscpd-client.test.js +0 -127
- package/clients/knip-client.test.js +0 -112
- package/clients/lsp/__tests__/client.test.js +0 -325
- package/clients/lsp/__tests__/config.test.js +0 -166
- package/clients/lsp/__tests__/error-recovery.test.js +0 -213
- package/clients/lsp/__tests__/integration.test.js +0 -127
- package/clients/lsp/__tests__/launch.test.js +0 -260
- package/clients/lsp/__tests__/server.test.js +0 -259
- package/clients/lsp/__tests__/service.test.js +0 -417
- package/clients/metrics-client.test.js +0 -141
- package/clients/ruff-client.test.js +0 -132
- package/clients/rust-client.test.js +0 -108
- package/clients/sanitize.test.js +0 -177
- package/clients/secrets-scanner.test.js +0 -100
- package/clients/services/__tests__/effect-integration.test.js +0 -86
- package/clients/test-runner-client.test.js +0 -192
- package/clients/todo-scanner.test.js +0 -301
- package/clients/type-coverage-client.test.js +0 -105
- package/clients/typescript-client.codefix.test.js +0 -157
- package/clients/typescript-client.test.js +0 -105
- package/commands/clients/ast-grep-client.js +0 -250
- package/commands/clients/ast-grep-parser.js +0 -86
- package/commands/clients/ast-grep-rule-manager.js +0 -91
- package/commands/clients/ast-grep-types.js +0 -9
- package/commands/clients/biome-client.js +0 -380
- package/commands/clients/complexity-client.js +0 -667
- package/commands/clients/file-kinds.js +0 -177
- package/commands/clients/file-utils.js +0 -40
- package/commands/clients/jscpd-client.js +0 -169
- package/commands/clients/knip-client.js +0 -211
- package/commands/clients/ruff-client.js +0 -297
- package/commands/clients/safe-spawn.js +0 -88
- package/commands/clients/scan-utils.js +0 -83
- package/commands/clients/sg-runner.js +0 -190
- package/commands/clients/types.js +0 -11
- package/commands/clients/typescript-client.js +0 -505
- package/commands/rate.test.js +0 -119
- package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
- package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
- package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
package/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as nodeFs from "node:fs";
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
// RELOADED: Testing format/lsp flow on large file
|
|
4
5
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
5
6
|
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
6
7
|
import { Type } from "@sinclair/typebox";
|
|
@@ -20,6 +21,7 @@ import { ComplexityClient } from "./clients/complexity-client.js";
|
|
|
20
21
|
import { DependencyChecker } from "./clients/dependency-checker.js";
|
|
21
22
|
import { dispatchLintWithBus } from "./clients/dispatch/bus-dispatcher.js";
|
|
22
23
|
import { dispatchLint } from "./clients/dispatch/integration.js";
|
|
24
|
+
import { createFileTime, FileTimeError } from "./clients/file-time.js";
|
|
23
25
|
import {
|
|
24
26
|
getFormatService,
|
|
25
27
|
resetFormatService,
|
|
@@ -584,6 +586,172 @@ export default function (pi: ExtensionAPI) {
|
|
|
584
586
|
},
|
|
585
587
|
});
|
|
586
588
|
|
|
589
|
+
// --- LSP Navigation Tool (requires --lens-lsp) ---
|
|
590
|
+
// Exposes go-to-definition, find-references, hover, documentSymbol, workspaceSymbol, goToImplementation
|
|
591
|
+
pi.registerTool({
|
|
592
|
+
name: "lsp_navigation",
|
|
593
|
+
label: "LSP Navigate",
|
|
594
|
+
description:
|
|
595
|
+
"Navigate code using LSP (Language Server Protocol). Requires --lens-lsp flag.\n" +
|
|
596
|
+
"Operations:\n" +
|
|
597
|
+
"- definition: Jump to where a symbol is defined\n" +
|
|
598
|
+
"- references: Find all usages of a symbol\n" +
|
|
599
|
+
"- hover: Get type/doc info at a position\n" +
|
|
600
|
+
"- documentSymbol: List all symbols (functions/classes/vars) in a file\n" +
|
|
601
|
+
"- workspaceSymbol: Search symbols across the whole project\n" +
|
|
602
|
+
"- implementation: Jump to interface implementations\n\n" +
|
|
603
|
+
"Line and character are 1-based (as shown in editors).",
|
|
604
|
+
promptSnippet:
|
|
605
|
+
"Use lsp_navigation to find definitions, references, and hover info via LSP",
|
|
606
|
+
parameters: Type.Object({
|
|
607
|
+
operation: Type.Union(
|
|
608
|
+
[
|
|
609
|
+
Type.Literal("definition"),
|
|
610
|
+
Type.Literal("references"),
|
|
611
|
+
Type.Literal("hover"),
|
|
612
|
+
Type.Literal("documentSymbol"),
|
|
613
|
+
Type.Literal("workspaceSymbol"),
|
|
614
|
+
Type.Literal("implementation"),
|
|
615
|
+
],
|
|
616
|
+
{ description: "LSP operation to perform" },
|
|
617
|
+
),
|
|
618
|
+
filePath: Type.String({
|
|
619
|
+
description: "Absolute or relative path to the file",
|
|
620
|
+
}),
|
|
621
|
+
line: Type.Optional(
|
|
622
|
+
Type.Number({
|
|
623
|
+
description:
|
|
624
|
+
"Line number (1-based). Required for definition/references/hover/implementation",
|
|
625
|
+
}),
|
|
626
|
+
),
|
|
627
|
+
character: Type.Optional(
|
|
628
|
+
Type.Number({
|
|
629
|
+
description:
|
|
630
|
+
"Character offset (1-based). Required for definition/references/hover/implementation",
|
|
631
|
+
}),
|
|
632
|
+
),
|
|
633
|
+
query: Type.Optional(
|
|
634
|
+
Type.String({
|
|
635
|
+
description: "Symbol name to search. Used by workspaceSymbol",
|
|
636
|
+
}),
|
|
637
|
+
),
|
|
638
|
+
}),
|
|
639
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
640
|
+
if (!pi.getFlag("lens-lsp")) {
|
|
641
|
+
return {
|
|
642
|
+
content: [
|
|
643
|
+
{
|
|
644
|
+
type: "text" as const,
|
|
645
|
+
text: "lsp_navigation requires the --lens-lsp flag. Start pi with --lens-lsp to enable.",
|
|
646
|
+
},
|
|
647
|
+
],
|
|
648
|
+
isError: true,
|
|
649
|
+
details: {},
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const {
|
|
654
|
+
operation,
|
|
655
|
+
filePath: rawPath,
|
|
656
|
+
line,
|
|
657
|
+
character,
|
|
658
|
+
query,
|
|
659
|
+
} = params as {
|
|
660
|
+
operation: string;
|
|
661
|
+
filePath: string;
|
|
662
|
+
line?: number;
|
|
663
|
+
character?: number;
|
|
664
|
+
query?: string;
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
const filePath = path.isAbsolute(rawPath)
|
|
668
|
+
? rawPath
|
|
669
|
+
: path.resolve(ctx.cwd || ".", rawPath);
|
|
670
|
+
|
|
671
|
+
const lspService = getLSPService();
|
|
672
|
+
const hasLSP = await lspService.hasLSP(filePath);
|
|
673
|
+
if (!hasLSP) {
|
|
674
|
+
return {
|
|
675
|
+
content: [
|
|
676
|
+
{
|
|
677
|
+
type: "text" as const,
|
|
678
|
+
text: `No LSP server available for ${path.basename(filePath)}. Check that the language server is installed.`,
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
isError: true,
|
|
682
|
+
details: {},
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Ensure file is open in LSP before querying
|
|
687
|
+
let fileContent: string | undefined;
|
|
688
|
+
try {
|
|
689
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
690
|
+
} catch {
|
|
691
|
+
/* ignore */
|
|
692
|
+
}
|
|
693
|
+
if (fileContent) await lspService.openFile(filePath, fileContent);
|
|
694
|
+
|
|
695
|
+
// Convert 1-based editor coords to 0-based LSP coords
|
|
696
|
+
const lspLine = (line ?? 1) - 1;
|
|
697
|
+
const lspChar = (character ?? 1) - 1;
|
|
698
|
+
|
|
699
|
+
let result: unknown;
|
|
700
|
+
try {
|
|
701
|
+
switch (operation) {
|
|
702
|
+
case "definition":
|
|
703
|
+
result = await lspService.definition(filePath, lspLine, lspChar);
|
|
704
|
+
break;
|
|
705
|
+
case "references":
|
|
706
|
+
result = await lspService.references(filePath, lspLine, lspChar);
|
|
707
|
+
break;
|
|
708
|
+
case "hover":
|
|
709
|
+
result = await lspService.hover(filePath, lspLine, lspChar);
|
|
710
|
+
break;
|
|
711
|
+
case "documentSymbol":
|
|
712
|
+
result = await lspService.documentSymbol(filePath);
|
|
713
|
+
break;
|
|
714
|
+
case "workspaceSymbol":
|
|
715
|
+
result = await lspService.workspaceSymbol(query ?? "");
|
|
716
|
+
break;
|
|
717
|
+
case "implementation":
|
|
718
|
+
result = await lspService.implementation(
|
|
719
|
+
filePath,
|
|
720
|
+
lspLine,
|
|
721
|
+
lspChar,
|
|
722
|
+
);
|
|
723
|
+
break;
|
|
724
|
+
default:
|
|
725
|
+
result = [];
|
|
726
|
+
}
|
|
727
|
+
} catch (err) {
|
|
728
|
+
return {
|
|
729
|
+
content: [
|
|
730
|
+
{
|
|
731
|
+
type: "text" as const,
|
|
732
|
+
text: `LSP error: ${err instanceof Error ? err.message : String(err)}`,
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
isError: true,
|
|
736
|
+
details: {},
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
const isEmpty = !result || (Array.isArray(result) && result.length === 0);
|
|
741
|
+
const output = isEmpty
|
|
742
|
+
? `No results for ${operation} at ${path.basename(filePath)}${line ? `:${line}:${character}` : ""}`
|
|
743
|
+
: JSON.stringify(result, null, 2);
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
content: [{ type: "text" as const, text: output }],
|
|
747
|
+
details: {
|
|
748
|
+
operation,
|
|
749
|
+
resultCount: Array.isArray(result) ? result.length : result ? 1 : 0,
|
|
750
|
+
},
|
|
751
|
+
};
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
|
|
587
755
|
let _cachedJscpdClones: import("./clients/jscpd-client.js").DuplicateClone[] =
|
|
588
756
|
[];
|
|
589
757
|
const cachedExports = new Map<string, string>(); // function name -> file path
|
|
@@ -593,11 +761,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
593
761
|
> = new Map();
|
|
594
762
|
|
|
595
763
|
// Delta baselines: store pre-write diagnostics to diff against post-write
|
|
596
|
-
const
|
|
764
|
+
const _astGrepBaselines = new Map<
|
|
597
765
|
string,
|
|
598
766
|
import("./clients/ast-grep-types.js").AstGrepDiagnostic[]
|
|
599
767
|
>();
|
|
600
|
-
const
|
|
768
|
+
const _biomeBaselines = new Map<
|
|
601
769
|
string,
|
|
602
770
|
import("./clients/biome-client.js").BiomeDiagnostic[]
|
|
603
771
|
>();
|
|
@@ -709,8 +877,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
709
877
|
dbg(`session_start TODO scan: ${todoResult.items.length} items`);
|
|
710
878
|
if (todoReport) parts.push(todoReport);
|
|
711
879
|
|
|
712
|
-
// Dead code scan ā use cache if fresh
|
|
713
|
-
if (knipClient.
|
|
880
|
+
// Dead code scan ā use cache if fresh, auto-install if needed
|
|
881
|
+
if (await knipClient.ensureAvailable()) {
|
|
714
882
|
const cached = cacheManager.readCache<ReturnType<KnipClient["analyze"]>>(
|
|
715
883
|
"knip",
|
|
716
884
|
cwd,
|
|
@@ -735,8 +903,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
735
903
|
dbg(`session_start Knip: not available`);
|
|
736
904
|
}
|
|
737
905
|
|
|
738
|
-
// Duplicate code detection ā use cache if fresh
|
|
739
|
-
if (jscpdClient.
|
|
906
|
+
// Duplicate code detection ā use cache if fresh, auto-install if needed
|
|
907
|
+
if (await jscpdClient.ensureAvailable()) {
|
|
740
908
|
const cached = cacheManager.readCache<ReturnType<JscpdClient["scan"]>>(
|
|
741
909
|
"jscpd",
|
|
742
910
|
cwd,
|
|
@@ -764,7 +932,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
764
932
|
// Note: type-coverage runs on-demand via /lens-booboo only (not at session_start)
|
|
765
933
|
|
|
766
934
|
// Scan for exported functions (cached for duplicate detection on write)
|
|
767
|
-
if (astGrepClient.
|
|
935
|
+
if (await astGrepClient.ensureAvailable()) {
|
|
768
936
|
const exports = await astGrepClient.scanExports(cwd, "typescript");
|
|
769
937
|
dbg(`session_start exports scan: ${exports.size} functions found`);
|
|
770
938
|
for (const [name, file] of exports) {
|
|
@@ -846,12 +1014,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
846
1014
|
|
|
847
1015
|
if (!filePath) return;
|
|
848
1016
|
|
|
1017
|
+
const _toolCallStart = Date.now();
|
|
849
1018
|
dbg(
|
|
850
1019
|
`tool_call fired for: ${filePath} (exists: ${nodeFs.existsSync(filePath)})`,
|
|
851
1020
|
);
|
|
852
1021
|
if (!nodeFs.existsSync(filePath)) return;
|
|
853
1022
|
|
|
854
1023
|
// Record complexity baseline for TS/JS files + capture history snapshot
|
|
1024
|
+
const complexityBaselineStart = Date.now();
|
|
855
1025
|
if (
|
|
856
1026
|
complexityClient.isSupportedFile(filePath) &&
|
|
857
1027
|
!complexityBaselines.has(filePath)
|
|
@@ -867,64 +1037,108 @@ export default function (pi: ExtensionAPI) {
|
|
|
867
1037
|
linesOfCode: baseline.linesOfCode,
|
|
868
1038
|
});
|
|
869
1039
|
}
|
|
1040
|
+
logLatency({
|
|
1041
|
+
type: "phase",
|
|
1042
|
+
toolName: event.toolName,
|
|
1043
|
+
filePath,
|
|
1044
|
+
phase: "complexity_baseline",
|
|
1045
|
+
durationMs: Date.now() - complexityBaselineStart,
|
|
1046
|
+
metadata: { hasBaseline: complexityBaselines.has(filePath) },
|
|
1047
|
+
});
|
|
870
1048
|
}
|
|
871
1049
|
|
|
872
1050
|
const hints: string[] = [];
|
|
1051
|
+
let _preCheckDuration = 0;
|
|
873
1052
|
|
|
874
1053
|
if (/\.(ts|tsx|js|jsx)$/.test(filePath) && !pi.getFlag("no-lsp")) {
|
|
1054
|
+
const tsPreCheckStart = Date.now();
|
|
875
1055
|
tsClient.updateFile(filePath, nodeFs.readFileSync(filePath, "utf-8"));
|
|
876
1056
|
const diags = tsClient.getDiagnostics(filePath);
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1057
|
+
// Pass pre-computed diags to avoid a second getSemanticDiagnostics call (~1s on large files)
|
|
1058
|
+
const fixes = tsClient.getAllCodeFixes(filePath, diags);
|
|
1059
|
+
const errorDiags = diags.filter((d) => d.severity === 1);
|
|
1060
|
+
if (errorDiags.length > 0) {
|
|
1061
|
+
hints.push(
|
|
1062
|
+
`ā Pre-write: file has ${errorDiags.length} TypeScript error(s):`,
|
|
1063
|
+
);
|
|
1064
|
+
// Show first 3 errors with quick fixes
|
|
1065
|
+
for (const d of errorDiags.slice(0, 3)) {
|
|
1066
|
+
const lineFixes = fixes.get(d.range.start.line);
|
|
1067
|
+
const fixHint = lineFixes?.[0]?.description
|
|
1068
|
+
? ` š” ${lineFixes[0].description}`
|
|
1069
|
+
: "";
|
|
881
1070
|
hints.push(
|
|
882
|
-
|
|
1071
|
+
` L${d.range.start.line + 1}: ${d.message.split("\n")[0].substring(0, 80)}${fixHint}`,
|
|
883
1072
|
);
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const fixHint = lineFixes?.[0]?.description
|
|
888
|
-
? ` š” ${lineFixes[0].description}`
|
|
889
|
-
: "";
|
|
890
|
-
hints.push(
|
|
891
|
-
` L${d.range.start.line + 1}: ${d.message.split("\n")[0].substring(0, 80)}${fixHint}`,
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
if (errorDiags.length > 3) {
|
|
895
|
-
hints.push(` ... and ${errorDiags.length - 3} more errors`);
|
|
896
|
-
}
|
|
1073
|
+
}
|
|
1074
|
+
if (errorDiags.length > 3) {
|
|
1075
|
+
hints.push(` ... and ${errorDiags.length - 3} more errors`);
|
|
897
1076
|
}
|
|
898
1077
|
}
|
|
1078
|
+
_preCheckDuration += Date.now() - tsPreCheckStart;
|
|
1079
|
+
logLatency({
|
|
1080
|
+
type: "phase",
|
|
1081
|
+
toolName: event.toolName,
|
|
1082
|
+
filePath,
|
|
1083
|
+
phase: "ts_pre_check",
|
|
1084
|
+
durationMs: Date.now() - tsPreCheckStart,
|
|
1085
|
+
metadata: { errorCount: errorDiags.length, hintCount: hints.length },
|
|
1086
|
+
});
|
|
899
1087
|
}
|
|
900
1088
|
|
|
901
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
.
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1089
|
+
// Unified LSP pre-write check (when --lens-lsp enabled)
|
|
1090
|
+
// This provides pre-write hints for all LSP languages (TypeScript, Python, Go, Rust, etc.)
|
|
1091
|
+
if (pi.getFlag("lens-lsp") && !pi.getFlag("no-lsp")) {
|
|
1092
|
+
const lspPreCheckStart = Date.now();
|
|
1093
|
+
const lspService = getLSPService();
|
|
1094
|
+
lspService
|
|
1095
|
+
.hasLSP(filePath)
|
|
1096
|
+
.then(async (hasLSP) => {
|
|
1097
|
+
if (hasLSP) {
|
|
1098
|
+
const content = nodeFs.readFileSync(filePath, "utf-8");
|
|
1099
|
+
await lspService.openFile(filePath, content);
|
|
1100
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
1101
|
+
const diags = await lspService.getDiagnostics(filePath);
|
|
1102
|
+
const errorDiags = diags.filter((d) => d.severity === 1);
|
|
1103
|
+
if (errorDiags.length > 0) {
|
|
1104
|
+
hints.push(
|
|
1105
|
+
`ā Pre-write: ${errorDiags.length} LSP error(s) detected:`,
|
|
1106
|
+
);
|
|
1107
|
+
for (const d of errorDiags.slice(0, 3)) {
|
|
1108
|
+
hints.push(
|
|
1109
|
+
` L${d.range?.start?.line ?? 0 + 1}: ${d.message.slice(0, 80)}`,
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
if (errorDiags.length > 3) {
|
|
1113
|
+
hints.push(` ... and ${errorDiags.length - 3} more`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
_preCheckDuration += Date.now() - lspPreCheckStart;
|
|
1117
|
+
logLatency({
|
|
1118
|
+
type: "phase",
|
|
1119
|
+
toolName: event.toolName,
|
|
1120
|
+
filePath,
|
|
1121
|
+
phase: "lsp_pre_check",
|
|
1122
|
+
durationMs: Date.now() - lspPreCheckStart,
|
|
1123
|
+
metadata: { hasLSP, errorCount: errorDiags?.length ?? 0 },
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
})
|
|
1127
|
+
.catch((err) => {
|
|
1128
|
+
logLatency({
|
|
1129
|
+
type: "phase",
|
|
1130
|
+
toolName: event.toolName,
|
|
1131
|
+
filePath,
|
|
1132
|
+
phase: "lsp_pre_check",
|
|
1133
|
+
durationMs: Date.now() - lspPreCheckStart,
|
|
1134
|
+
status: "failed",
|
|
1135
|
+
metadata: { error: String(err) },
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
914
1138
|
}
|
|
915
1139
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
biomeClient.isAvailable() &&
|
|
919
|
-
biomeClient.isSupportedFile(filePath)
|
|
920
|
-
) {
|
|
921
|
-
biomeBaselines.set(
|
|
922
|
-
filePath,
|
|
923
|
-
biomeClient
|
|
924
|
-
.checkFile(filePath)
|
|
925
|
-
.filter((d) => d.category === "lint" || d.severity === "error"),
|
|
926
|
-
);
|
|
927
|
-
}
|
|
1140
|
+
// Note: ast-grep baseline removed - we use ast-grep-napi in dispatch instead
|
|
1141
|
+
// Note: biome baseline removed - auto-fix handles this in post-write
|
|
928
1142
|
|
|
929
1143
|
// Architectural rules: Skip pre-write hints (too noisy)
|
|
930
1144
|
// Post-write violations will be shown via architect runner in dispatch
|
|
@@ -989,6 +1203,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
989
1203
|
);
|
|
990
1204
|
return;
|
|
991
1205
|
}
|
|
1206
|
+
|
|
1207
|
+
// --- FileTime assert: prevent stale writes (file modified since agent read it) ---
|
|
1208
|
+
const sessionFileTime = createFileTime("default");
|
|
1209
|
+
try {
|
|
1210
|
+
sessionFileTime.assert(filePath);
|
|
1211
|
+
} catch (err: unknown) {
|
|
1212
|
+
if (err instanceof FileTimeError) {
|
|
1213
|
+
// File was modified externally or never read - warn but don't block (for now)
|
|
1214
|
+
// In strict mode this could block; currently we just surface the warning
|
|
1215
|
+
const warning = `ā ļø FileTime warning: ${err.message}`;
|
|
1216
|
+
dbg(warning);
|
|
1217
|
+
// Don't return - let the operation proceed with warning
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
// Record this write so future assertions know the agent has the current state
|
|
1221
|
+
sessionFileTime.read(filePath);
|
|
992
1222
|
dbg(
|
|
993
1223
|
`tool_result: tracking turn state for ${event.toolName} on ${filePath}`,
|
|
994
1224
|
);
|
|
@@ -1156,7 +1386,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1156
1386
|
// Python: Ruff auto-fix (enabled by default)
|
|
1157
1387
|
if (
|
|
1158
1388
|
!noAutofixRuff &&
|
|
1159
|
-
ruffClient.
|
|
1389
|
+
(await ruffClient.ensureAvailable()) &&
|
|
1160
1390
|
ruffClient.isPythonFile(filePath)
|
|
1161
1391
|
) {
|
|
1162
1392
|
const result = ruffClient.fixFile(filePath);
|
|
@@ -1266,56 +1496,110 @@ export default function (pi: ExtensionAPI) {
|
|
|
1266
1496
|
}
|
|
1267
1497
|
phaseEnd("test_runner", { found: testInfoFound, ran: testRunnerRan });
|
|
1268
1498
|
|
|
1269
|
-
//
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
});
|
|
1293
|
-
|
|
1294
|
-
if (importantDiags.length > 0) {
|
|
1295
|
-
const tsOutput = importantDiags
|
|
1296
|
-
.slice(0, 5)
|
|
1297
|
-
.map((d) => {
|
|
1298
|
-
// DiagnosticSeverity.Error = 1
|
|
1299
|
-
const severity = d.severity === 1 ? "š“" : "š”";
|
|
1300
|
-
return ` ${severity} [TS${(d as any).code}] ${d.message.split("\n")[0]}`;
|
|
1301
|
-
})
|
|
1302
|
-
.join("\n");
|
|
1303
|
-
lspOutput += `\n\nš TypeScript Diagnostics:\n${tsOutput}`;
|
|
1304
|
-
}
|
|
1499
|
+
// Note: TypeScript diagnostics are handled by the ts-lsp dispatch runner above.
|
|
1500
|
+
// No inline TypeScriptClient check here ā dispatch already covers it.
|
|
1501
|
+
|
|
1502
|
+
// --- Complexity tracking (post-write) ---
|
|
1503
|
+
phaseStart("complexity_check");
|
|
1504
|
+
if (complexityClient.isSupportedFile(filePath)) {
|
|
1505
|
+
const oldBaseline = complexityBaselines.get(filePath);
|
|
1506
|
+
const newComplexity = complexityClient.analyzeFile(filePath);
|
|
1507
|
+
if (oldBaseline && newComplexity) {
|
|
1508
|
+
const complexityDelta = {
|
|
1509
|
+
cognitive:
|
|
1510
|
+
newComplexity.cognitiveComplexity - oldBaseline.cognitiveComplexity,
|
|
1511
|
+
maintainability:
|
|
1512
|
+
newComplexity.maintainabilityIndex -
|
|
1513
|
+
oldBaseline.maintainabilityIndex,
|
|
1514
|
+
lines: newComplexity.linesOfCode - oldBaseline.linesOfCode,
|
|
1515
|
+
};
|
|
1516
|
+
// Warn if complexity significantly increased
|
|
1517
|
+
if (
|
|
1518
|
+
complexityDelta.cognitive > 3 ||
|
|
1519
|
+
complexityDelta.maintainability < -5
|
|
1520
|
+
) {
|
|
1521
|
+
lspOutput += `\n\nā ļø Complexity increased: +${complexityDelta.cognitive} cognitive, ${complexityDelta.maintainability.toFixed(1)} maintainability`;
|
|
1305
1522
|
}
|
|
1306
|
-
|
|
1307
|
-
|
|
1523
|
+
phaseEnd("complexity_check", {
|
|
1524
|
+
delta: complexityDelta,
|
|
1525
|
+
warned:
|
|
1526
|
+
complexityDelta.cognitive > 3 ||
|
|
1527
|
+
complexityDelta.maintainability < -5,
|
|
1528
|
+
});
|
|
1529
|
+
} else {
|
|
1530
|
+
phaseEnd("complexity_check", { delta: null, warned: false });
|
|
1308
1531
|
}
|
|
1532
|
+
} else {
|
|
1533
|
+
phaseEnd("complexity_check", { skipped: true });
|
|
1309
1534
|
}
|
|
1310
|
-
phaseEnd("typescript_lsp", {
|
|
1311
|
-
isTsFile: filePath.endsWith(".ts") || filePath.endsWith(".tsx"),
|
|
1312
|
-
});
|
|
1313
1535
|
|
|
1314
1536
|
// Agent behavior warnings (blind writes, thrashing)
|
|
1315
1537
|
if (behaviorWarnings.length > 0) {
|
|
1316
1538
|
lspOutput += `\n\n${agentBehaviorClient.formatWarnings(behaviorWarnings)}`;
|
|
1317
1539
|
}
|
|
1318
1540
|
|
|
1541
|
+
// --- Cascade diagnostics: check other files for errors (when --lens-lsp) ---
|
|
1542
|
+
if (pi.getFlag("lens-lsp") && !pi.getFlag("no-lsp")) {
|
|
1543
|
+
const MAX_CASCADE_FILES = 5;
|
|
1544
|
+
const MAX_DIAGNOSTICS_PER_FILE = 20;
|
|
1545
|
+
const cascadeStart = Date.now();
|
|
1546
|
+
|
|
1547
|
+
try {
|
|
1548
|
+
const lspService = getLSPService();
|
|
1549
|
+
const allDiags = await lspService.getAllDiagnostics();
|
|
1550
|
+
const normalizedEditedPath = path.resolve(filePath);
|
|
1551
|
+
const otherFileErrors: Array<{
|
|
1552
|
+
file: string;
|
|
1553
|
+
errors: import("./clients/lsp/client.js").LSPDiagnostic[];
|
|
1554
|
+
}> = [];
|
|
1555
|
+
|
|
1556
|
+
for (const [diagPath, diags] of allDiags) {
|
|
1557
|
+
if (path.resolve(diagPath) === normalizedEditedPath) continue; // Skip edited file (dispatch already covered it)
|
|
1558
|
+
const errors = diags.filter((d) => d.severity === 1);
|
|
1559
|
+
if (errors.length > 0) {
|
|
1560
|
+
otherFileErrors.push({ file: diagPath, errors });
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (otherFileErrors.length > 0) {
|
|
1565
|
+
lspOutput += `\n\nš Cascade errors detected in ${otherFileErrors.length} other file(s):`;
|
|
1566
|
+
for (const { file, errors } of otherFileErrors.slice(
|
|
1567
|
+
0,
|
|
1568
|
+
MAX_CASCADE_FILES,
|
|
1569
|
+
)) {
|
|
1570
|
+
const limited = errors.slice(0, MAX_DIAGNOSTICS_PER_FILE);
|
|
1571
|
+
const suffix =
|
|
1572
|
+
errors.length > MAX_DIAGNOSTICS_PER_FILE
|
|
1573
|
+
? `\n... and ${errors.length - MAX_DIAGNOSTICS_PER_FILE} more`
|
|
1574
|
+
: "";
|
|
1575
|
+
// Structured XML format (like OpenCode) for cleaner parsing
|
|
1576
|
+
lspOutput += `\n<diagnostics file="${file}">`;
|
|
1577
|
+
for (const e of limited) {
|
|
1578
|
+
const line = (e.range?.start?.line ?? 0) + 1;
|
|
1579
|
+
const col = (e.range?.start?.character ?? 0) + 1;
|
|
1580
|
+
const code = e.code ? ` [${e.code}]` : "";
|
|
1581
|
+
lspOutput += `\n ${code} (${line}:${col}) ${e.message.split("\n")[0].slice(0, 100)}`;
|
|
1582
|
+
}
|
|
1583
|
+
lspOutput += `${suffix}\n</diagnostics>`;
|
|
1584
|
+
}
|
|
1585
|
+
if (otherFileErrors.length > MAX_CASCADE_FILES) {
|
|
1586
|
+
lspOutput += `\n... and ${otherFileErrors.length - MAX_CASCADE_FILES} more files with errors`;
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
logLatency({
|
|
1591
|
+
type: "phase",
|
|
1592
|
+
toolName,
|
|
1593
|
+
filePath,
|
|
1594
|
+
phase: "cascade_diagnostics",
|
|
1595
|
+
durationMs: Date.now() - cascadeStart,
|
|
1596
|
+
metadata: { filesWithErrors: otherFileErrors.length },
|
|
1597
|
+
});
|
|
1598
|
+
} catch (err) {
|
|
1599
|
+
dbg(`cascade diagnostics error: ${err}`);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1319
1603
|
// LATENCY TRACKING: Log timing before returning
|
|
1320
1604
|
const elapsed = Date.now() - toolResultStart;
|
|
1321
1605
|
phaseEnd("total", { hasOutput: !!lspOutput });
|
|
@@ -1419,7 +1703,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1419
1703
|
}
|
|
1420
1704
|
|
|
1421
1705
|
// madge: only check files where imports changed
|
|
1422
|
-
if (depChecker.
|
|
1706
|
+
if (await depChecker.ensureAvailable()) {
|
|
1423
1707
|
const madgeFiles = cacheManager.getFilesForMadge(cwd);
|
|
1424
1708
|
if (madgeFiles.length > 0) {
|
|
1425
1709
|
dbg(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "pi extension for real-time code quality ā 31 LSP servers, tree-sitter structural analysis, AST pattern matching, auto-install for TypeScript/Python tooling, duplicate detection, complexity metrics, and inline blockers with comprehensive /lens-booboo reports",
|
|
6
6
|
"repository": {
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@ast-grep/napi": "^0.42.0",
|
|
59
|
+
"@biomejs/biome": "^2.4.10",
|
|
59
60
|
"@types/node": "^22.10.5",
|
|
60
61
|
"js-yaml": "^4.1.1",
|
|
61
62
|
"typescript": "^5.0.0",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Nested Anchor Tags Detection
|
|
2
|
+
# Detects nested <a> tags which are invalid HTML
|
|
3
|
+
id: no-nested-links
|
|
4
|
+
name: Nested anchor tags
|
|
5
|
+
severity: error
|
|
6
|
+
category: correctness
|
|
7
|
+
language: tsx
|
|
8
|
+
|
|
9
|
+
message: "Nested <a> tags are invalid HTML and cause unexpected behavior"
|
|
10
|
+
|
|
11
|
+
description: |
|
|
12
|
+
HTML does not allow anchor tags nested inside other anchor tags.
|
|
13
|
+
This causes rendering issues and unpredictable click behavior.
|
|
14
|
+
|
|
15
|
+
query: |
|
|
16
|
+
(jsx_element
|
|
17
|
+
open_tag: (jsx_opening_element
|
|
18
|
+
(identifier) @OUTER
|
|
19
|
+
(#eq? @OUTER "a"))
|
|
20
|
+
(jsx_element
|
|
21
|
+
open_tag: (jsx_opening_element
|
|
22
|
+
(identifier) @INNER
|
|
23
|
+
(#eq? @INNER "a"))))
|
|
24
|
+
|
|
25
|
+
metavars:
|
|
26
|
+
- OUTER
|
|
27
|
+
- INNER
|
|
28
|
+
|
|
29
|
+
tags:
|
|
30
|
+
- correctness
|
|
31
|
+
- react
|
|
32
|
+
- jsx
|
|
33
|
+
- html
|
|
34
|
+
|
|
35
|
+
examples:
|
|
36
|
+
bad: |
|
|
37
|
+
<a href="/outer">
|
|
38
|
+
<a href="/inner">Click</a>
|
|
39
|
+
</a>
|
|
40
|
+
|
|
41
|
+
good: |
|
|
42
|
+
<a href="/outer">Outer link</a>
|
|
43
|
+
<a href="/inner">Inner link</a>
|
|
44
|
+
|
|
45
|
+
has_fix: false
|