decorated-pi 0.2.2 → 0.4.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 +82 -74
- package/extensions/file-times.ts +124 -0
- package/extensions/guidance.ts +5 -3
- package/extensions/index.ts +6 -2
- package/extensions/io.ts +587 -0
- package/extensions/lsp/client.ts +181 -428
- package/extensions/lsp/env.ts +45 -12
- package/extensions/lsp/format.ts +102 -237
- package/extensions/lsp/index.ts +8 -11
- package/extensions/lsp/manager.ts +249 -0
- package/extensions/lsp/prompt.ts +3 -42
- package/extensions/lsp/protocol.ts +219 -0
- package/extensions/lsp/servers.ts +80 -160
- package/extensions/lsp/tools.ts +175 -510
- package/extensions/lsp/types.ts +42 -0
- package/extensions/mcp/builtin.ts +126 -0
- package/extensions/mcp/client.ts +106 -0
- package/extensions/mcp/index.ts +123 -0
- package/extensions/{extend-model.ts → model-integration.ts} +127 -4
- package/extensions/patch.ts +842 -0
- package/extensions/providers/ark-coding.ts +2 -0
- package/extensions/safety/detect.ts +78 -707
- package/extensions/safety/entropy.ts +226 -0
- package/extensions/safety/index.ts +44 -97
- package/extensions/safety/patterns.ts +155 -0
- package/extensions/safety/types.ts +50 -0
- package/extensions/settings.ts +10 -0
- package/extensions/slash.ts +165 -9
- package/extensions/smart-at.ts +339 -111
- package/extensions/subdir-agents.ts +43 -13
- package/package.json +3 -4
- package/tsconfig.json +16 -0
- package/extensions/lsp/server-manager.ts +0 -309
- package/extensions/lsp/trust.ts +0 -45
package/extensions/lsp/tools.ts
CHANGED
|
@@ -1,538 +1,203 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LSP Tool Definitions —
|
|
3
|
-
*
|
|
4
|
-
* Based on @spences10/pi-lsp by Scott Spence
|
|
5
|
-
* https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
|
|
6
|
-
*
|
|
7
|
-
* Modifications: added lsp_find_symbol, lsp_rename, multi-file lsp_diagnostics
|
|
2
|
+
* LSP Tool Definitions — 2 tools for Pi.
|
|
8
3
|
*/
|
|
9
|
-
import {
|
|
4
|
+
import { keyHint, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
10
6
|
import { Type } from "typebox";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
filter_diagnostics,
|
|
14
|
-
find_symbol_matches,
|
|
15
|
-
format_diagnostics,
|
|
16
|
-
format_document_symbols,
|
|
17
|
-
format_hover,
|
|
18
|
-
format_locations,
|
|
19
|
-
format_symbol_matches,
|
|
20
|
-
format_tool_error,
|
|
21
|
-
SYMBOL_KIND_NAMES,
|
|
22
|
-
to_lsp_tool_error,
|
|
23
|
-
type SeverityFilter,
|
|
24
|
-
} from "./format.js";
|
|
25
|
-
import type { LspServerManager } from "./server-manager.js";
|
|
7
|
+
import { LspServerManager, LspToolError, formatToolError } from "./manager.js";
|
|
26
8
|
|
|
27
|
-
|
|
28
|
-
SYMBOL_KIND_NAMES.map((name) => Type.Literal(name))
|
|
29
|
-
);
|
|
9
|
+
// ─── TUI rendering ─────────────────────────────────────────────────────────
|
|
30
10
|
|
|
31
|
-
const
|
|
11
|
+
const LSP_RESULT_FOLD_LINES = 45;
|
|
32
12
|
|
|
33
|
-
function
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
return {
|
|
38
|
-
content: [{ type: "text" as const, text }],
|
|
39
|
-
details,
|
|
40
|
-
};
|
|
13
|
+
function trimTrailingEmptyLines(lines: string[]): string[] {
|
|
14
|
+
let end = lines.length;
|
|
15
|
+
while (end > 0 && lines[end - 1] === "") end -= 1;
|
|
16
|
+
return lines.slice(0, end);
|
|
41
17
|
}
|
|
42
18
|
|
|
43
|
-
function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
19
|
+
function collapseText(text: string, maxLines = LSP_RESULT_FOLD_LINES) {
|
|
20
|
+
const lines = trimTrailingEmptyLines(text.split("\n"));
|
|
21
|
+
const totalLines = lines.length;
|
|
22
|
+
const displayLines = lines.slice(0, maxLines);
|
|
23
|
+
const remainingLines = Math.max(0, totalLines - maxLines);
|
|
24
|
+
return { totalLines, displayLines, remainingLines };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getTextContent(result: { content?: Array<{ type: string; text?: string }> }): string {
|
|
28
|
+
return (result.content ?? []).filter((c): c is { type: "text"; text?: string } => c.type === "text").map((c) => c.text ?? "").join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatResultText(text: string, expanded: boolean, theme: any): string {
|
|
32
|
+
const { totalLines, displayLines, remainingLines } = collapseText(text, expanded ? Number.MAX_SAFE_INTEGER : LSP_RESULT_FOLD_LINES);
|
|
33
|
+
let rendered = displayLines.join("\n") ? theme.fg("toolOutput", displayLines.join("\n")) : "";
|
|
34
|
+
if (!expanded && remainingLines > 0) rendered += `${theme.fg("muted", `\n... (${remainingLines} more lines, ${totalLines} total,`)} ${keyHint("app.tools.expand", "to expand")})`;
|
|
35
|
+
return rendered;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function renderLspResult(result: any, options: { expanded: boolean }, theme: any, context: any) {
|
|
39
|
+
const component = context.lastComponent ?? new Text("", 0, 0);
|
|
40
|
+
component.setText(formatResultText(getTextContent(result), options.expanded, theme));
|
|
41
|
+
return component;
|
|
48
42
|
}
|
|
49
43
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
while (true) {
|
|
61
|
-
const index = next_index;
|
|
62
|
-
next_index += 1;
|
|
63
|
-
if (index >= items.length) return;
|
|
64
|
-
results[index] = await mapper(items[index]!, index);
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
);
|
|
68
|
-
return results;
|
|
44
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
type ToolResult = { content: Array<{ type: "text"; text: string }>; details: Record<string, unknown> };
|
|
47
|
+
|
|
48
|
+
function ok(text: string, details: Record<string, unknown> = {}): ToolResult {
|
|
49
|
+
return { content: [{ type: "text" as const, text }], details };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function err(details: any): ToolResult {
|
|
53
|
+
return ok(formatToolError(details), { ok: false, error: details });
|
|
69
54
|
}
|
|
70
55
|
|
|
71
|
-
async function
|
|
56
|
+
async function withFile(
|
|
72
57
|
manager: LspServerManager,
|
|
73
58
|
file: string,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
) {
|
|
77
|
-
const resolved = await manager.
|
|
78
|
-
if (!resolved.ok)
|
|
79
|
-
return make_tool_error(resolved.error);
|
|
80
|
-
}
|
|
59
|
+
fn: (s: { abs: string; uri: string; state: any }) => Promise<string>,
|
|
60
|
+
options: { timeoutMs?: number } = {},
|
|
61
|
+
): Promise<ToolResult> {
|
|
62
|
+
const resolved = await manager.resolveFileState(file, { timeoutMs: options.timeoutMs });
|
|
63
|
+
if (!resolved.ok) return err(resolved.error);
|
|
81
64
|
const { result } = resolved;
|
|
82
65
|
try {
|
|
83
|
-
const text = await
|
|
84
|
-
return
|
|
85
|
-
ok: true,
|
|
86
|
-
language: result.state.language,
|
|
87
|
-
command: result.state.command,
|
|
88
|
-
workspace_root: result.state.workspace_root,
|
|
89
|
-
});
|
|
66
|
+
const text = await fn(result);
|
|
67
|
+
return ok(text, { ok: true, language: result.state.language, workspace_root: result.state.workspaceRoot });
|
|
90
68
|
} catch (error) {
|
|
91
|
-
return
|
|
92
|
-
|
|
93
|
-
result.abs,
|
|
94
|
-
result.state.language,
|
|
95
|
-
result.state.workspace_root,
|
|
96
|
-
result.state.command,
|
|
97
|
-
result.state.install_hint,
|
|
98
|
-
error
|
|
99
|
-
)
|
|
100
|
-
);
|
|
69
|
+
if (error instanceof LspToolError) return err(error.details);
|
|
70
|
+
return err({ kind: "tool_execution_failed", file: result.abs, language: result.state.language, workspace_root: result.state.workspaceRoot, command: result.state.command, install_hint: result.state.installHint, message: error instanceof Error ? error.message : String(error) });
|
|
101
71
|
}
|
|
102
72
|
}
|
|
103
73
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
promptGuidelines: [
|
|
113
|
-
"Use lsp_diagnostics to validate focused code changes after editing or writing before reporting completion.",
|
|
114
|
-
],
|
|
115
|
-
parameters: Type.Object({
|
|
116
|
-
files: Type.Array(Type.String(), {
|
|
117
|
-
minItems: 1,
|
|
118
|
-
maxItems: 100,
|
|
119
|
-
description: "Files to check. Single file or list (relative to cwd or absolute).",
|
|
120
|
-
}),
|
|
121
|
-
severity: Type.Optional(
|
|
122
|
-
Type.Array(
|
|
123
|
-
Type.Union([
|
|
124
|
-
Type.Literal("error"),
|
|
125
|
-
Type.Literal("warning"),
|
|
126
|
-
Type.Literal("info"),
|
|
127
|
-
Type.Literal("hint"),
|
|
128
|
-
]),
|
|
129
|
-
{
|
|
130
|
-
description:
|
|
131
|
-
"Filter to specific severity levels. Default: error. Values: error, warning, info, hint. Picking a level shows it and all more severe levels (e.g. warning → error + warning).",
|
|
132
|
-
}
|
|
133
|
-
)
|
|
134
|
-
),
|
|
135
|
-
wait_ms: Type.Optional(
|
|
136
|
-
Type.Number({
|
|
137
|
-
description:
|
|
138
|
-
"Max ms to wait for diagnostics after opening each file. Default 1500.",
|
|
139
|
-
})
|
|
140
|
-
),
|
|
141
|
-
}),
|
|
142
|
-
execute: async (_id, params, _signal, _on_update, ctx) => {
|
|
143
|
-
const wait_ms = params.wait_ms ?? 1500;
|
|
144
|
-
const severities: SeverityFilter[] = params.severity ?? ["error"];
|
|
145
|
-
|
|
146
|
-
const lines_with_stats = await map_with_concurrency(
|
|
147
|
-
params.files,
|
|
148
|
-
DIAGNOSTICS_MANY_CONCURRENCY,
|
|
149
|
-
async (file) => {
|
|
150
|
-
const resolved = await manager.resolve_file_state(file, ctx);
|
|
151
|
-
if (!resolved.ok) {
|
|
152
|
-
return {
|
|
153
|
-
line: format_tool_error(resolved.error),
|
|
154
|
-
diagnostics: 0,
|
|
155
|
-
error: true,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
try {
|
|
159
|
-
const diagnostics =
|
|
160
|
-
await resolved.result.state.client.wait_for_diagnostics(
|
|
161
|
-
resolved.result.uri,
|
|
162
|
-
wait_ms
|
|
163
|
-
);
|
|
164
|
-
const filtered = filter_diagnostics(diagnostics, severities);
|
|
165
|
-
let errors = 0, warnings = 0, infos = 0;
|
|
166
|
-
for (const d of filtered) {
|
|
167
|
-
if (d.severity === 1) errors++;
|
|
168
|
-
else if (d.severity === 2) warnings++;
|
|
169
|
-
else infos++;
|
|
170
|
-
}
|
|
171
|
-
return {
|
|
172
|
-
line: format_diagnostics(resolved.result.abs, diagnostics, severities),
|
|
173
|
-
diagnostics: filtered.length,
|
|
174
|
-
errors,
|
|
175
|
-
warnings,
|
|
176
|
-
error: false,
|
|
177
|
-
};
|
|
178
|
-
} catch (error) {
|
|
179
|
-
return {
|
|
180
|
-
line: format_tool_error(
|
|
181
|
-
to_lsp_tool_error(
|
|
182
|
-
resolved.result.abs,
|
|
183
|
-
resolved.result.state.language,
|
|
184
|
-
resolved.result.state.workspace_root,
|
|
185
|
-
resolved.result.state.command,
|
|
186
|
-
resolved.result.state.install_hint,
|
|
187
|
-
error
|
|
188
|
-
)
|
|
189
|
-
),
|
|
190
|
-
diagnostics: 0,
|
|
191
|
-
error: true,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
let total_diag = 0;
|
|
198
|
-
let total_err = 0;
|
|
199
|
-
let total_warn = 0;
|
|
200
|
-
let clean_count = 0;
|
|
201
|
-
let fail_count = 0;
|
|
202
|
-
const lines: string[] = [];
|
|
203
|
-
for (const entry of lines_with_stats) {
|
|
204
|
-
lines.push(entry.line);
|
|
205
|
-
if (entry.error) {
|
|
206
|
-
fail_count += 1;
|
|
207
|
-
} else {
|
|
208
|
-
total_diag += entry.diagnostics;
|
|
209
|
-
total_err += (entry as any).errors ?? 0;
|
|
210
|
-
total_warn += (entry as any).warnings ?? 0;
|
|
211
|
-
if (entry.diagnostics === 0) clean_count += 1;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
const summary = total_err > 0 || total_warn > 0
|
|
215
|
-
? `Checked ${params.files.length} file(s): ${total_err} error(s), ${total_warn} warning(s), ${total_diag - total_err - total_warn} info/hint(s), ${clean_count} clean, ${fail_count} failed to check`
|
|
216
|
-
: `Checked ${params.files.length} file(s): ${total_diag} diagnostic(s), ${clean_count} clean, ${fail_count} failed to check`;
|
|
217
|
-
return make_tool_result(
|
|
218
|
-
[summary, ...lines].join("\n\n"),
|
|
219
|
-
{
|
|
220
|
-
ok: fail_count === 0 && total_err === 0,
|
|
221
|
-
checked: params.files.length,
|
|
222
|
-
diagnostic_count: total_diag,
|
|
223
|
-
error_count: total_err,
|
|
224
|
-
warning_count: total_warn,
|
|
225
|
-
clean_count,
|
|
226
|
-
fail_count,
|
|
227
|
-
}
|
|
228
|
-
);
|
|
229
|
-
},
|
|
230
|
-
})
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
pi.registerTool(
|
|
234
|
-
defineTool({
|
|
235
|
-
name: "lsp_find_symbol",
|
|
236
|
-
label: "LSP: find symbol",
|
|
237
|
-
description:
|
|
238
|
-
"Find symbols in a file by name or detail text using document symbols. Supports exact matching, kind filters, and top-level-only mode.",
|
|
239
|
-
promptSnippet: "Find symbols in a file by name, kind, or match mode",
|
|
240
|
-
promptGuidelines: [
|
|
241
|
-
"Use lsp_find_symbol to locate named symbols in a file when symbol structure matters more than broad text search.",
|
|
242
|
-
],
|
|
243
|
-
parameters: Type.Object({
|
|
244
|
-
file: Type.String({
|
|
245
|
-
description: "Path to the file whose symbols should be searched.",
|
|
246
|
-
}),
|
|
247
|
-
query: Type.String({
|
|
248
|
-
description: "Substring to match against symbol names/details.",
|
|
249
|
-
}),
|
|
250
|
-
max_results: Type.Optional(
|
|
251
|
-
Type.Number({
|
|
252
|
-
description: "Max number of matches to return. Default 20.",
|
|
253
|
-
})
|
|
254
|
-
),
|
|
255
|
-
top_level_only: Type.Optional(
|
|
256
|
-
Type.Boolean({
|
|
257
|
-
description: "Only match top-level symbols. Default false.",
|
|
258
|
-
})
|
|
259
|
-
),
|
|
260
|
-
exact_match: Type.Optional(
|
|
261
|
-
Type.Boolean({
|
|
262
|
-
description:
|
|
263
|
-
"Match whole symbol names/details exactly instead of substring matching. Default false.",
|
|
264
|
-
})
|
|
265
|
-
),
|
|
266
|
-
kinds: Type.Optional(
|
|
267
|
-
Type.Array(SYMBOL_KIND_SCHEMA, {
|
|
268
|
-
minItems: 1,
|
|
269
|
-
maxItems: SYMBOL_KIND_NAMES.length,
|
|
270
|
-
description: "Restrict matches to these symbol kinds.",
|
|
271
|
-
})
|
|
272
|
-
),
|
|
273
|
-
}),
|
|
274
|
-
execute: async (
|
|
275
|
-
_id,
|
|
276
|
-
params,
|
|
277
|
-
_signal,
|
|
278
|
-
_on_update,
|
|
279
|
-
ctx
|
|
280
|
-
) =>
|
|
281
|
-
with_file_state(
|
|
282
|
-
manager,
|
|
283
|
-
params.file,
|
|
284
|
-
ctx,
|
|
285
|
-
async (result) => {
|
|
286
|
-
const symbols =
|
|
287
|
-
await result.state.client.document_symbols(result.uri);
|
|
288
|
-
return format_symbol_matches(
|
|
289
|
-
result.abs,
|
|
290
|
-
params.query,
|
|
291
|
-
find_symbol_matches(symbols, params.query, {
|
|
292
|
-
max_results: params.max_results ?? 20,
|
|
293
|
-
top_level_only: params.top_level_only ?? false,
|
|
294
|
-
exact_match: params.exact_match ?? false,
|
|
295
|
-
kinds: new Set(params.kinds ?? []),
|
|
296
|
-
language: result.state.language,
|
|
297
|
-
})
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
),
|
|
301
|
-
})
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
pi.registerTool(
|
|
305
|
-
defineTool({
|
|
306
|
-
name: "lsp_hover",
|
|
307
|
-
label: "LSP: hover",
|
|
308
|
-
description:
|
|
309
|
-
"Get hover info (types, docs) at a position in a file. Positions are zero-based.",
|
|
310
|
-
promptSnippet: "Get types and documentation at a symbol position",
|
|
311
|
-
promptGuidelines: [
|
|
312
|
-
"Use lsp_hover to inspect the type, signature, or documentation of the symbol at a specific zero-based position.",
|
|
313
|
-
],
|
|
314
|
-
parameters: Type.Object({
|
|
315
|
-
file: Type.String({
|
|
316
|
-
description: "Path to the file containing the symbol.",
|
|
317
|
-
}),
|
|
318
|
-
line: Type.Number({
|
|
319
|
-
description: "Zero-based line number of the symbol.",
|
|
320
|
-
}),
|
|
321
|
-
character: Type.Number({
|
|
322
|
-
description: "Zero-based character offset of the symbol.",
|
|
323
|
-
}),
|
|
324
|
-
}),
|
|
325
|
-
execute: async (
|
|
326
|
-
_id,
|
|
327
|
-
params,
|
|
328
|
-
_signal,
|
|
329
|
-
_on_update,
|
|
330
|
-
ctx
|
|
331
|
-
) =>
|
|
332
|
-
with_file_state(
|
|
333
|
-
manager,
|
|
334
|
-
params.file,
|
|
335
|
-
ctx,
|
|
336
|
-
async (result) => {
|
|
337
|
-
const hover = await result.state.client.hover(result.uri, {
|
|
338
|
-
line: params.line,
|
|
339
|
-
character: params.character,
|
|
340
|
-
});
|
|
341
|
-
return format_hover(hover);
|
|
342
|
-
}
|
|
343
|
-
),
|
|
344
|
-
})
|
|
345
|
-
);
|
|
74
|
+
function withTimeout(promise: Promise<ToolResult>, ms: number, label: string): Promise<ToolResult> {
|
|
75
|
+
return Promise.race([
|
|
76
|
+
promise,
|
|
77
|
+
new Promise<ToolResult>((resolve) =>
|
|
78
|
+
setTimeout(() => resolve(err({ kind: "tool_timeout", message: `${label} timed out after ${ms}ms` })), ms)
|
|
79
|
+
),
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
346
82
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
label: "LSP: go to definition",
|
|
351
|
-
description:
|
|
352
|
-
"Find definition locations for the symbol at a position. Positions are zero-based.",
|
|
353
|
-
promptSnippet: "Find definition locations for a symbol at a position",
|
|
354
|
-
promptGuidelines: [
|
|
355
|
-
"Use lsp_definition to find the canonical definition location for the symbol at a specific zero-based position.",
|
|
356
|
-
],
|
|
357
|
-
parameters: Type.Object({
|
|
358
|
-
file: Type.String({
|
|
359
|
-
description: "Path to the file containing the symbol.",
|
|
360
|
-
}),
|
|
361
|
-
line: Type.Number({
|
|
362
|
-
description: "Zero-based line number of the symbol.",
|
|
363
|
-
}),
|
|
364
|
-
character: Type.Number({
|
|
365
|
-
description: "Zero-based character offset of the symbol.",
|
|
366
|
-
}),
|
|
367
|
-
}),
|
|
368
|
-
execute: async (
|
|
369
|
-
_id,
|
|
370
|
-
params,
|
|
371
|
-
_signal,
|
|
372
|
-
_on_update,
|
|
373
|
-
ctx
|
|
374
|
-
) =>
|
|
375
|
-
with_file_state(
|
|
376
|
-
manager,
|
|
377
|
-
params.file,
|
|
378
|
-
ctx,
|
|
379
|
-
async (result) => {
|
|
380
|
-
const locations = await result.state.client.definition(
|
|
381
|
-
result.uri,
|
|
382
|
-
{
|
|
383
|
-
line: params.line,
|
|
384
|
-
character: params.character,
|
|
385
|
-
}
|
|
386
|
-
);
|
|
387
|
-
return format_locations(locations, "No definition found.");
|
|
388
|
-
}
|
|
389
|
-
),
|
|
390
|
-
})
|
|
391
|
-
);
|
|
83
|
+
function severityLabel(s: number): string {
|
|
84
|
+
return s === 1 ? "error" : s === 2 ? "warning" : s === 3 ? "info" : "hint";
|
|
85
|
+
}
|
|
392
86
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
description:
|
|
398
|
-
"Find references to the symbol at a position. Positions are zero-based.",
|
|
399
|
-
promptSnippet: "Find references to a symbol at a position",
|
|
400
|
-
promptGuidelines: [
|
|
401
|
-
"Use lsp_references to find usages of a symbol more precisely than text search, optionally including the declaration site.",
|
|
402
|
-
],
|
|
403
|
-
parameters: Type.Object({
|
|
404
|
-
file: Type.String({
|
|
405
|
-
description: "Path to the file containing the symbol.",
|
|
406
|
-
}),
|
|
407
|
-
line: Type.Number({
|
|
408
|
-
description: "Zero-based line number of the symbol.",
|
|
409
|
-
}),
|
|
410
|
-
character: Type.Number({
|
|
411
|
-
description: "Zero-based character offset of the symbol.",
|
|
412
|
-
}),
|
|
413
|
-
include_declaration: Type.Optional(
|
|
414
|
-
Type.Boolean({
|
|
415
|
-
description:
|
|
416
|
-
"Whether to include the symbol declaration in reference results. Default true.",
|
|
417
|
-
})
|
|
418
|
-
),
|
|
419
|
-
}),
|
|
420
|
-
execute: async (
|
|
421
|
-
_id,
|
|
422
|
-
params,
|
|
423
|
-
_signal,
|
|
424
|
-
_on_update,
|
|
425
|
-
ctx
|
|
426
|
-
) =>
|
|
427
|
-
with_file_state(
|
|
428
|
-
manager,
|
|
429
|
-
params.file,
|
|
430
|
-
ctx,
|
|
431
|
-
async (result) => {
|
|
432
|
-
const locations = await result.state.client.references(
|
|
433
|
-
result.uri,
|
|
434
|
-
{ line: params.line, character: params.character },
|
|
435
|
-
params.include_declaration ?? true
|
|
436
|
-
);
|
|
437
|
-
return format_locations(locations, "No references found.");
|
|
438
|
-
}
|
|
439
|
-
),
|
|
440
|
-
})
|
|
441
|
-
);
|
|
87
|
+
function symbolKindLabel(kind: number): string {
|
|
88
|
+
const labels: Record<number, string> = { 2:"module",3:"namespace",5:"class",6:"method",7:"property",8:"field",9:"constructor",11:"interface",12:"function",13:"variable",14:"constant",23:"struct",24:"event" };
|
|
89
|
+
return labels[kind] ?? "symbol";
|
|
90
|
+
}
|
|
442
91
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}),
|
|
457
|
-
}),
|
|
458
|
-
execute: async (
|
|
459
|
-
_id,
|
|
460
|
-
params,
|
|
461
|
-
_signal,
|
|
462
|
-
_on_update,
|
|
463
|
-
ctx
|
|
464
|
-
) =>
|
|
465
|
-
with_file_state(
|
|
466
|
-
manager,
|
|
467
|
-
params.file,
|
|
468
|
-
ctx,
|
|
469
|
-
async (result) => {
|
|
470
|
-
const symbols =
|
|
471
|
-
await result.state.client.document_symbols(result.uri);
|
|
472
|
-
return format_document_symbols(result.abs, symbols);
|
|
473
|
-
}
|
|
474
|
-
),
|
|
475
|
-
})
|
|
476
|
-
);
|
|
92
|
+
function formatDocumentSymbols(file: string, symbols: any[]): string {
|
|
93
|
+
if (symbols.length === 0) return `${file}: no symbols`;
|
|
94
|
+
const lines = [`${file}: ${symbols.length} top-level symbol(s)`];
|
|
95
|
+
const append = (syms: any[], depth: number) => {
|
|
96
|
+
for (const s of syms) {
|
|
97
|
+
const detail = s.detail ? ` — ${s.detail}` : "";
|
|
98
|
+
lines.push(`${" ".repeat(depth)}${symbolKindLabel(s.kind)} ${s.name}${detail} @ ${s.range.start.line + 1}:${s.range.start.character + 1}`);
|
|
99
|
+
if (s.children?.length) append(s.children, depth + 1);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
append(symbols, 1);
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
477
105
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
106
|
+
// ─── Register tools ───────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
export function registerLspTools(pi: ExtensionAPI, manager: LspServerManager) {
|
|
109
|
+
|
|
110
|
+
// ── lsp_diagnostics ────────────────────────────────────────────────────
|
|
111
|
+
pi.registerTool({
|
|
112
|
+
name: "lsp_diagnostics",
|
|
113
|
+
label: "LSP: diagnostics",
|
|
114
|
+
description: "Get language server diagnostics for one or more files. Default filter: error. Supports optional severity filtering.",
|
|
115
|
+
promptSnippet: "Get language server diagnostics for one or more files",
|
|
116
|
+
promptGuidelines: [
|
|
117
|
+
"Use lsp_diagnostics to validate focused code changes after editing or writing before reporting completion.",
|
|
118
|
+
"Supported languages: typescript, c, cpp, python, rust, go, ruby, java, lua, svelte, json.",
|
|
119
|
+
],
|
|
120
|
+
renderResult: renderLspResult,
|
|
121
|
+
parameters: Type.Object({
|
|
122
|
+
paths: Type.Array(Type.String(), { minItems: 1, maxItems: 100, description: "Paths to check. One or more file paths (relative to cwd or absolute)." }),
|
|
123
|
+
severity: Type.Optional(Type.Array(Type.Union([Type.Literal("error"), Type.Literal("warning"), Type.Literal("info"), Type.Literal("hint")]), { description: "Filter to specific severity levels. Default: error." })),
|
|
124
|
+
wait_ms: Type.Optional(Type.Number({ description: "Max ms to wait for diagnostics. Default 1500." })),
|
|
125
|
+
timeout_ms: Type.Optional(Type.Number({ description: "Overall max ms including server startup. Default 30000." })),
|
|
126
|
+
}),
|
|
127
|
+
execute: async (_id, params, _signal, _update, _ctx): Promise<ToolResult> => {
|
|
128
|
+
const waitMs = params.wait_ms ?? 1500;
|
|
129
|
+
const totalTimeout = params.timeout_ms ?? 30_000;
|
|
130
|
+
const severities: ("error"|"warning"|"info"|"hint")[] = params.severity ?? ["error"];
|
|
131
|
+
const minSeverity = Math.min(...severities.map(s => ({ error: 1, warning: 2, info: 3, hint: 4 }[s])));
|
|
132
|
+
|
|
133
|
+
return withTimeout((async () => {
|
|
134
|
+
const results = await Promise.all(params.paths.map(async (file) => {
|
|
135
|
+
const resolved = await manager.resolveFileState(file, { timeoutMs: totalTimeout });
|
|
136
|
+
if (!resolved.ok) return { line: formatToolError(resolved.error), diagnostics: 0, errors: 0, warnings: 0, isError: true };
|
|
137
|
+
try {
|
|
138
|
+
const diagnostics = await resolved.result.state.client.waitForDiagnostics(resolved.result.uri, waitMs);
|
|
139
|
+
const filtered = diagnostics.filter((d: any) => (d.severity ?? 1) <= minSeverity);
|
|
140
|
+
let errors = 0, warnings = 0;
|
|
141
|
+
for (const d of filtered) { if (d.severity === 1) errors++; else if (d.severity === 2) warnings++; }
|
|
142
|
+
return { line: formatDiagnostics(resolved.result.abs, filtered), diagnostics: filtered.length, errors, warnings, isError: false };
|
|
143
|
+
} catch { return { line: formatToolError({ kind: "tool_execution_failed", file, message: "diagnostics request failed" }), diagnostics: 0, errors: 0, warnings: 0, isError: true }; }
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
let totalDiag = 0, totalErr = 0, totalWarn = 0, cleanCount = 0, failCount = 0;
|
|
147
|
+
const lines: string[] = [];
|
|
148
|
+
for (const r of results) {
|
|
149
|
+
lines.push(r.line);
|
|
150
|
+
if (r.isError) { failCount++; } else { totalDiag += r.diagnostics; totalErr += r.errors; totalWarn += r.warnings; if (r.diagnostics === 0) cleanCount++; }
|
|
151
|
+
}
|
|
152
|
+
const summary = totalErr > 0 || totalWarn > 0
|
|
153
|
+
? `Checked ${params.paths.length} file(s): ${totalErr} error(s), ${totalWarn} warning(s), ${totalDiag - totalErr - totalWarn} info/hint(s), ${cleanCount} clean, ${failCount} failed`
|
|
154
|
+
: `Checked ${params.paths.length} file(s): ${totalDiag} diagnostic(s), ${cleanCount} clean, ${failCount} failed`;
|
|
155
|
+
return ok([summary, ...lines].join("\n\n"), { ok: failCount === 0 && totalErr === 0, checked: params.paths.length, diagnostic_count: totalDiag, error_count: totalErr, warning_count: totalWarn });
|
|
156
|
+
})(), totalTimeout, "LSP diagnostics");
|
|
157
|
+
},
|
|
158
|
+
});
|
|
519
159
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
160
|
+
// ── lsp_document_symbols ───────────────────────────────────────────────
|
|
161
|
+
pi.registerTool({
|
|
162
|
+
name: "lsp_document_symbols",
|
|
163
|
+
label: "LSP: document symbols",
|
|
164
|
+
description: "List symbols in a file (functions, classes, variables) using the language server.",
|
|
165
|
+
promptSnippet: "List functions, classes, and variables in a file",
|
|
166
|
+
promptGuidelines: [
|
|
167
|
+
"Prefer lsp_document_symbols to find functions, classes, or variables in code instead of grep.",
|
|
168
|
+
"Supported languages: typescript, c, cpp, python, rust, go, ruby, java, lua, svelte.",
|
|
169
|
+
],
|
|
170
|
+
renderResult: renderLspResult,
|
|
171
|
+
parameters: Type.Object({
|
|
172
|
+
path: Type.String({ description: "Path to the file (relative or absolute)." }),
|
|
173
|
+
timeout_ms: Type.Optional(Type.Number({ description: "Overall max ms including server startup. Default 10000." })),
|
|
174
|
+
}),
|
|
175
|
+
execute: async (_id, params, _signal, _update, _ctx): Promise<ToolResult> => {
|
|
176
|
+
const timeoutMs = params.timeout_ms ?? 10_000;
|
|
177
|
+
return withTimeout(
|
|
178
|
+
withFile(manager, params.path, async (result) => {
|
|
179
|
+
const symbols = await result.state.client.documentSymbols(result.uri, timeoutMs);
|
|
180
|
+
return formatDocumentSymbols(result.abs, symbols);
|
|
181
|
+
}, { timeoutMs }),
|
|
182
|
+
timeoutMs,
|
|
183
|
+
"LSP document symbols",
|
|
184
|
+
);
|
|
185
|
+
},
|
|
186
|
+
});
|
|
187
|
+
}
|
|
525
188
|
|
|
526
|
-
|
|
527
|
-
for (const path of locations) {
|
|
528
|
-
const info = edits[path]!;
|
|
529
|
-
output += `${path}: change to "${info.newText}"\n`;
|
|
530
|
-
}
|
|
531
|
-
output += "\nUse the edit tool to apply these changes.";
|
|
189
|
+
// ─── Formatting ────────────────────────────────────────────────────────────
|
|
532
190
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
191
|
+
function formatDiagnostics(file: string, diagnostics: any[]): string {
|
|
192
|
+
if (diagnostics.length === 0) return `${file}: no diagnostics`;
|
|
193
|
+
const lines = [`${file}: ${diagnostics.length} diagnostic(s)`];
|
|
194
|
+
for (const d of diagnostics) {
|
|
195
|
+
const pos = `${d.range.start.line + 1}:${d.range.start.character + 1}`;
|
|
196
|
+
const source = d.source ? ` [${d.source}]` : "";
|
|
197
|
+
const code = d.code != null ? ` (${d.code})` : "";
|
|
198
|
+
lines.push(` ${pos} ${severityLabel(d.severity ?? 1)}${source}${code}: ${d.message}`);
|
|
199
|
+
}
|
|
200
|
+
return lines.join("\n");
|
|
538
201
|
}
|
|
202
|
+
|
|
203
|
+
export const __lspToolsTest = { collapse_lsp_text: collapseText };
|