pi-lens 3.8.21 → 3.8.23
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 +28 -0
- package/README.md +2 -0
- package/clients/dispatch/dispatcher.ts +75 -91
- package/clients/dispatch/fact-provider-types.ts +22 -0
- package/clients/dispatch/fact-rule-runner.ts +22 -0
- package/clients/dispatch/fact-runner.ts +28 -0
- package/clients/dispatch/fact-scheduler.ts +78 -0
- package/clients/dispatch/fact-store.ts +67 -0
- package/clients/dispatch/facts/comment-facts.ts +59 -0
- package/clients/dispatch/facts/file-content.ts +20 -0
- package/clients/dispatch/facts/function-facts.ts +177 -0
- package/clients/dispatch/facts/try-catch-facts.ts +80 -0
- package/clients/dispatch/integration.ts +130 -24
- package/clients/dispatch/priorities.ts +22 -0
- package/clients/dispatch/rules/async-noise.ts +43 -0
- package/clients/dispatch/rules/error-obscuring.ts +40 -0
- package/clients/dispatch/rules/error-swallowing.ts +35 -0
- package/clients/dispatch/rules/pass-through-wrappers.ts +52 -0
- package/clients/dispatch/rules/placeholder-comments.ts +47 -0
- package/clients/dispatch/runners/architect.ts +2 -1
- package/clients/dispatch/runners/ast-grep-napi.ts +2 -1
- package/clients/dispatch/runners/biome-check.ts +40 -8
- package/clients/dispatch/runners/biome.ts +2 -1
- package/clients/dispatch/runners/eslint.ts +34 -6
- package/clients/dispatch/runners/go-vet.ts +2 -1
- package/clients/dispatch/runners/golangci-lint.ts +2 -1
- package/clients/dispatch/runners/index.ts +29 -27
- package/clients/dispatch/runners/lsp.ts +60 -4
- package/clients/dispatch/runners/oxlint.ts +2 -1
- package/clients/dispatch/runners/pyright.ts +2 -1
- package/clients/dispatch/runners/python-slop.ts +2 -1
- package/clients/dispatch/runners/rubocop.ts +2 -1
- package/clients/dispatch/runners/ruff.ts +2 -1
- package/clients/dispatch/runners/rust-clippy.ts +2 -1
- package/clients/dispatch/runners/shellcheck.ts +2 -1
- package/clients/dispatch/runners/similarity.ts +2 -1
- package/clients/dispatch/runners/spellcheck.ts +2 -1
- package/clients/dispatch/runners/sqlfluff.ts +2 -1
- package/clients/dispatch/runners/tree-sitter.ts +469 -1
- package/clients/dispatch/runners/ts-lsp.ts +2 -1
- package/clients/dispatch/runners/type-safety.ts +2 -1
- package/clients/dispatch/runners/yamllint.ts +2 -1
- package/clients/dispatch/tool-profile.ts +40 -0
- package/clients/dispatch/types.ts +3 -13
- package/clients/lsp/client.ts +366 -12
- package/clients/lsp/index.ts +374 -76
- package/clients/lsp/launch.ts +42 -2
- package/clients/lsp/server.ts +186 -12
- package/clients/pipeline.ts +2 -2
- package/clients/runtime-context.ts +2 -2
- package/clients/runtime-session.ts +43 -5
- package/clients/session-summary.ts +21 -0
- package/clients/tree-sitter-client.ts +162 -0
- package/clients/tree-sitter-logger.ts +47 -0
- package/clients/tree-sitter-query-loader.ts +13 -2
- package/index.ts +67 -17
- package/package.json +3 -1
- package/rules/rule-catalog.json +64 -0
- package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
- package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
- package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
- package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
- package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
- package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
- package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
- package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
- package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
- package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
- package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
- package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
- package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
- package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
- package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
- package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
- package/scripts/validate-rule-catalog.mjs +227 -0
- package/skills/lsp-navigation/SKILL.md +15 -3
- package/tools/lsp-navigation.js +466 -79
- package/tools/lsp-navigation.ts +587 -85
package/clients/lsp/server.ts
CHANGED
|
@@ -86,6 +86,38 @@ function nodeBinCandidates(root: string, baseName: string): string[] {
|
|
|
86
86
|
return [localBase, baseName];
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
function rubyBinCandidates(baseName: string): string[] {
|
|
90
|
+
const candidates: string[] = [];
|
|
91
|
+
const userProfile = process.env.USERPROFILE;
|
|
92
|
+
if (userProfile) {
|
|
93
|
+
candidates.push(
|
|
94
|
+
path.join(
|
|
95
|
+
userProfile,
|
|
96
|
+
".local",
|
|
97
|
+
"share",
|
|
98
|
+
"mise",
|
|
99
|
+
"installs",
|
|
100
|
+
"ruby",
|
|
101
|
+
"bin",
|
|
102
|
+
`${baseName}.bat`,
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
candidates.push(
|
|
106
|
+
path.join(
|
|
107
|
+
userProfile,
|
|
108
|
+
".asdf",
|
|
109
|
+
"installs",
|
|
110
|
+
"ruby",
|
|
111
|
+
"bin",
|
|
112
|
+
`${baseName}.bat`,
|
|
113
|
+
),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
candidates.push(path.join("C:\\Ruby34-x64", "bin", `${baseName}.bat`));
|
|
117
|
+
candidates.push(path.join("C:\\Ruby33-x64", "bin", `${baseName}.bat`));
|
|
118
|
+
return candidates;
|
|
119
|
+
}
|
|
120
|
+
|
|
89
121
|
async function launchWithDirectOrPackageManager(
|
|
90
122
|
directCommands: string[],
|
|
91
123
|
packageName: string,
|
|
@@ -424,6 +456,55 @@ export const TypeScriptServer: LSPServerInfo = {
|
|
|
424
456
|
},
|
|
425
457
|
};
|
|
426
458
|
|
|
459
|
+
export const DenoServer: LSPServerInfo = {
|
|
460
|
+
id: "deno",
|
|
461
|
+
name: "Deno Language Server",
|
|
462
|
+
installPolicy: "none",
|
|
463
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
|
464
|
+
root: createRootDetector(["deno.json", "deno.jsonc"]),
|
|
465
|
+
async spawn(root) {
|
|
466
|
+
try {
|
|
467
|
+
const proc = await launchLSP("deno", ["lsp"], { cwd: root });
|
|
468
|
+
return { process: proc, source: "direct" };
|
|
469
|
+
} catch {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
export const BiomeLspServer: LSPServerInfo = {
|
|
476
|
+
id: "biome-lsp",
|
|
477
|
+
name: "Biome LSP Proxy",
|
|
478
|
+
installPolicy: "package-manager",
|
|
479
|
+
extensions: [
|
|
480
|
+
".ts",
|
|
481
|
+
".tsx",
|
|
482
|
+
".js",
|
|
483
|
+
".jsx",
|
|
484
|
+
".mjs",
|
|
485
|
+
".cjs",
|
|
486
|
+
".mts",
|
|
487
|
+
".cts",
|
|
488
|
+
".json",
|
|
489
|
+
".jsonc",
|
|
490
|
+
".css",
|
|
491
|
+
".scss",
|
|
492
|
+
".sass",
|
|
493
|
+
".less",
|
|
494
|
+
],
|
|
495
|
+
root: PriorityRoot([["biome.json", "biome.jsonc", "package.json"], [".git"]]),
|
|
496
|
+
async spawn(root, options) {
|
|
497
|
+
const launched = await launchWithDirectOrPackageManager(
|
|
498
|
+
nodeBinCandidates(root, "biome"),
|
|
499
|
+
"@biomejs/biome",
|
|
500
|
+
["lsp-proxy"],
|
|
501
|
+
{ cwd: root, allowInstall: options?.allowInstall },
|
|
502
|
+
);
|
|
503
|
+
if (!launched) return undefined;
|
|
504
|
+
return { process: launched.process, source: launched.source };
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
|
|
427
508
|
export const PythonServer: LSPServerInfo = {
|
|
428
509
|
id: "python",
|
|
429
510
|
name: "Pyright Language Server",
|
|
@@ -507,7 +588,7 @@ export const PythonServer: LSPServerInfo = {
|
|
|
507
588
|
}
|
|
508
589
|
|
|
509
590
|
// Spawn the LSP server
|
|
510
|
-
let proc;
|
|
591
|
+
let proc: LSPProcess;
|
|
511
592
|
if (langserverPath) {
|
|
512
593
|
// Use resolved langserver path
|
|
513
594
|
proc = await launchLSP(langserverPath, ["--stdio"], {
|
|
@@ -567,6 +648,30 @@ export const PythonServer: LSPServerInfo = {
|
|
|
567
648
|
},
|
|
568
649
|
};
|
|
569
650
|
|
|
651
|
+
export const PythonPylspServer: LSPServerInfo = {
|
|
652
|
+
id: "python-pylsp",
|
|
653
|
+
name: "Python LSP Server (pylsp)",
|
|
654
|
+
installPolicy: "none",
|
|
655
|
+
extensions: [".py", ".pyi"],
|
|
656
|
+
root: createRootDetector([
|
|
657
|
+
".git",
|
|
658
|
+
"pyproject.toml",
|
|
659
|
+
"setup.py",
|
|
660
|
+
"setup.cfg",
|
|
661
|
+
"requirements.txt",
|
|
662
|
+
"Pipfile",
|
|
663
|
+
"poetry.lock",
|
|
664
|
+
]),
|
|
665
|
+
async spawn(root) {
|
|
666
|
+
try {
|
|
667
|
+
const proc = await launchLSP("pylsp", [], { cwd: root });
|
|
668
|
+
return { process: proc, source: "direct" };
|
|
669
|
+
} catch {
|
|
670
|
+
return undefined;
|
|
671
|
+
}
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
|
|
570
675
|
export const GoServer: LSPServerInfo = {
|
|
571
676
|
id: "go",
|
|
572
677
|
name: "gopls",
|
|
@@ -642,24 +747,48 @@ export const RubyServer: LSPServerInfo = {
|
|
|
642
747
|
[],
|
|
643
748
|
{ cwd: root, allowInstall: options?.allowInstall },
|
|
644
749
|
async () => {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
["stdio"],
|
|
652
|
-
{ cwd: root, allowInstall: options?.allowInstall },
|
|
653
|
-
);
|
|
654
|
-
if (!fallback) throw new Error("ENOENT: command not found");
|
|
655
|
-
return fallback.process;
|
|
750
|
+
for (const command of ["ruby-lsp", ...rubyBinCandidates("ruby-lsp")]) {
|
|
751
|
+
try {
|
|
752
|
+
return await launchLSP(command, [], { cwd: root });
|
|
753
|
+
} catch {
|
|
754
|
+
// try next ruby-lsp candidate
|
|
755
|
+
}
|
|
656
756
|
}
|
|
757
|
+
|
|
758
|
+
for (const command of ["solargraph", ...rubyBinCandidates("solargraph")]) {
|
|
759
|
+
try {
|
|
760
|
+
return await launchLSP(command, ["stdio"], { cwd: root });
|
|
761
|
+
} catch {
|
|
762
|
+
// try next solargraph candidate
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
throw new Error("ENOENT: command not found");
|
|
657
767
|
},
|
|
658
768
|
);
|
|
659
769
|
return proc ? { process: proc, source: "interactive" } : undefined;
|
|
660
770
|
},
|
|
661
771
|
};
|
|
662
772
|
|
|
773
|
+
export const RubySolargraphServer: LSPServerInfo = {
|
|
774
|
+
id: "ruby-solargraph",
|
|
775
|
+
name: "Solargraph",
|
|
776
|
+
installPolicy: "none",
|
|
777
|
+
extensions: [".rb", ".rake", ".gemspec", ".ru"],
|
|
778
|
+
root: PriorityRoot([["Gemfile", ".ruby-version"], [".git"]]),
|
|
779
|
+
async spawn(root) {
|
|
780
|
+
for (const command of ["solargraph", ...rubyBinCandidates("solargraph")]) {
|
|
781
|
+
try {
|
|
782
|
+
const proc = await launchLSP(command, ["stdio"], { cwd: root });
|
|
783
|
+
return { process: proc, source: "direct" };
|
|
784
|
+
} catch {
|
|
785
|
+
// try next candidate
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return undefined;
|
|
789
|
+
},
|
|
790
|
+
};
|
|
791
|
+
|
|
663
792
|
export const PHPServer: LSPServerInfo = {
|
|
664
793
|
id: "php",
|
|
665
794
|
name: "Intelephense",
|
|
@@ -691,6 +820,16 @@ export const CSharpServer = createInteractiveServer({
|
|
|
691
820
|
command: "csharp-ls",
|
|
692
821
|
});
|
|
693
822
|
|
|
823
|
+
export const OmniSharpServer = createInteractiveServer({
|
|
824
|
+
id: "omnisharp",
|
|
825
|
+
name: "OmniSharp",
|
|
826
|
+
extensions: [".cs"],
|
|
827
|
+
root: createRootDetector([".sln", ".csproj", ".slnx"]),
|
|
828
|
+
language: "csharp",
|
|
829
|
+
command: "OmniSharp",
|
|
830
|
+
args: ["--languageserver"],
|
|
831
|
+
});
|
|
832
|
+
|
|
694
833
|
export const FSharpServer = createInteractiveServer({
|
|
695
834
|
id: "fsharp",
|
|
696
835
|
name: "FSAutocomplete",
|
|
@@ -912,6 +1051,34 @@ export const JsonServer: LSPServerInfo = {
|
|
|
912
1051
|
},
|
|
913
1052
|
};
|
|
914
1053
|
|
|
1054
|
+
export const HtmlServer: LSPServerInfo = {
|
|
1055
|
+
id: "html",
|
|
1056
|
+
name: "VSCode HTML Language Server",
|
|
1057
|
+
installPolicy: "package-manager",
|
|
1058
|
+
extensions: [".html", ".htm"],
|
|
1059
|
+
root: PriorityRoot([["package.json", "index.html", "vite.config.ts"], [".git"]]),
|
|
1060
|
+
async spawn(_root, options) {
|
|
1061
|
+
const launched = await launchWithDirectOrPackageManager(
|
|
1062
|
+
nodeBinCandidates(process.cwd(), "vscode-html-language-server"),
|
|
1063
|
+
"vscode-html-languageserver-bin",
|
|
1064
|
+
["--stdio"],
|
|
1065
|
+
{ cwd: process.cwd(), allowInstall: options?.allowInstall },
|
|
1066
|
+
);
|
|
1067
|
+
if (!launched) return undefined;
|
|
1068
|
+
return { process: launched.process, source: launched.source };
|
|
1069
|
+
},
|
|
1070
|
+
};
|
|
1071
|
+
|
|
1072
|
+
export const TomlServer = createInteractiveServer({
|
|
1073
|
+
id: "toml",
|
|
1074
|
+
name: "Taplo",
|
|
1075
|
+
extensions: [".toml"],
|
|
1076
|
+
root: PriorityRoot([["pyproject.toml", "Cargo.toml", "taplo.toml"], [".git"]]),
|
|
1077
|
+
language: "toml",
|
|
1078
|
+
command: "taplo",
|
|
1079
|
+
args: ["lsp", "stdio"],
|
|
1080
|
+
});
|
|
1081
|
+
|
|
915
1082
|
export const PrismaServer: LSPServerInfo = {
|
|
916
1083
|
id: "prisma",
|
|
917
1084
|
name: "Prisma Language Server",
|
|
@@ -1036,12 +1203,17 @@ export const CssServer: LSPServerInfo = {
|
|
|
1036
1203
|
|
|
1037
1204
|
export const LSP_SERVERS: LSPServerInfo[] = [
|
|
1038
1205
|
TypeScriptServer,
|
|
1206
|
+
DenoServer,
|
|
1207
|
+
BiomeLspServer,
|
|
1039
1208
|
PythonServer,
|
|
1209
|
+
PythonPylspServer,
|
|
1040
1210
|
GoServer,
|
|
1041
1211
|
RustServer,
|
|
1042
1212
|
RubyServer,
|
|
1213
|
+
RubySolargraphServer,
|
|
1043
1214
|
PHPServer,
|
|
1044
1215
|
CSharpServer,
|
|
1216
|
+
OmniSharpServer,
|
|
1045
1217
|
FSharpServer,
|
|
1046
1218
|
JavaServer,
|
|
1047
1219
|
KotlinServer,
|
|
@@ -1061,6 +1233,8 @@ export const LSP_SERVERS: LSPServerInfo[] = [
|
|
|
1061
1233
|
DockerServer,
|
|
1062
1234
|
YamlServer,
|
|
1063
1235
|
JsonServer,
|
|
1236
|
+
HtmlServer,
|
|
1237
|
+
TomlServer,
|
|
1064
1238
|
PrismaServer,
|
|
1065
1239
|
// Web frameworks & styling
|
|
1066
1240
|
VueServer,
|
package/clients/pipeline.ts
CHANGED
|
@@ -672,8 +672,8 @@ export async function runPipeline(
|
|
|
672
672
|
for (const e of limited) {
|
|
673
673
|
const line = (e.range?.start?.line ?? 0) + 1;
|
|
674
674
|
const col = (e.range?.start?.character ?? 0) + 1;
|
|
675
|
-
const code = e.code ? `
|
|
676
|
-
c += `\n ${
|
|
675
|
+
const code = e.code ? ` code=${String(e.code)}` : "";
|
|
676
|
+
c += `\n line ${line}, col ${col}${code}: ${e.message.split("\n")[0].slice(0, 100)}`;
|
|
677
677
|
}
|
|
678
678
|
c += `${suffix}\n</diagnostics>`;
|
|
679
679
|
}
|
|
@@ -29,7 +29,7 @@ export function consumeTurnEndFindings(
|
|
|
29
29
|
export function consumeSessionStartGuidance(
|
|
30
30
|
cacheManager: CacheManager,
|
|
31
31
|
cwd: string,
|
|
32
|
-
): { messages: Array<{ role: "
|
|
32
|
+
): { messages: Array<{ role: "user"; content: string }> } | undefined {
|
|
33
33
|
const guidance = cacheManager.readCache<{ content: string }>(
|
|
34
34
|
"session-start-guidance",
|
|
35
35
|
cwd,
|
|
@@ -45,7 +45,7 @@ export function consumeSessionStartGuidance(
|
|
|
45
45
|
return {
|
|
46
46
|
messages: [
|
|
47
47
|
{
|
|
48
|
-
role: "
|
|
48
|
+
role: "user",
|
|
49
49
|
content: `[pi-lens] Session guidance:\n\n${guidance.data.content}`,
|
|
50
50
|
},
|
|
51
51
|
],
|
|
@@ -62,11 +62,27 @@ interface SessionStartDeps {
|
|
|
62
62
|
resetLSPService: () => void;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
type StartupMode = "full" | "minimal" | "quick";
|
|
66
|
+
|
|
65
67
|
function isCommandAvailable(command: string, args: string[] = ["--version"]): boolean {
|
|
66
68
|
const result = safeSpawn(command, args, { timeout: 5000 });
|
|
67
69
|
return !result.error && result.status === 0;
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
function resolveStartupMode(): StartupMode {
|
|
73
|
+
const envMode = (process.env.PI_LENS_STARTUP_MODE ?? "").trim().toLowerCase();
|
|
74
|
+
if (envMode === "full" || envMode === "minimal" || envMode === "quick") {
|
|
75
|
+
return envMode;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const argv = process.argv;
|
|
79
|
+
if (argv.includes("--print") || argv.includes("-p")) {
|
|
80
|
+
return "quick";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return "full";
|
|
84
|
+
}
|
|
85
|
+
|
|
70
86
|
function getLanguageInstallHints(
|
|
71
87
|
languageProfile: ReturnType<typeof detectProjectLanguageProfile>,
|
|
72
88
|
): string[] {
|
|
@@ -99,6 +115,9 @@ export async function handleSessionStart(
|
|
|
99
115
|
deps: SessionStartDeps,
|
|
100
116
|
): Promise<void> {
|
|
101
117
|
const sessionStartMs = Date.now();
|
|
118
|
+
const startupMode = resolveStartupMode();
|
|
119
|
+
const allowBootstrapTasks = startupMode === "full";
|
|
120
|
+
const quickMode = startupMode === "quick";
|
|
102
121
|
const {
|
|
103
122
|
ctxCwd,
|
|
104
123
|
getFlag,
|
|
@@ -131,10 +150,14 @@ export async function handleSessionStart(
|
|
|
131
150
|
runtime.complexityBaselines.clear();
|
|
132
151
|
resetDispatchBaselines();
|
|
133
152
|
runtime.resetForSession();
|
|
153
|
+
dbg(`session_start startup mode: ${startupMode}`);
|
|
134
154
|
|
|
135
155
|
if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
|
|
136
156
|
resetLSPService();
|
|
137
157
|
dbg("session_start: LSP service reset");
|
|
158
|
+
dbg(
|
|
159
|
+
"session_start: phase0 workspace diagnostics observation enabled (capability probe only)",
|
|
160
|
+
);
|
|
138
161
|
}
|
|
139
162
|
|
|
140
163
|
if (getFlag("auto-install")) {
|
|
@@ -166,7 +189,7 @@ export async function handleSessionStart(
|
|
|
166
189
|
log(`Active tools: ${tools.join(", ")}`);
|
|
167
190
|
dbg(`session_start tools: ${tools.join(", ")}`);
|
|
168
191
|
|
|
169
|
-
if (getFlag("lens-lsp") && !getFlag("no-lsp")) {
|
|
192
|
+
if (allowBootstrapTasks && getFlag("lens-lsp") && !getFlag("no-lsp")) {
|
|
170
193
|
const cleaned = cleanStaleTsBuildInfo(ctxCwd ?? process.cwd());
|
|
171
194
|
if (cleaned.length > 0) {
|
|
172
195
|
notify(
|
|
@@ -179,6 +202,15 @@ export async function handleSessionStart(
|
|
|
179
202
|
|
|
180
203
|
const hasWorkspaceCwd = typeof ctxCwd === "string" && ctxCwd.length > 0;
|
|
181
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
|
+
|
|
182
214
|
const startupScan = resolveStartupScanContext(cwd);
|
|
183
215
|
const scanRoot = startupScan.projectRoot ?? cwd;
|
|
184
216
|
const useScanRootForSignals =
|
|
@@ -217,7 +249,9 @@ export async function handleSessionStart(
|
|
|
217
249
|
return true;
|
|
218
250
|
});
|
|
219
251
|
|
|
220
|
-
if (
|
|
252
|
+
if (!allowBootstrapTasks) {
|
|
253
|
+
dbg("session_start: skipping tool preinstall (startup mode)");
|
|
254
|
+
} else if (startupDefaults.length > 0) {
|
|
221
255
|
dbg(`session_start: pre-install defaults -> ${startupDefaults.join(", ")}`);
|
|
222
256
|
for (const tool of startupDefaults) {
|
|
223
257
|
const startedAt = Date.now();
|
|
@@ -247,7 +281,7 @@ export async function handleSessionStart(
|
|
|
247
281
|
dbg("session_start: no language defaults selected for pre-install");
|
|
248
282
|
}
|
|
249
283
|
|
|
250
|
-
{
|
|
284
|
+
if (allowBootstrapTasks) {
|
|
251
285
|
const pkgPath = path.join(analysisRoot, "package.json");
|
|
252
286
|
try {
|
|
253
287
|
const raw = await nodeFs.promises.readFile(pkgPath, "utf-8");
|
|
@@ -272,6 +306,8 @@ export async function handleSessionStart(
|
|
|
272
306
|
} catch {
|
|
273
307
|
// no package.json at cwd root
|
|
274
308
|
}
|
|
309
|
+
} else {
|
|
310
|
+
dbg("session_start: skipping prettier preinstall probe (startup mode)");
|
|
275
311
|
}
|
|
276
312
|
|
|
277
313
|
const hasArchitectRules = architectClient.loadConfig(analysisRoot);
|
|
@@ -349,7 +385,9 @@ export async function handleSessionStart(
|
|
|
349
385
|
// Each consumer already handles the "not ready yet" case gracefully
|
|
350
386
|
// (cachedExports.size > 0, cachedProjectIndex != null, cache miss paths).
|
|
351
387
|
|
|
352
|
-
if (!
|
|
388
|
+
if (!allowBootstrapTasks) {
|
|
389
|
+
dbg("session_start: skipping startup background scans (startup mode)");
|
|
390
|
+
} else if (!startupScan.canWarmCaches) {
|
|
353
391
|
dbg(
|
|
354
392
|
`session_start: skipping heavy scans (${startupScan.reason ?? "unknown"})`,
|
|
355
393
|
);
|
|
@@ -500,7 +538,7 @@ export async function handleSessionStart(
|
|
|
500
538
|
`session_start: background scans launched (${startupNotes.length} startup note(s))`,
|
|
501
539
|
);
|
|
502
540
|
|
|
503
|
-
const errorDebtEnabled = getFlag("error-debt");
|
|
541
|
+
const errorDebtEnabled = allowBootstrapTasks && getFlag("error-debt");
|
|
504
542
|
const pendingDebt = cacheManager.readCache<{
|
|
505
543
|
pendingCheck: boolean;
|
|
506
544
|
baselineTestsPassed: boolean;
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
|
|
5
5
|
import type { SessionStats } from "./diagnostic-tracker.js";
|
|
6
6
|
|
|
7
|
+
export interface SlopScoreSummary {
|
|
8
|
+
totalRuleDiagnostics: number;
|
|
9
|
+
totalKlocWritten: number;
|
|
10
|
+
scorePerKloc: number;
|
|
11
|
+
ruleCounts: Array<{ ruleId: string; count: number }>;
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
export function formatSessionSummary(stats: SessionStats): string {
|
|
8
15
|
if (stats.totalShown === 0) return "";
|
|
9
16
|
|
|
@@ -30,3 +37,17 @@ function pct(part: number, total: number): string {
|
|
|
30
37
|
if (total === 0) return "0";
|
|
31
38
|
return Math.round((part / total) * 100).toString();
|
|
32
39
|
}
|
|
40
|
+
|
|
41
|
+
export function formatSlopScoreSummary(summary: SlopScoreSummary): string {
|
|
42
|
+
if (summary.totalRuleDiagnostics === 0 || summary.totalKlocWritten <= 0) {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const topRules = summary.ruleCounts.slice(0, 3);
|
|
47
|
+
const detail =
|
|
48
|
+
topRules.length > 0
|
|
49
|
+
? ` (${topRules.map((entry) => `${entry.ruleId} ×${entry.count}`).join(", ")})`
|
|
50
|
+
: "";
|
|
51
|
+
|
|
52
|
+
return `Slop score: ${summary.scorePerKloc.toFixed(1)}/KLOC${detail}`;
|
|
53
|
+
}
|
|
@@ -892,6 +892,26 @@ export class TreeSitterClient {
|
|
|
892
892
|
if (!secretPatterns.some((p) => p.test(varName))) continue;
|
|
893
893
|
}
|
|
894
894
|
|
|
895
|
+
// Go: only keep bare-return-call matches when enclosing function returns error.
|
|
896
|
+
if (postFilter === "returns_error") {
|
|
897
|
+
const first = Object.values(captures)[0];
|
|
898
|
+
if (!first) continue;
|
|
899
|
+
const funcNode = this.navigator.findParent(first, [
|
|
900
|
+
"function_declaration",
|
|
901
|
+
"method_declaration",
|
|
902
|
+
]);
|
|
903
|
+
if (!funcNode) continue;
|
|
904
|
+
|
|
905
|
+
const fnText = String(funcNode.text ?? "");
|
|
906
|
+
const signature = fnText.split("{", 1)[0]?.trim() ?? "";
|
|
907
|
+
const returnPartMatch = signature.match(
|
|
908
|
+
/func\s*(?:\([^)]*\)\s*)?[A-Za-z_]\w*\s*\([^)]*\)\s*(.*)$/s,
|
|
909
|
+
);
|
|
910
|
+
const returnPart = returnPartMatch?.[1]?.trim() ?? "";
|
|
911
|
+
const returnsError = returnPart.length > 0 && /\berror\b/.test(returnPart);
|
|
912
|
+
if (!returnsError) continue;
|
|
913
|
+
}
|
|
914
|
+
|
|
895
915
|
// Python: except body that only contains pass (effectively empty)
|
|
896
916
|
if (postFilter === "python_empty_except") {
|
|
897
917
|
const bodyNode = captures.BODY;
|
|
@@ -921,6 +941,148 @@ export class TreeSitterClient {
|
|
|
921
941
|
}
|
|
922
942
|
}
|
|
923
943
|
|
|
944
|
+
// TS security/concurrency: strict sink filtering (query predicates are not reliable in this runtime)
|
|
945
|
+
if (postFilter === "ts_command_injection_sink") {
|
|
946
|
+
const mod = captures.MOD?.text ?? "";
|
|
947
|
+
const fn = captures.FN?.text ?? "";
|
|
948
|
+
if (mod !== "child_process") continue;
|
|
949
|
+
if (!/^(exec|execSync)$/.test(fn)) continue;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (postFilter === "ts_ssrf_sink") {
|
|
953
|
+
const fn = captures.FN?.text ?? "";
|
|
954
|
+
const obj = captures.OBJ?.text ?? "";
|
|
955
|
+
const allowedFns = new Set([
|
|
956
|
+
"fetch",
|
|
957
|
+
"request",
|
|
958
|
+
"get",
|
|
959
|
+
"post",
|
|
960
|
+
"put",
|
|
961
|
+
"patch",
|
|
962
|
+
"delete",
|
|
963
|
+
]);
|
|
964
|
+
if (!allowedFns.has(fn)) continue;
|
|
965
|
+
|
|
966
|
+
if (!obj) {
|
|
967
|
+
if (fn !== "fetch") continue;
|
|
968
|
+
} else {
|
|
969
|
+
const allowedObjs = new Set([
|
|
970
|
+
"axios",
|
|
971
|
+
"http",
|
|
972
|
+
"https",
|
|
973
|
+
"got",
|
|
974
|
+
"request",
|
|
975
|
+
"superagent",
|
|
976
|
+
"undici",
|
|
977
|
+
]);
|
|
978
|
+
if (!allowedObjs.has(obj)) continue;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (postFilter === "ts_weak_hash_algorithm") {
|
|
983
|
+
const fn = captures.FN?.text ?? "";
|
|
984
|
+
const alg = captures.ALG?.text ?? "";
|
|
985
|
+
if (fn !== "createHash") continue;
|
|
986
|
+
if (!/^(md5|sha1)$/i.test(alg)) continue;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (postFilter === "ts_insecure_random_source") {
|
|
990
|
+
const obj = captures.OBJ?.text ?? "";
|
|
991
|
+
const fn = captures.FN?.text ?? "";
|
|
992
|
+
if (obj !== "Math" || fn !== "random") continue;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (postFilter === "ts_detached_async_call") {
|
|
996
|
+
const fn = captures.FN?.text ?? "";
|
|
997
|
+
if (!/(Async$|fetch$|request$)/.test(fn)) {
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (postFilter === "py_command_injection_sink") {
|
|
1003
|
+
const mod = captures.MOD?.text ?? "";
|
|
1004
|
+
const fn = captures.FN?.text ?? "";
|
|
1005
|
+
const kw = captures.KW?.text ?? "";
|
|
1006
|
+
const isOs = mod === "os" && /^(system|popen)$/.test(fn);
|
|
1007
|
+
const isSubprocess =
|
|
1008
|
+
mod === "subprocess" &&
|
|
1009
|
+
/^(run|Popen|call|check_output|check_call)$/.test(fn) &&
|
|
1010
|
+
kw === "shell";
|
|
1011
|
+
if (!isOs && !isSubprocess) continue;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
if (postFilter === "go_command_injection_sink") {
|
|
1015
|
+
const pkg = captures.PKG?.text ?? "";
|
|
1016
|
+
const fn = captures.FN?.text ?? "";
|
|
1017
|
+
const shell = captures.SHELL?.text ?? "";
|
|
1018
|
+
const flag = captures.FLAG?.text ?? "";
|
|
1019
|
+
if (pkg !== "exec") continue;
|
|
1020
|
+
if (!/^(Command|CommandContext)$/.test(fn)) continue;
|
|
1021
|
+
if (!/^"(sh|bash|zsh|cmd|powershell|pwsh)"$/.test(shell)) continue;
|
|
1022
|
+
if (!/^"(-c|\/c)"$/.test(flag)) continue;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
if (postFilter === "ruby_command_injection_sink") {
|
|
1026
|
+
const fn = captures.FN?.text ?? "";
|
|
1027
|
+
if (!/^(system|exec|spawn|popen|capture3|capture2|capture2e)$/.test(fn)) {
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (postFilter === "py_ssrf_sink") {
|
|
1033
|
+
const mod = captures.MOD?.text ?? "";
|
|
1034
|
+
const fn = captures.FN?.text ?? "";
|
|
1035
|
+
if (mod !== "requests") continue;
|
|
1036
|
+
if (!/^(get|post|put|patch|delete|request|head|options)$/.test(fn))
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (postFilter === "py_path_traversal_sink") {
|
|
1041
|
+
const fn = captures.FN?.text ?? "";
|
|
1042
|
+
if (
|
|
1043
|
+
!/^(open|read_text|read_bytes|write_text|write_bytes|remove|unlink|rmdir)$/.test(
|
|
1044
|
+
fn,
|
|
1045
|
+
)
|
|
1046
|
+
)
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (postFilter === "go_path_traversal_sink") {
|
|
1051
|
+
const pkg = captures.PKG?.text ?? "";
|
|
1052
|
+
const fn = captures.FN?.text ?? "";
|
|
1053
|
+
if (!/^(os|ioutil)$/.test(pkg)) continue;
|
|
1054
|
+
if (!/^(Open|OpenFile|ReadFile|WriteFile|Create|Remove|RemoveAll)$/.test(fn))
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (postFilter === "py_sql_injection_sink") {
|
|
1059
|
+
const fn = captures.FN?.text ?? "";
|
|
1060
|
+
if (!/^(execute|executemany|query|raw)$/.test(fn)) continue;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
if (postFilter === "go_sql_injection_sink") {
|
|
1064
|
+
const dbFn = captures.DBFN?.text ?? "";
|
|
1065
|
+
const fmtPkg = captures.FMTPKG?.text ?? "";
|
|
1066
|
+
const fmtFn = captures.FMTFN?.text ?? "";
|
|
1067
|
+
if (!/^(Query|QueryContext|QueryRow|QueryRowContext|Exec|ExecContext)$/.test(dbFn))
|
|
1068
|
+
continue;
|
|
1069
|
+
if (fmtPkg !== "fmt" || fmtFn !== "Sprintf") continue;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (postFilter === "py_insecure_deserialization_sink") {
|
|
1073
|
+
const mod = captures.MOD?.text ?? "";
|
|
1074
|
+
const fn = captures.FN?.text ?? "";
|
|
1075
|
+
if (!/^(pickle|yaml)$/.test(mod)) continue;
|
|
1076
|
+
if (!/^(load|loads|unsafe_load)$/.test(fn)) continue;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (postFilter === "ruby_insecure_deserialization_sink") {
|
|
1080
|
+
const mod = captures.MOD?.text ?? "";
|
|
1081
|
+
const fn = captures.FN?.text ?? "";
|
|
1082
|
+
if (!/^(Marshal|YAML|Psych)$/.test(mod)) continue;
|
|
1083
|
+
if (!/^(load|unsafe_load)$/.test(fn)) continue;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
924
1086
|
// Use first capture for position info
|
|
925
1087
|
if (match.captures.length > 0) {
|
|
926
1088
|
const firstNode = match.captures[0].node;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
|
|
5
|
+
const TREE_SITTER_LOG_DIR = path.join(os.homedir(), ".pi-lens");
|
|
6
|
+
const TREE_SITTER_LOG_FILE = path.join(TREE_SITTER_LOG_DIR, "tree-sitter.log");
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
if (!fs.existsSync(TREE_SITTER_LOG_DIR)) {
|
|
10
|
+
fs.mkdirSync(TREE_SITTER_LOG_DIR, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
} catch {}
|
|
13
|
+
|
|
14
|
+
export interface TreeSitterLogEntry {
|
|
15
|
+
ts?: string;
|
|
16
|
+
phase:
|
|
17
|
+
| "runner_start"
|
|
18
|
+
| "runner_skip"
|
|
19
|
+
| "queries_loaded"
|
|
20
|
+
| "query_error"
|
|
21
|
+
| "runner_complete"
|
|
22
|
+
| "entity_diff"
|
|
23
|
+
| "blast_radius";
|
|
24
|
+
filePath: string;
|
|
25
|
+
languageId?: string;
|
|
26
|
+
queryId?: string;
|
|
27
|
+
status?: string;
|
|
28
|
+
diagnostics?: number;
|
|
29
|
+
blocking?: number;
|
|
30
|
+
queryCount?: number;
|
|
31
|
+
effectiveQueryCount?: number;
|
|
32
|
+
cacheHit?: boolean;
|
|
33
|
+
reason?: string;
|
|
34
|
+
error?: string;
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function logTreeSitter(entry: TreeSitterLogEntry): void {
|
|
39
|
+
const line = `${JSON.stringify({ ts: new Date().toISOString(), ...entry })}\n`;
|
|
40
|
+
try {
|
|
41
|
+
fs.appendFileSync(TREE_SITTER_LOG_FILE, line);
|
|
42
|
+
} catch {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getTreeSitterLogPath(): string {
|
|
46
|
+
return TREE_SITTER_LOG_FILE;
|
|
47
|
+
}
|