@oh-my-pi/pi-coding-agent 1.337.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.
Files changed (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,804 @@
1
+ import * as fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
4
+ import type { Theme } from "../../../modes/interactive/theme/theme.js";
5
+ import { resolveToCwd } from "../path-utils.js";
6
+ import { ensureFileOpen, getOrCreateClient, refreshFile, sendRequest } from "./client.js";
7
+ import { getServerForFile, hasCapability, type LspConfig, loadConfig } from "./config.js";
8
+ import { applyWorkspaceEdit } from "./edits.js";
9
+ import { renderCall, renderResult } from "./render.js";
10
+ import * as rustAnalyzer from "./rust-analyzer.js";
11
+ import {
12
+ type CallHierarchyIncomingCall,
13
+ type CallHierarchyItem,
14
+ type CallHierarchyOutgoingCall,
15
+ type CodeAction,
16
+ type Command,
17
+ type Diagnostic,
18
+ type DocumentSymbol,
19
+ type Hover,
20
+ type Location,
21
+ type LocationLink,
22
+ type LspClient,
23
+ type LspParams,
24
+ type LspToolDetails,
25
+ lspSchema,
26
+ type ServerConfig,
27
+ type SymbolInformation,
28
+ type WorkspaceEdit,
29
+ } from "./types.js";
30
+ import {
31
+ extractHoverText,
32
+ fileToUri,
33
+ formatDiagnostic,
34
+ formatDiagnosticsSummary,
35
+ formatDocumentSymbol,
36
+ formatLocation,
37
+ formatSymbolInformation,
38
+ formatWorkspaceEdit,
39
+ sleep,
40
+ symbolKindToIcon,
41
+ uriToFile,
42
+ } from "./utils.js";
43
+
44
+ export type { LspToolDetails } from "./types.js";
45
+
46
+ // Cache config per cwd to avoid repeated file I/O
47
+ const configCache = new Map<string, LspConfig>();
48
+
49
+ function getConfig(cwd: string): LspConfig {
50
+ let config = configCache.get(cwd);
51
+ if (!config) {
52
+ config = loadConfig(cwd);
53
+ configCache.set(cwd, config);
54
+ }
55
+ return config;
56
+ }
57
+
58
+ const FILE_SEARCH_MAX_DEPTH = 5;
59
+ const IGNORED_DIRS = new Set(["node_modules", "target", "dist", "build", ".git"]);
60
+
61
+ function findFileByExtensions(baseDir: string, extensions: string[], maxDepth: number): string | null {
62
+ const normalized = extensions.map((ext) => ext.toLowerCase());
63
+ const search = (dir: string, depth: number): string | null => {
64
+ if (depth > maxDepth) return null;
65
+ let entries: fs.Dirent[];
66
+ try {
67
+ entries = fs.readdirSync(dir, { withFileTypes: true });
68
+ } catch {
69
+ return null;
70
+ }
71
+
72
+ for (const entry of entries) {
73
+ if (entry.name.startsWith(".")) continue;
74
+ if (entry.isDirectory() && IGNORED_DIRS.has(entry.name)) continue;
75
+ const fullPath = path.join(dir, entry.name);
76
+
77
+ if (entry.isFile()) {
78
+ const lowerName = entry.name.toLowerCase();
79
+ if (normalized.some((ext) => lowerName.endsWith(ext))) {
80
+ return fullPath;
81
+ }
82
+ } else if (entry.isDirectory()) {
83
+ const found = search(fullPath, depth + 1);
84
+ if (found) return found;
85
+ }
86
+ }
87
+ return null;
88
+ };
89
+
90
+ return search(baseDir, 0);
91
+ }
92
+
93
+ function findFileForServer(cwd: string, serverConfig: ServerConfig): string | null {
94
+ return findFileByExtensions(cwd, serverConfig.fileTypes, FILE_SEARCH_MAX_DEPTH);
95
+ }
96
+
97
+ function getRustServer(config: LspConfig): [string, ServerConfig] | null {
98
+ const entries = Object.entries(config.servers) as Array<[string, ServerConfig]>;
99
+ const byName = entries.find(([name, server]) => name === "rust-analyzer" || server.command === "rust-analyzer");
100
+ if (byName) return byName;
101
+
102
+ for (const [name, server] of entries) {
103
+ if (
104
+ hasCapability(server, "flycheck") ||
105
+ hasCapability(server, "ssr") ||
106
+ hasCapability(server, "runnables") ||
107
+ hasCapability(server, "expandMacro") ||
108
+ hasCapability(server, "relatedTests")
109
+ ) {
110
+ return [name, server];
111
+ }
112
+ }
113
+
114
+ return null;
115
+ }
116
+
117
+ function getServerForWorkspaceAction(config: LspConfig, action: string): [string, ServerConfig] | null {
118
+ const entries = Object.entries(config.servers) as Array<[string, ServerConfig]>;
119
+ if (entries.length === 0) return null;
120
+
121
+ if (action === "workspace_symbols") {
122
+ return entries[0];
123
+ }
124
+
125
+ if (action === "flycheck" || action === "ssr" || action === "runnables" || action === "reload_workspace") {
126
+ return getRustServer(config);
127
+ }
128
+
129
+ return null;
130
+ }
131
+
132
+ async function waitForDiagnostics(client: LspClient, uri: string, timeoutMs = 3000): Promise<Diagnostic[]> {
133
+ const start = Date.now();
134
+ while (Date.now() - start < timeoutMs) {
135
+ const diagnostics = client.diagnostics.get(uri);
136
+ if (diagnostics !== undefined) return diagnostics;
137
+ await sleep(100);
138
+ }
139
+ return client.diagnostics.get(uri) ?? [];
140
+ }
141
+
142
+ export function createLspTool(cwd: string): AgentTool<typeof lspSchema, LspToolDetails, Theme> {
143
+ return {
144
+ name: "lsp",
145
+ label: "LSP",
146
+ description: `Interact with Language Server Protocol (LSP) servers to get code intelligence features.
147
+
148
+ Standard operations:
149
+ - diagnostics: Get errors/warnings for a file
150
+ - definition: Go to symbol definition
151
+ - references: Find all references to a symbol
152
+ - hover: Get type info and documentation
153
+ - symbols: List symbols in a file (functions, classes, etc.)
154
+ - workspace_symbols: Search for symbols across the project
155
+ - rename: Rename a symbol across the codebase
156
+ - actions: List and apply code actions (quick fixes, refactors)
157
+ - incoming_calls: Find all callers of a function
158
+ - outgoing_calls: Find all functions called by a function
159
+ - status: Show active language servers
160
+
161
+ Rust-analyzer specific (require rust-analyzer):
162
+ - flycheck: Run clippy/cargo check
163
+ - expand_macro: Show macro expansion at cursor
164
+ - ssr: Structural search-replace
165
+ - runnables: Find runnable tests/binaries
166
+ - related_tests: Find tests for a function
167
+ - reload_workspace: Reload Cargo.toml changes`,
168
+ parameters: lspSchema,
169
+ renderCall,
170
+ renderResult,
171
+ execute: async (_toolCallId, params: LspParams, _signal) => {
172
+ const {
173
+ action,
174
+ file,
175
+ files,
176
+ line,
177
+ column,
178
+ end_line,
179
+ end_character,
180
+ query,
181
+ new_name,
182
+ replacement,
183
+ kind,
184
+ apply,
185
+ action_index,
186
+ include_declaration,
187
+ } = params;
188
+
189
+ const config = getConfig(cwd);
190
+
191
+ // Status action doesn't need a file
192
+ if (action === "status") {
193
+ const servers = Object.keys(config.servers);
194
+ const output =
195
+ servers.length > 0
196
+ ? `Active language servers: ${servers.join(", ")}`
197
+ : "No language servers configured for this project";
198
+ return {
199
+ content: [{ type: "text", text: output }],
200
+ details: { action, success: true },
201
+ };
202
+ }
203
+
204
+ // Diagnostics can be batch or single-file
205
+ if (action === "diagnostics") {
206
+ const targets = files?.length ? files : file ? [file] : null;
207
+ if (!targets) {
208
+ return {
209
+ content: [{ type: "text", text: "Error: file or files parameter required for diagnostics" }],
210
+ details: { action, success: false },
211
+ };
212
+ }
213
+
214
+ const detailed = Boolean(files?.length);
215
+ const results: string[] = [];
216
+ let lastServerName: string | undefined;
217
+
218
+ for (const target of targets) {
219
+ const resolved = resolveToCwd(target, cwd);
220
+ const serverInfo = getServerForFile(config, resolved);
221
+ if (!serverInfo) {
222
+ results.push(`✗ ${target}: No language server found`);
223
+ continue;
224
+ }
225
+
226
+ const [serverName, serverConfig] = serverInfo;
227
+ lastServerName = serverName;
228
+
229
+ const client = await getOrCreateClient(serverConfig, cwd);
230
+ await refreshFile(client, resolved);
231
+
232
+ const uri = fileToUri(resolved);
233
+ const diagnostics = await waitForDiagnostics(client, uri);
234
+ const relPath = path.relative(cwd, resolved);
235
+
236
+ if (!detailed && targets.length === 1) {
237
+ if (diagnostics.length === 0) {
238
+ return {
239
+ content: [{ type: "text", text: "No diagnostics" }],
240
+ details: { action, serverName, success: true },
241
+ };
242
+ }
243
+
244
+ const summary = formatDiagnosticsSummary(diagnostics);
245
+ const formatted = diagnostics.map((d) => formatDiagnostic(d, relPath));
246
+ const output = `${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}`;
247
+ return {
248
+ content: [{ type: "text", text: output }],
249
+ details: { action, serverName, success: true },
250
+ };
251
+ }
252
+
253
+ if (diagnostics.length === 0) {
254
+ results.push(`✓ ${relPath}: no issues`);
255
+ } else {
256
+ const summary = formatDiagnosticsSummary(diagnostics);
257
+ results.push(`✗ ${relPath}: ${summary}`);
258
+ for (const diag of diagnostics) {
259
+ results.push(` ${formatDiagnostic(diag, relPath)}`);
260
+ }
261
+ }
262
+ }
263
+
264
+ return {
265
+ content: [{ type: "text", text: results.join("\n") }],
266
+ details: { action, serverName: lastServerName, success: true },
267
+ };
268
+ }
269
+
270
+ const requiresFile =
271
+ !file &&
272
+ action !== "workspace_symbols" &&
273
+ action !== "flycheck" &&
274
+ action !== "ssr" &&
275
+ action !== "runnables" &&
276
+ action !== "reload_workspace";
277
+
278
+ if (requiresFile) {
279
+ return {
280
+ content: [{ type: "text", text: "Error: file parameter required for this action" }],
281
+ details: { action, success: false },
282
+ };
283
+ }
284
+
285
+ const resolvedFile = file ? resolveToCwd(file, cwd) : null;
286
+ const serverInfo = resolvedFile
287
+ ? getServerForFile(config, resolvedFile)
288
+ : getServerForWorkspaceAction(config, action);
289
+
290
+ if (!serverInfo) {
291
+ return {
292
+ content: [{ type: "text", text: "No language server found for this action" }],
293
+ details: { action, success: false },
294
+ };
295
+ }
296
+
297
+ const [serverName, serverConfig] = serverInfo;
298
+
299
+ try {
300
+ const client = await getOrCreateClient(serverConfig, cwd);
301
+ let targetFile = resolvedFile;
302
+ if (action === "runnables" && !targetFile) {
303
+ targetFile = findFileForServer(cwd, serverConfig);
304
+ if (!targetFile) {
305
+ return {
306
+ content: [{ type: "text", text: "Error: no matching files found for runnables" }],
307
+ details: { action, serverName, success: false },
308
+ };
309
+ }
310
+ }
311
+
312
+ if (targetFile) {
313
+ await ensureFileOpen(client, targetFile);
314
+ }
315
+
316
+ const uri = targetFile ? fileToUri(targetFile) : "";
317
+ const position = { line: (line || 1) - 1, character: (column || 1) - 1 };
318
+
319
+ let output: string;
320
+
321
+ switch (action) {
322
+ // =====================================================================
323
+ // Standard LSP Operations
324
+ // =====================================================================
325
+
326
+ case "definition": {
327
+ const result = (await sendRequest(client, "textDocument/definition", {
328
+ textDocument: { uri },
329
+ position,
330
+ })) as Location | Location[] | LocationLink | LocationLink[] | null;
331
+
332
+ if (!result) {
333
+ output = "No definition found";
334
+ } else {
335
+ const raw = Array.isArray(result) ? result : [result];
336
+ const locations = raw.flatMap((loc) => {
337
+ if ("uri" in loc) {
338
+ return [loc as Location];
339
+ }
340
+ if ("targetUri" in loc) {
341
+ // Use targetSelectionRange (the precise identifier range) with fallback to targetRange
342
+ const link = loc as LocationLink;
343
+ return [{ uri: link.targetUri, range: link.targetSelectionRange ?? link.targetRange }];
344
+ }
345
+ return [];
346
+ });
347
+
348
+ if (locations.length === 0) {
349
+ output = "No definition found";
350
+ } else {
351
+ output = `Found ${locations.length} definition(s):\n${locations
352
+ .map((loc) => ` ${formatLocation(loc, cwd)}`)
353
+ .join("\n")}`;
354
+ }
355
+ }
356
+ break;
357
+ }
358
+
359
+ case "references": {
360
+ const result = (await sendRequest(client, "textDocument/references", {
361
+ textDocument: { uri },
362
+ position,
363
+ context: { includeDeclaration: include_declaration ?? true },
364
+ })) as Location[] | null;
365
+
366
+ if (!result || result.length === 0) {
367
+ output = "No references found";
368
+ } else {
369
+ const lines = result.map((loc) => ` ${formatLocation(loc, cwd)}`);
370
+ output = `Found ${result.length} reference(s):\n${lines.join("\n")}`;
371
+ }
372
+ break;
373
+ }
374
+
375
+ case "hover": {
376
+ const result = (await sendRequest(client, "textDocument/hover", {
377
+ textDocument: { uri },
378
+ position,
379
+ })) as Hover | null;
380
+
381
+ if (!result || !result.contents) {
382
+ output = "No hover information";
383
+ } else {
384
+ output = extractHoverText(result.contents);
385
+ }
386
+ break;
387
+ }
388
+
389
+ case "symbols": {
390
+ const result = (await sendRequest(client, "textDocument/documentSymbol", {
391
+ textDocument: { uri },
392
+ })) as (DocumentSymbol | SymbolInformation)[] | null;
393
+
394
+ if (!result || result.length === 0) {
395
+ output = "No symbols found";
396
+ } else if (!targetFile) {
397
+ return {
398
+ content: [{ type: "text", text: "Error: file parameter required for symbols" }],
399
+ details: { action, serverName, success: false },
400
+ };
401
+ } else {
402
+ const relPath = path.relative(cwd, targetFile);
403
+ // Check if hierarchical (DocumentSymbol) or flat (SymbolInformation)
404
+ if ("selectionRange" in result[0]) {
405
+ // Hierarchical
406
+ const lines = (result as DocumentSymbol[]).flatMap((s) => formatDocumentSymbol(s));
407
+ output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
408
+ } else {
409
+ // Flat
410
+ const lines = (result as SymbolInformation[]).map((s) => {
411
+ const line = s.location.range.start.line + 1;
412
+ const icon = symbolKindToIcon(s.kind);
413
+ return `${icon} ${s.name} @ line ${line}`;
414
+ });
415
+ output = `Symbols in ${relPath}:\n${lines.join("\n")}`;
416
+ }
417
+ }
418
+ break;
419
+ }
420
+
421
+ case "workspace_symbols": {
422
+ if (!query) {
423
+ return {
424
+ content: [{ type: "text", text: "Error: query parameter required for workspace_symbols" }],
425
+ details: { action, serverName, success: false },
426
+ };
427
+ }
428
+
429
+ const result = (await sendRequest(client, "workspace/symbol", { query })) as
430
+ | SymbolInformation[]
431
+ | null;
432
+
433
+ if (!result || result.length === 0) {
434
+ output = `No symbols matching "${query}"`;
435
+ } else {
436
+ const lines = result.map((s) => formatSymbolInformation(s, cwd));
437
+ output = `Found ${result.length} symbol(s) matching "${query}":\n${lines
438
+ .map((l) => ` ${l}`)
439
+ .join("\n")}`;
440
+ }
441
+ break;
442
+ }
443
+
444
+ case "rename": {
445
+ if (!new_name) {
446
+ return {
447
+ content: [{ type: "text", text: "Error: new_name parameter required for rename" }],
448
+ details: { action, serverName, success: false },
449
+ };
450
+ }
451
+
452
+ const result = (await sendRequest(client, "textDocument/rename", {
453
+ textDocument: { uri },
454
+ position,
455
+ newName: new_name,
456
+ })) as WorkspaceEdit | null;
457
+
458
+ if (!result) {
459
+ output = "Rename returned no edits";
460
+ } else {
461
+ const shouldApply = apply !== false;
462
+ if (shouldApply) {
463
+ const applied = await applyWorkspaceEdit(result, cwd);
464
+ output = `Applied rename:\n${applied.map((a) => ` ${a}`).join("\n")}`;
465
+ } else {
466
+ const preview = formatWorkspaceEdit(result, cwd);
467
+ output = `Rename preview:\n${preview.map((p) => ` ${p}`).join("\n")}`;
468
+ }
469
+ }
470
+ break;
471
+ }
472
+
473
+ case "actions": {
474
+ if (!targetFile) {
475
+ return {
476
+ content: [{ type: "text", text: "Error: file parameter required for actions" }],
477
+ details: { action, serverName, success: false },
478
+ };
479
+ }
480
+
481
+ await refreshFile(client, targetFile);
482
+ const diagnostics = await waitForDiagnostics(client, uri);
483
+ const endLine = (end_line ?? line ?? 1) - 1;
484
+ const endCharacter = (end_character ?? column ?? 1) - 1;
485
+ const range = { start: position, end: { line: endLine, character: endCharacter } };
486
+ const relevantDiagnostics = diagnostics.filter(
487
+ (d) => d.range.start.line <= range.end.line && d.range.end.line >= range.start.line,
488
+ );
489
+
490
+ const codeActionContext: { diagnostics: Diagnostic[]; only?: string[] } = {
491
+ diagnostics: relevantDiagnostics,
492
+ };
493
+ if (kind) {
494
+ codeActionContext.only = [kind];
495
+ }
496
+
497
+ const result = (await sendRequest(client, "textDocument/codeAction", {
498
+ textDocument: { uri },
499
+ range,
500
+ context: codeActionContext,
501
+ })) as Array<CodeAction | Command> | null;
502
+
503
+ if (!result || result.length === 0) {
504
+ output = "No code actions available";
505
+ } else if (action_index !== undefined) {
506
+ // Apply specific action
507
+ if (action_index < 0 || action_index >= result.length) {
508
+ return {
509
+ content: [
510
+ {
511
+ type: "text",
512
+ text: `Error: action_index ${action_index} out of range (0-${result.length - 1})`,
513
+ },
514
+ ],
515
+ details: { action, serverName, success: false },
516
+ };
517
+ }
518
+
519
+ const isCommand = (candidate: CodeAction | Command): candidate is Command =>
520
+ typeof (candidate as Command).command === "string";
521
+ const isCodeAction = (candidate: CodeAction | Command): candidate is CodeAction =>
522
+ !isCommand(candidate);
523
+ const getCommandPayload = (
524
+ candidate: CodeAction | Command,
525
+ ): { command: string; arguments?: unknown[] } | null => {
526
+ if (isCommand(candidate)) {
527
+ return { command: candidate.command, arguments: candidate.arguments };
528
+ }
529
+ if (candidate.command) {
530
+ return { command: candidate.command.command, arguments: candidate.command.arguments };
531
+ }
532
+ return null;
533
+ };
534
+
535
+ const codeAction = result[action_index];
536
+
537
+ // Resolve if needed
538
+ let resolvedAction = codeAction;
539
+ if (
540
+ isCodeAction(codeAction) &&
541
+ !codeAction.edit &&
542
+ codeAction.data &&
543
+ client.serverCapabilities?.codeActionProvider
544
+ ) {
545
+ const provider = client.serverCapabilities.codeActionProvider;
546
+ if (typeof provider === "object" && provider.resolveProvider) {
547
+ resolvedAction = (await sendRequest(client, "codeAction/resolve", codeAction)) as CodeAction;
548
+ }
549
+ }
550
+
551
+ if (isCodeAction(resolvedAction) && resolvedAction.edit) {
552
+ const applied = await applyWorkspaceEdit(resolvedAction.edit, cwd);
553
+ output = `Applied "${codeAction.title}":\n${applied.map((a) => ` ${a}`).join("\n")}`;
554
+ } else {
555
+ const commandPayload = getCommandPayload(resolvedAction);
556
+ if (commandPayload) {
557
+ await sendRequest(client, "workspace/executeCommand", commandPayload);
558
+ output = `Executed "${codeAction.title}"`;
559
+ } else {
560
+ output = `Code action "${codeAction.title}" has no edits or command to apply`;
561
+ }
562
+ }
563
+ } else {
564
+ // List available actions
565
+ const lines = result.map((actionItem, i) => {
566
+ if ("kind" in actionItem || "isPreferred" in actionItem || "edit" in actionItem) {
567
+ const actionDetails = actionItem as CodeAction;
568
+ const preferred = actionDetails.isPreferred ? " (preferred)" : "";
569
+ const kindInfo = actionDetails.kind ? ` [${actionDetails.kind}]` : "";
570
+ return ` [${i}] ${actionDetails.title}${kindInfo}${preferred}`;
571
+ }
572
+ return ` [${i}] ${actionItem.title}`;
573
+ });
574
+ output = `Available code actions:\n${lines.join(
575
+ "\n",
576
+ )}\n\nUse action_index parameter to apply a specific action.`;
577
+ }
578
+ break;
579
+ }
580
+
581
+ case "incoming_calls":
582
+ case "outgoing_calls": {
583
+ // First, prepare the call hierarchy item at the cursor position
584
+ const prepareResult = (await sendRequest(client, "textDocument/prepareCallHierarchy", {
585
+ textDocument: { uri },
586
+ position,
587
+ })) as CallHierarchyItem[] | null;
588
+
589
+ if (!prepareResult || prepareResult.length === 0) {
590
+ output = "No callable symbol found at this position";
591
+ break;
592
+ }
593
+
594
+ const item = prepareResult[0];
595
+
596
+ if (action === "incoming_calls") {
597
+ const calls = (await sendRequest(client, "callHierarchy/incomingCalls", { item })) as
598
+ | CallHierarchyIncomingCall[]
599
+ | null;
600
+
601
+ if (!calls || calls.length === 0) {
602
+ output = `No callers found for "${item.name}"`;
603
+ } else {
604
+ const lines = calls.map((call) => {
605
+ const loc = { uri: call.from.uri, range: call.from.selectionRange };
606
+ const detail = call.from.detail ? ` (${call.from.detail})` : "";
607
+ return ` ${call.from.name}${detail} @ ${formatLocation(loc, cwd)}`;
608
+ });
609
+ output = `Found ${calls.length} caller(s) of "${item.name}":\n${lines.join("\n")}`;
610
+ }
611
+ } else {
612
+ const calls = (await sendRequest(client, "callHierarchy/outgoingCalls", { item })) as
613
+ | CallHierarchyOutgoingCall[]
614
+ | null;
615
+
616
+ if (!calls || calls.length === 0) {
617
+ output = `"${item.name}" doesn't call any functions`;
618
+ } else {
619
+ const lines = calls.map((call) => {
620
+ const loc = { uri: call.to.uri, range: call.to.selectionRange };
621
+ const detail = call.to.detail ? ` (${call.to.detail})` : "";
622
+ return ` ${call.to.name}${detail} @ ${formatLocation(loc, cwd)}`;
623
+ });
624
+ output = `"${item.name}" calls ${calls.length} function(s):\n${lines.join("\n")}`;
625
+ }
626
+ }
627
+ break;
628
+ }
629
+
630
+ // =====================================================================
631
+ // Rust-Analyzer Specific Operations
632
+ // =====================================================================
633
+
634
+ case "flycheck": {
635
+ if (!hasCapability(serverConfig, "flycheck")) {
636
+ return {
637
+ content: [{ type: "text", text: "Error: flycheck requires rust-analyzer" }],
638
+ details: { action, serverName, success: false },
639
+ };
640
+ }
641
+
642
+ await rustAnalyzer.flycheck(client, resolvedFile ?? undefined);
643
+ const collected: Array<{ filePath: string; diagnostic: Diagnostic }> = [];
644
+ for (const [diagUri, diags] of client.diagnostics.entries()) {
645
+ const relPath = path.relative(cwd, uriToFile(diagUri));
646
+ for (const diag of diags) {
647
+ collected.push({ filePath: relPath, diagnostic: diag });
648
+ }
649
+ }
650
+
651
+ if (collected.length === 0) {
652
+ output = "Flycheck: no issues found";
653
+ } else {
654
+ const summary = formatDiagnosticsSummary(collected.map((d) => d.diagnostic));
655
+ const formatted = collected.slice(0, 20).map((d) => formatDiagnostic(d.diagnostic, d.filePath));
656
+ const more = collected.length > 20 ? `\n ... and ${collected.length - 20} more` : "";
657
+ output = `Flycheck ${summary}:\n${formatted.map((f) => ` ${f}`).join("\n")}${more}`;
658
+ }
659
+ break;
660
+ }
661
+
662
+ case "expand_macro": {
663
+ if (!hasCapability(serverConfig, "expandMacro")) {
664
+ return {
665
+ content: [{ type: "text", text: "Error: expand_macro requires rust-analyzer" }],
666
+ details: { action, serverName, success: false },
667
+ };
668
+ }
669
+
670
+ if (!targetFile) {
671
+ return {
672
+ content: [{ type: "text", text: "Error: file parameter required for expand_macro" }],
673
+ details: { action, serverName, success: false },
674
+ };
675
+ }
676
+
677
+ const result = await rustAnalyzer.expandMacro(client, targetFile, line || 1, column || 1);
678
+ if (!result) {
679
+ output = "No macro expansion at this position";
680
+ } else {
681
+ output = `Macro: ${result.name}\n\nExpansion:\n${result.expansion}`;
682
+ }
683
+ break;
684
+ }
685
+
686
+ case "ssr": {
687
+ if (!hasCapability(serverConfig, "ssr")) {
688
+ return {
689
+ content: [{ type: "text", text: "Error: ssr requires rust-analyzer" }],
690
+ details: { action, serverName, success: false },
691
+ };
692
+ }
693
+
694
+ if (!query) {
695
+ return {
696
+ content: [{ type: "text", text: "Error: query parameter (pattern) required for ssr" }],
697
+ details: { action, serverName, success: false },
698
+ };
699
+ }
700
+
701
+ if (!replacement) {
702
+ return {
703
+ content: [{ type: "text", text: "Error: replacement parameter required for ssr" }],
704
+ details: { action, serverName, success: false },
705
+ };
706
+ }
707
+
708
+ const shouldApply = apply === true;
709
+ const result = await rustAnalyzer.ssr(client, query, replacement, !shouldApply);
710
+
711
+ if (shouldApply) {
712
+ const applied = await applyWorkspaceEdit(result, cwd);
713
+ output =
714
+ applied.length > 0
715
+ ? `Applied SSR:\n${applied.map((a) => ` ${a}`).join("\n")}`
716
+ : "SSR: no matches found";
717
+ } else {
718
+ const preview = formatWorkspaceEdit(result, cwd);
719
+ output =
720
+ preview.length > 0
721
+ ? `SSR preview:\n${preview.map((p) => ` ${p}`).join("\n")}`
722
+ : "SSR: no matches found";
723
+ }
724
+ break;
725
+ }
726
+
727
+ case "runnables": {
728
+ if (!hasCapability(serverConfig, "runnables")) {
729
+ return {
730
+ content: [{ type: "text", text: "Error: runnables requires rust-analyzer" }],
731
+ details: { action, serverName, success: false },
732
+ };
733
+ }
734
+
735
+ if (!targetFile) {
736
+ return {
737
+ content: [{ type: "text", text: "Error: file parameter required for runnables" }],
738
+ details: { action, serverName, success: false },
739
+ };
740
+ }
741
+
742
+ const result = await rustAnalyzer.runnables(client, targetFile, line);
743
+ if (result.length === 0) {
744
+ output = "No runnables found";
745
+ } else {
746
+ const lines = result.map((r) => {
747
+ const args = r.args?.cargoArgs?.join(" ") || "";
748
+ return ` [${r.kind}] ${r.label}${args ? ` (cargo ${args})` : ""}`;
749
+ });
750
+ output = `Found ${result.length} runnable(s):\n${lines.join("\n")}`;
751
+ }
752
+ break;
753
+ }
754
+
755
+ case "related_tests": {
756
+ if (!hasCapability(serverConfig, "relatedTests")) {
757
+ return {
758
+ content: [{ type: "text", text: "Error: related_tests requires rust-analyzer" }],
759
+ details: { action, serverName, success: false },
760
+ };
761
+ }
762
+
763
+ if (!targetFile) {
764
+ return {
765
+ content: [{ type: "text", text: "Error: file parameter required for related_tests" }],
766
+ details: { action, serverName, success: false },
767
+ };
768
+ }
769
+
770
+ const result = await rustAnalyzer.relatedTests(client, targetFile, line || 1, column || 1);
771
+ if (result.length === 0) {
772
+ output = "No related tests found";
773
+ } else {
774
+ output = `Found ${result.length} related test(s):\n${result.map((t) => ` ${t}`).join("\n")}`;
775
+ }
776
+ break;
777
+ }
778
+
779
+ case "reload_workspace": {
780
+ await rustAnalyzer.reloadWorkspace(client);
781
+ output = "Workspace reloaded successfully";
782
+ break;
783
+ }
784
+
785
+ default:
786
+ output = `Unknown action: ${action}`;
787
+ }
788
+
789
+ return {
790
+ content: [{ type: "text", text: output }],
791
+ details: { serverName, action, success: true },
792
+ };
793
+ } catch (err) {
794
+ const errorMessage = err instanceof Error ? err.message : String(err);
795
+ return {
796
+ content: [{ type: "text", text: `LSP error: ${errorMessage}` }],
797
+ details: { serverName, action, success: false },
798
+ };
799
+ }
800
+ },
801
+ };
802
+ }
803
+
804
+ export const lspTool = createLspTool(process.cwd());