pi-lens 1.1.2 → 1.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/README.md +12 -1
- package/clients/ast-grep-client.ts +183 -11
- package/index.ts +70 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -69,7 +69,16 @@ Example:
|
|
|
69
69
|
| `/find-todos [path]` | Scan for TODO/FIXME/HACK annotations |
|
|
70
70
|
| `/dead-code` | Find unused exports/files/dependencies (requires knip) |
|
|
71
71
|
| `/check-deps` | Circular dependency scan (requires madge) |
|
|
72
|
-
| `/format [file
|
|
72
|
+
| `/format [file|--all]` | Apply Biome formatting |
|
|
73
|
+
|
|
74
|
+
### On-demand tools
|
|
75
|
+
|
|
76
|
+
| Tool | Description |
|
|
77
|
+
|---|---|
|
|
78
|
+
| **`ast_grep_search`** | Search code patterns using AST-aware matching. Supports meta-variables: `$VAR` (single node), `$$$` (multiple). Example: `console.log($MSG)` |
|
|
79
|
+
| **`ast_grep_replace`** | Replace code patterns with AST-aware rewriting. Dry-run by default, use `apply=true` to apply changes. Example: `pattern='console.log($MSG)' rewrite='logger.info($MSG)'` |
|
|
80
|
+
|
|
81
|
+
Supported languages: c, cpp, csharp, css, dart, elixir, go, haskell, html, java, javascript, json, kotlin, lua, php, python, ruby, rust, scala, sql, swift, tsx, typescript, yaml
|
|
73
82
|
|
|
74
83
|
---
|
|
75
84
|
|
|
@@ -110,6 +119,8 @@ pip install ruff
|
|
|
110
119
|
|
|
111
120
|
Rules live in `rules/ast-grep-rules/rules/`. All rules are YAML files you can edit or extend.
|
|
112
121
|
|
|
122
|
+
Each rule includes a `message` and `note` that are shown in diagnostics, so the agent understands why something violated a rule and how to fix it.
|
|
123
|
+
|
|
113
124
|
**Security**
|
|
114
125
|
`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`
|
|
115
126
|
|
|
@@ -8,12 +8,26 @@
|
|
|
8
8
|
* Rules: ./rules/ directory
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import * as fs from "node:fs";
|
|
14
14
|
|
|
15
15
|
// --- Types ---
|
|
16
16
|
|
|
17
|
+
export interface RuleDescription {
|
|
18
|
+
id: string;
|
|
19
|
+
message: string;
|
|
20
|
+
note?: string;
|
|
21
|
+
severity: "error" | "warning" | "info" | "hint";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AstGrepMatch {
|
|
25
|
+
file: string;
|
|
26
|
+
range: { start: { line: number; column: number }; end: { line: number; column: number } };
|
|
27
|
+
text: string;
|
|
28
|
+
replacement?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
export interface AstGrepDiagnostic {
|
|
18
32
|
line: number;
|
|
19
33
|
column: number;
|
|
@@ -22,6 +36,7 @@ export interface AstGrepDiagnostic {
|
|
|
22
36
|
severity: "error" | "warning" | "info" | "hint";
|
|
23
37
|
message: string;
|
|
24
38
|
rule: string;
|
|
39
|
+
ruleDescription?: RuleDescription;
|
|
25
40
|
file: string;
|
|
26
41
|
fix?: string;
|
|
27
42
|
}
|
|
@@ -61,17 +76,89 @@ export class AstGrepClient {
|
|
|
61
76
|
private available: boolean | null = null;
|
|
62
77
|
private ruleDir: string;
|
|
63
78
|
private log: (msg: string) => void;
|
|
79
|
+
private ruleDescriptions: Map<string, RuleDescription> | null = null;
|
|
64
80
|
|
|
65
81
|
constructor(ruleDir?: string, verbose = false) {
|
|
66
82
|
this.ruleDir = ruleDir || path.join(typeof __dirname !== "undefined" ? __dirname : ".", "..", "rules");
|
|
67
83
|
this.log = verbose
|
|
68
84
|
? (msg: string) => console.log(`[ast-grep] ${msg}`)
|
|
69
85
|
: () => {};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Load rule descriptions from YAML files
|
|
90
|
+
*/
|
|
91
|
+
private loadRuleDescriptions(): Map<string, RuleDescription> {
|
|
92
|
+
if (this.ruleDescriptions !== null) return this.ruleDescriptions;
|
|
93
|
+
|
|
94
|
+
const descriptions = new Map<string, RuleDescription>();
|
|
95
|
+
|
|
96
|
+
// Find the rules directory - check more specific paths first
|
|
97
|
+
const possiblePaths = [
|
|
98
|
+
path.join(this.ruleDir, "ast-grep-rules", "rules"),
|
|
99
|
+
path.join(this.ruleDir, "rules"),
|
|
100
|
+
this.ruleDir,
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
let rulesPath = possiblePaths.find(p => fs.existsSync(p));
|
|
104
|
+
|
|
105
|
+
if (!rulesPath) {
|
|
106
|
+
this.log(`Rule descriptions: no rules directory found in ${possiblePaths.join(", ")}`);
|
|
107
|
+
this.ruleDescriptions = descriptions;
|
|
108
|
+
return descriptions;
|
|
109
|
+
}
|
|
110
|
+
|
|
70
111
|
try {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
const files = fs.readdirSync(rulesPath).filter(f => f.endsWith(".yml"));
|
|
113
|
+
this.log(`Loaded ${files.length} rule descriptions from ${rulesPath}`);
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
const filePath = path.join(rulesPath, file);
|
|
116
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
117
|
+
const rule = this.parseRuleYaml(content);
|
|
118
|
+
if (rule) {
|
|
119
|
+
descriptions.set(rule.id, rule);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} catch (err: any) {
|
|
123
|
+
this.log(`Failed to load rule descriptions: ${err.message}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.ruleDescriptions = descriptions;
|
|
127
|
+
return descriptions;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Simple YAML parser for rule descriptions
|
|
132
|
+
*/
|
|
133
|
+
private parseRuleYaml(content: string): RuleDescription | null {
|
|
134
|
+
const result: Partial<RuleDescription> = {};
|
|
135
|
+
|
|
136
|
+
// Extract id
|
|
137
|
+
const idMatch = content.match(/^id:\s*(.+)$/m);
|
|
138
|
+
if (idMatch) result.id = idMatch[1].trim();
|
|
139
|
+
|
|
140
|
+
// Extract message (handle quoted strings)
|
|
141
|
+
const msgMatch = content.match(/^message:\s*"([^"]+)"/m) || content.match(/^message:\s*'([^']+)'/m) || content.match(/^message:\s*(.+)$/m);
|
|
142
|
+
if (msgMatch) result.message = (msgMatch[3] || msgMatch[2] || msgMatch[1]).trim();
|
|
143
|
+
|
|
144
|
+
// Extract note (multiline, indented lines)
|
|
145
|
+
const noteMatch = content.match(/^note:\s*\|([\s\S]*?)(?=^\w|\n\n|\nrule:)/m);
|
|
146
|
+
if (noteMatch) {
|
|
147
|
+
result.note = noteMatch[1]
|
|
148
|
+
.split("\n")
|
|
149
|
+
.map(line => line.trim())
|
|
150
|
+
.filter(line => line.length > 0)
|
|
151
|
+
.join(" ");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Extract severity
|
|
155
|
+
const sevMatch = content.match(/^severity:\s*(.+)$/m);
|
|
156
|
+
if (sevMatch) result.severity = this.mapSeverity(sevMatch[1].trim());
|
|
157
|
+
|
|
158
|
+
if (result.id && result.message) {
|
|
159
|
+
return result as RuleDescription;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
75
162
|
}
|
|
76
163
|
|
|
77
164
|
/**
|
|
@@ -94,6 +181,72 @@ export class AstGrepClient {
|
|
|
94
181
|
return this.available;
|
|
95
182
|
}
|
|
96
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Search for AST patterns in files
|
|
186
|
+
*/
|
|
187
|
+
async search(pattern: string, lang: string, paths: string[]): Promise<{ matches: AstGrepMatch[]; error?: string }> {
|
|
188
|
+
return this.runSg(["run", "-p", pattern, "--lang", lang, "--json=compact", ...paths]);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Search and replace AST patterns
|
|
193
|
+
*/
|
|
194
|
+
async replace(pattern: string, rewrite: string, lang: string, paths: string[], apply = false): Promise<{ matches: AstGrepMatch[]; applied: boolean; error?: string }> {
|
|
195
|
+
const args = ["run", "-p", pattern, "-r", rewrite, "--lang", lang, "--json=compact"];
|
|
196
|
+
if (apply) args.push("--update-all");
|
|
197
|
+
args.push(...paths);
|
|
198
|
+
|
|
199
|
+
const result = await this.runSg(args);
|
|
200
|
+
return { matches: result.matches, applied: apply, error: result.error };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private runSg(args: string[]): Promise<{ matches: AstGrepMatch[]; error?: string }> {
|
|
204
|
+
return new Promise((resolve) => {
|
|
205
|
+
const proc = spawn("npx", ["sg", ...args], { stdio: ["ignore", "pipe", "pipe"], shell: true });
|
|
206
|
+
let stdout = "";
|
|
207
|
+
let stderr = "";
|
|
208
|
+
|
|
209
|
+
proc.stdout.on("data", (data: Buffer) => (stdout += data.toString()));
|
|
210
|
+
proc.stderr.on("data", (data: Buffer) => (stderr += data.toString()));
|
|
211
|
+
|
|
212
|
+
proc.on("error", (err: Error) => {
|
|
213
|
+
if (err.message.includes("ENOENT")) {
|
|
214
|
+
resolve({ matches: [], error: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli" });
|
|
215
|
+
} else {
|
|
216
|
+
resolve({ matches: [], error: err.message });
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
proc.on("close", (code: number | null) => {
|
|
221
|
+
if (code !== 0 && !stdout.trim()) {
|
|
222
|
+
resolve({ matches: [], error: stderr.includes("No files found") ? undefined : stderr.trim() || `Exit code ${code}` });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (!stdout.trim()) { resolve({ matches: [] }); return; }
|
|
226
|
+
try {
|
|
227
|
+
const parsed = JSON.parse(stdout);
|
|
228
|
+
const matches = Array.isArray(parsed) ? parsed : [parsed];
|
|
229
|
+
resolve({ matches });
|
|
230
|
+
} catch {
|
|
231
|
+
resolve({ matches: [], error: "Failed to parse output" });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
formatMatches(matches: AstGrepMatch[], isDryRun = false): string {
|
|
238
|
+
if (matches.length === 0) return "No matches found";
|
|
239
|
+
const MAX = 50;
|
|
240
|
+
const shown = matches.slice(0, MAX);
|
|
241
|
+
const lines = shown.map((m) => {
|
|
242
|
+
const loc = `${m.file}:${m.range.start.line + 1}:${m.range.start.column + 1}`;
|
|
243
|
+
const text = m.text.length > 100 ? m.text.slice(0, 100) + "..." : m.text;
|
|
244
|
+
return isDryRun && m.replacement ? `${loc}\n - ${text}\n + ${m.replacement}` : `${loc}: ${text}`;
|
|
245
|
+
});
|
|
246
|
+
if (matches.length > MAX) lines.unshift(`Found ${matches.length} matches (showing first ${MAX}):`);
|
|
247
|
+
return lines.join("\n");
|
|
248
|
+
}
|
|
249
|
+
|
|
97
250
|
/**
|
|
98
251
|
* Scan a file against all rules
|
|
99
252
|
*/
|
|
@@ -137,22 +290,33 @@ export class AstGrepClient {
|
|
|
137
290
|
|
|
138
291
|
const errors = diags.filter(d => d.severity === "error");
|
|
139
292
|
const warnings = diags.filter(d => d.severity === "warning");
|
|
293
|
+
const hints = diags.filter(d => d.severity === "hint");
|
|
140
294
|
|
|
141
295
|
let output = `[ast-grep] ${diags.length} structural issue(s)`;
|
|
142
296
|
if (errors.length) output += ` — ${errors.length} error(s)`;
|
|
143
297
|
if (warnings.length) output += ` — ${warnings.length} warning(s)`;
|
|
298
|
+
if (hints.length) output += ` — ${hints.length} hint(s)`;
|
|
144
299
|
output += ":\n";
|
|
145
300
|
|
|
146
|
-
for (const d of diags.slice(0,
|
|
301
|
+
for (const d of diags.slice(0, 10)) {
|
|
147
302
|
const loc = d.line === d.endLine
|
|
148
303
|
? `L${d.line}`
|
|
149
304
|
: `L${d.line}-${d.endLine}`;
|
|
150
|
-
const
|
|
151
|
-
|
|
305
|
+
const ruleInfo = d.ruleDescription
|
|
306
|
+
? `${d.rule}: ${d.ruleDescription.message}`
|
|
307
|
+
: d.rule;
|
|
308
|
+
const fix = d.fix || d.ruleDescription?.note ? " [fixable]" : "";
|
|
309
|
+
output += ` ${ruleInfo} (${loc})${fix}\n`;
|
|
310
|
+
|
|
311
|
+
// Include note for errors to provide fix guidance
|
|
312
|
+
if (d.severity === "error" && d.ruleDescription?.note) {
|
|
313
|
+
const shortNote = d.ruleDescription.note.split("\n")[0];
|
|
314
|
+
output += ` → ${shortNote}\n`;
|
|
315
|
+
}
|
|
152
316
|
}
|
|
153
317
|
|
|
154
|
-
if (diags.length >
|
|
155
|
-
output += ` ... and ${diags.length -
|
|
318
|
+
if (diags.length > 10) {
|
|
319
|
+
output += ` ... and ${diags.length - 10} more\n`;
|
|
156
320
|
}
|
|
157
321
|
|
|
158
322
|
return output;
|
|
@@ -214,6 +378,7 @@ export class AstGrepClient {
|
|
|
214
378
|
severity: this.mapSeverity(item.severity),
|
|
215
379
|
message: item.message || "Unknown issue",
|
|
216
380
|
rule: item.ruleId || "unknown",
|
|
381
|
+
ruleDescription: this.getRuleDescription(item.ruleId || "unknown"),
|
|
217
382
|
file: filePath,
|
|
218
383
|
};
|
|
219
384
|
}
|
|
@@ -229,6 +394,7 @@ export class AstGrepClient {
|
|
|
229
394
|
const start = span.range?.start || { line: 0, column: 0 };
|
|
230
395
|
const end = span.range?.end || start;
|
|
231
396
|
|
|
397
|
+
const ruleId = item.name || item.ruleId || "unknown";
|
|
232
398
|
return {
|
|
233
399
|
line: start.line + 1,
|
|
234
400
|
column: start.column,
|
|
@@ -236,7 +402,8 @@ export class AstGrepClient {
|
|
|
236
402
|
endColumn: end.column,
|
|
237
403
|
severity: this.mapSeverity(item.severity || item.Severity || "warning"),
|
|
238
404
|
message: item.Message?.text || item.message || "Unknown issue",
|
|
239
|
-
rule:
|
|
405
|
+
rule: ruleId,
|
|
406
|
+
ruleDescription: this.getRuleDescription(ruleId),
|
|
240
407
|
file: filePath,
|
|
241
408
|
};
|
|
242
409
|
}
|
|
@@ -244,6 +411,11 @@ export class AstGrepClient {
|
|
|
244
411
|
return null;
|
|
245
412
|
}
|
|
246
413
|
|
|
414
|
+
private getRuleDescription(ruleId: string): RuleDescription | undefined {
|
|
415
|
+
const descriptions = this.loadRuleDescriptions();
|
|
416
|
+
return descriptions.get(ruleId);
|
|
417
|
+
}
|
|
418
|
+
|
|
247
419
|
private mapSeverity(severity: string): AstGrepDiagnostic["severity"] {
|
|
248
420
|
const lower = severity.toLowerCase();
|
|
249
421
|
if (lower === "error") return "error";
|
package/index.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import * as nodeFs from "node:fs";
|
|
29
29
|
import * as os from "node:os";
|
|
30
30
|
import * as path from "node:path";
|
|
31
|
+
import { Type } from "@sinclair/typebox";
|
|
31
32
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
32
33
|
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
33
34
|
import { AstGrepClient } from "./clients/ast-grep-client.js";
|
|
@@ -55,13 +56,12 @@ function dbg(msg: string) {
|
|
|
55
56
|
let _verbose = false;
|
|
56
57
|
|
|
57
58
|
function log(msg: string) {
|
|
58
|
-
console.log(`[pi-lens] ${msg}`);
|
|
59
|
+
if (_verbose) console.log(`[pi-lens] ${msg}`);
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// --- Extension ---
|
|
62
63
|
|
|
63
64
|
export default function (pi: ExtensionAPI) {
|
|
64
|
-
log("Extension loaded");
|
|
65
65
|
|
|
66
66
|
const tsClient = new TypeScriptClient();
|
|
67
67
|
const astGrepClient = new AstGrepClient();
|
|
@@ -252,6 +252,74 @@ export default function (pi: ExtensionAPI) {
|
|
|
252
252
|
},
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
+
// --- Tools ---
|
|
256
|
+
|
|
257
|
+
const LANGUAGES = [
|
|
258
|
+
"c", "cpp", "csharp", "css", "dart", "elixir", "go", "haskell", "html",
|
|
259
|
+
"java", "javascript", "json", "kotlin", "lua", "php", "python", "ruby",
|
|
260
|
+
"rust", "scala", "sql", "swift", "tsx", "typescript", "yaml",
|
|
261
|
+
] as const;
|
|
262
|
+
|
|
263
|
+
pi.registerTool({
|
|
264
|
+
name: "ast_grep_search",
|
|
265
|
+
label: "AST Search",
|
|
266
|
+
description: "Search code patterns using AST-aware matching. Use meta-variables: $VAR (single node), $$$ (multiple). Examples: 'console.log($MSG)', 'def $FUNC($$$):'",
|
|
267
|
+
parameters: Type.Object({
|
|
268
|
+
pattern: Type.String({ description: "AST pattern with meta-variables" }),
|
|
269
|
+
lang: Type.Union(LANGUAGES.map((l) => Type.Literal(l)), { description: "Target language" }),
|
|
270
|
+
paths: Type.Optional(Type.Array(Type.String(), { description: "Paths to search (default: .)" })),
|
|
271
|
+
}),
|
|
272
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
273
|
+
if (!astGrepClient.isAvailable()) {
|
|
274
|
+
return { content: [{ type: "text", text: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli" }], isError: true, details: {} };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const { pattern, lang, paths } = params as { pattern: string; lang: string; paths?: string[] };
|
|
278
|
+
const searchPaths = paths?.length ? paths : [ctx.cwd || "."];
|
|
279
|
+
const result = await astGrepClient.search(pattern, lang, searchPaths);
|
|
280
|
+
|
|
281
|
+
if (result.error) {
|
|
282
|
+
return { content: [{ type: "text", text: `Error: ${result.error}` }], isError: true, details: {} };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const output = astGrepClient.formatMatches(result.matches);
|
|
286
|
+
return { content: [{ type: "text", text: output }], details: { matchCount: result.matches.length } };
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
pi.registerTool({
|
|
291
|
+
name: "ast_grep_replace",
|
|
292
|
+
label: "AST Replace",
|
|
293
|
+
description: "Replace code patterns with AST-aware rewriting. Dry-run by default (preview changes). Use apply=true to apply. Example: pattern='console.log($MSG)' rewrite='logger.info($MSG)'",
|
|
294
|
+
parameters: Type.Object({
|
|
295
|
+
pattern: Type.String({ description: "AST pattern to match" }),
|
|
296
|
+
rewrite: Type.String({ description: "Replacement pattern" }),
|
|
297
|
+
lang: Type.Union(LANGUAGES.map((l) => Type.Literal(l)), { description: "Target language" }),
|
|
298
|
+
paths: Type.Optional(Type.Array(Type.String(), { description: "Paths to search (default: .)" })),
|
|
299
|
+
apply: Type.Optional(Type.Boolean({ description: "Apply changes (default: false)" })),
|
|
300
|
+
}),
|
|
301
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
302
|
+
if (!astGrepClient.isAvailable()) {
|
|
303
|
+
return { content: [{ type: "text", text: "ast-grep CLI not found. Install: npm i -D @ast-grep/cli" }], isError: true, details: {} };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const { pattern, rewrite, lang, paths, apply } = params as { pattern: string; rewrite: string; lang: string; paths?: string[]; apply?: boolean };
|
|
307
|
+
const searchPaths = paths?.length ? paths : [ctx.cwd || "."];
|
|
308
|
+
const result = await astGrepClient.replace(pattern, rewrite, lang, searchPaths, apply ?? false);
|
|
309
|
+
|
|
310
|
+
if (result.error) {
|
|
311
|
+
return { content: [{ type: "text", text: `Error: ${result.error}` }], isError: true, details: {} };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const isDryRun = !apply;
|
|
315
|
+
let output = astGrepClient.formatMatches(result.matches, isDryRun);
|
|
316
|
+
if (isDryRun && result.matches.length > 0) output += "\n\n(Dry run - use apply=true to apply)";
|
|
317
|
+
if (apply && result.matches.length > 0) output = `Applied ${result.matches.length} replacements:\n${output}`;
|
|
318
|
+
|
|
319
|
+
return { content: [{ type: "text", text: output }], details: { matchCount: result.matches.length, applied: apply ?? false } };
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
255
323
|
// Delivered once into the first tool_result of the session, then cleared
|
|
256
324
|
let sessionSummary: string | null = null;
|
|
257
325
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lens",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Real-time code feedback for pi — TypeScript LSP, Biome, ast-grep, Ruff, TODO scanner, dead code, duplicate detection, type coverage",
|
|
5
|
+
"repository": "github:apmantza/pi-lens",
|
|
5
6
|
"main": "index.ts",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"build": "tsc",
|