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