pi-lens 3.8.39 → 3.8.41

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 (65) hide show
  1. package/CHANGELOG.md +84 -5
  2. package/README.md +37 -1
  3. package/clients/biome-client.ts +5 -4
  4. package/clients/cache/rule-cache.ts +1 -1
  5. package/clients/complexity-client.ts +1 -1
  6. package/clients/dependency-checker.ts +1 -1
  7. package/clients/dispatch/diagnostic-taxonomy.ts +13 -1
  8. package/clients/dispatch/dispatcher.ts +9 -0
  9. package/clients/dispatch/fact-scheduler.ts +1 -1
  10. package/clients/dispatch/integration.ts +58 -3
  11. package/clients/dispatch/runners/index.ts +2 -0
  12. package/clients/dispatch/runners/semgrep.ts +269 -0
  13. package/clients/dispatch/runners/shellcheck.ts +2 -8
  14. package/clients/dispatch/runners/tree-sitter.ts +32 -11
  15. package/clients/dispatch/tool-profile.ts +1 -0
  16. package/clients/format-service.ts +10 -0
  17. package/clients/formatters.ts +22 -8
  18. package/clients/installer/index.ts +3 -3
  19. package/clients/knip-client.ts +360 -362
  20. package/clients/lsp/aggregation.ts +91 -0
  21. package/clients/lsp/client.ts +91 -38
  22. package/clients/lsp/index.ts +88 -72
  23. package/clients/lsp/launch.ts +107 -34
  24. package/clients/lsp/server-strategies.ts +71 -0
  25. package/clients/lsp/server.ts +76 -57
  26. package/clients/path-utils.ts +17 -0
  27. package/clients/pipeline.ts +23 -5
  28. package/clients/production-readiness.ts +2 -2
  29. package/clients/read-guard-logger.ts +41 -1
  30. package/clients/read-guard-tool-lines.ts +17 -4
  31. package/clients/read-guard.ts +95 -46
  32. package/clients/runtime-agent-end.ts +3 -0
  33. package/clients/runtime-session.ts +5 -0
  34. package/clients/runtime-tool-result.ts +48 -1
  35. package/clients/runtime-turn.ts +48 -4
  36. package/clients/sanitize.ts +1 -1
  37. package/clients/semgrep-config.ts +213 -0
  38. package/clients/tool-policy.ts +1982 -1936
  39. package/clients/tree-sitter-client.ts +1 -1
  40. package/clients/widget-state.ts +283 -0
  41. package/commands/booboo.ts +34 -2
  42. package/index.ts +231 -17
  43. package/package.json +3 -2
  44. package/rules/rule-catalog.json +25 -1
  45. package/rules/tree-sitter-queries/cobol/lock-table-cobol.yml +35 -0
  46. package/rules/tree-sitter-queries/cpp/unnecessary-bit-ops.yml +58 -0
  47. package/rules/tree-sitter-queries/java/infinite-loop.yml +58 -0
  48. package/rules/tree-sitter-queries/java/infinite-recursion.yml +58 -0
  49. package/rules/tree-sitter-queries/java/mockito-initialized.yml +66 -0
  50. package/rules/tree-sitter-queries/java/name-capitalization-conflict.yml +54 -0
  51. package/rules/tree-sitter-queries/java/no-octal-values.yml +48 -0
  52. package/rules/tree-sitter-queries/java/resources-closed.yml +57 -0
  53. package/rules/tree-sitter-queries/java/short-circuit-logic.yml +57 -0
  54. package/rules/tree-sitter-queries/java/tests-include-assertions.yml +60 -0
  55. package/rules/tree-sitter-queries/java/unnecessary-bit-ops-java.yml +57 -0
  56. package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +64 -0
  57. package/rules/tree-sitter-queries/plsql/lock-table.yml +42 -0
  58. package/rules/tree-sitter-queries/plsql/nchar-nvarchar2-bytes.yml +54 -0
  59. package/rules/tree-sitter-queries/python/no-super-torchscript.yml +52 -0
  60. package/rules/tree-sitter-queries/typescript/default-not-last.yml +54 -0
  61. package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +51 -0
  62. package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +54 -0
  63. package/rules/tree-sitter-queries/typescript/infinite-loop.yml +55 -0
  64. package/rules/tree-sitter-queries/typescript/self-assignment.yml +46 -0
  65. package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +64 -0
@@ -1,362 +1,360 @@
1
- /**
2
- * Knip Client for pi-local
3
- *
4
- * Detects unused exports, files, dependencies, and more.
5
- * Essential for safe refactoring — I need to know what's dead code
6
- * before I can clean it up.
7
- *
8
- * Requires: npm install -D knip
9
- * Docs: https://knip.dev/
10
- */
11
-
12
- import * as fs from "node:fs";
13
- import * as path from "node:path";
14
- import { safeSpawn, safeSpawnAsync } from "./safe-spawn.js";
15
-
16
- // --- Types ---
17
-
18
- export interface KnipIssue {
19
- type: "export" | "file" | "dependency" | "devDependency" | "unlisted" | "bin";
20
- name: string;
21
- file?: string;
22
- line?: number;
23
- package?: string;
24
- }
25
-
26
- export interface KnipResult {
27
- success: boolean;
28
- issues: KnipIssue[];
29
- unusedExports: KnipIssue[];
30
- unusedFiles: KnipIssue[];
31
- unusedDeps: KnipIssue[];
32
- unlistedDeps: KnipIssue[];
33
- summary: string;
34
- }
35
-
36
- // --- Client ---
37
-
38
- export class KnipClient {
39
- private knipAvailable: boolean | null = null;
40
- private log: (msg: string) => void;
41
-
42
- constructor(verbose = false) {
43
- this.log = verbose
44
- ? (msg: string) => console.error(`[knip] ${msg}`)
45
- : () => {};
46
- }
47
-
48
- private resolveProjectRoot(startDir: string): string {
49
- let current = path.resolve(startDir);
50
- while (true) {
51
- const markers = [
52
- "package.json",
53
- "knip.json",
54
- "knip.ts",
55
- "knip.config.js",
56
- "knip.config.ts",
57
- ];
58
- if (markers.some((m) => fs.existsSync(path.join(current, m)))) {
59
- return current;
60
- }
61
- const parent = path.dirname(current);
62
- if (parent === current) return path.resolve(startDir);
63
- current = parent;
64
- }
65
- }
66
-
67
- /**
68
- * Check if knip CLI is available, auto-install if not
69
- */
70
- async ensureAvailable(): Promise<boolean> {
71
- // Fast path: already checked
72
- if (this.knipAvailable !== null) return this.knipAvailable;
73
-
74
- // Check if available in PATH (fast)
75
- const pathResult = await safeSpawnAsync("knip", ["--version"], {
76
- timeout: 5000,
77
- });
78
- if (!pathResult.error && pathResult.status === 0) {
79
- this.knipAvailable = true;
80
- this.log("Knip found in PATH");
81
- return true;
82
- }
83
-
84
- // Auto-install via pi-lens installer
85
- this.log("Knip not found, attempting auto-install...");
86
- const { ensureTool } = await import("./installer/index.js");
87
- const installedPath = await ensureTool("knip");
88
-
89
- if (installedPath) {
90
- this.knipAvailable = true;
91
- this.log(`Knip auto-installed: ${installedPath}`);
92
- return true;
93
- }
94
-
95
- this.knipAvailable = false;
96
- return false;
97
- }
98
-
99
- /**
100
- * Check if knip CLI is available (legacy sync method)
101
- * Prefer ensureAvailable() for auto-install behavior
102
- */
103
- isAvailable(): boolean {
104
- if (this.knipAvailable !== null) return this.knipAvailable;
105
-
106
- const result = safeSpawn("npx", ["knip", "--version"], {
107
- timeout: 10000,
108
- });
109
-
110
- this.knipAvailable = !result.error && result.status === 0;
111
- if (this.knipAvailable) {
112
- this.log(`Knip available`);
113
- }
114
-
115
- return this.knipAvailable;
116
- }
117
-
118
- /**
119
- * Run knip analysis on the project
120
- */
121
- analyze(cwd?: string, ignore?: string[]): KnipResult {
122
- if (!this.isAvailable()) {
123
- return {
124
- success: false,
125
- issues: [],
126
- unusedExports: [],
127
- unusedFiles: [],
128
- unusedDeps: [],
129
- unlistedDeps: [],
130
- summary: "Knip not available. Install with: npm install -D knip",
131
- };
132
- }
133
-
134
- const targetDir = this.resolveProjectRoot(cwd || process.cwd());
135
-
136
- try {
137
- const args = [
138
- "knip",
139
- "--reporter=json",
140
- "--include",
141
- "files,exports,types,dependencies,unlisted",
142
- ];
143
- if (ignore && ignore.length > 0) {
144
- args.push("--ignore", ignore.join(","));
145
- }
146
-
147
- const result = safeSpawn("npx", args, {
148
- timeout: 30000,
149
- cwd: targetDir,
150
- });
151
-
152
- // Knip exits 0 on success (even with issues), 1 on errors
153
- const output = result.stdout || "";
154
- this.log(`Knip output length: ${output.length}`);
155
- if (output.length < 500) {
156
- this.log(`Knip output sample: ${output}`);
157
- }
158
- if (!output.trim()) {
159
- return {
160
- success: true,
161
- issues: [],
162
- unusedExports: [],
163
- unusedFiles: [],
164
- unusedDeps: [],
165
- unlistedDeps: [],
166
- summary: "No issues found",
167
- };
168
- }
169
-
170
- return this.parseOutput(output);
171
- } catch (err: any) {
172
- this.log(`Analysis error: ${err.message}`);
173
- return {
174
- success: false,
175
- issues: [],
176
- unusedExports: [],
177
- unusedFiles: [],
178
- unusedDeps: [],
179
- unlistedDeps: [],
180
- summary: `Error: ${err.message}`,
181
- };
182
- }
183
- }
184
-
185
- /**
186
- * Find unused exports in a specific file
187
- */
188
- findUnusedExports(filePath: string): string[] {
189
- const result = this.analyze(
190
- this.resolveProjectRoot(path.dirname(filePath)),
191
- );
192
- const basename = path.basename(filePath);
193
-
194
- return result.unusedExports
195
- .filter((e) => e.file?.includes(basename))
196
- .map((e) => e.name);
197
- }
198
-
199
- /**
200
- * Format results for LLM consumption
201
- */
202
- formatResult(result: KnipResult, maxItems = 20): string {
203
- if (!result.success) return `[Knip] ${result.summary}`;
204
- if (result.issues.length === 0) return "";
205
-
206
- let output = `[Knip] ${result.issues.length} issue(s)`;
207
- if (result.unusedExports.length)
208
- output += ` — ${result.unusedExports.length} unused export(s)`;
209
- if (result.unusedFiles.length)
210
- output += ` — ${result.unusedFiles.length} unused file(s)`;
211
- if (result.unusedDeps.length)
212
- output += ` — ${result.unusedDeps.length} unused dep(s)`;
213
- if (result.unlistedDeps.length)
214
- output += ` — ${result.unlistedDeps.length} unlisted dep(s)`;
215
- output += ":\n";
216
-
217
- // Show unused exports first (most useful for refactoring)
218
- if (result.unusedExports.length > 0) {
219
- output += "\n Unused exports:\n";
220
- for (const issue of result.unusedExports.slice(0, maxItems)) {
221
- const loc = issue.file ? ` (${path.basename(issue.file)})` : "";
222
- output += ` - ${issue.name}${loc}\n`;
223
- }
224
- if (result.unusedExports.length > maxItems) {
225
- output += ` ... and ${result.unusedExports.length - maxItems} more\n`;
226
- }
227
- }
228
-
229
- // Show unused files
230
- if (result.unusedFiles.length > 0) {
231
- output += "\n Unused files:\n";
232
- for (const issue of result.unusedFiles.slice(0, 10)) {
233
- output += ` - ${issue.name}\n`;
234
- }
235
- }
236
-
237
- // Show unused deps (might be worth removing)
238
- if (result.unusedDeps.length > 0) {
239
- output += "\n Unused dependencies:\n";
240
- for (const issue of result.unusedDeps) {
241
- output += ` - ${issue.package || issue.name}\n`;
242
- }
243
- }
244
-
245
- return output;
246
- }
247
-
248
- // --- Internal ---
249
-
250
- private parseOutput(output: string): KnipResult {
251
- try {
252
- const data = JSON.parse(output);
253
- const issues: KnipIssue[] = [];
254
- const unusedExports: KnipIssue[] = [];
255
- const unusedFiles: KnipIssue[] = [];
256
- const unusedDeps: KnipIssue[] = [];
257
- const unlistedDeps: KnipIssue[] = [];
258
-
259
- const addIssue = (issue: KnipIssue) => {
260
- issues.push(issue);
261
- if (issue.type === "export") unusedExports.push(issue);
262
- if (issue.type === "file") unusedFiles.push(issue);
263
- if (issue.type === "dependency" || issue.type === "devDependency") {
264
- unusedDeps.push(issue);
265
- }
266
- if (issue.type === "unlisted" || issue.type === "bin") {
267
- unlistedDeps.push(issue);
268
- }
269
- };
270
-
271
- // Knip JSON format (grouped): { issues: [ { file, exports:[], files:[], dependencies:[], ... } ] }
272
- const fileEntries: any[] = Array.isArray(data?.issues) ? data.issues : [];
273
-
274
- for (const entry of fileEntries) {
275
- const file: string = entry.file ?? "";
276
-
277
- const push = (
278
- arr: any[],
279
- type: KnipIssue["type"],
280
- _target: KnipIssue[],
281
- ) => {
282
- for (const item of arr) {
283
- addIssue({
284
- type,
285
- name: item.name ?? item.symbol ?? String(item),
286
- file,
287
- line: item.line,
288
- package: item.package,
289
- });
290
- }
291
- };
292
-
293
- push(entry.exports ?? [], "export", unusedExports);
294
- push(entry.types ?? [], "export", unusedExports);
295
- push(entry.files ?? [], "file", unusedFiles);
296
- push(entry.dependencies ?? [], "dependency", unusedDeps);
297
- push(entry.devDependencies ?? [], "devDependency", unusedDeps);
298
- push(entry.unlisted ?? [], "unlisted", unlistedDeps);
299
- push(entry.binaries ?? [], "bin", unlistedDeps);
300
- }
301
-
302
- // Fallback format: flat list of issue objects
303
- if (issues.length === 0 && Array.isArray(data)) {
304
- for (const item of data) {
305
- if (!item || typeof item !== "object") continue;
306
- const rawType = String(
307
- item.type ?? item.issueType ?? item.kind ?? "file",
308
- ).toLowerCase();
309
- const type: KnipIssue["type"] =
310
- rawType === "export" || rawType === "exports"
311
- ? "export"
312
- : rawType === "dependency"
313
- ? "dependency"
314
- : rawType === "devdependency"
315
- ? "devDependency"
316
- : rawType === "unlisted"
317
- ? "unlisted"
318
- : rawType === "bin" || rawType === "binaries"
319
- ? "bin"
320
- : rawType === "file"
321
- ? "file"
322
- : "file";
323
- addIssue({
324
- type,
325
- name: String(
326
- item.name ??
327
- item.symbol ??
328
- item.package ??
329
- item.message ??
330
- "unknown",
331
- ),
332
- file: item.file ?? item.path ?? item.location?.file,
333
- line: item.line ?? item.location?.line,
334
- package: item.package,
335
- });
336
- }
337
- }
338
-
339
- return {
340
- success: true,
341
- issues,
342
- unusedExports,
343
- unusedFiles,
344
- unusedDeps,
345
- unlistedDeps,
346
- summary: `Found ${issues.length} issues`,
347
- };
348
- } catch (err) {
349
- void err;
350
- this.log("Failed to parse knip JSON output");
351
- return {
352
- success: false,
353
- issues: [],
354
- unusedExports: [],
355
- unusedFiles: [],
356
- unusedDeps: [],
357
- unlistedDeps: [],
358
- summary: "Failed to parse output",
359
- };
360
- }
361
- }
362
- }
1
+ /**
2
+ * Knip Client for pi-local
3
+ *
4
+ * Detects unused exports, files, dependencies, and more.
5
+ * Essential for safe refactoring — I need to know what's dead code
6
+ * before I can clean it up.
7
+ *
8
+ * Requires: npm install -D knip
9
+ * Docs: https://knip.dev/
10
+ */
11
+
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { safeSpawn, safeSpawnAsync } from "./safe-spawn.js";
15
+
16
+ // --- Types ---
17
+
18
+ export interface KnipIssue {
19
+ type: "export" | "file" | "dependency" | "devDependency" | "unlisted" | "bin";
20
+ name: string;
21
+ file?: string;
22
+ line?: number;
23
+ package?: string;
24
+ }
25
+
26
+ export interface KnipResult {
27
+ success: boolean;
28
+ issues: KnipIssue[];
29
+ unusedExports: KnipIssue[];
30
+ unusedFiles: KnipIssue[];
31
+ unusedDeps: KnipIssue[];
32
+ unlistedDeps: KnipIssue[];
33
+ summary: string;
34
+ }
35
+
36
+ // --- Client ---
37
+
38
+ export class KnipClient {
39
+ private knipAvailable: boolean | null = null;
40
+ private log: (msg: string) => void;
41
+
42
+ constructor(verbose = false) {
43
+ this.log = verbose
44
+ ? (msg: string) => console.error(`[knip] ${msg}`)
45
+ : () => {};
46
+ }
47
+
48
+ private resolveProjectRoot(startDir: string): string {
49
+ let current = path.resolve(startDir);
50
+ while (true) {
51
+ const markers = [
52
+ "package.json",
53
+ "knip.json",
54
+ "knip.ts",
55
+ "knip.config.js",
56
+ "knip.config.ts",
57
+ ];
58
+ if (markers.some((m) => fs.existsSync(path.join(current, m)))) {
59
+ return current;
60
+ }
61
+ const parent = path.dirname(current);
62
+ if (parent === current) return path.resolve(startDir);
63
+ current = parent;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Check if knip CLI is available, auto-install if not
69
+ */
70
+ async ensureAvailable(): Promise<boolean> {
71
+ // Fast path: already checked
72
+ if (this.knipAvailable !== null) return this.knipAvailable;
73
+
74
+ // Check if available in PATH (fast)
75
+ const pathResult = await safeSpawnAsync("knip", ["--version"], {
76
+ timeout: 5000,
77
+ });
78
+ if (!pathResult.error && pathResult.status === 0) {
79
+ this.knipAvailable = true;
80
+ this.log("Knip found in PATH");
81
+ return true;
82
+ }
83
+
84
+ // Auto-install via pi-lens installer
85
+ this.log("Knip not found, attempting auto-install...");
86
+ const { ensureTool } = await import("./installer/index.js");
87
+ const installedPath = await ensureTool("knip");
88
+
89
+ if (installedPath) {
90
+ this.knipAvailable = true;
91
+ this.log(`Knip auto-installed: ${installedPath}`);
92
+ return true;
93
+ }
94
+
95
+ this.knipAvailable = false;
96
+ return false;
97
+ }
98
+
99
+ /**
100
+ * Check if knip CLI is available (legacy sync method)
101
+ * Prefer ensureAvailable() for auto-install behavior
102
+ */
103
+ isAvailable(): boolean {
104
+ if (this.knipAvailable !== null) return this.knipAvailable;
105
+
106
+ const result = safeSpawn("npx", ["knip", "--version"], {
107
+ timeout: 10000,
108
+ });
109
+
110
+ this.knipAvailable = !result.error && result.status === 0;
111
+ if (this.knipAvailable) {
112
+ this.log(`Knip available`);
113
+ }
114
+
115
+ return this.knipAvailable;
116
+ }
117
+
118
+ /**
119
+ * Run knip analysis on the project
120
+ */
121
+ analyze(cwd?: string, ignore?: string[]): KnipResult {
122
+ if (!this.isAvailable()) {
123
+ return {
124
+ success: false,
125
+ issues: [],
126
+ unusedExports: [],
127
+ unusedFiles: [],
128
+ unusedDeps: [],
129
+ unlistedDeps: [],
130
+ summary: "Knip not available. Install with: npm install -D knip",
131
+ };
132
+ }
133
+
134
+ const targetDir = this.resolveProjectRoot(cwd || process.cwd());
135
+
136
+ try {
137
+ const args = [
138
+ "knip",
139
+ "--reporter=json",
140
+ "--include",
141
+ "files,exports,types,dependencies,unlisted",
142
+ ];
143
+ if (ignore && ignore.length > 0) {
144
+ args.push("--ignore", ignore.join(","));
145
+ }
146
+
147
+ const result = safeSpawn("npx", args, {
148
+ timeout: 30000,
149
+ cwd: targetDir,
150
+ });
151
+
152
+ // Knip exits 0 on success (even with issues), 1 on errors
153
+ const output = result.stdout || "";
154
+ this.log(`Knip output length: ${output.length}`);
155
+ if (output.length < 500) {
156
+ this.log(`Knip output sample: ${output}`);
157
+ }
158
+ if (!output.trim()) {
159
+ return {
160
+ success: true,
161
+ issues: [],
162
+ unusedExports: [],
163
+ unusedFiles: [],
164
+ unusedDeps: [],
165
+ unlistedDeps: [],
166
+ summary: "No issues found",
167
+ };
168
+ }
169
+
170
+ return this.parseOutput(output);
171
+ } catch (err: any) {
172
+ this.log(`Analysis error: ${err.message}`);
173
+ return {
174
+ success: false,
175
+ issues: [],
176
+ unusedExports: [],
177
+ unusedFiles: [],
178
+ unusedDeps: [],
179
+ unlistedDeps: [],
180
+ summary: `Error: ${err.message}`,
181
+ };
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Find unused exports in a specific file
187
+ */
188
+ findUnusedExports(filePath: string): string[] {
189
+ const result = this.analyze(
190
+ this.resolveProjectRoot(path.dirname(filePath)),
191
+ );
192
+ const basename = path.basename(filePath);
193
+
194
+ return result.unusedExports
195
+ .filter((e) => e.file?.includes(basename))
196
+ .map((e) => e.name);
197
+ }
198
+
199
+ /**
200
+ * Format results for LLM consumption
201
+ */
202
+ formatResult(result: KnipResult, maxItems = 20): string {
203
+ if (!result.success) return `[Knip] ${result.summary}`;
204
+ if (result.issues.length === 0) return "";
205
+
206
+ let output = `[Knip] ${result.issues.length} issue(s)`;
207
+ if (result.unusedExports.length)
208
+ output += ` — ${result.unusedExports.length} unused export(s)`;
209
+ if (result.unusedFiles.length)
210
+ output += ` — ${result.unusedFiles.length} unused file(s)`;
211
+ if (result.unusedDeps.length)
212
+ output += ` — ${result.unusedDeps.length} unused dep(s)`;
213
+ if (result.unlistedDeps.length)
214
+ output += ` — ${result.unlistedDeps.length} unlisted dep(s)`;
215
+ output += ":\n";
216
+
217
+ // Show unused exports first (most useful for refactoring)
218
+ if (result.unusedExports.length > 0) {
219
+ output += "\n Unused exports:\n";
220
+ for (const issue of result.unusedExports.slice(0, maxItems)) {
221
+ const loc = issue.file ? ` (${path.basename(issue.file)})` : "";
222
+ output += ` - ${issue.name}${loc}\n`;
223
+ }
224
+ if (result.unusedExports.length > maxItems) {
225
+ output += ` ... and ${result.unusedExports.length - maxItems} more\n`;
226
+ }
227
+ }
228
+
229
+ // Show unused files
230
+ if (result.unusedFiles.length > 0) {
231
+ output += "\n Unused files:\n";
232
+ for (const issue of result.unusedFiles.slice(0, 10)) {
233
+ output += ` - ${issue.name}\n`;
234
+ }
235
+ }
236
+
237
+ // Show unused deps (might be worth removing)
238
+ if (result.unusedDeps.length > 0) {
239
+ output += "\n Unused dependencies:\n";
240
+ for (const issue of result.unusedDeps) {
241
+ output += ` - ${issue.package || issue.name}\n`;
242
+ }
243
+ }
244
+
245
+ return output;
246
+ }
247
+
248
+ // --- Internal ---
249
+
250
+ private parseOutput(output: string): KnipResult {
251
+ try {
252
+ const data = JSON.parse(output);
253
+ const issues: KnipIssue[] = [];
254
+ const unusedExports: KnipIssue[] = [];
255
+ const unusedFiles: KnipIssue[] = [];
256
+ const unusedDeps: KnipIssue[] = [];
257
+ const unlistedDeps: KnipIssue[] = [];
258
+
259
+ const addIssue = (issue: KnipIssue) => {
260
+ issues.push(issue);
261
+ if (issue.type === "export") unusedExports.push(issue);
262
+ if (issue.type === "file") unusedFiles.push(issue);
263
+ if (issue.type === "dependency" || issue.type === "devDependency") {
264
+ unusedDeps.push(issue);
265
+ }
266
+ if (issue.type === "unlisted" || issue.type === "bin") {
267
+ unlistedDeps.push(issue);
268
+ }
269
+ };
270
+
271
+ // Knip JSON format (grouped): { issues: [ { file, exports:[], files:[], dependencies:[], ... } ] }
272
+ const fileEntries: any[] = Array.isArray(data?.issues) ? data.issues : [];
273
+
274
+ for (const entry of fileEntries) {
275
+ const file: string = entry.file ?? "";
276
+
277
+ const push = (
278
+ arr: any[],
279
+ type: KnipIssue["type"],
280
+ _target: KnipIssue[],
281
+ ) => {
282
+ for (const item of arr) {
283
+ addIssue({
284
+ type,
285
+ name: item.name ?? item.symbol ?? String(item),
286
+ file,
287
+ line: item.line,
288
+ package: item.package,
289
+ });
290
+ }
291
+ };
292
+
293
+ push(entry.exports ?? [], "export", unusedExports);
294
+ push(entry.types ?? [], "export", unusedExports);
295
+ push(entry.files ?? [], "file", unusedFiles);
296
+ push(entry.dependencies ?? [], "dependency", unusedDeps);
297
+ push(entry.devDependencies ?? [], "devDependency", unusedDeps);
298
+ push(entry.unlisted ?? [], "unlisted", unlistedDeps);
299
+ push(entry.binaries ?? [], "bin", unlistedDeps);
300
+ }
301
+
302
+ // Fallback format: flat list of issue objects
303
+ if (issues.length === 0 && Array.isArray(data)) {
304
+ for (const item of data) {
305
+ if (!item || typeof item !== "object") continue;
306
+ const rawType = String(
307
+ item.type ?? item.issueType ?? item.kind ?? "file",
308
+ ).toLowerCase();
309
+ const type: KnipIssue["type"] =
310
+ rawType === "export" || rawType === "exports"
311
+ ? "export"
312
+ : rawType === "dependency"
313
+ ? "dependency"
314
+ : rawType === "devdependency"
315
+ ? "devDependency"
316
+ : rawType === "unlisted"
317
+ ? "unlisted"
318
+ : rawType === "bin" || rawType === "binaries"
319
+ ? "bin"
320
+ : "file";
321
+ addIssue({
322
+ type,
323
+ name: String(
324
+ item.name ??
325
+ item.symbol ??
326
+ item.package ??
327
+ item.message ??
328
+ "unknown",
329
+ ),
330
+ file: item.file ?? item.path ?? item.location?.file,
331
+ line: item.line ?? item.location?.line,
332
+ package: item.package,
333
+ });
334
+ }
335
+ }
336
+
337
+ return {
338
+ success: true,
339
+ issues,
340
+ unusedExports,
341
+ unusedFiles,
342
+ unusedDeps,
343
+ unlistedDeps,
344
+ summary: `Found ${issues.length} issues`,
345
+ };
346
+ } catch (err) {
347
+ void err;
348
+ this.log("Failed to parse knip JSON output");
349
+ return {
350
+ success: false,
351
+ issues: [],
352
+ unusedExports: [],
353
+ unusedFiles: [],
354
+ unusedDeps: [],
355
+ unlistedDeps: [],
356
+ summary: "Failed to parse output",
357
+ };
358
+ }
359
+ }
360
+ }