pi-lens 1.1.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 (79) hide show
  1. package/README.md +152 -0
  2. package/clients/ast-grep-client.ts +254 -0
  3. package/clients/biome-client.ts +362 -0
  4. package/clients/dependency-checker.ts +368 -0
  5. package/clients/jscpd-client.ts +144 -0
  6. package/clients/knip-client.ts +244 -0
  7. package/clients/ruff-client.ts +295 -0
  8. package/clients/todo-scanner.ts +161 -0
  9. package/clients/type-coverage-client.ts +141 -0
  10. package/clients/types.ts +59 -0
  11. package/clients/typescript-client.ts +542 -0
  12. package/index.ts +471 -0
  13. package/package.json +50 -0
  14. package/rules/.sgconfig.yml +4 -0
  15. package/rules/ast-grep-rules/.sgconfig.yml +4 -0
  16. package/rules/ast-grep-rules/rules/array-callback-return.yml +33 -0
  17. package/rules/ast-grep-rules/rules/constructor-super.yml +22 -0
  18. package/rules/ast-grep-rules/rules/empty-catch.yml +16 -0
  19. package/rules/ast-grep-rules/rules/getter-return.yml +59 -0
  20. package/rules/ast-grep-rules/rules/hardcoded-url.yml +12 -0
  21. package/rules/ast-grep-rules/rules/in-correct-optional-input-type.yml +63 -0
  22. package/rules/ast-grep-rules/rules/missing-component-decorator.yml +30 -0
  23. package/rules/ast-grep-rules/rules/nested-ternary.yml +10 -0
  24. package/rules/ast-grep-rules/rules/no-alert.yml +6 -0
  25. package/rules/ast-grep-rules/rules/no-any-type.yml +11 -0
  26. package/rules/ast-grep-rules/rules/no-array-constructor.yml +9 -0
  27. package/rules/ast-grep-rules/rules/no-as-any.yml +8 -0
  28. package/rules/ast-grep-rules/rules/no-async-promise-executor.yml +15 -0
  29. package/rules/ast-grep-rules/rules/no-await-in-loop.yml +29 -0
  30. package/rules/ast-grep-rules/rules/no-await-in-promise-all.yml +19 -0
  31. package/rules/ast-grep-rules/rules/no-bare-except.yml +13 -0
  32. package/rules/ast-grep-rules/rules/no-case-declarations.yml +16 -0
  33. package/rules/ast-grep-rules/rules/no-compare-neg-zero.yml +13 -0
  34. package/rules/ast-grep-rules/rules/no-comparison-to-none.yml +12 -0
  35. package/rules/ast-grep-rules/rules/no-cond-assign.yml +36 -0
  36. package/rules/ast-grep-rules/rules/no-console-log.yml +9 -0
  37. package/rules/ast-grep-rules/rules/no-constant-condition.yml +25 -0
  38. package/rules/ast-grep-rules/rules/no-constructor-return.yml +28 -0
  39. package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +13 -0
  40. package/rules/ast-grep-rules/rules/no-debugger.yml +10 -0
  41. package/rules/ast-grep-rules/rules/no-delete-operator.yml +9 -0
  42. package/rules/ast-grep-rules/rules/no-dupe-args.yml +15 -0
  43. package/rules/ast-grep-rules/rules/no-dupe-class-members.yml +76 -0
  44. package/rules/ast-grep-rules/rules/no-dupe-keys.yml +73 -0
  45. package/rules/ast-grep-rules/rules/no-eval.yml +12 -0
  46. package/rules/ast-grep-rules/rules/no-extra-boolean-cast.yml +25 -0
  47. package/rules/ast-grep-rules/rules/no-hardcoded-secrets.yml +13 -0
  48. package/rules/ast-grep-rules/rules/no-implied-eval.yml +15 -0
  49. package/rules/ast-grep-rules/rules/no-inline-styles.yml +18 -0
  50. package/rules/ast-grep-rules/rules/no-inner-html.yml +13 -0
  51. package/rules/ast-grep-rules/rules/no-insecure-randomness.yml +20 -0
  52. package/rules/ast-grep-rules/rules/no-javascript-url.yml +11 -0
  53. package/rules/ast-grep-rules/rules/no-lonely-if.yml +13 -0
  54. package/rules/ast-grep-rules/rules/no-mutable-default.yml +11 -0
  55. package/rules/ast-grep-rules/rules/no-nested-links.yml +28 -0
  56. package/rules/ast-grep-rules/rules/no-new-symbol.yml +8 -0
  57. package/rules/ast-grep-rules/rules/no-new-wrappers.yml +13 -0
  58. package/rules/ast-grep-rules/rules/no-non-null-assertion.yml +14 -0
  59. package/rules/ast-grep-rules/rules/no-open-redirect.yml +15 -0
  60. package/rules/ast-grep-rules/rules/no-prototype-builtins-native.yml +17 -0
  61. package/rules/ast-grep-rules/rules/no-prototype-builtins.yml +15 -0
  62. package/rules/ast-grep-rules/rules/no-return-await.yml +15 -0
  63. package/rules/ast-grep-rules/rules/no-shadow.yml +20 -0
  64. package/rules/ast-grep-rules/rules/no-sql-in-code.yml +13 -0
  65. package/rules/ast-grep-rules/rules/no-star-imports.yml +11 -0
  66. package/rules/ast-grep-rules/rules/no-string-ref.yml +15 -0
  67. package/rules/ast-grep-rules/rules/no-throw-string.yml +12 -0
  68. package/rules/ast-grep-rules/rules/no-unnecessary-state-initializer.yml +17 -0
  69. package/rules/ast-grep-rules/rules/no-useless-concat.yml +17 -0
  70. package/rules/ast-grep-rules/rules/no-var.yml +10 -0
  71. package/rules/ast-grep-rules/rules/prefer-const.yml +16 -0
  72. package/rules/ast-grep-rules/rules/prefer-nullish-coalescing.yml +23 -0
  73. package/rules/ast-grep-rules/rules/prefer-optional-chain.yml +14 -0
  74. package/rules/ast-grep-rules/rules/prefer-template.yml +20 -0
  75. package/rules/ast-grep-rules/rules/require-await.yml +14 -0
  76. package/rules/ast-grep-rules/rules/strict-equality.yml +10 -0
  77. package/rules/ast-grep-rules/rules/strict-inequality.yml +10 -0
  78. package/rules/ast-grep-rules/rules/switch-needs-default.yml +12 -0
  79. package/tsconfig.json +14 -0
package/README.md ADDED
@@ -0,0 +1,152 @@
1
+ # pi-lens
2
+
3
+ Real-time code quality feedback for [pi](https://github.com/mariozechner/pi-coding-agent). Every write and edit is automatically analysed — diagnostics are injected directly into the tool result so the agent sees them without any extra steps.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ ### On every write / edit
10
+
11
+ | Tool | What it checks |
12
+ |---|---|
13
+ | **TypeScript LSP** | Type errors and warnings, using the project's `tsconfig.json` (walks up from the file to find it; falls back to `ES2020 + DOM` defaults) |
14
+ | **ast-grep** | 60+ structural rules: `no-var`, `no-eval`, `no-debugger`, `no-as-any`, `prefer-template`, `no-throw-string`, `no-hardcoded-secrets`, `no-return-await`, nested ternaries, strict equality, and more |
15
+ | **Biome** | Lint + format for JS/TS/JSX/TSX/CSS/JSON. Auto-fixes on every write by default |
16
+ | **Ruff** | Lint + format for Python. Auto-fixes on every write by default |
17
+
18
+ ### Pre-write hints
19
+
20
+ Before every write or edit to an existing file, the agent sees a summary of what's already broken:
21
+
22
+ ```
23
+ ⚠ Pre-write: file already has 5 TypeScript error(s) — fix before adding more
24
+ ⚠ Pre-write: file already has 9 Biome issue(s)
25
+ ⚠ Pre-write: file already has 8 structural violations
26
+ ```
27
+
28
+ Prevents piling new errors on top of existing ones.
29
+
30
+ ### Session start summary (injected into first tool result)
31
+
32
+ On every new session, the following scans run against the whole project and are delivered once into the first tool result:
33
+
34
+ | Tool | What it reports |
35
+ |---|---|
36
+ | **TODO scanner** | All TODO / FIXME / HACK / BUG / DEPRECATED annotations, sorted by severity |
37
+ | **Knip** | Unused exports, types, and unlisted dependencies |
38
+ | **jscpd** | Duplicate code blocks — file, line, size, percentage of codebase |
39
+ | **type-coverage** | Percentage of identifiers properly typed; lists exact locations of `any` |
40
+
41
+ Example:
42
+
43
+ ```
44
+ [Session Start]
45
+ [TODOs] 3 annotation(s) found (2 FIXME, 1 TODO):
46
+ 🔴 src/auth.ts:42 — FIXME: token refresh not implemented
47
+ 🟠 src/parser.ts:17 — HACK: bypassing validation
48
+ 📝 src/api.ts:88 — TODO: add rate limiting
49
+
50
+ [Knip] 2 issue(s) — 2 unused export(s):
51
+ Unused exports:
52
+ - legacyFormat (utils.ts)
53
+ - oldParser (parser.ts)
54
+
55
+ [jscpd] 2 duplicate block(s) — 1.2% of codebase (47/3920 lines):
56
+ 16 lines — openrouter.ts:183 ↔ openrouter.ts:135
57
+ 11 lines — cline-auth.ts:51 ↔ kilo-auth.ts:9
58
+
59
+ [type-coverage] ⚠ 94.3% typed (3870/4107 identifiers):
60
+ auth.ts:138:44 — undefined as any
61
+ config.ts:52:12 — err
62
+ ... and 12 more
63
+ ```
64
+
65
+ ### On-demand commands
66
+
67
+ | Command | Description |
68
+ |---|---|
69
+ | `/find-todos [path]` | Scan for TODO/FIXME/HACK annotations |
70
+ | `/dead-code` | Find unused exports/files/dependencies (requires knip) |
71
+ | `/check-deps` | Circular dependency scan (requires madge) |
72
+ | `/format [file\|--all]` | Apply Biome formatting |
73
+
74
+ ---
75
+
76
+ ## Installation
77
+
78
+ ```bash
79
+ # Core (required for JS/TS feedback)
80
+ npm install -D @biomejs/biome @ast-grep/cli
81
+
82
+ # Dead code + duplicate detection + type coverage (highly recommended)
83
+ npm install -D knip jscpd type-coverage
84
+
85
+ # Circular dependency detection
86
+ npm install -D madge
87
+
88
+ # Python support
89
+ pip install ruff
90
+ ```
91
+
92
+ ---
93
+
94
+ ## Flags
95
+
96
+ | Flag | Default | Description |
97
+ |---|---|---|
98
+ | `--autofix-biome` | **`true`** | Auto-fix Biome lint/format issues on every write |
99
+ | `--autofix-ruff` | **`true`** | Auto-fix Ruff issues on every write |
100
+ | `--no-biome` | `false` | Disable Biome |
101
+ | `--no-ast-grep` | `false` | Disable ast-grep |
102
+ | `--no-ruff` | `false` | Disable Ruff |
103
+ | `--no-lsp` | `false` | Disable TypeScript LSP |
104
+ | `--lens-verbose` | `false` | Enable verbose logging |
105
+
106
+ ---
107
+
108
+ ## ast-grep rules
109
+
110
+ Rules live in `rules/ast-grep-rules/rules/`. All rules are YAML files you can edit or extend.
111
+
112
+ **Security**
113
+ `no-eval`, `no-implied-eval`, `no-hardcoded-secrets`, `no-insecure-randomness`, `no-open-redirect`, `no-sql-in-code`, `no-inner-html`, `no-dangerously-set-inner-html`, `no-javascript-url`
114
+
115
+ **TypeScript**
116
+ `no-any-type`, `no-as-any`, `no-non-null-assertion`
117
+
118
+ **Style**
119
+ `no-var`, `prefer-const`, `prefer-template`, `no-useless-concat`, `prefer-nullish-coalescing`, `prefer-optional-chain`, `nested-ternary`, `no-lonely-if`
120
+
121
+ **Correctness**
122
+ `no-debugger`, `no-throw-string`, `no-return-await`, `no-await-in-loop`, `no-await-in-promise-all`, `require-await`, `empty-catch`, `strict-equality`, `strict-inequality`
123
+
124
+ **Patterns**
125
+ `no-console-log`, `no-alert`, `no-delete-operator`, `no-shadow`, `no-star-imports`, `switch-needs-default`
126
+
127
+ ---
128
+
129
+ ## External dependencies summary
130
+
131
+ | Package | Install | Purpose |
132
+ |---|---|---|
133
+ | `@biomejs/biome` | `npm i -D @biomejs/biome` | JS/TS/CSS/JSON lint + format + autofix |
134
+ | `@ast-grep/cli` | `npm i -D @ast-grep/cli` | 60+ structural pattern rules |
135
+ | `knip` | `npm i -D knip` | Unused exports, types, unlisted deps |
136
+ | `jscpd` | `npm i -D jscpd` | Copy-paste / duplicate code detection |
137
+ | `type-coverage` | `npm i -D type-coverage` | TypeScript `any` coverage percentage |
138
+ | `madge` | `npm i -D madge` | Circular dependency detection |
139
+ | `ruff` | `pip install ruff` | Python lint + format + autofix |
140
+
141
+ ---
142
+
143
+ ## TypeScript LSP — tsconfig detection
144
+
145
+ The LSP walks up from the edited file's directory until it finds a `tsconfig.json`. If found, it uses that project's exact `compilerOptions` (paths, strict settings, lib, etc.). If not found, it falls back to sensible defaults:
146
+
147
+ - `target: ES2020`
148
+ - `lib: ["es2020", "dom", "dom.iterable"]`
149
+ - `moduleResolution: bundler`
150
+ - `strict: true`
151
+
152
+ The compiler options are refreshed automatically when you switch between projects within a session.
@@ -0,0 +1,254 @@
1
+ /**
2
+ * AstGrep Client for pi-lens
3
+ *
4
+ * Structural code analysis using ast-grep CLI.
5
+ * Scans files against YAML rule definitions.
6
+ *
7
+ * Requires: npm install -D @ast-grep/cli
8
+ * Rules: ./rules/ directory
9
+ */
10
+
11
+ import { spawnSync } from "node:child_process";
12
+ import * as path from "node:path";
13
+ import * as fs from "node:fs";
14
+
15
+ // --- Types ---
16
+
17
+ export interface AstGrepDiagnostic {
18
+ line: number;
19
+ column: number;
20
+ endLine: number;
21
+ endColumn: number;
22
+ severity: "error" | "warning" | "info" | "hint";
23
+ message: string;
24
+ rule: string;
25
+ file: string;
26
+ fix?: string;
27
+ }
28
+
29
+ // New ast-grep JSON format
30
+ interface AstGrepJsonDiagnostic {
31
+ ruleId: string;
32
+ severity: string;
33
+ message: string;
34
+ note?: string;
35
+ labels: Array<{
36
+ text: string;
37
+ range: {
38
+ start: { line: number; column: number };
39
+ end: { line: number; column: number };
40
+ };
41
+ file?: string;
42
+ style: string;
43
+ }>;
44
+ // Legacy format support
45
+ Message?: { text: string };
46
+ Severity?: string;
47
+ spans?: Array<{
48
+ context: string;
49
+ range: {
50
+ start: { line: number; column: number };
51
+ end: { line: number; column: number };
52
+ };
53
+ file: string;
54
+ }>;
55
+ name?: string;
56
+ }
57
+
58
+ // --- Client ---
59
+
60
+ export class AstGrepClient {
61
+ private available: boolean | null = null;
62
+ private ruleDir: string;
63
+ private log: (msg: string) => void;
64
+
65
+ constructor(ruleDir?: string, verbose = false) {
66
+ this.ruleDir = ruleDir || path.join(typeof __dirname !== "undefined" ? __dirname : ".", "..", "rules");
67
+ this.log = verbose
68
+ ? (msg: string) => console.log(`[ast-grep] ${msg}`)
69
+ : () => {};
70
+ try {
71
+ const nodeFs2 = require("node:fs") as typeof import("node:fs");
72
+ nodeFs2.appendFileSync("C:/Users/R3LiC/Desktop/pi-lens-debug.log",
73
+ `[${new Date().toISOString()}] AstGrepClient constructed, __dirname=${typeof __dirname !== "undefined" ? __dirname : "undefined"}, ruleDir=${this.ruleDir}\n`);
74
+ } catch {}
75
+ }
76
+
77
+ /**
78
+ * Check if ast-grep CLI is available
79
+ */
80
+ isAvailable(): boolean {
81
+ if (this.available !== null) return this.available;
82
+
83
+ const result = spawnSync("npx", ["sg", "--version"], {
84
+ encoding: "utf-8",
85
+ timeout: 10000,
86
+ shell: true,
87
+ });
88
+
89
+ this.available = !result.error && result.status === 0;
90
+ if (this.available) {
91
+ this.log("ast-grep available");
92
+ }
93
+
94
+ return this.available;
95
+ }
96
+
97
+ /**
98
+ * Scan a file against all rules
99
+ */
100
+ scanFile(filePath: string): AstGrepDiagnostic[] {
101
+ if (!this.isAvailable()) return [];
102
+
103
+ const absolutePath = path.resolve(filePath);
104
+ if (!fs.existsSync(absolutePath)) return [];
105
+
106
+ const configPath = path.join(this.ruleDir, ".sgconfig.yml");
107
+
108
+ try {
109
+ const result = spawnSync("npx", [
110
+ "sg",
111
+ "scan",
112
+ "--config", configPath,
113
+ "--json",
114
+ absolutePath,
115
+ ], {
116
+ encoding: "utf-8",
117
+ timeout: 15000,
118
+ shell: true,
119
+ });
120
+
121
+ // ast-grep exits 1 when it finds issues
122
+ const output = result.stdout || result.stderr || "";
123
+ if (!output.trim()) return [];
124
+
125
+ return this.parseOutput(output, absolutePath);
126
+ } catch (err: any) {
127
+ this.log(`Scan error: ${err.message}`);
128
+ return [];
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Format diagnostics for LLM consumption
134
+ */
135
+ formatDiagnostics(diags: AstGrepDiagnostic[]): string {
136
+ if (diags.length === 0) return "";
137
+
138
+ const errors = diags.filter(d => d.severity === "error");
139
+ const warnings = diags.filter(d => d.severity === "warning");
140
+
141
+ let output = `[ast-grep] ${diags.length} structural issue(s)`;
142
+ if (errors.length) output += ` — ${errors.length} error(s)`;
143
+ if (warnings.length) output += ` — ${warnings.length} warning(s)`;
144
+ output += ":\n";
145
+
146
+ for (const d of diags.slice(0, 15)) {
147
+ const loc = d.line === d.endLine
148
+ ? `L${d.line}`
149
+ : `L${d.line}-${d.endLine}`;
150
+ const fix = d.fix ? " [fixable]" : "";
151
+ output += ` [${d.rule}] ${loc} ${d.message}${fix}\n`;
152
+ }
153
+
154
+ if (diags.length > 15) {
155
+ output += ` ... and ${diags.length - 15} more\n`;
156
+ }
157
+
158
+ return output;
159
+ }
160
+
161
+ // --- Internal ---
162
+
163
+ private parseOutput(output: string, filterFile: string): AstGrepDiagnostic[] {
164
+ const diagnostics: AstGrepDiagnostic[] = [];
165
+ const resolvedFilterFile = path.resolve(filterFile);
166
+
167
+ // Try parsing as JSON array first (new format)
168
+ try {
169
+ const items: AstGrepJsonDiagnostic[] = JSON.parse(output);
170
+ if (Array.isArray(items)) {
171
+ for (const item of items) {
172
+ const diag = this.parseDiagnostic(item, resolvedFilterFile);
173
+ if (diag) diagnostics.push(diag);
174
+ }
175
+ return diagnostics;
176
+ }
177
+ } catch {
178
+ // Not a JSON array, try ndjson format (legacy)
179
+ }
180
+
181
+ // Parse ndjson (one JSON object per line) - legacy format
182
+ const lines = output.split("\n").filter(l => l.trim());
183
+
184
+ for (const line of lines) {
185
+ try {
186
+ const item: AstGrepJsonDiagnostic = JSON.parse(line);
187
+ const diag = this.parseDiagnostic(item, resolvedFilterFile);
188
+ if (diag) diagnostics.push(diag);
189
+ } catch {
190
+ // Skip unparseable lines
191
+ }
192
+ }
193
+
194
+ return diagnostics;
195
+ }
196
+
197
+ private parseDiagnostic(item: AstGrepJsonDiagnostic, filterFile: string): AstGrepDiagnostic | null {
198
+ // New format uses labels array
199
+ if (item.labels && item.labels.length > 0) {
200
+ const label = item.labels.find(l => l.style === "primary") || item.labels[0];
201
+ const filePath = path.resolve(label.file || filterFile);
202
+
203
+ // Filter to our file
204
+ if (filePath !== filterFile) return null;
205
+
206
+ const start = label.range?.start || { line: 0, column: 0 };
207
+ const end = label.range?.end || start;
208
+
209
+ return {
210
+ line: start.line + 1, // ast-grep is 0-indexed, we want 1-indexed
211
+ column: start.column,
212
+ endLine: end.line + 1,
213
+ endColumn: end.column,
214
+ severity: this.mapSeverity(item.severity),
215
+ message: item.message || "Unknown issue",
216
+ rule: item.ruleId || "unknown",
217
+ file: filePath,
218
+ };
219
+ }
220
+
221
+ // Legacy format uses spans array
222
+ if (item.spans && item.spans.length > 0) {
223
+ const span = item.spans[0];
224
+ const filePath = path.resolve(span.file || filterFile);
225
+
226
+ // Filter to our file
227
+ if (filePath !== filterFile) return null;
228
+
229
+ const start = span.range?.start || { line: 0, column: 0 };
230
+ const end = span.range?.end || start;
231
+
232
+ return {
233
+ line: start.line + 1,
234
+ column: start.column,
235
+ endLine: end.line + 1,
236
+ endColumn: end.column,
237
+ severity: this.mapSeverity(item.severity || item.Severity || "warning"),
238
+ message: item.Message?.text || item.message || "Unknown issue",
239
+ rule: item.name || item.ruleId || "unknown",
240
+ file: filePath,
241
+ };
242
+ }
243
+
244
+ return null;
245
+ }
246
+
247
+ private mapSeverity(severity: string): AstGrepDiagnostic["severity"] {
248
+ const lower = severity.toLowerCase();
249
+ if (lower === "error") return "error";
250
+ if (lower === "warning") return "warning";
251
+ if (lower === "info") return "info";
252
+ return "hint";
253
+ }
254
+ }