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.
Files changed (34) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +70 -1
  3. package/clients/ast-grep-client.js +12 -12
  4. package/clients/ast-grep-client.ts +21 -11
  5. package/clients/dispatch/dispatcher.js +2 -2
  6. package/clients/dispatch/dispatcher.ts +2 -2
  7. package/clients/dispatch/runners/index.js +3 -1
  8. package/clients/dispatch/runners/index.ts +3 -1
  9. package/clients/dispatch/runners/pyright.js +68 -0
  10. package/clients/dispatch/runners/pyright.test.js +84 -0
  11. package/clients/dispatch/runners/pyright.test.ts +109 -0
  12. package/clients/dispatch/runners/pyright.ts +102 -0
  13. package/clients/dispatch/runners/secrets.js +109 -0
  14. package/clients/secrets-scanner.js +113 -0
  15. package/clients/secrets-scanner.test.js +100 -0
  16. package/clients/secrets-scanner.test.ts +113 -0
  17. package/clients/secrets-scanner.ts +134 -0
  18. package/clients/sg-runner.js +15 -2
  19. package/clients/sg-runner.ts +25 -2
  20. package/commands/fix.js +48 -50
  21. package/commands/fix.ts +71 -61
  22. package/commands/rate.js +285 -0
  23. package/commands/rate.test.js +119 -0
  24. package/commands/rate.test.ts +131 -0
  25. package/commands/rate.ts +348 -0
  26. package/commands/refactor.js +33 -9
  27. package/commands/refactor.ts +44 -11
  28. package/default-architect.yaml +7 -0
  29. package/index.ts +58 -10
  30. package/package.json +1 -1
  31. package/rules/ast-grep-rules/rules/no-default-export.yml +19 -0
  32. package/rules/ast-grep-rules/rules/no-hardcoded-secrets.yml +9 -6
  33. package/rules/ast-grep-rules/rules/no-process-env.yml +12 -12
  34. package/rules/ast-grep-rules/rules/no-relative-imports.yml +21 -0
@@ -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
- `**Code** (\`${relFile}\` lines ${snippetStart}–${snippetEnd}):`,
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?",
@@ -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
- let output = astGrepClient.formatMatches(result.matches, isDryRun);
828
- if (isDryRun && result.matches.length > 0)
829
- output += "\n\n(Dry run - use apply=true to apply)";
830
- if (apply && result.matches.length > 0)
831
- output = `Applied ${result.matches.length} replacements:\n${output}`;
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
- const content = nodeFs.readFileSync(filePath, "utf-8");
1232
- metricsClient.recordWrite(filePath, content);
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.1.0",
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: "Potential hardcoded secret or API key detected"
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
- Grade 3.1 — high risk, improved security.
8
+
9
+ BAD: const apiKey = "sk-live-1234567890"
10
+ GOOD: const apiKey = process.env.API_KEY
9
11
  rule:
10
12
  any:
11
- - all:
12
- - pattern: $VAR = $SECRET
13
- - has:
14
- regex: "(?i)(password|passwd|pwd|secret|token|api_?key|apikey|access_?key|auth|private_?key|client_?secret)"
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: "Do not access process.env directly — use dependency injection or a config module"
4
- severity: error
3
+ message: "Avoid direct process.env access for secrets — use config service"
4
+ severity: warning
5
5
  note: |
6
- Direct process.env access makes code untestable and tightly coupled to the
7
- runtime environment. Use a configuration service or DI container instead.
6
+ Direct process.env access for sensitive values should go through
7
+ a config service for testability and centralization.
8
8
 
9
- BAD: const dbUrl = process.env.DATABASE_URL;
10
- GOOD: constructor(private config: ConfigService) {}
11
- const dbUrl = this.config.get('DATABASE_URL');
9
+ BAD: const key = process.env.API_KEY
10
+ const { password } = process.env
12
11
 
13
- Grade 2.0 — critical architectural violation for testability.
12
+ GOOD: const key = config.apiKey
13
+ constructor(private config: ConfigService) {}
14
14
  rule:
15
- any:
16
- - pattern: process.env.$KEY
17
- - pattern: process.env[$KEY]
18
- - pattern: Deno.env.get($KEY)
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';