@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,605 @@
1
+ import * as fs from "node:fs";
2
+ import { applyWorkspaceEdit } from "./edits.js";
3
+ import type {
4
+ Diagnostic,
5
+ LspClient,
6
+ LspJsonRpcNotification,
7
+ LspJsonRpcRequest,
8
+ LspJsonRpcResponse,
9
+ ServerConfig,
10
+ WorkspaceEdit,
11
+ } from "./types.js";
12
+ import { detectLanguageId, fileToUri } from "./utils.js";
13
+
14
+ // =============================================================================
15
+ // Client State
16
+ // =============================================================================
17
+
18
+ const clients = new Map<string, LspClient>();
19
+
20
+ // Idle timeout: shutdown clients after 5 minutes of inactivity
21
+ const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
22
+ const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
23
+
24
+ // Background task to shutdown idle clients
25
+ let idleCheckInterval: Timer | null = null;
26
+
27
+ function startIdleChecker(): void {
28
+ if (idleCheckInterval) return;
29
+ idleCheckInterval = setInterval(() => {
30
+ const now = Date.now();
31
+ for (const [key, client] of Array.from(clients.entries())) {
32
+ if (now - client.lastActivity > IDLE_TIMEOUT_MS) {
33
+ console.log(`[LSP] Shutting down idle client: ${key}`);
34
+ shutdownClient(key);
35
+ }
36
+ }
37
+ }, IDLE_CHECK_INTERVAL_MS);
38
+ }
39
+
40
+ function stopIdleChecker(): void {
41
+ if (idleCheckInterval) {
42
+ clearInterval(idleCheckInterval);
43
+ idleCheckInterval = null;
44
+ }
45
+ }
46
+
47
+ // =============================================================================
48
+ // Client Capabilities
49
+ // =============================================================================
50
+
51
+ const CLIENT_CAPABILITIES = {
52
+ textDocument: {
53
+ synchronization: {
54
+ didSave: true,
55
+ dynamicRegistration: false,
56
+ willSave: false,
57
+ willSaveWaitUntil: false,
58
+ },
59
+ hover: {
60
+ contentFormat: ["markdown", "plaintext"],
61
+ dynamicRegistration: false,
62
+ },
63
+ definition: {
64
+ dynamicRegistration: false,
65
+ linkSupport: true,
66
+ },
67
+ references: {
68
+ dynamicRegistration: false,
69
+ },
70
+ documentSymbol: {
71
+ dynamicRegistration: false,
72
+ hierarchicalDocumentSymbolSupport: true,
73
+ symbolKind: {
74
+ valueSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26],
75
+ },
76
+ },
77
+ rename: {
78
+ dynamicRegistration: false,
79
+ prepareSupport: true,
80
+ },
81
+ codeAction: {
82
+ dynamicRegistration: false,
83
+ codeActionLiteralSupport: {
84
+ codeActionKind: {
85
+ valueSet: [
86
+ "quickfix",
87
+ "refactor",
88
+ "refactor.extract",
89
+ "refactor.inline",
90
+ "refactor.rewrite",
91
+ "source",
92
+ "source.organizeImports",
93
+ "source.fixAll",
94
+ ],
95
+ },
96
+ },
97
+ resolveSupport: {
98
+ properties: ["edit"],
99
+ },
100
+ },
101
+ publishDiagnostics: {
102
+ relatedInformation: true,
103
+ versionSupport: false,
104
+ tagSupport: { valueSet: [1, 2] },
105
+ codeDescriptionSupport: true,
106
+ dataSupport: true,
107
+ },
108
+ },
109
+ workspace: {
110
+ applyEdit: true,
111
+ workspaceEdit: {
112
+ documentChanges: true,
113
+ resourceOperations: ["create", "rename", "delete"],
114
+ failureHandling: "textOnlyTransactional",
115
+ },
116
+ configuration: true,
117
+ symbol: {
118
+ dynamicRegistration: false,
119
+ symbolKind: {
120
+ valueSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26],
121
+ },
122
+ },
123
+ },
124
+ experimental: {
125
+ snippetTextEdit: true,
126
+ },
127
+ };
128
+
129
+ // =============================================================================
130
+ // LSP Message Protocol
131
+ // =============================================================================
132
+
133
+ /**
134
+ * Parse a single LSP message from a buffer.
135
+ * Returns the parsed message and remaining buffer, or null if incomplete.
136
+ */
137
+ function parseMessage(
138
+ buffer: Uint8Array,
139
+ ): { message: LspJsonRpcResponse | LspJsonRpcNotification; remaining: Uint8Array } | null {
140
+ // Only decode enough to find the header
141
+ const headerEndIndex = findHeaderEnd(buffer);
142
+ if (headerEndIndex === -1) return null;
143
+
144
+ const headerText = new TextDecoder().decode(buffer.slice(0, headerEndIndex));
145
+ const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i);
146
+ if (!contentLengthMatch) return null;
147
+
148
+ const contentLength = Number.parseInt(contentLengthMatch[1], 10);
149
+ const messageStart = headerEndIndex + 4; // Skip \r\n\r\n
150
+ const messageEnd = messageStart + contentLength;
151
+
152
+ if (buffer.length < messageEnd) return null;
153
+
154
+ const messageBytes = buffer.slice(messageStart, messageEnd);
155
+ const messageText = new TextDecoder().decode(messageBytes);
156
+ const remaining = buffer.slice(messageEnd);
157
+
158
+ return {
159
+ message: JSON.parse(messageText),
160
+ remaining,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Find the end of the header section (before \r\n\r\n)
166
+ */
167
+ function findHeaderEnd(buffer: Uint8Array): number {
168
+ for (let i = 0; i < buffer.length - 3; i++) {
169
+ if (buffer[i] === 13 && buffer[i + 1] === 10 && buffer[i + 2] === 13 && buffer[i + 3] === 10) {
170
+ return i;
171
+ }
172
+ }
173
+ return -1;
174
+ }
175
+
176
+ /**
177
+ * Concatenate two Uint8Arrays efficiently
178
+ */
179
+ function concatBuffers(a: Uint8Array, b: Uint8Array): Uint8Array {
180
+ const result = new Uint8Array(a.length + b.length);
181
+ result.set(a);
182
+ result.set(b, a.length);
183
+ return result;
184
+ }
185
+
186
+ async function writeMessage(
187
+ sink: import("bun").FileSink,
188
+ message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
189
+ ): Promise<void> {
190
+ const content = JSON.stringify(message);
191
+ const contentBytes = new TextEncoder().encode(content);
192
+ const header = `Content-Length: ${contentBytes.length}\r\n\r\n`;
193
+ const fullMessage = new TextEncoder().encode(header + content);
194
+
195
+ sink.write(fullMessage);
196
+ await sink.flush();
197
+ }
198
+
199
+ // =============================================================================
200
+ // Message Reader
201
+ // =============================================================================
202
+
203
+ /**
204
+ * Start background message reader for a client.
205
+ * Routes responses to pending requests and handles notifications.
206
+ */
207
+ async function startMessageReader(client: LspClient): Promise<void> {
208
+ if (client.isReading) return;
209
+ client.isReading = true;
210
+
211
+ const reader = (client.process.stdout as ReadableStream<Uint8Array>).getReader();
212
+
213
+ try {
214
+ while (true) {
215
+ const { done, value } = await reader.read();
216
+ if (done) break;
217
+
218
+ client.messageBuffer = concatBuffers(client.messageBuffer, value);
219
+
220
+ // Process all complete messages in buffer
221
+ let parsed = parseMessage(client.messageBuffer);
222
+ while (parsed) {
223
+ const { message, remaining } = parsed;
224
+ client.messageBuffer = remaining;
225
+
226
+ // Route message
227
+ if ("id" in message && message.id !== undefined) {
228
+ // Response to a request
229
+ const pending = client.pendingRequests.get(message.id);
230
+ if (pending) {
231
+ client.pendingRequests.delete(message.id);
232
+ if ("error" in message && message.error) {
233
+ pending.reject(new Error(`LSP error: ${message.error.message}`));
234
+ } else {
235
+ pending.resolve(message.result);
236
+ }
237
+ } else if ("method" in message) {
238
+ await handleServerRequest(client, message as LspJsonRpcRequest);
239
+ }
240
+ } else if ("method" in message) {
241
+ // Server notification
242
+ if (message.method === "textDocument/publishDiagnostics" && message.params) {
243
+ const params = message.params as { uri: string; diagnostics: Diagnostic[] };
244
+ client.diagnostics.set(params.uri, params.diagnostics);
245
+ }
246
+ }
247
+
248
+ parsed = parseMessage(client.messageBuffer);
249
+ }
250
+ }
251
+ } catch (err) {
252
+ // Connection closed or error - reject all pending requests
253
+ for (const pending of Array.from(client.pendingRequests.values())) {
254
+ pending.reject(new Error(`LSP connection closed: ${err}`));
255
+ }
256
+ client.pendingRequests.clear();
257
+ } finally {
258
+ reader.releaseLock();
259
+ client.isReading = false;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Handle workspace/configuration requests from the server.
265
+ */
266
+ async function handleConfigurationRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
267
+ if (typeof message.id !== "number") return;
268
+ const params = message.params as { items?: Array<{ section?: string }> };
269
+ const items = params?.items ?? [];
270
+ const result = items.map((item) => {
271
+ const section = item.section ?? "";
272
+ return client.config.settings?.[section] ?? {};
273
+ });
274
+ await sendResponse(client, message.id, result, "workspace/configuration");
275
+ }
276
+
277
+ /**
278
+ * Handle workspace/applyEdit requests from the server.
279
+ */
280
+ async function handleApplyEditRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
281
+ if (typeof message.id !== "number") return;
282
+ const params = message.params as { edit?: WorkspaceEdit };
283
+ if (!params?.edit) {
284
+ await sendResponse(
285
+ client,
286
+ message.id,
287
+ { applied: false, failureReason: "No edit provided" },
288
+ "workspace/applyEdit",
289
+ );
290
+ return;
291
+ }
292
+
293
+ try {
294
+ await applyWorkspaceEdit(params.edit, client.cwd);
295
+ await sendResponse(client, message.id, { applied: true }, "workspace/applyEdit");
296
+ } catch (err) {
297
+ await sendResponse(client, message.id, { applied: false, failureReason: String(err) }, "workspace/applyEdit");
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Respond to a server-initiated request.
303
+ */
304
+ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
305
+ if (message.method === "workspace/configuration") {
306
+ await handleConfigurationRequest(client, message);
307
+ return;
308
+ }
309
+ if (message.method === "workspace/applyEdit") {
310
+ await handleApplyEditRequest(client, message);
311
+ return;
312
+ }
313
+ if (typeof message.id !== "number") return;
314
+ await sendResponse(client, message.id, null, message.method, {
315
+ code: -32601,
316
+ message: `Method not found: ${message.method}`,
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Send an LSP response to the server.
322
+ */
323
+ async function sendResponse(
324
+ client: LspClient,
325
+ id: number,
326
+ result: unknown,
327
+ method: string,
328
+ error?: { code: number; message: string; data?: unknown },
329
+ ): Promise<void> {
330
+ const response: LspJsonRpcResponse = {
331
+ jsonrpc: "2.0",
332
+ id,
333
+ ...(error ? { error } : { result }),
334
+ };
335
+
336
+ try {
337
+ await writeMessage(client.process.stdin as import("bun").FileSink, response);
338
+ } catch (err) {
339
+ console.error(`[LSP] Failed to respond to ${method}: ${err}`);
340
+ }
341
+ }
342
+
343
+ // =============================================================================
344
+ // Client Management
345
+ // =============================================================================
346
+
347
+ /**
348
+ * Get or create an LSP client for the given server configuration and working directory.
349
+ */
350
+ export async function getOrCreateClient(config: ServerConfig, cwd: string): Promise<LspClient> {
351
+ const key = `${config.command}:${cwd}`;
352
+
353
+ if (clients.has(key)) {
354
+ const client = clients.get(key)!;
355
+ client.lastActivity = Date.now();
356
+ return client;
357
+ }
358
+
359
+ const args = config.args ?? [];
360
+ const proc = Bun.spawn([config.command, ...args], {
361
+ cwd,
362
+ stdin: "pipe",
363
+ stdout: "pipe",
364
+ stderr: "pipe",
365
+ });
366
+
367
+ const client: LspClient = {
368
+ name: key,
369
+ cwd,
370
+ process: proc,
371
+ config,
372
+ requestId: 0,
373
+ diagnostics: new Map(),
374
+ openFiles: new Map(),
375
+ pendingRequests: new Map(),
376
+ messageBuffer: new Uint8Array(0),
377
+ isReading: false,
378
+ lastActivity: Date.now(),
379
+ };
380
+ clients.set(key, client);
381
+
382
+ // Start idle checker if not already running
383
+ startIdleChecker();
384
+
385
+ // Register crash recovery - remove client on process exit
386
+ proc.exited.then(() => {
387
+ console.log(`[LSP] Process exited: ${key}`);
388
+ clients.delete(key);
389
+ if (clients.size === 0) {
390
+ stopIdleChecker();
391
+ }
392
+ });
393
+
394
+ // Start background message reader
395
+ startMessageReader(client);
396
+
397
+ try {
398
+ // Send initialize request
399
+ const initResult = (await sendRequest(client, "initialize", {
400
+ processId: process.pid,
401
+ rootUri: fileToUri(cwd),
402
+ rootPath: cwd,
403
+ capabilities: CLIENT_CAPABILITIES,
404
+ initializationOptions: config.initOptions ?? {},
405
+ workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
406
+ })) as { capabilities?: unknown };
407
+
408
+ if (!initResult) {
409
+ throw new Error("Failed to initialize LSP: no response");
410
+ }
411
+
412
+ client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
413
+
414
+ // Send initialized notification
415
+ await sendNotification(client, "initialized", {});
416
+
417
+ return client;
418
+ } catch (err) {
419
+ // Clean up on initialization failure
420
+ clients.delete(key);
421
+ proc.kill();
422
+ throw err;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Ensure a file is opened in the LSP client.
428
+ * Sends didOpen notification if the file is not already tracked.
429
+ */
430
+ export async function ensureFileOpen(client: LspClient, filePath: string): Promise<void> {
431
+ const uri = fileToUri(filePath);
432
+ if (client.openFiles.has(uri)) {
433
+ return;
434
+ }
435
+
436
+ const content = fs.readFileSync(filePath, "utf-8");
437
+ const languageId = detectLanguageId(filePath);
438
+
439
+ await sendNotification(client, "textDocument/didOpen", {
440
+ textDocument: {
441
+ uri,
442
+ languageId,
443
+ version: 1,
444
+ text: content,
445
+ },
446
+ });
447
+
448
+ client.openFiles.set(uri, { version: 1, languageId });
449
+ client.lastActivity = Date.now();
450
+ }
451
+
452
+ /**
453
+ * Refresh a file in the LSP client.
454
+ * Increments version, sends didChange and didSave notifications.
455
+ */
456
+ export async function refreshFile(client: LspClient, filePath: string): Promise<void> {
457
+ const uri = fileToUri(filePath);
458
+ const info = client.openFiles.get(uri);
459
+
460
+ if (!info) {
461
+ await ensureFileOpen(client, filePath);
462
+ return;
463
+ }
464
+
465
+ const content = fs.readFileSync(filePath, "utf-8");
466
+ info.version++;
467
+
468
+ await sendNotification(client, "textDocument/didChange", {
469
+ textDocument: { uri, version: info.version },
470
+ contentChanges: [{ text: content }],
471
+ });
472
+
473
+ await sendNotification(client, "textDocument/didSave", {
474
+ textDocument: { uri },
475
+ text: content,
476
+ });
477
+
478
+ client.lastActivity = Date.now();
479
+ }
480
+
481
+ /**
482
+ * Shutdown a specific client by key.
483
+ */
484
+ export function shutdownClient(key: string): void {
485
+ const client = clients.get(key);
486
+ if (!client) return;
487
+
488
+ // Reject all pending requests
489
+ for (const pending of Array.from(client.pendingRequests.values())) {
490
+ pending.reject(new Error("LSP client shutdown"));
491
+ }
492
+ client.pendingRequests.clear();
493
+
494
+ // Send shutdown request (best effort, don't wait)
495
+ sendRequest(client, "shutdown", null).catch(() => {});
496
+
497
+ // Kill process
498
+ client.process.kill();
499
+ clients.delete(key);
500
+
501
+ if (clients.size === 0) {
502
+ stopIdleChecker();
503
+ }
504
+ }
505
+
506
+ // =============================================================================
507
+ // LSP Protocol Methods
508
+ // =============================================================================
509
+
510
+ /**
511
+ * Send an LSP request and wait for response.
512
+ */
513
+ export async function sendRequest(client: LspClient, method: string, params: unknown): Promise<unknown> {
514
+ const id = ++client.requestId;
515
+ const request: LspJsonRpcRequest = {
516
+ jsonrpc: "2.0",
517
+ id,
518
+ method,
519
+ params,
520
+ };
521
+
522
+ client.lastActivity = Date.now();
523
+
524
+ return new Promise((resolve, reject) => {
525
+ // Set timeout
526
+ const timeout = setTimeout(() => {
527
+ if (client.pendingRequests.has(id)) {
528
+ client.pendingRequests.delete(id);
529
+ reject(new Error(`LSP request ${method} timed out`));
530
+ }
531
+ }, 30000);
532
+
533
+ // Register pending request with timeout wrapper
534
+ client.pendingRequests.set(id, {
535
+ resolve: (result) => {
536
+ clearTimeout(timeout);
537
+ resolve(result);
538
+ },
539
+ reject: (err) => {
540
+ clearTimeout(timeout);
541
+ reject(err);
542
+ },
543
+ method,
544
+ });
545
+
546
+ // Write request
547
+ writeMessage(client.process.stdin as import("bun").FileSink, request).catch((err) => {
548
+ clearTimeout(timeout);
549
+ client.pendingRequests.delete(id);
550
+ reject(err);
551
+ });
552
+ });
553
+ }
554
+
555
+ /**
556
+ * Send an LSP notification (no response expected).
557
+ */
558
+ export async function sendNotification(client: LspClient, method: string, params: unknown): Promise<void> {
559
+ const notification: LspJsonRpcNotification = {
560
+ jsonrpc: "2.0",
561
+ method,
562
+ params,
563
+ };
564
+
565
+ client.lastActivity = Date.now();
566
+ await writeMessage(client.process.stdin as import("bun").FileSink, notification);
567
+ }
568
+
569
+ /**
570
+ * Shutdown all LSP clients.
571
+ */
572
+ export function shutdownAll(): void {
573
+ stopIdleChecker();
574
+
575
+ for (const client of Array.from(clients.values())) {
576
+ // Reject all pending requests
577
+ for (const pending of Array.from(client.pendingRequests.values())) {
578
+ pending.reject(new Error("LSP client shutdown"));
579
+ }
580
+ client.pendingRequests.clear();
581
+
582
+ // Send shutdown request (best effort, don't wait)
583
+ sendRequest(client, "shutdown", null).catch(() => {});
584
+
585
+ client.process.kill();
586
+ }
587
+ clients.clear();
588
+ }
589
+
590
+ // =============================================================================
591
+ // Process Cleanup
592
+ // =============================================================================
593
+
594
+ // Register cleanup on module unload
595
+ if (typeof process !== "undefined") {
596
+ process.on("beforeExit", shutdownAll);
597
+ process.on("SIGINT", () => {
598
+ shutdownAll();
599
+ process.exit(0);
600
+ });
601
+ process.on("SIGTERM", () => {
602
+ shutdownAll();
603
+ process.exit(0);
604
+ });
605
+ }