gsd-pi 2.8.2 → 2.9.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 (193) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +5 -0
  3. package/dist/loader.js +1 -1
  4. package/dist/update-check.d.ts +24 -0
  5. package/dist/update-check.js +93 -0
  6. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js +758 -0
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js +267 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js +709 -0
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  33. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  34. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  35. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  36. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  37. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  38. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  39. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js +64 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  41. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  43. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  44. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  45. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  46. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js +1 -0
  47. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  48. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  49. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  50. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +4 -0
  51. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  52. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  53. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  54. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  55. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  56. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  57. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  58. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
  59. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  60. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
  61. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  62. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
  63. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  64. package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  65. package/node_modules/@gsd/pi-coding-agent/src/core/extensions/types.ts +4 -2
  66. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/client.ts +880 -0
  67. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/config.ts +325 -0
  68. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  69. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  70. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  71. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/index.ts +943 -0
  72. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  73. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  74. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  75. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/types.ts +421 -0
  76. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  77. package/node_modules/@gsd/pi-coding-agent/src/core/slash-commands.ts +1 -0
  78. package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +10 -0
  79. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  80. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
  81. package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
  82. package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
  83. package/package.json +1 -1
  84. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
  85. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  88. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/lsp/client.js +758 -0
  90. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  92. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/lsp/config.js +267 -0
  94. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  96. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  98. package/packages/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  99. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  100. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  102. package/packages/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  104. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/lsp/index.js +709 -0
  106. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  108. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  110. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  112. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  113. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  114. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  115. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  116. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  117. package/packages/pi-coding-agent/dist/core/lsp/types.js +64 -0
  118. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  119. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  120. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  121. package/packages/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  122. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  123. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  124. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  125. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  126. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  127. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/tools/index.js +4 -0
  129. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  131. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  133. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +80 -1
  137. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
  139. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
  141. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
  142. package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
  143. package/packages/pi-coding-agent/src/core/extensions/types.ts +4 -2
  144. package/packages/pi-coding-agent/src/core/lsp/client.ts +880 -0
  145. package/packages/pi-coding-agent/src/core/lsp/config.ts +325 -0
  146. package/packages/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  147. package/packages/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  148. package/packages/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  149. package/packages/pi-coding-agent/src/core/lsp/index.ts +943 -0
  150. package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  151. package/packages/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  152. package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  153. package/packages/pi-coding-agent/src/core/lsp/types.ts +421 -0
  154. package/packages/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  155. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  156. package/packages/pi-coding-agent/src/core/tools/index.ts +10 -0
  157. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  158. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +94 -2
  159. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
  160. package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
  161. package/src/resources/extensions/ask-user-questions.ts +42 -2
  162. package/src/resources/extensions/bg-shell/index.ts +34 -37
  163. package/src/resources/extensions/browser-tools/core.d.ts +205 -0
  164. package/src/resources/extensions/browser-tools/index.ts +2 -2
  165. package/src/resources/extensions/browser-tools/refs.ts +1 -1
  166. package/src/resources/extensions/browser-tools/tools/session.ts +1 -1
  167. package/src/resources/extensions/context7/index.ts +2 -2
  168. package/src/resources/extensions/get-secrets-from-user.ts +3 -2
  169. package/src/resources/extensions/google-search/index.ts +1 -1
  170. package/src/resources/extensions/gsd/auto.ts +126 -12
  171. package/src/resources/extensions/gsd/commands.ts +218 -3
  172. package/src/resources/extensions/gsd/doctor.ts +1 -1
  173. package/src/resources/extensions/gsd/git-service.ts +163 -13
  174. package/src/resources/extensions/gsd/guided-flow.ts +19 -9
  175. package/src/resources/extensions/gsd/index.ts +17 -7
  176. package/src/resources/extensions/gsd/preferences.ts +1 -1
  177. package/src/resources/extensions/gsd/tests/git-service.test.ts +226 -0
  178. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +2 -2
  179. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +1 -1
  180. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +10 -10
  181. package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +87 -0
  182. package/src/resources/extensions/gsd/tests/worktree.test.ts +352 -0
  183. package/src/resources/extensions/gsd/types.ts +1 -0
  184. package/src/resources/extensions/gsd/worktree.ts +20 -1
  185. package/src/resources/extensions/mac-tools/index.ts +1 -1
  186. package/src/resources/extensions/search-the-web/command-search-provider.ts +1 -1
  187. package/src/resources/extensions/search-the-web/format.ts +1 -1
  188. package/src/resources/extensions/search-the-web/index.ts +5 -5
  189. package/src/resources/extensions/search-the-web/native-search.ts +5 -6
  190. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +7 -7
  191. package/src/resources/extensions/search-the-web/tool-llm-context.ts +11 -11
  192. package/src/resources/extensions/search-the-web/tool-search.ts +10 -10
  193. package/src/resources/extensions/shared/interview-ui.ts +2 -2
@@ -0,0 +1,880 @@
1
+ import { spawn } from "node:child_process";
2
+ import * as fsPromises from "node:fs/promises";
3
+ import type { Writable } from "node:stream";
4
+ import { killProcessTree } from "../../utils/shell.js";
5
+ import { ToolAbortError, isEnoent, throwIfAborted, untilAborted } from "./helpers.js";
6
+ import { applyWorkspaceEdit } from "./edits.js";
7
+ import { getLspmuxCommand, isLspmuxSupported } from "./lspmux.js";
8
+ import type {
9
+ Diagnostic,
10
+ LspClient,
11
+ LspJsonRpcNotification,
12
+ LspJsonRpcRequest,
13
+ LspJsonRpcResponse,
14
+ ServerConfig,
15
+ WorkspaceEdit,
16
+ } from "./types.js";
17
+ import { detectLanguageId, fileToUri } from "./utils.js";
18
+
19
+ // =============================================================================
20
+ // Client State
21
+ // =============================================================================
22
+
23
+ const clients = new Map<string, LspClient>();
24
+ const clientLocks = new Map<string, Promise<LspClient>>();
25
+ const fileOperationLocks = new Map<string, Promise<void>>();
26
+
27
+ // Idle timeout configuration (disabled by default)
28
+ let idleTimeoutMs: number | null = null;
29
+ let idleCheckInterval: ReturnType<typeof setInterval> | null = null;
30
+ const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
31
+
32
+ /**
33
+ * Configure the idle timeout for LSP clients.
34
+ */
35
+ export function setIdleTimeout(ms: number | null | undefined): void {
36
+ idleTimeoutMs = ms ?? null;
37
+
38
+ if (idleTimeoutMs && idleTimeoutMs > 0) {
39
+ startIdleChecker();
40
+ } else {
41
+ stopIdleChecker();
42
+ }
43
+ }
44
+
45
+ function startIdleChecker(): void {
46
+ if (idleCheckInterval) return;
47
+ idleCheckInterval = setInterval(() => {
48
+ if (!idleTimeoutMs) return;
49
+ const now = Date.now();
50
+ for (const [key, client] of Array.from(clients.entries())) {
51
+ if (now - client.lastActivity > idleTimeoutMs) {
52
+ shutdownClient(key);
53
+ }
54
+ }
55
+ }, IDLE_CHECK_INTERVAL_MS);
56
+ }
57
+
58
+ function stopIdleChecker(): void {
59
+ if (idleCheckInterval) {
60
+ clearInterval(idleCheckInterval);
61
+ idleCheckInterval = null;
62
+ }
63
+ }
64
+
65
+ // =============================================================================
66
+ // Client Capabilities
67
+ // =============================================================================
68
+
69
+ const CLIENT_CAPABILITIES = {
70
+ textDocument: {
71
+ synchronization: {
72
+ didSave: true,
73
+ dynamicRegistration: false,
74
+ willSave: false,
75
+ willSaveWaitUntil: false,
76
+ },
77
+ hover: {
78
+ contentFormat: ["markdown", "plaintext"],
79
+ dynamicRegistration: false,
80
+ },
81
+ definition: {
82
+ dynamicRegistration: false,
83
+ linkSupport: true,
84
+ },
85
+ typeDefinition: {
86
+ dynamicRegistration: false,
87
+ linkSupport: true,
88
+ },
89
+ implementation: {
90
+ dynamicRegistration: false,
91
+ linkSupport: true,
92
+ },
93
+ references: {
94
+ dynamicRegistration: false,
95
+ },
96
+ documentSymbol: {
97
+ dynamicRegistration: false,
98
+ hierarchicalDocumentSymbolSupport: true,
99
+ symbolKind: {
100
+ 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],
101
+ },
102
+ },
103
+ rename: {
104
+ dynamicRegistration: false,
105
+ prepareSupport: true,
106
+ },
107
+ codeAction: {
108
+ dynamicRegistration: false,
109
+ codeActionLiteralSupport: {
110
+ codeActionKind: {
111
+ valueSet: [
112
+ "quickfix",
113
+ "refactor",
114
+ "refactor.extract",
115
+ "refactor.inline",
116
+ "refactor.rewrite",
117
+ "source",
118
+ "source.organizeImports",
119
+ "source.fixAll",
120
+ ],
121
+ },
122
+ },
123
+ resolveSupport: {
124
+ properties: ["edit"],
125
+ },
126
+ },
127
+ formatting: {
128
+ dynamicRegistration: false,
129
+ },
130
+ rangeFormatting: {
131
+ dynamicRegistration: false,
132
+ },
133
+ publishDiagnostics: {
134
+ relatedInformation: true,
135
+ versionSupport: false,
136
+ tagSupport: { valueSet: [1, 2] },
137
+ codeDescriptionSupport: true,
138
+ dataSupport: true,
139
+ },
140
+ },
141
+ workspace: {
142
+ applyEdit: true,
143
+ workspaceEdit: {
144
+ documentChanges: true,
145
+ resourceOperations: ["create", "rename", "delete"],
146
+ failureHandling: "textOnlyTransactional",
147
+ },
148
+ configuration: true,
149
+ symbol: {
150
+ dynamicRegistration: false,
151
+ symbolKind: {
152
+ 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],
153
+ },
154
+ },
155
+ },
156
+ experimental: {
157
+ snippetTextEdit: true,
158
+ },
159
+ };
160
+
161
+ // =============================================================================
162
+ // LSP Message Protocol
163
+ // =============================================================================
164
+
165
+ function parseMessage(
166
+ buffer: Buffer,
167
+ ): { message: LspJsonRpcResponse | LspJsonRpcNotification | null; remaining: Buffer } | null {
168
+ const headerEndIndex = findHeaderEnd(buffer);
169
+ if (headerEndIndex === -1) return null;
170
+
171
+ const headerText = new TextDecoder().decode(buffer.slice(0, headerEndIndex));
172
+ const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i);
173
+ if (!contentLengthMatch) return null;
174
+
175
+ const contentLength = Number.parseInt(contentLengthMatch[1], 10);
176
+ const messageStart = headerEndIndex + 4; // Skip \r\n\r\n
177
+ const messageEnd = messageStart + contentLength;
178
+
179
+ if (buffer.length < messageEnd) return null;
180
+
181
+ const messageBytes = buffer.subarray(messageStart, messageEnd);
182
+ const messageText = new TextDecoder().decode(messageBytes);
183
+ const remaining = Buffer.from(buffer.subarray(messageEnd));
184
+
185
+ let message: LspJsonRpcResponse | LspJsonRpcNotification;
186
+ try {
187
+ message = JSON.parse(messageText);
188
+ } catch {
189
+ // Malformed JSON from LSP server — skip this message and advance past it
190
+ return { message: null, remaining };
191
+ }
192
+
193
+ return { message, remaining };
194
+ }
195
+
196
+ function findHeaderEnd(buffer: Uint8Array): number {
197
+ for (let i = 0; i < buffer.length - 3; i++) {
198
+ if (buffer[i] === 13 && buffer[i + 1] === 10 && buffer[i + 2] === 13 && buffer[i + 3] === 10) {
199
+ return i;
200
+ }
201
+ }
202
+ return -1;
203
+ }
204
+
205
+ async function writeMessage(
206
+ stdin: Writable | null,
207
+ message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
208
+ ): Promise<void> {
209
+ if (!stdin) {
210
+ throw new Error("LSP process stdin is not available");
211
+ }
212
+ const content = JSON.stringify(message);
213
+ const header = `Content-Length: ${Buffer.byteLength(content, "utf-8")}\r\n\r\n`;
214
+ return new Promise((resolve, reject) => {
215
+ stdin.write(header + content, (err?: Error | null) => {
216
+ if (err) reject(err);
217
+ else resolve();
218
+ });
219
+ });
220
+ }
221
+
222
+ // =============================================================================
223
+ // Message Reader
224
+ // =============================================================================
225
+
226
+ async function startMessageReader(client: LspClient): Promise<void> {
227
+ if (client.isReading) return;
228
+ client.isReading = true;
229
+
230
+ const stdout = client.proc.stdout;
231
+ if (!stdout) {
232
+ client.isReading = false;
233
+ return;
234
+ }
235
+
236
+ return new Promise<void>((resolve) => {
237
+ stdout.on("data", async (chunk: Buffer) => {
238
+ const currentBuffer: Buffer = Buffer.concat([client.messageBuffer, chunk]);
239
+ client.messageBuffer = currentBuffer;
240
+
241
+ let workingBuffer = currentBuffer;
242
+ let parsed = parseMessage(workingBuffer);
243
+ while (parsed) {
244
+ const { message, remaining } = parsed;
245
+ workingBuffer = remaining;
246
+
247
+ if (!message) {
248
+ parsed = parseMessage(workingBuffer);
249
+ continue;
250
+ }
251
+
252
+ if ("id" in message && message.id !== undefined) {
253
+ const pending = client.pendingRequests.get(message.id);
254
+ if (pending) {
255
+ client.pendingRequests.delete(message.id);
256
+ if ("error" in message && message.error) {
257
+ pending.reject(new Error(`LSP error: ${message.error.message}`));
258
+ } else {
259
+ pending.resolve(message.result);
260
+ }
261
+ } else if ("method" in message) {
262
+ await handleServerRequest(client, message as LspJsonRpcRequest);
263
+ }
264
+ } else if ("method" in message) {
265
+ if (message.method === "textDocument/publishDiagnostics" && message.params) {
266
+ const params = message.params as { uri: string; diagnostics: Diagnostic[] };
267
+ client.diagnostics.set(params.uri, params.diagnostics);
268
+ client.diagnosticsVersion += 1;
269
+ }
270
+ }
271
+
272
+ parsed = parseMessage(workingBuffer);
273
+ }
274
+
275
+ client.messageBuffer = workingBuffer;
276
+ });
277
+
278
+ stdout.on("end", () => {
279
+ client.isReading = false;
280
+ resolve();
281
+ });
282
+
283
+ stdout.on("error", () => {
284
+ client.isReading = false;
285
+ resolve();
286
+ });
287
+ });
288
+ }
289
+
290
+ // =============================================================================
291
+ // Server Request Handlers
292
+ // =============================================================================
293
+
294
+ async function handleConfigurationRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
295
+ if (typeof message.id !== "number") return;
296
+ const params = message.params as { items?: Array<{ section?: string }> };
297
+ const items = params?.items ?? [];
298
+ const result = items.map(item => {
299
+ const section = item.section ?? "";
300
+ return client.config.settings?.[section] ?? {};
301
+ });
302
+ await sendResponse(client, message.id, result, "workspace/configuration");
303
+ }
304
+
305
+ async function handleApplyEditRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
306
+ if (typeof message.id !== "number") return;
307
+ const params = message.params as { edit?: WorkspaceEdit };
308
+ if (!params?.edit) {
309
+ await sendResponse(
310
+ client,
311
+ message.id,
312
+ { applied: false, failureReason: "No edit provided" },
313
+ "workspace/applyEdit",
314
+ );
315
+ return;
316
+ }
317
+
318
+ try {
319
+ await applyWorkspaceEdit(params.edit, client.cwd);
320
+ await sendResponse(client, message.id, { applied: true }, "workspace/applyEdit");
321
+ } catch (err: unknown) {
322
+ await sendResponse(client, message.id, { applied: false, failureReason: String(err) }, "workspace/applyEdit");
323
+ }
324
+ }
325
+
326
+ async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
327
+ if (message.method === "workspace/configuration") {
328
+ await handleConfigurationRequest(client, message);
329
+ return;
330
+ }
331
+ if (message.method === "workspace/applyEdit") {
332
+ await handleApplyEditRequest(client, message);
333
+ return;
334
+ }
335
+ if (typeof message.id !== "number") return;
336
+ await sendResponse(client, message.id, null, message.method, {
337
+ code: -32601,
338
+ message: `Method not found: ${message.method}`,
339
+ });
340
+ }
341
+
342
+ async function sendResponse(
343
+ client: LspClient,
344
+ id: number,
345
+ result: unknown,
346
+ _method: string,
347
+ error?: { code: number; message: string; data?: unknown },
348
+ ): Promise<void> {
349
+ const response: LspJsonRpcResponse = {
350
+ jsonrpc: "2.0",
351
+ id,
352
+ ...(error ? { error } : { result }),
353
+ };
354
+
355
+ try {
356
+ await writeMessage(client.proc.stdin, response);
357
+ } catch {
358
+ // Failed to respond to server request
359
+ }
360
+ }
361
+
362
+ // =============================================================================
363
+ // Stderr Buffer
364
+ // =============================================================================
365
+
366
+ async function startStderrReader(client: LspClient): Promise<void> {
367
+ const stderr = client.proc.stderr;
368
+ if (!stderr) return;
369
+
370
+ return new Promise<void>((resolve) => {
371
+ stderr.on("data", (chunk: Buffer) => {
372
+ const text = chunk.toString("utf-8");
373
+ client.stderrBuffer += text;
374
+ if (client.stderrBuffer.length > 4096) {
375
+ client.stderrBuffer = client.stderrBuffer.slice(-4096);
376
+ }
377
+ });
378
+
379
+ stderr.on("end", () => {
380
+ resolve();
381
+ });
382
+
383
+ stderr.on("error", () => {
384
+ resolve();
385
+ });
386
+ });
387
+ }
388
+
389
+ // =============================================================================
390
+ // Client Management
391
+ // =============================================================================
392
+
393
+ /** Timeout for warmup initialize requests (5 seconds) */
394
+ export const WARMUP_TIMEOUT_MS = 5000;
395
+
396
+ /**
397
+ * Get or create an LSP client for the given server configuration and working directory.
398
+ */
399
+ export async function getOrCreateClient(config: ServerConfig, cwd: string, initTimeoutMs?: number): Promise<LspClient> {
400
+ const key = `${config.command}:${cwd}`;
401
+
402
+ const existingClient = clients.get(key);
403
+ if (existingClient) {
404
+ existingClient.lastActivity = Date.now();
405
+ return existingClient;
406
+ }
407
+
408
+ const existingLock = clientLocks.get(key);
409
+ if (existingLock) {
410
+ return existingLock;
411
+ }
412
+
413
+ const clientPromise = (async () => {
414
+ const baseCommand = config.resolvedCommand ?? config.command;
415
+ const baseArgs = config.args ?? [];
416
+
417
+ // Wrap with lspmux if available and supported
418
+ const { command, args, env } = isLspmuxSupported(baseCommand)
419
+ ? await getLspmuxCommand(baseCommand, baseArgs)
420
+ : { command: baseCommand, args: baseArgs };
421
+
422
+ const proc = spawn(command, args, {
423
+ cwd,
424
+ stdio: ["pipe", "pipe", "pipe"],
425
+ env: env ? { ...process.env, ...env } : undefined,
426
+ });
427
+
428
+ const exitedPromise = new Promise<number>((resolve) => {
429
+ proc.on("exit", (code: number | null) => resolve(code ?? 1));
430
+ });
431
+
432
+ const client: LspClient = {
433
+ name: key,
434
+ cwd,
435
+ proc: {
436
+ stdin: proc.stdin,
437
+ stdout: proc.stdout,
438
+ stderr: proc.stderr,
439
+ pid: proc.pid ?? 0,
440
+ exitCode: null,
441
+ exited: exitedPromise,
442
+ kill: (signal?: number) => proc.kill(signal),
443
+ },
444
+ config,
445
+ requestId: 0,
446
+ diagnostics: new Map(),
447
+ diagnosticsVersion: 0,
448
+ openFiles: new Map(),
449
+ pendingRequests: new Map(),
450
+ messageBuffer: Buffer.alloc(0),
451
+ isReading: false,
452
+ lastActivity: Date.now(),
453
+ stderrBuffer: "",
454
+ };
455
+ clients.set(key, client);
456
+
457
+ // Register crash recovery
458
+ exitedPromise.then((code: number) => {
459
+ client.proc.exitCode = code;
460
+ clients.delete(key);
461
+ clientLocks.delete(key);
462
+
463
+ if (client.pendingRequests.size > 0) {
464
+ const stderr = client.stderrBuffer.trim();
465
+ const err = new Error(
466
+ stderr ? `LSP server exited (code ${code}): ${stderr}` : `LSP server exited unexpectedly (code ${code})`,
467
+ );
468
+ for (const pending of client.pendingRequests.values()) {
469
+ pending.reject(err);
470
+ }
471
+ client.pendingRequests.clear();
472
+ }
473
+ });
474
+
475
+ // Start background readers
476
+ startMessageReader(client);
477
+ startStderrReader(client);
478
+
479
+ try {
480
+ const initResult = (await sendRequest(
481
+ client,
482
+ "initialize",
483
+ {
484
+ processId: process.pid,
485
+ rootUri: fileToUri(cwd),
486
+ rootPath: cwd,
487
+ capabilities: CLIENT_CAPABILITIES,
488
+ initializationOptions: config.initOptions ?? {},
489
+ workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
490
+ },
491
+ undefined, // signal
492
+ initTimeoutMs,
493
+ )) as { capabilities?: unknown };
494
+
495
+ if (!initResult) {
496
+ throw new Error("Failed to initialize LSP: no response");
497
+ }
498
+
499
+ client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
500
+
501
+ await sendNotification(client, "initialized", {});
502
+
503
+ return client;
504
+ } catch (err) {
505
+ clients.delete(key);
506
+ clientLocks.delete(key);
507
+ try {
508
+ killProcessTree(proc.pid ?? 0);
509
+ } catch {
510
+ proc.kill();
511
+ }
512
+ throw err;
513
+ } finally {
514
+ clientLocks.delete(key);
515
+ }
516
+ })();
517
+
518
+ clientLocks.set(key, clientPromise);
519
+ return clientPromise;
520
+ }
521
+
522
+ /**
523
+ * Ensure a file is opened in the LSP client.
524
+ */
525
+ export async function ensureFileOpen(client: LspClient, filePath: string, signal?: AbortSignal): Promise<void> {
526
+ throwIfAborted(signal);
527
+ const uri = fileToUri(filePath);
528
+ const lockKey = `${client.name}:${uri}`;
529
+
530
+ if (client.openFiles.has(uri)) {
531
+ return;
532
+ }
533
+
534
+ const existingLock = fileOperationLocks.get(lockKey);
535
+ if (existingLock) {
536
+ await untilAborted(signal, () => existingLock);
537
+ return;
538
+ }
539
+
540
+ const openPromise = (async () => {
541
+ throwIfAborted(signal);
542
+ if (client.openFiles.has(uri)) {
543
+ return;
544
+ }
545
+
546
+ let content: string;
547
+ try {
548
+ content = await fsPromises.readFile(filePath, "utf-8");
549
+ throwIfAborted(signal);
550
+ } catch (err: unknown) {
551
+ if (isEnoent(err)) return;
552
+ throw err;
553
+ }
554
+ const languageId = detectLanguageId(filePath);
555
+ throwIfAborted(signal);
556
+
557
+ await sendNotification(client, "textDocument/didOpen", {
558
+ textDocument: {
559
+ uri,
560
+ languageId,
561
+ version: 1,
562
+ text: content,
563
+ },
564
+ });
565
+
566
+ client.openFiles.set(uri, { version: 1, languageId });
567
+ client.lastActivity = Date.now();
568
+ })();
569
+
570
+ fileOperationLocks.set(lockKey, openPromise);
571
+ try {
572
+ await openPromise;
573
+ } finally {
574
+ fileOperationLocks.delete(lockKey);
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Sync in-memory content to the LSP client without reading from disk.
580
+ */
581
+ export async function syncContent(
582
+ client: LspClient,
583
+ filePath: string,
584
+ content: string,
585
+ signal?: AbortSignal,
586
+ ): Promise<void> {
587
+ const uri = fileToUri(filePath);
588
+ const lockKey = `${client.name}:${uri}`;
589
+ throwIfAborted(signal);
590
+
591
+ const existingLock = fileOperationLocks.get(lockKey);
592
+ if (existingLock) {
593
+ await untilAborted(signal, () => existingLock);
594
+ }
595
+
596
+ const syncPromise = (async () => {
597
+ client.diagnostics.delete(uri);
598
+
599
+ const info = client.openFiles.get(uri);
600
+
601
+ if (!info) {
602
+ const languageId = detectLanguageId(filePath);
603
+ throwIfAborted(signal);
604
+ await sendNotification(client, "textDocument/didOpen", {
605
+ textDocument: {
606
+ uri,
607
+ languageId,
608
+ version: 1,
609
+ text: content,
610
+ },
611
+ });
612
+ client.openFiles.set(uri, { version: 1, languageId });
613
+ client.lastActivity = Date.now();
614
+ return;
615
+ }
616
+
617
+ const version = ++info.version;
618
+ throwIfAborted(signal);
619
+ await sendNotification(client, "textDocument/didChange", {
620
+ textDocument: { uri, version },
621
+ contentChanges: [{ text: content }],
622
+ });
623
+ client.lastActivity = Date.now();
624
+ })();
625
+
626
+ fileOperationLocks.set(lockKey, syncPromise);
627
+ try {
628
+ await syncPromise;
629
+ } finally {
630
+ fileOperationLocks.delete(lockKey);
631
+ }
632
+ }
633
+
634
+ /**
635
+ * Notify LSP that a file was saved.
636
+ */
637
+ export async function notifySaved(client: LspClient, filePath: string, signal?: AbortSignal): Promise<void> {
638
+ const uri = fileToUri(filePath);
639
+ const info = client.openFiles.get(uri);
640
+ if (!info) return;
641
+
642
+ throwIfAborted(signal);
643
+ await sendNotification(client, "textDocument/didSave", {
644
+ textDocument: { uri },
645
+ });
646
+ client.lastActivity = Date.now();
647
+ }
648
+
649
+ /**
650
+ * Refresh a file in the LSP client.
651
+ */
652
+ export async function refreshFile(client: LspClient, filePath: string, signal?: AbortSignal): Promise<void> {
653
+ throwIfAborted(signal);
654
+ const uri = fileToUri(filePath);
655
+ const lockKey = `${client.name}:${uri}`;
656
+
657
+ const existingLock = fileOperationLocks.get(lockKey);
658
+ if (existingLock) {
659
+ await untilAborted(signal, () => existingLock);
660
+ }
661
+
662
+ const refreshPromise = (async () => {
663
+ throwIfAborted(signal);
664
+ const info = client.openFiles.get(uri);
665
+
666
+ if (!info) {
667
+ await ensureFileOpen(client, filePath, signal);
668
+ return;
669
+ }
670
+
671
+ let content: string;
672
+ try {
673
+ content = await fsPromises.readFile(filePath, "utf-8");
674
+ throwIfAborted(signal);
675
+ } catch (err: unknown) {
676
+ if (isEnoent(err)) return;
677
+ throw err;
678
+ }
679
+ const version = ++info.version;
680
+ throwIfAborted(signal);
681
+
682
+ await sendNotification(client, "textDocument/didChange", {
683
+ textDocument: { uri, version },
684
+ contentChanges: [{ text: content }],
685
+ });
686
+ throwIfAborted(signal);
687
+
688
+ await sendNotification(client, "textDocument/didSave", {
689
+ textDocument: { uri },
690
+ text: content,
691
+ });
692
+
693
+ client.lastActivity = Date.now();
694
+ })();
695
+
696
+ fileOperationLocks.set(lockKey, refreshPromise);
697
+ try {
698
+ await refreshPromise;
699
+ } finally {
700
+ fileOperationLocks.delete(lockKey);
701
+ }
702
+ }
703
+
704
+ /**
705
+ * Shutdown a specific client by key.
706
+ */
707
+ export function shutdownClient(key: string): void {
708
+ const client = clients.get(key);
709
+ if (!client) return;
710
+
711
+ for (const pending of Array.from(client.pendingRequests.values())) {
712
+ pending.reject(new Error("LSP client shutdown"));
713
+ }
714
+ client.pendingRequests.clear();
715
+
716
+ sendRequest(client, "shutdown", null).catch(() => {});
717
+
718
+ try {
719
+ killProcessTree(client.proc.pid);
720
+ } catch {
721
+ client.proc.kill();
722
+ }
723
+ clients.delete(key);
724
+ }
725
+
726
+ // =============================================================================
727
+ // LSP Protocol Methods
728
+ // =============================================================================
729
+
730
+ const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
731
+
732
+ export async function sendRequest(
733
+ client: LspClient,
734
+ method: string,
735
+ params: unknown,
736
+ signal?: AbortSignal,
737
+ timeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS,
738
+ ): Promise<unknown> {
739
+ const id = ++client.requestId;
740
+ if (signal?.aborted) {
741
+ const reason = signal.reason instanceof Error ? signal.reason : new ToolAbortError();
742
+ return Promise.reject(reason);
743
+ }
744
+
745
+ const request: LspJsonRpcRequest = {
746
+ jsonrpc: "2.0",
747
+ id,
748
+ method,
749
+ params,
750
+ };
751
+
752
+ client.lastActivity = Date.now();
753
+
754
+ const { promise, resolve, reject } = Promise.withResolvers<unknown>();
755
+ let timeout: NodeJS.Timeout | undefined;
756
+ const cleanup = () => {
757
+ if (signal) {
758
+ signal.removeEventListener("abort", abortHandler);
759
+ }
760
+ };
761
+ const abortHandler = () => {
762
+ if (client.pendingRequests.has(id)) {
763
+ client.pendingRequests.delete(id);
764
+ }
765
+ void sendNotification(client, "$/cancelRequest", { id }).catch(() => {});
766
+ if (timeout) clearTimeout(timeout);
767
+ cleanup();
768
+ const reason = signal?.reason instanceof Error ? signal.reason : new ToolAbortError();
769
+ reject(reason);
770
+ };
771
+
772
+ timeout = setTimeout(() => {
773
+ if (client.pendingRequests.has(id)) {
774
+ client.pendingRequests.delete(id);
775
+ const err = new Error(`LSP request ${method} timed out after ${timeoutMs}ms`);
776
+ cleanup();
777
+ reject(err);
778
+ }
779
+ }, timeoutMs);
780
+ if (signal) {
781
+ signal.addEventListener("abort", abortHandler, { once: true });
782
+ if (signal.aborted) {
783
+ abortHandler();
784
+ return promise;
785
+ }
786
+ }
787
+
788
+ client.pendingRequests.set(id, {
789
+ resolve: (result: unknown) => {
790
+ if (timeout) clearTimeout(timeout);
791
+ cleanup();
792
+ resolve(result);
793
+ },
794
+ reject: (err: Error) => {
795
+ if (timeout) clearTimeout(timeout);
796
+ cleanup();
797
+ reject(err);
798
+ },
799
+ method,
800
+ });
801
+
802
+ writeMessage(client.proc.stdin, request).catch((err: Error) => {
803
+ if (timeout) clearTimeout(timeout);
804
+ client.pendingRequests.delete(id);
805
+ cleanup();
806
+ reject(err);
807
+ });
808
+ return promise;
809
+ }
810
+
811
+ export async function sendNotification(client: LspClient, method: string, params: unknown): Promise<void> {
812
+ const notification: LspJsonRpcNotification = {
813
+ jsonrpc: "2.0",
814
+ method,
815
+ params,
816
+ };
817
+
818
+ client.lastActivity = Date.now();
819
+ await writeMessage(client.proc.stdin, notification);
820
+ }
821
+
822
+ /**
823
+ * Shutdown all LSP clients.
824
+ */
825
+ export function shutdownAll(): void {
826
+ const clientsToShutdown = Array.from(clients.values());
827
+ clients.clear();
828
+
829
+ const err = new Error("LSP client shutdown");
830
+ for (const client of clientsToShutdown) {
831
+ const reqs = Array.from(client.pendingRequests.values());
832
+ client.pendingRequests.clear();
833
+ for (const pending of reqs) {
834
+ pending.reject(err);
835
+ }
836
+
837
+ void (async () => {
838
+ const timeout = new Promise<void>(resolve => setTimeout(resolve, 5_000));
839
+ const result = sendRequest(client, "shutdown", null).catch(() => {});
840
+ await Promise.race([result, timeout]);
841
+ try {
842
+ killProcessTree(client.proc.pid);
843
+ } catch {
844
+ client.proc.kill();
845
+ }
846
+ })().catch(() => {});
847
+ }
848
+ }
849
+
850
+ /** Status of an LSP server */
851
+ export interface LspServerStatus {
852
+ name: string;
853
+ status: "connecting" | "ready" | "error";
854
+ fileTypes: string[];
855
+ error?: string;
856
+ }
857
+
858
+ export function getActiveClients(): LspServerStatus[] {
859
+ return Array.from(clients.values()).map(client => ({
860
+ name: client.config.command,
861
+ status: "ready" as const,
862
+ fileTypes: client.config.fileTypes,
863
+ }));
864
+ }
865
+
866
+ // =============================================================================
867
+ // Process Cleanup
868
+ // =============================================================================
869
+
870
+ if (typeof process !== "undefined") {
871
+ process.on("beforeExit", shutdownAll);
872
+ process.on("SIGINT", () => {
873
+ shutdownAll();
874
+ process.exit(0);
875
+ });
876
+ process.on("SIGTERM", () => {
877
+ shutdownAll();
878
+ process.exit(0);
879
+ });
880
+ }