pi-lens 2.1.0 ā 2.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 +27 -0
- package/README.md +70 -1
- package/clients/ast-grep-client.js +12 -12
- package/clients/ast-grep-client.ts +21 -11
- package/clients/dispatch/dispatcher.js +2 -2
- package/clients/dispatch/dispatcher.ts +2 -2
- package/clients/dispatch/runners/index.js +3 -1
- package/clients/dispatch/runners/index.ts +3 -1
- package/clients/dispatch/runners/pyright.js +68 -0
- package/clients/dispatch/runners/pyright.test.js +84 -0
- package/clients/dispatch/runners/pyright.test.ts +109 -0
- package/clients/dispatch/runners/pyright.ts +102 -0
- package/clients/dispatch/runners/secrets.js +109 -0
- package/clients/secrets-scanner.js +113 -0
- package/clients/secrets-scanner.test.js +100 -0
- package/clients/secrets-scanner.test.ts +113 -0
- package/clients/secrets-scanner.ts +134 -0
- package/clients/sg-runner.js +15 -2
- package/clients/sg-runner.ts +25 -2
- package/commands/fix.js +48 -50
- package/commands/fix.ts +71 -61
- package/commands/rate.js +285 -0
- package/commands/rate.test.js +119 -0
- package/commands/rate.test.ts +131 -0
- package/commands/rate.ts +348 -0
- package/commands/refactor.js +33 -9
- package/commands/refactor.ts +44 -11
- package/default-architect.yaml +7 -0
- package/index.ts +58 -10
- package/package.json +1 -1
- package/rules/ast-grep-rules/rules/no-default-export.yml +19 -0
- package/rules/ast-grep-rules/rules/no-hardcoded-secrets.yml +9 -6
- package/rules/ast-grep-rules/rules/no-process-env.yml +12 -12
- package/rules/ast-grep-rules/rules/no-relative-imports.yml +21 -0
package/commands/refactor.ts
CHANGED
|
@@ -9,7 +9,6 @@ import type { AstGrepClient } from "../clients/ast-grep-client.js";
|
|
|
9
9
|
import { createAutoLoop } from "../clients/auto-loop.js";
|
|
10
10
|
import type { ComplexityClient } from "../clients/complexity-client.js";
|
|
11
11
|
import {
|
|
12
|
-
extractCodeSnippet,
|
|
13
12
|
scanArchitectViolations,
|
|
14
13
|
scanComplexityMetrics,
|
|
15
14
|
scanSkipViolations,
|
|
@@ -103,18 +102,33 @@ export async function handleRefactor(
|
|
|
103
102
|
return;
|
|
104
103
|
}
|
|
105
104
|
|
|
105
|
+
// --- Write ranked list to TSV for agent reference ---
|
|
106
|
+
const reportDir = path.join(process.cwd(), ".pi-lens", "reports");
|
|
107
|
+
nodeFs.mkdirSync(reportDir, { recursive: true });
|
|
108
|
+
const reportPath = path.join(reportDir, "refactor-ranked.tsv");
|
|
109
|
+
|
|
110
|
+
const tsvRows: string[] = [
|
|
111
|
+
"rank\tfile\tscore\tmi\tcognitive\tnesting\tviolations",
|
|
112
|
+
];
|
|
113
|
+
scored.slice(0, 50).forEach((f, i) => {
|
|
114
|
+
const m = metricsByFile.get(f.file);
|
|
115
|
+
const skipCount = skipByFile.get(f.file)?.length ?? 0;
|
|
116
|
+
const archCount = architectViolations?.get(f.file)?.length ?? 0;
|
|
117
|
+
const totalViolations = skipCount + archCount;
|
|
118
|
+
const relPath = path.relative(targetPath, f.file).replace(/\\/g, "/");
|
|
119
|
+
tsvRows.push(
|
|
120
|
+
`${i + 1}\t${relPath}\t${f.score}\t${m?.mi.toFixed(1) ?? "-"}\t${m?.cognitive ?? "-"}\t${m?.nesting ?? "-"}\t${totalViolations}`,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
nodeFs.writeFileSync(reportPath, tsvRows.join("\n"), "utf-8");
|
|
124
|
+
|
|
125
|
+
// --- Current worst offender ---
|
|
106
126
|
const { file: worstFile, score } = scored[0];
|
|
107
127
|
const relFile = path.relative(targetPath, worstFile).replace(/\\/g, "/");
|
|
108
128
|
const issues = skipByFile.get(worstFile) ?? [];
|
|
109
129
|
const metrics = metricsByFile.get(worstFile);
|
|
110
130
|
const archIssues = architectViolations.get(worstFile) ?? [];
|
|
111
131
|
|
|
112
|
-
const snippetResult =
|
|
113
|
-
issues.length > 0 ? extractCodeSnippet(worstFile, issues[0].line) : null;
|
|
114
|
-
const snippet = snippetResult?.snippet ?? "";
|
|
115
|
-
const snippetStart = snippetResult?.start ?? 1;
|
|
116
|
-
const snippetEnd = snippetResult?.end ?? 1;
|
|
117
|
-
|
|
118
132
|
const ruleGroups = new Map<string, number>();
|
|
119
133
|
for (const i of issues)
|
|
120
134
|
ruleGroups.set(i.rule, (ruleGroups.get(i.rule) ?? 0) + 1);
|
|
@@ -131,6 +145,27 @@ export async function handleRefactor(
|
|
|
131
145
|
? `MI: ${metrics.mi.toFixed(1)}, Cognitive: ${metrics.cognitive}, Nesting: ${metrics.nesting}`
|
|
132
146
|
: "";
|
|
133
147
|
|
|
148
|
+
// First violation line for quick reference
|
|
149
|
+
const firstViolationLine = issues.length > 0 ? issues[0].line : null;
|
|
150
|
+
|
|
151
|
+
// --- Compact terminal summary ---
|
|
152
|
+
const topFiles = scored
|
|
153
|
+
.slice(0, 5)
|
|
154
|
+
.map((f, i) => {
|
|
155
|
+
const name = path.relative(targetPath, f.file).replace(/\\/g, "/");
|
|
156
|
+
return ` ${i + 1}. ${name} (score: ${f.score})`;
|
|
157
|
+
})
|
|
158
|
+
.join("\n");
|
|
159
|
+
|
|
160
|
+
ctx.ui.notify(
|
|
161
|
+
`šļø Worst: ${relFile} (score: ${score}) ā ${scored.length} files with debt`,
|
|
162
|
+
"info",
|
|
163
|
+
);
|
|
164
|
+
console.log(
|
|
165
|
+
`\nš Top ${Math.min(scored.length, 5)} worst offenders:\n${topFiles}\nš Full ranked list: .pi-lens/reports/refactor-ranked.tsv\n`,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// --- Steer message for agent ---
|
|
134
169
|
const steer = [
|
|
135
170
|
`šļø BOOBOO REFACTOR ā worst offender identified`,
|
|
136
171
|
"",
|
|
@@ -142,11 +177,9 @@ export async function handleRefactor(
|
|
|
142
177
|
archIssues.length > 0
|
|
143
178
|
? `**Architectural rules violated**:\n${archSummary}`
|
|
144
179
|
: "",
|
|
180
|
+
firstViolationLine ? `First violation at line ${firstViolationLine}` : "",
|
|
145
181
|
"",
|
|
146
|
-
|
|
147
|
-
"```typescript",
|
|
148
|
-
snippet,
|
|
149
|
-
"```",
|
|
182
|
+
`š Full details: .pi-lens/reports/refactor-ranked.tsv ā read \`${relFile}\` when ready`,
|
|
150
183
|
"",
|
|
151
184
|
"**Your job**:",
|
|
152
185
|
"1. Analyze this code ā what's the most impactful refactoring for this file?",
|
package/default-architect.yaml
CHANGED
|
@@ -67,6 +67,13 @@ rules:
|
|
|
67
67
|
- pattern: '\.then\('
|
|
68
68
|
message: "Prefer async/await over .then() chains for better readability and error handling."
|
|
69
69
|
|
|
70
|
+
# --- Grep-ability & Agent Search ---
|
|
71
|
+
# Note: 'export default' is acceptable for entry points (index.ts)
|
|
72
|
+
- pattern: "**/*.{ts,tsx}"
|
|
73
|
+
must_not:
|
|
74
|
+
- pattern: "from\s+['\"]\.\./\.\./\.\./"
|
|
75
|
+
message: "Avoid deep relative imports (3+ levels) ā use absolute imports (@app/...) for agent reasoning."
|
|
76
|
+
|
|
70
77
|
# =============================================================================
|
|
71
78
|
# PYTHON-SPECIFIC RULES
|
|
72
79
|
# =============================================================================
|
package/index.ts
CHANGED
|
@@ -31,12 +31,14 @@ import {
|
|
|
31
31
|
} from "./clients/rules-scanner.js";
|
|
32
32
|
import { RustClient } from "./clients/rust-client.js";
|
|
33
33
|
import { getSourceFiles } from "./clients/scan-utils.js";
|
|
34
|
+
import { formatSecrets, scanForSecrets } from "./clients/secrets-scanner.js";
|
|
34
35
|
import { TestRunnerClient } from "./clients/test-runner-client.js";
|
|
35
36
|
import { TodoScanner } from "./clients/todo-scanner.js";
|
|
36
37
|
import { TypeCoverageClient } from "./clients/type-coverage-client.js";
|
|
37
38
|
import { TypeScriptClient } from "./clients/typescript-client.js";
|
|
38
39
|
import { handleBooboo } from "./commands/booboo.js";
|
|
39
40
|
import { handleFix } from "./commands/fix.js";
|
|
41
|
+
import { handleRate } from "./commands/rate.js";
|
|
40
42
|
import { handleRefactor, initRefactorLoop } from "./commands/refactor.js";
|
|
41
43
|
|
|
42
44
|
/** Parse a diff to extract modified line ranges in the new file.
|
|
@@ -618,6 +620,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
618
620
|
},
|
|
619
621
|
});
|
|
620
622
|
|
|
623
|
+
pi.registerCommand("lens-rate", {
|
|
624
|
+
description:
|
|
625
|
+
"Show code quality score with visual breakdown. Usage: /lens-rate [path]",
|
|
626
|
+
handler: async (args, ctx) => {
|
|
627
|
+
const result = await handleRate(args, ctx, {
|
|
628
|
+
complexity: complexityClient,
|
|
629
|
+
knip: knipClient,
|
|
630
|
+
typeCoverage: typeCoverageClient,
|
|
631
|
+
architect: architectClient,
|
|
632
|
+
});
|
|
633
|
+
ctx.ui.notify(result, "info");
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
|
|
621
637
|
pi.registerCommand("lens-format", {
|
|
622
638
|
description:
|
|
623
639
|
"Apply Biome formatting to files. Usage: /lens-format [file-path] or /lens-format --all",
|
|
@@ -707,7 +723,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
707
723
|
name: "ast_grep_search",
|
|
708
724
|
label: "AST Search",
|
|
709
725
|
description:
|
|
710
|
-
"Search code using AST-aware pattern matching. IMPORTANT: Use specific AST patterns, NOT text search. Examples:\n- Find function: 'function $NAME() { $$$BODY }'\n- Find call: 'fetchMetrics($ARGS)'\n- Find import: 'import { $NAMES } from \"$PATH\"'\n- Generic identifier (broad): 'fetchMetrics'\n\nAlways prefer specific patterns with context over bare identifiers. Use 'paths' to scope to specific files/folders.",
|
|
726
|
+
"Search code using AST-aware pattern matching. IMPORTANT: Use specific AST patterns, NOT text search. Examples:\n- Find function: 'function $NAME() { $$$BODY }'\n- Find call: 'fetchMetrics($ARGS)'\n- Find import: 'import { $NAMES } from \"$PATH\"'\n- Generic identifier (broad): 'fetchMetrics'\n\nAlways prefer specific patterns with context over bare identifiers. Use 'paths' to scope to specific files/folders. Use 'selector' to extract specific nodes (e.g., just the function name). Use 'context' to show surrounding lines.",
|
|
711
727
|
promptSnippet: "Use ast_grep_search for AST-aware code search",
|
|
712
728
|
parameters: Type.Object({
|
|
713
729
|
pattern: Type.String({
|
|
@@ -722,6 +738,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
722
738
|
description: "Specific files/folders to search",
|
|
723
739
|
}),
|
|
724
740
|
),
|
|
741
|
+
selector: Type.Optional(
|
|
742
|
+
Type.String({
|
|
743
|
+
description:
|
|
744
|
+
"Extract specific AST node kind (e.g., 'name', 'body', 'parameter'). Use with patterns like '$NAME($$$)' to extract just the name.",
|
|
745
|
+
}),
|
|
746
|
+
),
|
|
747
|
+
context: Type.Optional(
|
|
748
|
+
Type.Number({
|
|
749
|
+
description: "Show N lines before/after each match for context",
|
|
750
|
+
}),
|
|
751
|
+
),
|
|
725
752
|
}),
|
|
726
753
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
727
754
|
if (!astGrepClient.isAvailable()) {
|
|
@@ -737,13 +764,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
737
764
|
};
|
|
738
765
|
}
|
|
739
766
|
|
|
740
|
-
const { pattern, lang, paths } = params as {
|
|
767
|
+
const { pattern, lang, paths, selector, context } = params as {
|
|
741
768
|
pattern: string;
|
|
742
769
|
lang: string;
|
|
743
770
|
paths?: string[];
|
|
771
|
+
selector?: string;
|
|
772
|
+
context?: number;
|
|
744
773
|
};
|
|
745
774
|
const searchPaths = paths?.length ? paths : [ctx.cwd || "."];
|
|
746
|
-
const result = await astGrepClient.search(pattern, lang, searchPaths
|
|
775
|
+
const result = await astGrepClient.search(pattern, lang, searchPaths, {
|
|
776
|
+
selector,
|
|
777
|
+
context,
|
|
778
|
+
});
|
|
747
779
|
|
|
748
780
|
if (result.error) {
|
|
749
781
|
return {
|
|
@@ -824,11 +856,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
824
856
|
}
|
|
825
857
|
|
|
826
858
|
const isDryRun = !apply;
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
859
|
+
const output = astGrepClient.formatMatches(
|
|
860
|
+
result.matches,
|
|
861
|
+
isDryRun,
|
|
862
|
+
true, // showModeIndicator
|
|
863
|
+
);
|
|
832
864
|
|
|
833
865
|
return {
|
|
834
866
|
content: [{ type: "text", text: output }],
|
|
@@ -1227,13 +1259,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
1227
1259
|
|
|
1228
1260
|
// Record write for metrics (silent tracking)
|
|
1229
1261
|
|
|
1262
|
+
let fileContent: string | undefined;
|
|
1230
1263
|
try {
|
|
1231
|
-
|
|
1232
|
-
metricsClient.recordWrite(filePath,
|
|
1264
|
+
fileContent = nodeFs.readFileSync(filePath, "utf-8");
|
|
1265
|
+
metricsClient.recordWrite(filePath, fileContent);
|
|
1233
1266
|
} catch (err) {
|
|
1234
1267
|
void err;
|
|
1235
1268
|
}
|
|
1236
1269
|
|
|
1270
|
+
// --- Secrets scan (blocking - must check before other linting) ---
|
|
1271
|
+
if (fileContent) {
|
|
1272
|
+
const secretFindings = scanForSecrets(fileContent);
|
|
1273
|
+
if (secretFindings.length > 0) {
|
|
1274
|
+
const secretsOutput = formatSecrets(secretFindings, filePath);
|
|
1275
|
+
return {
|
|
1276
|
+
content: [
|
|
1277
|
+
...event.content,
|
|
1278
|
+
{ type: "text" as const, text: `\n\n${secretsOutput}` },
|
|
1279
|
+
],
|
|
1280
|
+
isError: true,
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1237
1285
|
let lspOutput = preHint ? `\n\n${preHint}` : "";
|
|
1238
1286
|
|
|
1239
1287
|
// --- Declarative dispatch: run all applicable lint tools ---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time code quality feedback for pi ā TypeScript LSP, Biome, ast-grep, Ruff, complexity metrics, duplicate detection. Includes automated fix loop (/lens-booboo-fix) and interactive architectural refactoring (/lens-booboo-refactor) with browser-based interviews.",
|
|
6
6
|
"repository": {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
id: no-default-export
|
|
2
|
+
message: >-
|
|
3
|
+
Use named exports instead of default exports.
|
|
4
|
+
Named exports improve grep-ability: agents can precisely locate
|
|
5
|
+
`export const Foo` and all `import { Foo } from` usages.
|
|
6
|
+
severity: info
|
|
7
|
+
language: typescript
|
|
8
|
+
rule:
|
|
9
|
+
pattern: export default $X
|
|
10
|
+
note: |
|
|
11
|
+
Named exports make code searchable for agents and humans.
|
|
12
|
+
|
|
13
|
+
Before:
|
|
14
|
+
export default function helper() { }
|
|
15
|
+
|
|
16
|
+
After:
|
|
17
|
+
export function helper() { }
|
|
18
|
+
|
|
19
|
+
Exception: Entry points (index.ts) typically require default exports.
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
id: no-hardcoded-secrets
|
|
2
2
|
language: TypeScript
|
|
3
|
-
message: "
|
|
3
|
+
message: "Hardcoded secret detected ā use process.env instead"
|
|
4
4
|
severity: error
|
|
5
5
|
note: |
|
|
6
6
|
Hardcoded secrets in source code can be exposed through version control.
|
|
7
7
|
Use environment variables or a secrets manager instead.
|
|
8
|
-
|
|
8
|
+
|
|
9
|
+
BAD: const apiKey = "sk-live-1234567890"
|
|
10
|
+
GOOD: const apiKey = process.env.API_KEY
|
|
9
11
|
rule:
|
|
10
12
|
any:
|
|
11
|
-
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
- pattern: const $VAR = "$_"
|
|
14
|
+
- pattern: const $VAR = '$_'
|
|
15
|
+
constraints:
|
|
16
|
+
VAR:
|
|
17
|
+
regex: "(password|passwd|pwd|secret|token|apiKey|api_key|apikey|apiSecret|accessKey|access_key|auth|privateKey|private_key|clientSecret|client_secret|credentials|bearer)"
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
id: no-process-env
|
|
2
2
|
language: TypeScript
|
|
3
|
-
message: "
|
|
4
|
-
severity:
|
|
3
|
+
message: "Avoid direct process.env access for secrets ā use config service"
|
|
4
|
+
severity: warning
|
|
5
5
|
note: |
|
|
6
|
-
Direct process.env access
|
|
7
|
-
|
|
6
|
+
Direct process.env access for sensitive values should go through
|
|
7
|
+
a config service for testability and centralization.
|
|
8
8
|
|
|
9
|
-
BAD:
|
|
10
|
-
|
|
11
|
-
const dbUrl = this.config.get('DATABASE_URL');
|
|
9
|
+
BAD: const key = process.env.API_KEY
|
|
10
|
+
const { password } = process.env
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
GOOD: const key = config.apiKey
|
|
13
|
+
constructor(private config: ConfigService) {}
|
|
14
14
|
rule:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
pattern: process.env.$KEY
|
|
16
|
+
constraints:
|
|
17
|
+
KEY:
|
|
18
|
+
regex: "(PASSWORD|PASSWD|SECRET|TOKEN|API_KEY|APIKEY|ACCESS_KEY|PRIVATE_KEY|CLIENT_SECRET|CREDENTIALS|BEARER)"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
id: no-relative-cross-package-import
|
|
2
|
+
message: >-
|
|
3
|
+
Avoid relative imports across package boundaries (../../).
|
|
4
|
+
Use absolute imports (@app/..., @components/...) for better agent reasoning
|
|
5
|
+
about code provenance and safer refactors.
|
|
6
|
+
severity: info
|
|
7
|
+
language: typescript
|
|
8
|
+
rule:
|
|
9
|
+
any:
|
|
10
|
+
- pattern: import { $$$ } from '../../../$$$'
|
|
11
|
+
- pattern: import '$$$/../../../$$$'
|
|
12
|
+
- pattern: import * as $N from '../../../$$$'
|
|
13
|
+
- pattern: import $N from '../../../$$$'
|
|
14
|
+
note: |
|
|
15
|
+
Relative imports with 3+ levels (../../../) indicate cross-package imports.
|
|
16
|
+
|
|
17
|
+
Before:
|
|
18
|
+
import { helper } from '../../../utils/helper';
|
|
19
|
+
|
|
20
|
+
After:
|
|
21
|
+
import { helper } from '@/utils/helper';
|