pi-lens 3.8.21 → 3.8.22

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 (47) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +2 -0
  3. package/clients/dispatch/runners/lsp.ts +58 -3
  4. package/clients/dispatch/runners/tree-sitter.ts +467 -0
  5. package/clients/lsp/client.ts +229 -3
  6. package/clients/lsp/index.ts +111 -1
  7. package/clients/pipeline.ts +2 -2
  8. package/clients/runtime-session.ts +43 -5
  9. package/clients/tree-sitter-client.ts +162 -0
  10. package/clients/tree-sitter-logger.ts +47 -0
  11. package/clients/tree-sitter-query-loader.ts +13 -2
  12. package/package.json +3 -1
  13. package/rules/rule-catalog.json +64 -0
  14. package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
  15. package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
  16. package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
  17. package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
  18. package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
  19. package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
  20. package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
  21. package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
  22. package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
  23. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
  24. package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
  25. package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
  26. package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
  27. package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
  28. package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
  29. package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
  30. package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
  31. package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
  32. package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
  33. package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
  34. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
  35. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
  36. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
  37. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
  38. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
  39. package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
  40. package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
  41. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
  42. package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
  43. package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
  44. package/scripts/validate-rule-catalog.mjs +227 -0
  45. package/skills/lsp-navigation/SKILL.md +15 -3
  46. package/tools/lsp-navigation.js +259 -28
  47. package/tools/lsp-navigation.ts +294 -29
@@ -10,24 +10,96 @@ import { Type } from "@sinclair/typebox";
10
10
  import type { LSPCallHierarchyItem } from "../clients/lsp/client.js";
11
11
  import { getLSPService } from "../clients/lsp/index.js";
12
12
 
13
+ function operationSupportStatus(
14
+ operation: string,
15
+ support: import("../clients/lsp/client.js").LSPOperationSupport | null,
16
+ ): boolean | null {
17
+ if (!support) return null;
18
+ if (operation === "definition") return support.definition;
19
+ if (operation === "references") return support.references;
20
+ if (operation === "hover") return support.hover;
21
+ if (operation === "signatureHelp") return support.signatureHelp;
22
+ if (operation === "documentSymbol") return support.documentSymbol;
23
+ if (operation === "workspaceSymbol") return support.workspaceSymbol;
24
+ if (operation === "codeAction") return support.codeAction;
25
+ if (operation === "rename") return support.rename;
26
+ if (operation === "implementation") return support.implementation;
27
+ if (
28
+ operation === "prepareCallHierarchy" ||
29
+ operation === "incomingCalls" ||
30
+ operation === "outgoingCalls"
31
+ )
32
+ return support.callHierarchy;
33
+ return null;
34
+ }
35
+
36
+ function emptyReasonForOperation(operation: string): string {
37
+ if (operation === "signatureHelp") return "position-sensitive-or-no-signature";
38
+ if (operation === "codeAction") return "no-applicable-actions";
39
+ if (operation === "rename") return "no-rename-edits-or-symbol-not-renamable";
40
+ if (operation === "workspaceSymbol")
41
+ return "no-matching-symbols-or-server-index-unavailable";
42
+ if (operation === "incomingCalls" || operation === "outgoingCalls")
43
+ return "no-call-hierarchy-results";
44
+ return "no-results";
45
+ }
46
+
47
+ function classifyCodeActions(
48
+ actions: Array<{ kind?: string }> | undefined,
49
+ ): { quickfix: number; refactor: number; other: number } {
50
+ if (!actions || actions.length === 0) return { quickfix: 0, refactor: 0, other: 0 };
51
+ let quickfix = 0;
52
+ let refactor = 0;
53
+ let other = 0;
54
+ for (const action of actions) {
55
+ const kind = action.kind ?? "";
56
+ if (kind.startsWith("quickfix")) quickfix += 1;
57
+ else if (kind.startsWith("refactor")) refactor += 1;
58
+ else other += 1;
59
+ }
60
+ return { quickfix, refactor, other };
61
+ }
62
+
63
+ async function openFileBestEffort(
64
+ lspService: ReturnType<typeof getLSPService>,
65
+ filePath: string,
66
+ ): Promise<void> {
67
+ let fileContent: string | undefined;
68
+ try {
69
+ fileContent = nodeFs.readFileSync(filePath, "utf-8");
70
+ } catch {
71
+ return;
72
+ }
73
+ if (!fileContent) return;
74
+ try {
75
+ await lspService.openFile(filePath, fileContent);
76
+ } catch {
77
+ /* LSP server may not be ready yet — proceed anyway */
78
+ }
79
+ }
80
+
13
81
  export function createLspNavigationTool(
14
82
  getFlag: (name: string) => boolean | string | undefined,
15
83
  ) {
16
84
  return {
17
85
  name: "lsp_navigation" as const,
18
86
  label: "LSP Navigate",
19
- description:
87
+ description:
20
88
  "Navigate code using LSP (Language Server Protocol). Requires --lens-lsp flag.\n" +
21
89
  "Operations:\n" +
22
90
  "- definition: Jump to where a symbol is defined\n" +
23
91
  "- references: Find all usages of a symbol\n" +
24
92
  "- hover: Get type/doc info at a position\n" +
93
+ "- signatureHelp: Show callable signatures at cursor\n" +
25
94
  "- documentSymbol: List all symbols (functions/classes/vars) in a file\n" +
26
- "- workspaceSymbol: Search symbols across the whole project\n" +
95
+ "- workspaceSymbol: Search symbols across the whole project (best with filePath context)\n" +
96
+ "- codeAction: Find available quick fixes/refactors at a range\n" +
97
+ "- rename: Compute workspace edits for renaming a symbol\n" +
27
98
  "- implementation: Jump to interface implementations\n" +
28
99
  "- prepareCallHierarchy: Get callable item at position (for incoming/outgoing)\n" +
29
100
  "- incomingCalls: Find all functions/methods that CALL this function\n" +
30
- "- outgoingCalls: Find all functions/methods CALLED by this function\n\n" +
101
+ "- outgoingCalls: Find all functions/methods CALLED by this function\n" +
102
+ "- workspaceDiagnostics: List all diagnostics tracked by active LSP clients\n\n" +
31
103
  "Line and character are 1-based (as shown in editors).",
32
104
  promptSnippet:
33
105
  "Use lsp_navigation to find definitions, references, and hover info via LSP",
@@ -37,18 +109,25 @@ export function createLspNavigationTool(
37
109
  Type.Literal("definition"),
38
110
  Type.Literal("references"),
39
111
  Type.Literal("hover"),
112
+ Type.Literal("signatureHelp"),
40
113
  Type.Literal("documentSymbol"),
41
114
  Type.Literal("workspaceSymbol"),
115
+ Type.Literal("codeAction"),
116
+ Type.Literal("rename"),
42
117
  Type.Literal("implementation"),
43
118
  Type.Literal("prepareCallHierarchy"),
44
119
  Type.Literal("incomingCalls"),
45
120
  Type.Literal("outgoingCalls"),
121
+ Type.Literal("workspaceDiagnostics"),
46
122
  ],
47
123
  { description: "LSP operation to perform" },
48
124
  ),
49
- filePath: Type.String({
50
- description: "Absolute or relative path to the file",
51
- }),
125
+ filePath: Type.Optional(
126
+ Type.String({
127
+ description:
128
+ "Absolute or relative file path. Required for file-scoped operations; optional for workspaceSymbol/workspaceDiagnostics.",
129
+ }),
130
+ ),
52
131
  line: Type.Optional(
53
132
  Type.Number({
54
133
  description:
@@ -61,9 +140,27 @@ export function createLspNavigationTool(
61
140
  "Character offset (1-based). Required for definition/references/hover/implementation",
62
141
  }),
63
142
  ),
143
+ endLine: Type.Optional(
144
+ Type.Number({
145
+ description:
146
+ "End line (1-based). Optional; used by codeAction range.",
147
+ }),
148
+ ),
149
+ endCharacter: Type.Optional(
150
+ Type.Number({
151
+ description:
152
+ "End character (1-based). Optional; used by codeAction range.",
153
+ }),
154
+ ),
155
+ newName: Type.Optional(
156
+ Type.String({
157
+ description: "Required for rename operation.",
158
+ }),
159
+ ),
64
160
  query: Type.Optional(
65
161
  Type.String({
66
- description: "Symbol name to search. Used by workspaceSymbol",
162
+ description:
163
+ "Symbol name to search. Used by workspaceSymbol (best with filePath for active project context).",
67
164
  }),
68
165
  ),
69
166
  callHierarchyItem: Type.Optional(
@@ -107,6 +204,9 @@ export function createLspNavigationTool(
107
204
  _onUpdate: unknown,
108
205
  ctx: { cwd?: string },
109
206
  ) {
207
+ let supported: boolean | null = null;
208
+ let diagnosticsMode: "pull" | "push-only" | "unknown" = "unknown";
209
+
110
210
  if (!getFlag("lens-lsp") || getFlag("no-lsp")) {
111
211
  return {
112
212
  content: [
@@ -125,22 +225,84 @@ export function createLspNavigationTool(
125
225
  filePath: rawPath,
126
226
  line,
127
227
  character,
228
+ endLine,
229
+ endCharacter,
230
+ newName,
128
231
  query,
129
232
  } = params as {
130
233
  operation: string;
131
- filePath: string;
234
+ filePath?: string;
132
235
  line?: number;
133
236
  character?: number;
237
+ endLine?: number;
238
+ endCharacter?: number;
239
+ newName?: string;
134
240
  query?: string;
135
241
  };
136
242
 
137
- const filePath = path.isAbsolute(rawPath)
138
- ? rawPath
139
- : path.resolve(ctx.cwd || ".", rawPath);
243
+ const isCallHierarchyTraversal =
244
+ operation === "incomingCalls" || operation === "outgoingCalls";
245
+ const needsFilePath =
246
+ operation !== "workspaceDiagnostics" &&
247
+ operation !== "workspaceSymbol" &&
248
+ !isCallHierarchyTraversal;
249
+ if (needsFilePath && (!rawPath || rawPath.trim().length === 0)) {
250
+ return {
251
+ content: [
252
+ {
253
+ type: "text" as const,
254
+ text: `filePath is required for ${operation}`,
255
+ },
256
+ ],
257
+ isError: true,
258
+ details: {},
259
+ };
260
+ }
261
+
262
+ const filePath = rawPath
263
+ ? path.isAbsolute(rawPath)
264
+ ? rawPath
265
+ : path.resolve(ctx.cwd || ".", rawPath)
266
+ : "";
140
267
 
141
268
  const lspService = getLSPService();
142
- const hasLSP = await lspService.hasLSP(filePath);
143
- if (!hasLSP) {
269
+ if (operation === "workspaceDiagnostics") {
270
+ const allDiagnostics = await lspService.getAllDiagnostics();
271
+ const wsDiagSupport = await lspService.getWorkspaceDiagnosticsSupport(
272
+ rawPath ? filePath : undefined,
273
+ );
274
+ diagnosticsMode = wsDiagSupport?.mode ?? "unknown";
275
+ const result = Array.from(allDiagnostics.entries()).map(
276
+ ([trackedFile, diags]) => ({
277
+ filePath: trackedFile,
278
+ diagnostics: diags,
279
+ count: diags.length,
280
+ }),
281
+ );
282
+ const note =
283
+ diagnosticsMode === "push-only"
284
+ ? "Note: push-only tracked diagnostics snapshot (not full workspace pull diagnostics)."
285
+ : diagnosticsMode === "pull"
286
+ ? "Note: server advertises workspace pull diagnostics support."
287
+ : "Note: workspace diagnostics mode unknown (no active capability snapshot).";
288
+ return {
289
+ content: [
290
+ {
291
+ type: "text" as const,
292
+ text: `${note}\n${JSON.stringify(result, null, 2)}`,
293
+ },
294
+ ],
295
+ details: {
296
+ operation,
297
+ resultCount: result.length,
298
+ diagnosticsMode,
299
+ coverage: "tracked-open-files",
300
+ },
301
+ };
302
+ }
303
+
304
+ const hasLSP = filePath ? await lspService.hasLSP(filePath) : false;
305
+ if (needsFilePath && !hasLSP) {
144
306
  return {
145
307
  content: [
146
308
  {
@@ -153,24 +315,30 @@ export function createLspNavigationTool(
153
315
  };
154
316
  }
155
317
 
156
- // Ensure file is open in LSP before querying
157
- let fileContent: string | undefined;
158
- try {
159
- fileContent = nodeFs.readFileSync(filePath, "utf-8");
160
- } catch {
161
- /* ignore */
162
- }
163
- if (fileContent) {
164
- try {
165
- await lspService.openFile(filePath, fileContent);
166
- } catch {
167
- /* LSP server may not be ready yet — proceed anyway */
318
+ if (needsFilePath) {
319
+ const support = await lspService.getOperationSupport(filePath);
320
+ supported = operationSupportStatus(operation, support);
321
+ if (supported === false) {
322
+ return {
323
+ content: [
324
+ {
325
+ type: "text" as const,
326
+ text: `LSP server for ${path.basename(filePath)} does not advertise support for ${operation}`,
327
+ },
328
+ ],
329
+ isError: true,
330
+ details: { operation, supported: false, emptyReason: "unsupported" },
331
+ };
168
332
  }
333
+
334
+ await openFileBestEffort(lspService, filePath);
169
335
  }
170
336
 
171
337
  // Convert 1-based editor coords to 0-based LSP coords
172
338
  const lspLine = (line ?? 1) - 1;
173
339
  const lspChar = (character ?? 1) - 1;
340
+ const lspEndLine = (endLine ?? line ?? 1) - 1;
341
+ const lspEndChar = (endCharacter ?? character ?? 1) - 1;
174
342
 
175
343
  let result: unknown;
176
344
  try {
@@ -184,19 +352,91 @@ export function createLspNavigationTool(
184
352
  case "hover":
185
353
  result = await lspService.hover(filePath, lspLine, lspChar);
186
354
  break;
355
+ case "signatureHelp":
356
+ result = await lspService.signatureHelp(filePath, lspLine, lspChar);
357
+ break;
187
358
  case "documentSymbol":
188
359
  result = await lspService.documentSymbol(filePath);
189
360
  break;
190
361
  case "workspaceSymbol":
191
- result = await lspService.workspaceSymbol(query ?? "");
362
+ supported = operationSupportStatus(
363
+ operation,
364
+ await lspService.getOperationSupport(rawPath ? filePath : undefined),
365
+ );
366
+ if (supported === false) {
367
+ return {
368
+ content: [
369
+ {
370
+ type: "text" as const,
371
+ text: "Active LSP server does not advertise support for workspaceSymbol",
372
+ },
373
+ ],
374
+ isError: true,
375
+ details: {
376
+ operation,
377
+ supported: false,
378
+ emptyReason: "unsupported",
379
+ },
380
+ };
381
+ }
382
+ if (!query || query.trim().length === 0) {
383
+ return {
384
+ content: [
385
+ {
386
+ type: "text" as const,
387
+ text: "query parameter required for workspaceSymbol",
388
+ },
389
+ ],
390
+ isError: true,
391
+ details: {},
392
+ };
393
+ }
394
+ if (rawPath) {
395
+ await openFileBestEffort(lspService, filePath);
396
+ }
397
+ try {
398
+ result = await lspService.workspaceSymbol(
399
+ query ?? "",
400
+ rawPath ? filePath : undefined,
401
+ );
402
+ } catch (err) {
403
+ const msg = err instanceof Error ? err.message : String(err);
404
+ if (rawPath && /No Project/i.test(msg)) {
405
+ await openFileBestEffort(lspService, filePath);
406
+ await new Promise((resolve) => setTimeout(resolve, 120));
407
+ result = await lspService.workspaceSymbol(query ?? "", filePath);
408
+ } else {
409
+ throw err;
410
+ }
411
+ }
192
412
  break;
193
- case "implementation":
194
- result = await lspService.implementation(
413
+ case "codeAction":
414
+ result = await lspService.codeAction(
195
415
  filePath,
196
416
  lspLine,
197
417
  lspChar,
418
+ lspEndLine,
419
+ lspEndChar,
198
420
  );
199
421
  break;
422
+ case "rename":
423
+ if (!newName || newName.trim().length === 0) {
424
+ return {
425
+ content: [
426
+ {
427
+ type: "text" as const,
428
+ text: "newName parameter required for rename",
429
+ },
430
+ ],
431
+ isError: true,
432
+ details: {},
433
+ };
434
+ }
435
+ result = await lspService.rename(filePath, lspLine, lspChar, newName);
436
+ break;
437
+ case "implementation":
438
+ result = await lspService.implementation(filePath, lspLine, lspChar);
439
+ break;
200
440
  case "prepareCallHierarchy":
201
441
  result = await lspService.prepareCallHierarchy(
202
442
  filePath,
@@ -259,14 +499,39 @@ export function createLspNavigationTool(
259
499
  }
260
500
 
261
501
  const isEmpty = !result || (Array.isArray(result) && result.length === 0);
262
- const output = isEmpty
502
+ let output = isEmpty
263
503
  ? `No results for ${operation} at ${path.basename(filePath)}${line ? `:${line}:${character}` : ""}`
264
504
  : JSON.stringify(result, null, 2);
505
+ if (isEmpty && operation === "workspaceSymbol" && !rawPath) {
506
+ output +=
507
+ "\nHint: provide filePath to scope workspaceSymbol to the active language server/root.";
508
+ }
509
+ if (
510
+ operation === "references" &&
511
+ Array.isArray(result) &&
512
+ result.length <= 2
513
+ ) {
514
+ output +=
515
+ "\nHint: references from usage sites can be partial; retry from the symbol definition for broader cross-file results.";
516
+ }
517
+ const actionStats =
518
+ operation === "codeAction" && Array.isArray(result)
519
+ ? classifyCodeActions(result as Array<{ kind?: string }>)
520
+ : null;
521
+ if (operation === "codeAction" && actionStats) {
522
+ if (actionStats.quickfix === 0 && actionStats.refactor > 0) {
523
+ output +=
524
+ "\nNote: no diagnostic quick fixes returned; refactor-only actions available.";
525
+ }
526
+ }
265
527
 
266
528
  return {
267
529
  content: [{ type: "text" as const, text: output }],
268
530
  details: {
269
531
  operation,
532
+ supported,
533
+ emptyReason: isEmpty ? emptyReasonForOperation(operation) : undefined,
534
+ codeActionKinds: actionStats ?? undefined,
270
535
  resultCount: Array.isArray(result) ? result.length : result ? 1 : 0,
271
536
  },
272
537
  };