oh-my-opencode 4.18.0 → 4.18.1

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 (192) hide show
  1. package/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3654 -0
  2. package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3071 -0
  3. package/.agents/skills/work-with-pr/SKILL.md +16 -37
  4. package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
  5. package/.opencode/skills/work-with-pr/SKILL.md +16 -37
  6. package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
  7. package/dist/cli/index.js +457 -154
  8. package/dist/cli-node/index.js +457 -154
  9. package/dist/index.js +415 -388
  10. package/package.json +16 -16
  11. package/packages/lsp-core/package.json +4 -0
  12. package/packages/lsp-core/src/index.ts +1 -0
  13. package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
  14. package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
  15. package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
  16. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
  17. package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
  18. package/packages/lsp-core/src/lsp/client.ts +262 -80
  19. package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
  20. package/packages/lsp-core/src/lsp/connection.ts +12 -6
  21. package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +104 -0
  22. package/packages/lsp-core/src/lsp/directory-diagnostics.ts +60 -27
  23. package/packages/lsp-core/src/lsp/errors.ts +11 -0
  24. package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
  25. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
  26. package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
  27. package/packages/lsp-core/src/lsp/formatters.ts +3 -0
  28. package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
  29. package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
  30. package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
  31. package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
  32. package/packages/lsp-core/src/lsp/transport.ts +96 -70
  33. package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
  34. package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
  35. package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
  36. package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
  37. package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
  38. package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
  39. package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
  40. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
  41. package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
  42. package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
  43. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
  44. package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
  45. package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
  46. package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
  47. package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
  48. package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
  49. package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
  50. package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
  51. package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
  52. package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
  53. package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
  54. package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
  55. package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
  56. package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
  57. package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
  58. package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
  59. package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
  60. package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
  61. package/packages/lsp-core/src/mcp.ts +18 -7
  62. package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
  63. package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
  64. package/packages/lsp-core/src/post-edit/index.ts +1 -0
  65. package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
  66. package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
  67. package/packages/lsp-core/src/request-context.test.ts +171 -0
  68. package/packages/lsp-core/src/request-context.ts +222 -9
  69. package/packages/lsp-core/src/tool-surface.test.ts +4 -1
  70. package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
  71. package/packages/lsp-core/src/tools/navigation.ts +12 -12
  72. package/packages/lsp-core/src/tools/rename.ts +10 -15
  73. package/packages/lsp-core/src/tools/symbols.ts +11 -11
  74. package/packages/lsp-core/src/tools/types.ts +2 -1
  75. package/packages/lsp-daemon/dist/cli.js +3114 -747
  76. package/packages/lsp-daemon/dist/client.d.ts +105 -0
  77. package/packages/lsp-daemon/dist/client.js +5851 -0
  78. package/packages/lsp-daemon/dist/daemon-client.d.ts +11 -6
  79. package/packages/lsp-daemon/dist/daemon-client.js +113 -30
  80. package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
  81. package/packages/lsp-daemon/dist/daemon-server.js +40 -15
  82. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +8 -7
  83. package/packages/lsp-daemon/dist/ensure-daemon.js +67 -44
  84. package/packages/lsp-daemon/dist/index.d.ts +2 -2
  85. package/packages/lsp-daemon/dist/index.js +2862 -754
  86. package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
  87. package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
  88. package/packages/lsp-daemon/dist/lock.js +14 -4
  89. package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
  90. package/packages/lsp-daemon/dist/ownership.js +168 -0
  91. package/packages/lsp-daemon/dist/paths.d.ts +33 -9
  92. package/packages/lsp-daemon/dist/paths.js +72 -33
  93. package/packages/lsp-daemon/dist/proxy.d.ts +3 -0
  94. package/packages/lsp-daemon/dist/proxy.js +54 -3
  95. package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
  96. package/packages/lsp-daemon/dist/request-routing.js +71 -22
  97. package/packages/lsp-daemon/dist/run-daemon.js +9 -2
  98. package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
  99. package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
  100. package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
  101. package/packages/lsp-daemon/package.json +12 -3
  102. package/packages/lsp-tools-mcp/dist/cli.js +2115 -442
  103. package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
  104. package/packages/lsp-tools-mcp/dist/mcp.js +2127 -454
  105. package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
  106. package/packages/lsp-tools-mcp/dist/tools.js +2118 -446
  107. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  108. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  109. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  110. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  111. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  112. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  113. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  114. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  115. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  116. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  117. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
  118. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +2959 -944
  119. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
  120. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
  121. package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
  122. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
  123. package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
  124. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
  125. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
  126. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  127. package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
  128. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
  129. package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
  130. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
  131. package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
  132. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
  133. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
  134. package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
  135. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
  136. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
  137. package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
  138. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +19 -5
  139. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
  140. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +1 -1
  141. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  142. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  143. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
  144. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  145. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  146. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  147. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  148. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  149. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  150. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  151. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  152. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  153. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  154. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
  155. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
  156. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  157. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  158. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  159. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  160. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  161. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  162. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  163. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  164. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  165. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  166. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  167. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  168. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  169. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  170. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  171. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  172. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  173. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  174. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  175. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  176. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  177. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  178. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  179. package/packages/omo-codex/plugin/package-lock.json +26 -14
  180. package/packages/omo-codex/plugin/package.json +1 -1
  181. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
  182. package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
  183. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
  184. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
  185. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
  186. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
  187. package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
  188. package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
  189. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
  190. package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
  191. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
  192. package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
@@ -1,31 +1,16 @@
1
1
  // ../lsp-core/src/lsp/cleanup-errors.ts
2
- function reportBestEffortCleanupError(operation, error) {
3
- if (process.env["CODEX_LSP_DEBUG_CLEANUP"] !== "1")
4
- return;
2
+ function writeCleanupError(message) {
3
+ process.stderr.write(`${message}
4
+ `);
5
+ }
6
+ function reportBestEffortCleanupError(operation, error, logger = writeCleanupError) {
5
7
  const message = error instanceof Error ? error.message : String(error);
6
- console.error(`[codex-lsp] ignored ${operation} failure during cleanup: ${message}`);
8
+ logger(`[lsp] ignored ${operation} failure during cleanup: ${message}`);
7
9
  }
8
10
 
9
11
  // ../lsp-core/src/lsp/client.ts
10
- import { readFileSync } from "node:fs";
11
- import { resolve } from "node:path";
12
- import { pathToFileURL as pathToFileURL2 } from "node:url";
13
-
14
- // ../lsp-core/src/request-context.ts
15
- import { AsyncLocalStorage } from "node:async_hooks";
16
- var storage = new AsyncLocalStorage;
17
- function runWithRequestContext(context, fn) {
18
- return storage.run(context, fn);
19
- }
20
- function contextCwd() {
21
- return storage.getStore()?.cwd ?? process.cwd();
22
- }
23
- function contextEnv(key) {
24
- const store = storage.getStore();
25
- if (store?.env)
26
- return store.env[key];
27
- return process.env[key];
28
- }
12
+ import { resolve as resolve5 } from "node:path";
13
+ import { pathToFileURL as pathToFileURL3 } from "node:url";
29
14
 
30
15
  // ../lsp-core/src/lsp/connection.ts
31
16
  import { pathToFileURL } from "node:url";
@@ -89,7 +74,12 @@ class LspInvalidPathError extends Error {
89
74
  }
90
75
 
91
76
  class LspServerLookupError extends Error {
77
+ lookup;
92
78
  name = "LspServerLookupError";
79
+ constructor(message, lookup) {
80
+ super(message);
81
+ this.lookup = lookup;
82
+ }
93
83
  }
94
84
 
95
85
  class LspServerInitializingError extends Error {
@@ -155,28 +145,80 @@ class JsonRpcConnection {
155
145
  onError(handler) {
156
146
  this.errorHandlers.push(handler);
157
147
  }
158
- async sendRequest(method, params) {
148
+ async sendRequest(method, params, options = {}) {
159
149
  if (this.disposed)
160
150
  throw new Error("JSON-RPC connection is disposed");
161
151
  const id = this.nextRequestId;
162
152
  this.nextRequestId += 1;
153
+ const key = String(id);
163
154
  const message = params === undefined ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
155
+ let requestWritten = false;
156
+ let cancelAfterWrite = false;
157
+ let settled = false;
158
+ const writeCancel = () => this.writeMessage({ jsonrpc: "2.0", method: "$/cancelRequest", params: { id } });
164
159
  const responsePromise = new Promise((resolve, reject) => {
165
- this.pendingRequests.set(String(id), {
160
+ const cleanup = () => {
161
+ options.signal?.removeEventListener("abort", onAbort);
162
+ };
163
+ const settleCancel = () => {
164
+ if (settled)
165
+ return;
166
+ settled = true;
167
+ this.pendingRequests.delete(key);
168
+ cleanup();
169
+ const rejectCancelled = () => reject(abortError(options.signal));
170
+ if (!requestWritten) {
171
+ cancelAfterWrite = true;
172
+ rejectCancelled();
173
+ return;
174
+ }
175
+ writeCancel().then(rejectCancelled, (error) => {
176
+ this.emitError(toError(error));
177
+ rejectCancelled();
178
+ });
179
+ };
180
+ const onAbort = () => settleCancel();
181
+ this.pendingRequests.set(key, {
166
182
  resolve(result) {
183
+ settled = true;
184
+ cleanup();
167
185
  resolve(result);
168
186
  },
169
- reject
187
+ reject(error) {
188
+ settled = true;
189
+ cleanup();
190
+ reject(error);
191
+ },
192
+ cleanup
170
193
  });
194
+ if (options.signal?.aborted) {
195
+ settleCancel();
196
+ return;
197
+ }
198
+ options.signal?.addEventListener("abort", onAbort, { once: true });
171
199
  });
200
+ if (settled)
201
+ return responsePromise;
172
202
  try {
173
203
  await this.writeMessage(message);
204
+ requestWritten = true;
205
+ if (cancelAfterWrite)
206
+ await writeCancel();
174
207
  } catch (error) {
175
- this.pendingRequests.delete(String(id));
208
+ if (settled)
209
+ return responsePromise;
210
+ const pending = this.pendingRequests.get(key);
211
+ if (pending) {
212
+ pending.cleanup();
213
+ this.pendingRequests.delete(key);
214
+ }
176
215
  throw error;
177
216
  }
178
217
  return responsePromise;
179
218
  }
219
+ pendingRequestCount() {
220
+ return this.pendingRequests.size;
221
+ }
180
222
  async sendNotification(method, params) {
181
223
  if (this.disposed)
182
224
  return;
@@ -193,6 +235,7 @@ class JsonRpcConnection {
193
235
  this.reader.off("error", this.handleStreamError);
194
236
  this.writer.off("error", this.handleStreamError);
195
237
  for (const pending of this.pendingRequests.values()) {
238
+ pending.cleanup();
196
239
  pending.reject(new Error("JSON-RPC connection disposed"));
197
240
  }
198
241
  this.pendingRequests.clear();
@@ -268,6 +311,7 @@ class JsonRpcConnection {
268
311
  if (!pending)
269
312
  return;
270
313
  this.pendingRequests.delete(String(id));
314
+ pending.cleanup();
271
315
  if ("error" in message) {
272
316
  pending.reject(jsonRpcErrorToError(message["error"]));
273
317
  return;
@@ -281,7 +325,11 @@ class JsonRpcConnection {
281
325
  try {
282
326
  handler(params);
283
327
  } catch (error) {
284
- this.emitError(toError(error));
328
+ if (error instanceof Error) {
329
+ this.emitError(error);
330
+ return;
331
+ }
332
+ this.emitError(new Error(String(error)));
285
333
  }
286
334
  }
287
335
  handleRequest(message) {
@@ -322,6 +370,14 @@ ${body}`;
322
370
  }
323
371
  }
324
372
  }
373
+ function abortError(signal) {
374
+ const reason = signal?.reason;
375
+ if (reason instanceof Error)
376
+ return reason;
377
+ const error = new Error(typeof reason === "string" ? reason : "LSP request cancelled");
378
+ error.name = "AbortError";
379
+ return error;
380
+ }
325
381
  function parseContentLength(headers) {
326
382
  for (const line of headers.split(`\r
327
383
  `)) {
@@ -508,7 +564,7 @@ function spawnProcess(command, options) {
508
564
  return wrap(proc);
509
565
  }
510
566
 
511
- // ../lsp-core/src/lsp/transport.ts
567
+ // ../lsp-core/src/lsp/transport-protocol.ts
512
568
  function isRecord(value) {
513
569
  return typeof value === "object" && value !== null && !Array.isArray(value);
514
570
  }
@@ -528,7 +584,32 @@ function parseDiagnosticsParams(params) {
528
584
  if (!isRecord(params) || typeof params["uri"] !== "string")
529
585
  return null;
530
586
  const diagnostics = Array.isArray(params["diagnostics"]) ? params["diagnostics"].filter(isDiagnostic) : [];
531
- return { uri: params["uri"], diagnostics };
587
+ const version = typeof params["version"] === "number" ? params["version"] : undefined;
588
+ return { uri: params["uri"], diagnostics, ...version === undefined ? {} : { version } };
589
+ }
590
+ function createLspSpawnEnv(_root, input) {
591
+ return { ...input };
592
+ }
593
+ function isDiagnostic(value) {
594
+ return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
595
+ }
596
+ function isRange(value) {
597
+ return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
598
+ }
599
+ function isPosition(value) {
600
+ return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
601
+ }
602
+
603
+ // ../lsp-core/src/lsp/transport.ts
604
+ class LspClientNotStartedError extends Error {
605
+ serverId;
606
+ root;
607
+ name = "LspClientNotStartedError";
608
+ constructor(serverId, root) {
609
+ super("LSP client not started");
610
+ this.serverId = serverId;
611
+ this.root = root;
612
+ }
532
613
  }
533
614
 
534
615
  class LspClientTransport {
@@ -541,6 +622,8 @@ class LspClientTransport {
541
622
  diagnosticsStore = new Map;
542
623
  requestTimeoutMs;
543
624
  initializeTimeoutMs;
625
+ workspaceApplyEditHandler = null;
626
+ diagnosticPullSupported = false;
544
627
  constructor(root, server, timeouts = {}) {
545
628
  this.root = root;
546
629
  this.server = server;
@@ -553,6 +636,21 @@ class LspClientTransport {
553
636
  command() {
554
637
  return [...this.server.command];
555
638
  }
639
+ setWorkspaceApplyEditHandler(handler) {
640
+ this.workspaceApplyEditHandler = handler;
641
+ }
642
+ hasWorkspaceApplyEditHandler() {
643
+ return this.workspaceApplyEditHandler !== null;
644
+ }
645
+ setDiagnosticPullSupported(supported) {
646
+ this.diagnosticPullSupported = supported;
647
+ }
648
+ isDiagnosticPullSupported() {
649
+ return this.diagnosticPullSupported;
650
+ }
651
+ handlePublishDiagnostics(params) {
652
+ this.diagnosticsStore.set(params.uri, [...params.diagnostics]);
653
+ }
556
654
  async start() {
557
655
  const env = createLspSpawnEnv(this.root, {
558
656
  ...process.env,
@@ -563,7 +661,6 @@ class LspClientTransport {
563
661
  env
564
662
  });
565
663
  this.startStderrReading();
566
- await new Promise((resolve) => setTimeout(resolve, 100));
567
664
  if (this.proc.exitCode !== null) {
568
665
  const stderr = this.stderrBuffer.join(`
569
666
  `);
@@ -573,7 +670,7 @@ class LspClientTransport {
573
670
  this.connection.onNotification("textDocument/publishDiagnostics", (params) => {
574
671
  const diagnosticsParams = parseDiagnosticsParams(params);
575
672
  if (diagnosticsParams?.uri) {
576
- this.diagnosticsStore.set(diagnosticsParams.uri, diagnosticsParams.diagnostics);
673
+ this.handlePublishDiagnostics(diagnosticsParams);
577
674
  }
578
675
  });
579
676
  this.connection.onRequest("workspace/configuration", (params) => {
@@ -586,6 +683,9 @@ class LspClientTransport {
586
683
  });
587
684
  this.connection.onRequest("client/registerCapability", () => null);
588
685
  this.connection.onRequest("window/workDoneProgress/create", () => null);
686
+ if (this.workspaceApplyEditHandler) {
687
+ this.connection.onRequest("workspace/applyEdit", this.workspaceApplyEditHandler);
688
+ }
589
689
  this.connection.onClose(() => {
590
690
  this.processExited = true;
591
691
  });
@@ -614,30 +714,25 @@ class LspClientTransport {
614
714
  }
615
715
  async sendRequest(method, ...args) {
616
716
  if (!this.connection)
617
- throw new Error("LSP client not started");
717
+ throw new LspClientNotStartedError(this.server.id, this.root);
618
718
  if (this.processExited || this.proc && this.proc.exitCode !== null) {
619
719
  const stderrTail = this.stderrBuffer.slice(-10).join(`
620
720
  `);
621
721
  throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, stderrTail || undefined);
622
722
  }
623
- const timeoutMs = args[1]?.timeoutMs ?? this.requestTimeoutMs;
624
- let timeoutHandle = null;
625
- const timeoutPromise = new Promise((_, reject) => {
626
- timeoutHandle = setTimeout(() => {
627
- const stderrTail = this.stderrBuffer.slice(-5).join(`
723
+ const options = args[1];
724
+ const timeoutMs = options?.timeoutMs ?? this.requestTimeoutMs;
725
+ const timeoutController = new AbortController;
726
+ const timeoutHandle = setTimeout(() => {
727
+ const stderrTail = this.stderrBuffer.slice(-5).join(`
628
728
  `);
629
- reject(new LspRequestTimeoutError(method, stderrTail || undefined));
630
- }, timeoutMs);
631
- });
729
+ timeoutController.abort(new LspRequestTimeoutError(method, stderrTail || undefined));
730
+ }, timeoutMs);
731
+ const combinedSignal = combineAbortSignals(options?.signal, timeoutController.signal);
632
732
  try {
633
- const requestPromise = args.length === 0 ? this.connection.sendRequest(method) : this.connection.sendRequest(method, args[0]);
634
- const result = await Promise.race([requestPromise, timeoutPromise]);
635
- if (timeoutHandle !== null)
636
- clearTimeout(timeoutHandle);
733
+ const result = args.length === 0 ? await this.connection.sendRequest(method, undefined, { signal: combinedSignal.signal }) : await this.connection.sendRequest(method, args[0], { signal: combinedSignal.signal });
637
734
  return result;
638
735
  } catch (error) {
639
- if (timeoutHandle !== null)
640
- clearTimeout(timeoutHandle);
641
736
  if (this.processExited || this.proc && this.proc.exitCode !== null) {
642
737
  throw new LspProcessExitedError(this.server.id, this.root, this.proc?.exitCode ?? null, this.stderrBuffer.slice(-10).join(`
643
738
  `) || undefined);
@@ -646,6 +741,9 @@ class LspClientTransport {
646
741
  throw new LspConnectionClosedError(this.server.id, this.root, error.message);
647
742
  }
648
743
  throw error;
744
+ } finally {
745
+ clearTimeout(timeoutHandle);
746
+ combinedSignal.dispose();
649
747
  }
650
748
  }
651
749
  async sendNotification(method, ...args) {
@@ -674,17 +772,17 @@ class LspClientTransport {
674
772
  try {
675
773
  await this.sendRequest("shutdown");
676
774
  } catch (error) {
677
- reportBestEffortCleanupError("shutdown request", error);
775
+ reportBestEffortCleanupError("shutdown request", error instanceof Error ? error : String(error));
678
776
  }
679
777
  try {
680
778
  await this.sendNotification("exit");
681
779
  } catch (error) {
682
- reportBestEffortCleanupError("exit notification", error);
780
+ reportBestEffortCleanupError("exit notification", error instanceof Error ? error : String(error));
683
781
  }
684
782
  try {
685
783
  this.connection.dispose();
686
784
  } catch (error) {
687
- reportBestEffortCleanupError("connection dispose", error);
785
+ reportBestEffortCleanupError("connection dispose", error instanceof Error ? error : String(error));
688
786
  }
689
787
  this.connection = null;
690
788
  }
@@ -715,11 +813,11 @@ class LspClientTransport {
715
813
  new Promise((resolve) => setTimeout(resolve, STOP_SIGKILL_GRACE_MS))
716
814
  ]);
717
815
  } catch (error) {
718
- reportBestEffortCleanupError("hard process kill", error);
816
+ reportBestEffortCleanupError("hard process kill", error instanceof Error ? error : String(error));
719
817
  }
720
818
  }
721
819
  } catch (error) {
722
- reportBestEffortCleanupError("process stop", error);
820
+ reportBestEffortCleanupError("process stop", error instanceof Error ? error : String(error));
723
821
  }
724
822
  }
725
823
  this.processExited = true;
@@ -729,26 +827,45 @@ class LspClientTransport {
729
827
  return this.diagnosticsStore.get(uri) ?? [];
730
828
  }
731
829
  }
732
- function createLspSpawnEnv(_root, input) {
733
- return { ...input };
734
- }
735
- function isDiagnostic(value) {
736
- return isRecord(value) && isRange(value["range"]) && typeof value["message"] === "string";
737
- }
738
- function isRange(value) {
739
- return isRecord(value) && isPosition(value["start"]) && isPosition(value["end"]);
740
- }
741
- function isPosition(value) {
742
- return isRecord(value) && typeof value["line"] === "number" && typeof value["character"] === "number";
830
+ function combineAbortSignals(primary, secondary) {
831
+ const controller = new AbortController;
832
+ const abortFrom = (signal) => {
833
+ if (!controller.signal.aborted)
834
+ controller.abort(signal.reason);
835
+ };
836
+ const onPrimaryAbort = () => {
837
+ if (primary)
838
+ abortFrom(primary);
839
+ };
840
+ const onSecondaryAbort = () => abortFrom(secondary);
841
+ if (primary?.aborted)
842
+ abortFrom(primary);
843
+ else
844
+ primary?.addEventListener("abort", onPrimaryAbort, { once: true });
845
+ if (secondary.aborted)
846
+ abortFrom(secondary);
847
+ else
848
+ secondary.addEventListener("abort", onSecondaryAbort, { once: true });
849
+ return {
850
+ signal: controller.signal,
851
+ dispose: () => {
852
+ primary?.removeEventListener("abort", onPrimaryAbort);
853
+ secondary.removeEventListener("abort", onSecondaryAbort);
854
+ }
855
+ };
743
856
  }
744
857
 
745
858
  // ../lsp-core/src/lsp/connection.ts
746
- var INITIALIZE_SETTLE_MS = 300;
859
+ function supportsDiagnosticPull(capabilities) {
860
+ if (capabilities === undefined)
861
+ return false;
862
+ return Object.hasOwn(capabilities, "diagnosticProvider");
863
+ }
747
864
 
748
865
  class LspClientConnection extends LspClientTransport {
749
866
  async initialize() {
750
867
  const rootUri = pathToFileURL(this.root).href;
751
- await this.sendRequest("initialize", {
868
+ const result = await this.sendRequest("initialize", {
752
869
  processId: process.pid,
753
870
  rootUri,
754
871
  rootPath: this.root,
@@ -762,8 +879,7 @@ class LspClientConnection extends LspClientTransport {
762
879
  publishDiagnostics: {},
763
880
  rename: {
764
881
  prepareSupport: true,
765
- prepareSupportDefaultBehavior: 1,
766
- honorsChangeAnnotations: true
882
+ prepareSupportDefaultBehavior: 1
767
883
  },
768
884
  codeAction: {
769
885
  codeActionLiteralSupport: {
@@ -792,22 +908,28 @@ class LspClientConnection extends LspClientTransport {
792
908
  symbol: {},
793
909
  workspaceFolders: true,
794
910
  configuration: true,
795
- applyEdit: true,
911
+ ...this.hasWorkspaceApplyEditHandler() ? { applyEdit: true } : {},
796
912
  workspaceEdit: {
797
- documentChanges: true
913
+ documentChanges: true,
914
+ resourceOperations: ["create", "rename", "delete"]
798
915
  }
799
916
  }
800
917
  },
801
918
  initializationOptions: this.server.initialization
802
919
  }, { timeoutMs: this.initializeTimeoutMs });
920
+ this.setDiagnosticPullSupported(supportsDiagnosticPull(result?.capabilities));
803
921
  await this.sendNotification("initialized");
804
922
  await this.sendNotification("workspace/didChangeConfiguration", {
805
923
  settings: { json: { validate: { enable: true } } }
806
924
  });
807
- await new Promise((r) => setTimeout(r, INITIALIZE_SETTLE_MS));
808
925
  }
809
926
  }
810
927
 
928
+ // ../lsp-core/src/lsp/workspace-document-state.ts
929
+ import { readFileSync, realpathSync } from "node:fs";
930
+ import { relative, resolve } from "node:path";
931
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
932
+
811
933
  // ../lsp-core/src/lsp/effective-extension.ts
812
934
  import { basename, extname } from "node:path";
813
935
  var BASENAME_EXTENSIONS = {
@@ -990,191 +1112,1662 @@ function getLanguageId(ext) {
990
1112
  return EXT_TO_LANG[ext] ?? "plaintext";
991
1113
  }
992
1114
 
993
- // ../lsp-core/src/lsp/client.ts
994
- var POST_OPEN_DELAY_MS = 1000;
995
- var POST_DIAGNOSTICS_WAIT_MS = 500;
1115
+ // ../lsp-core/src/lsp/workspace-document-state.ts
1116
+ var WATCHED_FILE_BATCH_SIZE = 128;
1117
+ var DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
1118
+ function canonicalPath(filePath) {
1119
+ const absolute = resolve(filePath);
1120
+ try {
1121
+ return realpathSync(absolute);
1122
+ } catch {
1123
+ return absolute;
1124
+ }
1125
+ }
1126
+ function isSameOrDescendant(candidate, parent) {
1127
+ const suffix = relative(parent, candidate);
1128
+ return suffix === "" || !suffix.startsWith("..") && suffix !== "..";
1129
+ }
1130
+ function movedPath(candidate, oldPath, newPath) {
1131
+ const suffix = relative(oldPath, candidate);
1132
+ return suffix === "" ? newPath : resolve(newPath, suffix);
1133
+ }
996
1134
 
997
- class LspClient extends LspClientConnection {
998
- openedFiles = new Set;
999
- documentVersions = new Map;
1000
- lastSyncedText = new Map;
1001
- diagnosticPullErrors = [];
1002
- getDiagnosticPullErrors() {
1003
- return this.diagnosticPullErrors;
1135
+ class WorkspaceDocumentState {
1136
+ sendNotification;
1137
+ clearDiagnostics;
1138
+ openDocuments = new Map;
1139
+ openByUri = new Map;
1140
+ openPromises = new Map;
1141
+ now;
1142
+ versionlessPublishQuiescenceMs;
1143
+ constructor(sendNotification, clearDiagnostics, options = {}) {
1144
+ this.sendNotification = sendNotification;
1145
+ this.clearDiagnostics = clearDiagnostics;
1146
+ this.now = options.now ?? (() => Date.now());
1147
+ this.versionlessPublishQuiescenceMs = options.versionlessPublishQuiescenceMs ?? DEFAULT_VERSIONLESS_PUBLISH_QUIESCENCE_MS;
1004
1148
  }
1005
1149
  async openFile(filePath) {
1006
- const absPath = resolve(contextCwd(), filePath);
1007
- const uri = pathToFileURL2(absPath).href;
1008
- const text = readFileSync(absPath, "utf-8");
1009
- if (!this.openedFiles.has(absPath)) {
1010
- const ext = effectiveExtension(absPath);
1011
- const languageId = getLanguageId(ext);
1012
- const version = 1;
1013
- await this.sendNotification("textDocument/didOpen", {
1014
- textDocument: {
1015
- uri,
1016
- languageId,
1017
- version,
1018
- text
1019
- }
1020
- });
1021
- this.openedFiles.add(absPath);
1022
- this.documentVersions.set(uri, version);
1023
- this.lastSyncedText.set(uri, text);
1024
- await new Promise((r) => setTimeout(r, POST_OPEN_DELAY_MS));
1025
- return;
1150
+ const path = canonicalPath(filePath);
1151
+ const existingOpen = this.openPromises.get(path);
1152
+ if (existingOpen) {
1153
+ await existingOpen;
1154
+ return this.openFile(path);
1026
1155
  }
1027
- const prevText = this.lastSyncedText.get(uri);
1028
- if (prevText === text) {
1156
+ const text = readFileSync(path, "utf-8");
1157
+ const existing = this.openDocuments.get(path);
1158
+ if (!existing)
1159
+ return this.openDocumentSingleFlight(path, text);
1160
+ if (existing.text === text)
1029
1161
  return;
1030
- }
1031
- const nextVersion = (this.documentVersions.get(uri) ?? 1) + 1;
1032
- this.documentVersions.set(uri, nextVersion);
1033
- this.lastSyncedText.set(uri, text);
1034
- await this.sendNotification("textDocument/didChange", {
1035
- textDocument: { uri, version: nextVersion },
1036
- contentChanges: [{ text }]
1037
- });
1038
- await this.sendNotification("textDocument/didSave", {
1039
- textDocument: { uri },
1040
- text
1041
- });
1162
+ await this.changeDocument(existing, text);
1042
1163
  }
1043
- async definition(filePath, line, character) {
1044
- const absPath = resolve(contextCwd(), filePath);
1045
- await this.openFile(absPath);
1046
- return this.sendRequest("textDocument/definition", {
1047
- textDocument: { uri: pathToFileURL2(absPath).href },
1048
- position: { line: line - 1, character }
1049
- });
1164
+ getVersion(filePath) {
1165
+ return this.openDocuments.get(canonicalPath(filePath))?.version;
1050
1166
  }
1051
- async references(filePath, line, character, includeDeclaration = true) {
1052
- const absPath = resolve(contextCwd(), filePath);
1053
- await this.openFile(absPath);
1054
- return this.sendRequest("textDocument/references", {
1055
- textDocument: { uri: pathToFileURL2(absPath).href },
1056
- position: { line: line - 1, character },
1057
- context: { includeDeclaration }
1058
- });
1167
+ getStoredDiagnostics(uri) {
1168
+ const state = this.openByUri.get(uri);
1169
+ if (!state)
1170
+ return [];
1171
+ return state.lastPublish?.diagnostics ?? state.pullCache?.diagnostics ?? [];
1172
+ }
1173
+ captureDiagnosticSnapshot(filePath) {
1174
+ const state = this.openDocuments.get(canonicalPath(filePath));
1175
+ if (!state)
1176
+ return null;
1177
+ return {
1178
+ path: state.path,
1179
+ uri: state.uri,
1180
+ version: state.version,
1181
+ documentGeneration: state.generation,
1182
+ publishGeneration: state.publishGeneration
1183
+ };
1059
1184
  }
1060
- async documentSymbols(filePath) {
1061
- const absPath = resolve(contextCwd(), filePath);
1062
- await this.openFile(absPath);
1063
- return this.sendRequest("textDocument/documentSymbol", {
1064
- textDocument: { uri: pathToFileURL2(absPath).href }
1065
- });
1185
+ isCurrentSnapshot(snapshot) {
1186
+ const state = this.openDocuments.get(snapshot.path);
1187
+ return state !== undefined && state.uri === snapshot.uri && state.version === snapshot.version && state.generation === snapshot.documentGeneration;
1066
1188
  }
1067
- async workspaceSymbols(query) {
1068
- return this.sendRequest("workspace/symbol", { query });
1189
+ getPullCache(snapshot) {
1190
+ const state = this.openByUri.get(snapshot.uri);
1191
+ if (!state?.pullCache || state.pullCache.documentVersion !== snapshot.version)
1192
+ return null;
1193
+ return state.pullCache;
1069
1194
  }
1070
- isUnsupportedDiagnosticPullError(error) {
1071
- if (!(error instanceof Error))
1072
- return false;
1073
- const code = "code" in error && typeof error.code === "number" ? error.code : undefined;
1074
- if (code === -32601)
1075
- return true;
1076
- return /unsupported|not supported|method not found|unknown request/i.test(error.message);
1195
+ recordPullDiagnostics(snapshot, report) {
1196
+ const state = this.openByUri.get(snapshot.uri);
1197
+ if (!state)
1198
+ return;
1199
+ state.pullCache = {
1200
+ documentVersion: snapshot.version,
1201
+ diagnostics: [...report.diagnostics],
1202
+ ...report.resultId === undefined ? {} : { resultId: report.resultId }
1203
+ };
1077
1204
  }
1078
- async diagnostics(filePath) {
1079
- const absPath = resolve(contextCwd(), filePath);
1080
- const uri = pathToFileURL2(absPath).href;
1081
- await this.openFile(absPath);
1082
- await new Promise((r) => setTimeout(r, POST_DIAGNOSTICS_WAIT_MS));
1083
- try {
1084
- const result = await this.sendRequest("textDocument/diagnostic", {
1085
- textDocument: { uri }
1086
- });
1087
- if (result.items) {
1088
- return { items: result.items };
1205
+ recordPublishedDiagnostics(params) {
1206
+ const state = this.openByUri.get(params.uri);
1207
+ if (!state)
1208
+ return;
1209
+ state.publishGeneration += 1;
1210
+ state.lastPublish = {
1211
+ diagnostics: [...params.diagnostics],
1212
+ publishGeneration: state.publishGeneration,
1213
+ documentGenerationAtArrival: state.generation,
1214
+ arrivedAt: this.now(),
1215
+ ...params.version === undefined ? {} : { version: params.version }
1216
+ };
1217
+ this.notifyWaiters(state);
1218
+ }
1219
+ resolvePushDiagnostics(snapshot) {
1220
+ const state = this.openByUri.get(snapshot.uri);
1221
+ if (!state?.lastPublish)
1222
+ return { status: "missing" };
1223
+ const publish = state.lastPublish;
1224
+ if (publish.version !== undefined) {
1225
+ return publish.version === snapshot.version ? { status: "ready", diagnostics: publish.diagnostics } : { status: "missing" };
1226
+ }
1227
+ if (publish.documentGenerationAtArrival < snapshot.documentGeneration)
1228
+ return { status: "missing" };
1229
+ const readyAt = publish.arrivedAt + this.versionlessPublishQuiescenceMs;
1230
+ const waitMs = Math.max(0, readyAt - this.now());
1231
+ return waitMs === 0 ? { status: "ready", diagnostics: publish.diagnostics } : { status: "wait", waitMs };
1232
+ }
1233
+ waitForDiagnosticsActivity(snapshot, timeoutMs) {
1234
+ const state = this.openByUri.get(snapshot.uri);
1235
+ if (!state || timeoutMs <= 0)
1236
+ return Promise.resolve();
1237
+ return new Promise((resolveActivity) => {
1238
+ let settled = false;
1239
+ const finish = () => {
1240
+ if (settled)
1241
+ return;
1242
+ settled = true;
1243
+ clearTimeout(timer);
1244
+ state.waiters.delete(finish);
1245
+ resolveActivity();
1246
+ };
1247
+ const timer = setTimeout(finish, timeoutMs);
1248
+ if (typeof timer.unref === "function")
1249
+ timer.unref();
1250
+ state.waiters.add(finish);
1251
+ });
1252
+ }
1253
+ validateVersions(operations) {
1254
+ const versions = new Map([...this.openDocuments].map(([path, state]) => [path, state.version]));
1255
+ for (const operation of operations) {
1256
+ if (operation.kind === "text") {
1257
+ const current = versions.get(operation.path);
1258
+ if (operation.documentVersion !== null && current !== operation.documentVersion) {
1259
+ const observed = current === undefined ? "closed document" : `open document version ${current}`;
1260
+ return {
1261
+ changeIndex: operation.changeIndex,
1262
+ message: `document version ${operation.documentVersion} does not match ${observed} for ${operation.path}`
1263
+ };
1264
+ }
1265
+ if (current !== undefined)
1266
+ versions.set(operation.path, current + 1);
1267
+ continue;
1089
1268
  }
1090
- } catch (error) {
1091
- if (!this.isUnsupportedDiagnosticPullError(error)) {
1092
- this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
1269
+ if (operation.kind === "rename") {
1270
+ const moved = [...versions].filter(([path]) => isSameOrDescendant(path, operation.oldPath));
1271
+ for (const [path] of moved)
1272
+ versions.delete(path);
1273
+ for (const [path] of moved)
1274
+ versions.set(movedPath(path, operation.oldPath, operation.newPath), 1);
1275
+ continue;
1276
+ }
1277
+ if (operation.kind === "delete") {
1278
+ for (const path of [...versions.keys()]) {
1279
+ if (isSameOrDescendant(path, operation.path))
1280
+ versions.delete(path);
1281
+ }
1282
+ continue;
1283
+ }
1284
+ if (operation.kind === "create" && operation.replaced && versions.has(operation.path)) {
1285
+ versions.set(operation.path, 1);
1093
1286
  }
1094
1287
  }
1095
- return { items: this.getStoredDiagnostics(uri) };
1288
+ return null;
1096
1289
  }
1097
- async prepareRename(filePath, line, character) {
1098
- const absPath = resolve(contextCwd(), filePath);
1099
- await this.openFile(absPath);
1100
- return this.sendRequest("textDocument/prepareRename", {
1101
- textDocument: { uri: pathToFileURL2(absPath).href },
1102
- position: { line: line - 1, character }
1103
- });
1290
+ async synchronize(delta) {
1291
+ const watched = [];
1292
+ for (const mutation of delta.operations)
1293
+ await this.synchronizeMutation(mutation, watched);
1294
+ for (let index = 0;index < watched.length; index += WATCHED_FILE_BATCH_SIZE) {
1295
+ await this.sendNotification("workspace/didChangeWatchedFiles", {
1296
+ changes: watched.slice(index, index + WATCHED_FILE_BATCH_SIZE)
1297
+ });
1298
+ }
1104
1299
  }
1105
- async rename(filePath, line, character, newName) {
1106
- const absPath = resolve(contextCwd(), filePath);
1107
- await this.openFile(absPath);
1108
- return this.sendRequest("textDocument/rename", {
1109
- textDocument: { uri: pathToFileURL2(absPath).href },
1110
- position: { line: line - 1, character },
1111
- newName
1300
+ async synchronizeMutation(mutation, watched) {
1301
+ if (mutation.kind === "text") {
1302
+ const state = this.openDocuments.get(mutation.path);
1303
+ if (state)
1304
+ await this.changeDocument(state, mutation.afterText);
1305
+ else
1306
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: 2 });
1307
+ return;
1308
+ }
1309
+ if (mutation.kind === "create") {
1310
+ const state = this.openDocuments.get(mutation.path);
1311
+ if (state) {
1312
+ await this.closeDocument(state);
1313
+ await this.openDocumentSingleFlight(mutation.path, readFileSync(mutation.path, "utf-8"));
1314
+ } else {
1315
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: mutation.replaced ? 2 : 1 });
1316
+ }
1317
+ return;
1318
+ }
1319
+ if (mutation.kind === "rename") {
1320
+ const moved = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.oldPath));
1321
+ for (const state of moved)
1322
+ await this.closeDocument(state);
1323
+ for (const state of moved) {
1324
+ const path = movedPath(state.path, mutation.oldPath, mutation.newPath);
1325
+ await this.openDocumentSingleFlight(path, readFileSync(path, "utf-8"));
1326
+ }
1327
+ if (moved.length === 0) {
1328
+ watched.push({ uri: pathToFileURL2(mutation.oldPath).href, type: 3 });
1329
+ watched.push({ uri: pathToFileURL2(mutation.newPath).href, type: 1 });
1330
+ }
1331
+ return;
1332
+ }
1333
+ const removed = [...this.openDocuments.values()].filter((state) => isSameOrDescendant(state.path, mutation.path));
1334
+ for (const state of removed)
1335
+ await this.closeDocument(state);
1336
+ if (removed.length === 0)
1337
+ watched.push({ uri: pathToFileURL2(mutation.path).href, type: 3 });
1338
+ }
1339
+ async openDocumentSingleFlight(path, text) {
1340
+ const existing = this.openPromises.get(path);
1341
+ if (existing)
1342
+ return existing;
1343
+ const open = (async () => {
1344
+ const state = {
1345
+ path,
1346
+ uri: pathToFileURL2(path).href,
1347
+ languageId: getLanguageId(effectiveExtension(path)),
1348
+ text,
1349
+ version: 1,
1350
+ generation: 1,
1351
+ publishGeneration: 0,
1352
+ waiters: new Set
1353
+ };
1354
+ this.openDocuments.set(path, state);
1355
+ this.openByUri.set(state.uri, state);
1356
+ this.notifyWaiters(state);
1357
+ await this.sendNotification("textDocument/didOpen", {
1358
+ textDocument: { uri: state.uri, languageId: state.languageId, version: state.version, text }
1359
+ });
1360
+ })().finally(() => {
1361
+ this.openPromises.delete(path);
1112
1362
  });
1363
+ this.openPromises.set(path, open);
1364
+ return open;
1365
+ }
1366
+ async changeDocument(state, text) {
1367
+ state.text = text;
1368
+ state.version += 1;
1369
+ state.generation += 1;
1370
+ this.clearDiagnostics(state.uri);
1371
+ this.notifyWaiters(state);
1372
+ await this.sendNotification("textDocument/didChange", {
1373
+ textDocument: { uri: state.uri, version: state.version },
1374
+ contentChanges: [{ text }]
1375
+ });
1376
+ await this.sendNotification("textDocument/didSave", { textDocument: { uri: state.uri }, text });
1377
+ }
1378
+ async closeDocument(state) {
1379
+ this.openDocuments.delete(state.path);
1380
+ this.openByUri.delete(state.uri);
1381
+ this.clearDiagnostics(state.uri);
1382
+ this.notifyWaiters(state);
1383
+ await this.sendNotification("textDocument/didClose", { textDocument: { uri: state.uri } });
1384
+ }
1385
+ notifyWaiters(state) {
1386
+ for (const waiter of [...state.waiters])
1387
+ waiter();
1113
1388
  }
1114
1389
  }
1115
1390
 
1116
- // ../lsp-core/src/lsp/process-signal-cleanup.ts
1117
- function installProcessSignalCleanup(cleanup) {
1118
- const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM"];
1119
- const handler = () => {
1120
- cleanup().catch((error) => {
1121
- reportBestEffortCleanupError("signal cleanup", error);
1122
- });
1123
- };
1124
- for (const signal of signals) {
1125
- process.on(signal, handler);
1126
- }
1127
- return () => {
1128
- for (const signal of signals) {
1129
- process.removeListener(signal, handler);
1130
- }
1131
- };
1391
+ // ../lsp-core/src/lsp/workspace-apply-edit-failure.ts
1392
+ var CONCURRENT_FAILURE_REASON_BY_PHASE = {
1393
+ applying: "workspace/applyEdit is already in progress for this workspace mutation",
1394
+ settled: "workspace/applyEdit was already handled for this workspace mutation"
1395
+ };
1396
+ function workspaceApplyEditConcurrentFailureReason(phase) {
1397
+ return CONCURRENT_FAILURE_REASON_BY_PHASE[phase];
1132
1398
  }
1133
1399
 
1134
- // ../lsp-core/src/lsp/manager.ts
1135
- async function stopClientBestEffort(client) {
1400
+ // ../lsp-core/src/lsp/workspace-edit-commit.ts
1401
+ import { existsSync as existsSync3, lstatSync as lstatSync2, renameSync, rmSync, writeFileSync } from "node:fs";
1402
+
1403
+ // ../lsp-core/src/lsp/workspace-edit-path.ts
1404
+ import { existsSync as existsSync2, lstatSync, readFileSync as readFileSync2, readdirSync, realpathSync as realpathSync2 } from "node:fs";
1405
+ import { dirname, isAbsolute, relative as relative2, resolve as resolve2 } from "node:path";
1406
+ import { fileURLToPath } from "node:url";
1407
+
1408
+ class WorkspaceEditPathError extends Error {
1409
+ path;
1410
+ detail;
1411
+ name = "WorkspaceEditPathError";
1412
+ constructor(path, detail) {
1413
+ super(`${detail}: ${path}`);
1414
+ this.path = path;
1415
+ this.detail = detail;
1416
+ }
1417
+ }
1418
+ function isPathInsideWorkspace(filePath, workspaceRoot) {
1419
+ const relativePath = relative2(workspaceRoot, filePath);
1420
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute(relativePath);
1421
+ }
1422
+ function canonicalizeMissingPath(filePath) {
1423
+ let ancestor = filePath;
1424
+ while (!existsSync2(ancestor)) {
1425
+ const parent = dirname(ancestor);
1426
+ if (parent === ancestor)
1427
+ throw new WorkspaceEditPathError(filePath, "no existing ancestor");
1428
+ ancestor = parent;
1429
+ }
1430
+ return resolve2(realpathSync2(ancestor), relative2(ancestor, filePath));
1431
+ }
1432
+ function canonicalWorkspaceRoot(workspaceRoot) {
1136
1433
  try {
1137
- await client.stop();
1434
+ const canonical = realpathSync2(resolve2(workspaceRoot));
1435
+ if (!lstatSync(canonical).isDirectory()) {
1436
+ return { success: false, error: `workspace root is not a directory: ${workspaceRoot}` };
1437
+ }
1438
+ return {
1439
+ success: true,
1440
+ path: canonical,
1441
+ requestedPath: resolve2(workspaceRoot),
1442
+ followedSymbolicLink: existsSync2(resolve2(workspaceRoot)) && lstatSync(resolve2(workspaceRoot)).isSymbolicLink()
1443
+ };
1138
1444
  } catch (error) {
1139
- reportBestEffortCleanupError("client stop", error);
1445
+ const detail = error instanceof Error ? error.message : String(error);
1446
+ return { success: false, error: `workspace root ${workspaceRoot}: ${detail}` };
1140
1447
  }
1141
1448
  }
1142
- function awaitWithSignal(promise, signal) {
1143
- if (!signal)
1144
- return promise;
1145
- return new Promise((resolve2, reject) => {
1146
- let settled = false;
1147
- const onAbort = () => {
1148
- if (settled)
1149
- return;
1150
- settled = true;
1151
- reject(new DOMException("Aborted", "AbortError"));
1152
- };
1153
- if (signal.aborted) {
1154
- onAbort();
1155
- return;
1449
+ function uriToCanonicalWorkspacePath(uri, workspaceRoot) {
1450
+ let requestedPath;
1451
+ try {
1452
+ const parsed = new URL(uri);
1453
+ if (parsed.protocol !== "file:" || parsed.search !== "" || parsed.hash !== "") {
1454
+ return { success: false, error: `non-file URI ${uri}` };
1156
1455
  }
1157
- signal.addEventListener("abort", onAbort, { once: true });
1158
- promise.then((value) => {
1159
- if (settled)
1160
- return;
1161
- settled = true;
1162
- signal.removeEventListener("abort", onAbort);
1163
- resolve2(value);
1164
- }, (err) => {
1165
- if (settled)
1166
- return;
1167
- settled = true;
1168
- signal.removeEventListener("abort", onAbort);
1169
- reject(err);
1170
- });
1171
- });
1456
+ requestedPath = resolve2(fileURLToPath(parsed));
1457
+ } catch (error) {
1458
+ const detail = error instanceof Error ? error.message : String(error);
1459
+ return { success: false, error: `non-file URI ${uri}: ${detail}` };
1460
+ }
1461
+ try {
1462
+ const canonical = existsSync2(requestedPath) ? realpathSync2(requestedPath) : canonicalizeMissingPath(requestedPath);
1463
+ if (!isPathInsideWorkspace(canonical, workspaceRoot)) {
1464
+ return { success: false, error: `${requestedPath}: outside workspace ${workspaceRoot}` };
1465
+ }
1466
+ return {
1467
+ success: true,
1468
+ path: canonical,
1469
+ requestedPath,
1470
+ followedSymbolicLink: existsSync2(requestedPath) && lstatSync(requestedPath).isSymbolicLink()
1471
+ };
1472
+ } catch (error) {
1473
+ const detail = error instanceof Error ? error.message : String(error);
1474
+ return { success: false, error: `${requestedPath}: ${detail}` };
1475
+ }
1476
+ }
1477
+ function snapshotPath(path, includeChildren) {
1478
+ if (!existsSync2(path))
1479
+ return { kind: "missing" };
1480
+ const stats = lstatSync(path);
1481
+ if (stats.isFile())
1482
+ return { kind: "file", content: readFileSync2(path, "utf-8") };
1483
+ if (stats.isDirectory()) {
1484
+ return includeChildren ? { kind: "directory", children: readdirSync(path).sort() } : { kind: "directory" };
1485
+ }
1486
+ throw new WorkspaceEditPathError(path, "unsupported filesystem entry");
1172
1487
  }
1173
1488
 
1174
- class LspManager {
1175
- clients = new Map;
1176
- reaperHandle = null;
1177
- signalDisposer = null;
1489
+ // ../lsp-core/src/lsp/workspace-edit-commit.ts
1490
+ var DEFAULT_IO = {
1491
+ writeFile(path, content) {
1492
+ writeFileSync(path, content, "utf-8");
1493
+ },
1494
+ rename(oldPath, newPath) {
1495
+ renameSync(oldPath, newPath);
1496
+ },
1497
+ remove(path, recursive) {
1498
+ rmSync(path, { recursive, force: false });
1499
+ }
1500
+ };
1501
+ function snapshotsEqual(expected, actual) {
1502
+ if (expected.kind !== actual.kind)
1503
+ return false;
1504
+ if (expected.kind === "file" && actual.kind === "file")
1505
+ return expected.content === actual.content;
1506
+ if (expected.kind === "directory" && actual.kind === "directory" && expected.children !== undefined) {
1507
+ return JSON.stringify(expected.children) === JSON.stringify(actual.children);
1508
+ }
1509
+ return true;
1510
+ }
1511
+ function liveSnapshot(path, expected) {
1512
+ return snapshotPath(path, expected.kind === "directory" && expected.children !== undefined);
1513
+ }
1514
+ function firstOperationIndex(plan) {
1515
+ return plan.operations[0]?.changeIndex ?? 0;
1516
+ }
1517
+ function failedCommit(plan, failure) {
1518
+ const { message, changeIndex, mutations = [], filesModified = [], totalEdits = 0, lateAbort = false } = failure;
1519
+ return {
1520
+ result: {
1521
+ success: false,
1522
+ filesModified,
1523
+ totalEdits,
1524
+ errors: [`change ${changeIndex}: ${message}`],
1525
+ failedChange: changeIndex,
1526
+ ...lateAbort ? { lateAbort: true } : {}
1527
+ },
1528
+ delta: mutationDelta(mutations),
1529
+ fingerprint: plan.fingerprint
1530
+ };
1531
+ }
1532
+ function verifySnapshots(plan) {
1533
+ for (const [path, expected] of plan.snapshots) {
1534
+ let actual;
1535
+ try {
1536
+ actual = liveSnapshot(path, expected);
1537
+ } catch (error) {
1538
+ const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
1539
+ const detail = error instanceof Error ? error.message : String(error);
1540
+ return failedCommit(plan, { message: `cannot verify snapshot for ${path}: ${detail}`, changeIndex });
1541
+ }
1542
+ if (!snapshotsEqual(expected, actual)) {
1543
+ const changeIndex = plan.firstChangeByPath.get(path) ?? firstOperationIndex(plan);
1544
+ return failedCommit(plan, { message: `workspace state changed before commit: ${path}`, changeIndex });
1545
+ }
1546
+ }
1547
+ return null;
1548
+ }
1549
+ function addModifiedPath(paths, path) {
1550
+ if (!paths.includes(path))
1551
+ paths.push(path);
1552
+ }
1553
+ function reportedPath(plan, path) {
1554
+ return plan.reportedPathByCanonical.get(path) ?? path;
1555
+ }
1556
+ function changedPathsForMutation(mutation) {
1557
+ return mutation.kind === "rename" ? [mutation.oldPath, mutation.newPath] : [mutation.path];
1558
+ }
1559
+ function mutationDelta(operations) {
1560
+ const changedPaths = new Set;
1561
+ for (const operation of operations) {
1562
+ for (const path of changedPathsForMutation(operation))
1563
+ changedPaths.add(path);
1564
+ }
1565
+ return { operations, changedPaths: [...changedPaths].sort() };
1566
+ }
1567
+ function resolveIo(overrides) {
1568
+ return {
1569
+ writeFile: overrides?.writeFile ?? DEFAULT_IO.writeFile,
1570
+ rename: overrides?.rename ?? DEFAULT_IO.rename,
1571
+ remove: overrides?.remove ?? DEFAULT_IO.remove
1572
+ };
1573
+ }
1574
+ function commitOperation(context, operation) {
1575
+ const { plan, io, accumulator } = context;
1576
+ if (operation.kind === "noop")
1577
+ return;
1578
+ if (operation.kind === "text") {
1579
+ io.writeFile(operation.path, operation.afterText);
1580
+ accumulator.mutations.push({
1581
+ kind: "text",
1582
+ path: operation.path,
1583
+ beforeText: operation.beforeText,
1584
+ afterText: operation.afterText
1585
+ });
1586
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
1587
+ accumulator.totalEdits += operation.editCount;
1588
+ return;
1589
+ }
1590
+ if (operation.kind === "create") {
1591
+ io.writeFile(operation.path, "");
1592
+ accumulator.mutations.push({ kind: "create", path: operation.path, replaced: operation.replaced });
1593
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
1594
+ return;
1595
+ }
1596
+ if (operation.kind === "rename") {
1597
+ if (operation.replaceDestination) {
1598
+ const targetKind = existsSync3(operation.newPath) && lstatSync2(operation.newPath).isDirectory() ? "directory" : "file";
1599
+ io.remove(operation.newPath, targetKind === "directory");
1600
+ accumulator.mutations.push({ kind: "delete", path: operation.newPath, targetKind });
1601
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
1602
+ }
1603
+ io.rename(operation.oldPath, operation.newPath);
1604
+ accumulator.mutations.push({
1605
+ kind: "rename",
1606
+ oldPath: operation.oldPath,
1607
+ newPath: operation.newPath,
1608
+ sourceKind: operation.sourceKind
1609
+ });
1610
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.newPath));
1611
+ return;
1612
+ }
1613
+ io.remove(operation.path, operation.recursive);
1614
+ accumulator.mutations.push({
1615
+ kind: "delete",
1616
+ path: operation.path,
1617
+ targetKind: operation.targetKind
1618
+ });
1619
+ addModifiedPath(accumulator.filesModified, reportedPath(plan, operation.path));
1620
+ }
1621
+ function commitWorkspaceEditPlan(plan, options = {}) {
1622
+ if (options.signal?.aborted) {
1623
+ return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
1624
+ }
1625
+ const stale = verifySnapshots(plan);
1626
+ if (stale)
1627
+ return stale;
1628
+ if (options.signal?.aborted) {
1629
+ return failedCommit(plan, { message: "cancelled before commit", changeIndex: firstOperationIndex(plan) });
1630
+ }
1631
+ const io = resolveIo(options.io);
1632
+ const accumulator = { mutations: [], filesModified: [], totalEdits: 0 };
1633
+ const context = { plan, io, accumulator };
1634
+ let lateAbort = false;
1635
+ for (const operation of plan.operations) {
1636
+ try {
1637
+ commitOperation(context, operation);
1638
+ } catch (error) {
1639
+ const detail = error instanceof Error ? error.message : String(error);
1640
+ return failedCommit(plan, {
1641
+ message: `I/O failure during ${operation.kind}: ${detail}`,
1642
+ changeIndex: operation.changeIndex,
1643
+ mutations: accumulator.mutations,
1644
+ filesModified: accumulator.filesModified,
1645
+ totalEdits: accumulator.totalEdits,
1646
+ lateAbort: lateAbort || options.signal?.aborted === true
1647
+ });
1648
+ }
1649
+ if (options.signal?.aborted)
1650
+ lateAbort = true;
1651
+ }
1652
+ const result = {
1653
+ success: true,
1654
+ filesModified: accumulator.filesModified,
1655
+ totalEdits: accumulator.totalEdits,
1656
+ errors: [],
1657
+ ...lateAbort ? { lateAbort: true } : {}
1658
+ };
1659
+ return { result, delta: mutationDelta(accumulator.mutations), fingerprint: plan.fingerprint };
1660
+ }
1661
+
1662
+ // ../lsp-core/src/lsp/workspace-edit-fingerprint.ts
1663
+ import { createHash } from "node:crypto";
1664
+ function canonicalFingerprint(operations) {
1665
+ const canonical = operations.map((operation) => {
1666
+ switch (operation.kind) {
1667
+ case "text":
1668
+ return {
1669
+ kind: operation.kind,
1670
+ changeIndex: operation.changeIndex,
1671
+ path: operation.path,
1672
+ edits: operation.edits,
1673
+ version: operation.version
1674
+ };
1675
+ case "rename":
1676
+ return {
1677
+ kind: operation.kind,
1678
+ changeIndex: operation.changeIndex,
1679
+ oldPath: operation.oldPath,
1680
+ newPath: operation.newPath,
1681
+ overwrite: operation.overwrite,
1682
+ ignoreIfExists: operation.ignoreIfExists
1683
+ };
1684
+ case "create":
1685
+ return {
1686
+ kind: operation.kind,
1687
+ changeIndex: operation.changeIndex,
1688
+ path: operation.path,
1689
+ overwrite: operation.overwrite,
1690
+ ignoreIfExists: operation.ignoreIfExists
1691
+ };
1692
+ case "delete":
1693
+ return {
1694
+ kind: operation.kind,
1695
+ changeIndex: operation.changeIndex,
1696
+ path: operation.path,
1697
+ recursive: operation.recursive,
1698
+ ignoreIfNotExists: operation.ignoreIfNotExists
1699
+ };
1700
+ }
1701
+ });
1702
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
1703
+ }
1704
+
1705
+ // ../lsp-core/src/lsp/workspace-edit-types.ts
1706
+ class WorkspaceEditValidationError extends Error {
1707
+ changeIndex;
1708
+ detail;
1709
+ name = "WorkspaceEditValidationError";
1710
+ constructor(changeIndex, detail) {
1711
+ super(`change ${changeIndex}: ${detail}`);
1712
+ this.changeIndex = changeIndex;
1713
+ this.detail = detail;
1714
+ }
1715
+ }
1716
+
1717
+ // ../lsp-core/src/lsp/workspace-edit-parse-helpers.ts
1718
+ function isRecord2(value) {
1719
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1720
+ }
1721
+ function parsePosition(value) {
1722
+ if (!isRecord2(value) || typeof value["line"] !== "number" || typeof value["character"] !== "number") {
1723
+ return null;
1724
+ }
1725
+ return { line: value["line"], character: value["character"] };
1726
+ }
1727
+ function parseRange(value) {
1728
+ if (!isRecord2(value))
1729
+ return null;
1730
+ const start = parsePosition(value["start"]);
1731
+ const end = parsePosition(value["end"]);
1732
+ return start && end ? { start, end } : null;
1733
+ }
1734
+ function parseTextEdits(value, changeIndex) {
1735
+ if (!Array.isArray(value)) {
1736
+ throw new WorkspaceEditValidationError(changeIndex, "text edits must be an array");
1737
+ }
1738
+ const edits = [];
1739
+ for (const candidate of value) {
1740
+ if (!isRecord2(candidate) || typeof candidate["newText"] !== "string") {
1741
+ throw new WorkspaceEditValidationError(changeIndex, "text edit requires range and newText");
1742
+ }
1743
+ if ("annotationId" in candidate) {
1744
+ throw new WorkspaceEditValidationError(changeIndex, "annotated text edits are unsupported");
1745
+ }
1746
+ const range = parseRange(candidate["range"]);
1747
+ if (!range)
1748
+ throw new WorkspaceEditValidationError(changeIndex, "text edit range is malformed");
1749
+ edits.push({ range, newText: candidate["newText"] });
1750
+ }
1751
+ return edits;
1752
+ }
1753
+ function parseBooleanOption(options, key, changeIndex) {
1754
+ const value = options[key];
1755
+ if (value === undefined)
1756
+ return false;
1757
+ if (typeof value !== "boolean") {
1758
+ throw new WorkspaceEditValidationError(changeIndex, `${key} must be boolean`);
1759
+ }
1760
+ return value;
1761
+ }
1762
+ function parseOptions(value, allowed, changeIndex) {
1763
+ if (value === undefined)
1764
+ return {};
1765
+ if (!isRecord2(value))
1766
+ throw new WorkspaceEditValidationError(changeIndex, "resource options must be an object");
1767
+ for (const key of Object.keys(value)) {
1768
+ if (!allowed.includes(key))
1769
+ throw new WorkspaceEditValidationError(changeIndex, `unsupported resource option ${key}`);
1770
+ }
1771
+ const parsed = {};
1772
+ for (const key of allowed)
1773
+ parsed[key] = parseBooleanOption(value, key, changeIndex);
1774
+ return parsed;
1775
+ }
1776
+
1777
+ // ../lsp-core/src/lsp/workspace-edit-resource-parser.ts
1778
+ function parseResourceChange(input) {
1779
+ const kind = input.change["kind"];
1780
+ if (kind === "create" || kind === "delete") {
1781
+ parseSinglePathResource(input, kind);
1782
+ return;
1783
+ }
1784
+ if (kind !== "rename") {
1785
+ throw new WorkspaceEditValidationError(input.changeIndex, `unsupported resource operation ${String(kind)}`);
1786
+ }
1787
+ parseRename(input);
1788
+ }
1789
+ function parseSinglePathResource(input, kind) {
1790
+ const { change, changeIndex, workspaceRoot, target } = input;
1791
+ if (typeof change["uri"] !== "string")
1792
+ throw new WorkspaceEditValidationError(changeIndex, `${kind}.uri is required`);
1793
+ const resolvedPath = uriToCanonicalWorkspacePath(change["uri"], workspaceRoot);
1794
+ if (!resolvedPath.success) {
1795
+ target.failures.push({ changeIndex, message: resolvedPath.error });
1796
+ return;
1797
+ }
1798
+ if (kind === "create") {
1799
+ const options2 = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
1800
+ target.operations.push({
1801
+ kind,
1802
+ changeIndex,
1803
+ path: resolvedPath.path,
1804
+ reportedPath: resolvedPath.requestedPath,
1805
+ overwrite: options2["overwrite"] ?? false,
1806
+ ignoreIfExists: options2["ignoreIfExists"] ?? false,
1807
+ followedSymbolicLink: resolvedPath.followedSymbolicLink
1808
+ });
1809
+ return;
1810
+ }
1811
+ const options = parseOptions(change["options"], ["recursive", "ignoreIfNotExists"], changeIndex);
1812
+ target.operations.push({
1813
+ kind,
1814
+ changeIndex,
1815
+ path: resolvedPath.path,
1816
+ reportedPath: resolvedPath.requestedPath,
1817
+ recursive: options["recursive"] ?? false,
1818
+ ignoreIfNotExists: options["ignoreIfNotExists"] ?? false,
1819
+ followedSymbolicLink: resolvedPath.followedSymbolicLink
1820
+ });
1821
+ }
1822
+ function parseRename(input) {
1823
+ const { change, changeIndex, workspaceRoot, target } = input;
1824
+ if (typeof change["oldUri"] !== "string" || typeof change["newUri"] !== "string") {
1825
+ throw new WorkspaceEditValidationError(changeIndex, "rename requires oldUri and newUri");
1826
+ }
1827
+ const oldPath = uriToCanonicalWorkspacePath(change["oldUri"], workspaceRoot);
1828
+ const newPath = uriToCanonicalWorkspacePath(change["newUri"], workspaceRoot);
1829
+ if (!oldPath.success || !newPath.success) {
1830
+ target.failures.push({
1831
+ changeIndex,
1832
+ message: !oldPath.success ? oldPath.error : !newPath.success ? newPath.error : "invalid rename path"
1833
+ });
1834
+ return;
1835
+ }
1836
+ const options = parseOptions(change["options"], ["overwrite", "ignoreIfExists"], changeIndex);
1837
+ target.operations.push({
1838
+ kind: "rename",
1839
+ changeIndex,
1840
+ oldPath: oldPath.path,
1841
+ newPath: newPath.path,
1842
+ reportedOldPath: oldPath.requestedPath,
1843
+ reportedNewPath: newPath.requestedPath,
1844
+ overwrite: options["overwrite"] ?? false,
1845
+ ignoreIfExists: options["ignoreIfExists"] ?? false,
1846
+ followedSymbolicLink: oldPath.followedSymbolicLink || newPath.followedSymbolicLink
1847
+ });
1848
+ }
1849
+
1850
+ // ../lsp-core/src/lsp/workspace-edit-parser.ts
1851
+ function failureResult(failures) {
1852
+ const sorted = [...failures].sort((left, right) => left.changeIndex - right.changeIndex);
1853
+ const first = sorted[0];
1854
+ return {
1855
+ success: false,
1856
+ filesModified: [],
1857
+ totalEdits: 0,
1858
+ errors: sorted.map((failure) => `change ${failure.changeIndex}: ${failure.message}`),
1859
+ ...first ? { failedChange: first.changeIndex } : {}
1860
+ };
1861
+ }
1862
+ function parseWorkspaceEdit(edit, workspaceRoot) {
1863
+ if (!isRecord2(edit))
1864
+ return { operations: [], failures: [{ changeIndex: 0, message: "No edit provided" }] };
1865
+ if (edit["changeAnnotations"] !== undefined) {
1866
+ return { operations: [], failures: [{ changeIndex: 0, message: "change annotations are unsupported" }] };
1867
+ }
1868
+ const hasChanges = edit["changes"] !== undefined;
1869
+ const hasDocumentChanges = edit["documentChanges"] !== undefined;
1870
+ if (hasChanges && hasDocumentChanges) {
1871
+ return {
1872
+ operations: [],
1873
+ failures: [{ changeIndex: 0, message: "changes and documentChanges cannot be combined" }]
1874
+ };
1875
+ }
1876
+ const target = { operations: [], failures: [] };
1877
+ if (hasChanges)
1878
+ return parseChanges(edit["changes"], workspaceRoot, target);
1879
+ return parseDocumentChanges(edit["documentChanges"], workspaceRoot, target);
1880
+ }
1881
+ function parseChanges(value, workspaceRoot, target) {
1882
+ if (!isRecord2(value))
1883
+ return { ...target, failures: [{ changeIndex: 0, message: "changes must be an object" }] };
1884
+ const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
1885
+ for (const [changeIndex, [uri, rawEdits]] of entries.entries()) {
1886
+ const resolvedPath = uriToCanonicalWorkspacePath(uri, workspaceRoot);
1887
+ if (!resolvedPath.success) {
1888
+ target.failures.push({ changeIndex, message: resolvedPath.error });
1889
+ continue;
1890
+ }
1891
+ try {
1892
+ target.operations.push({
1893
+ kind: "text",
1894
+ changeIndex,
1895
+ path: resolvedPath.path,
1896
+ reportedPath: resolvedPath.requestedPath,
1897
+ edits: parseTextEdits(rawEdits, changeIndex),
1898
+ version: null
1899
+ });
1900
+ } catch (error) {
1901
+ if (error instanceof WorkspaceEditValidationError) {
1902
+ target.failures.push({ changeIndex, message: error.detail });
1903
+ continue;
1904
+ }
1905
+ throw error;
1906
+ }
1907
+ }
1908
+ return target;
1909
+ }
1910
+ function parseDocumentChanges(value, workspaceRoot, target) {
1911
+ if (value === undefined)
1912
+ return target;
1913
+ if (!Array.isArray(value)) {
1914
+ return { ...target, failures: [{ changeIndex: 0, message: "documentChanges must be an array" }] };
1915
+ }
1916
+ for (const [changeIndex, change] of value.entries()) {
1917
+ try {
1918
+ parseDocumentChange({ change, changeIndex, workspaceRoot, target });
1919
+ } catch (error) {
1920
+ if (error instanceof WorkspaceEditValidationError) {
1921
+ target.failures.push({ changeIndex, message: error.detail });
1922
+ continue;
1923
+ }
1924
+ throw error;
1925
+ }
1926
+ }
1927
+ return target;
1928
+ }
1929
+ function parseDocumentChange(input) {
1930
+ const { change, changeIndex, workspaceRoot, target } = input;
1931
+ if (!isRecord2(change))
1932
+ throw new WorkspaceEditValidationError(changeIndex, "document change must be an object");
1933
+ if ("annotationId" in change) {
1934
+ throw new WorkspaceEditValidationError(changeIndex, "annotated resource operations are unsupported");
1935
+ }
1936
+ if (typeof change["kind"] === "string") {
1937
+ parseResourceChange({ change, changeIndex, workspaceRoot, target });
1938
+ return;
1939
+ }
1940
+ const identifier = change["textDocument"];
1941
+ if (!isRecord2(identifier) || typeof identifier["uri"] !== "string") {
1942
+ throw new WorkspaceEditValidationError(changeIndex, "textDocument.uri is required");
1943
+ }
1944
+ const version = identifier["version"];
1945
+ if (version !== null && (!Number.isInteger(version) || typeof version !== "number" || version < 0)) {
1946
+ throw new WorkspaceEditValidationError(changeIndex, "document version must be null or a non-negative integer");
1947
+ }
1948
+ const resolvedPath = uriToCanonicalWorkspacePath(identifier["uri"], workspaceRoot);
1949
+ if (!resolvedPath.success) {
1950
+ target.failures.push({ changeIndex, message: resolvedPath.error });
1951
+ return;
1952
+ }
1953
+ target.operations.push({
1954
+ kind: "text",
1955
+ changeIndex,
1956
+ path: resolvedPath.path,
1957
+ reportedPath: resolvedPath.requestedPath,
1958
+ edits: parseTextEdits(change["edits"], changeIndex),
1959
+ version
1960
+ });
1961
+ }
1962
+
1963
+ // ../lsp-core/src/lsp/workspace-edit-simulation.ts
1964
+ import { dirname as dirname2, relative as relative3, resolve as resolve3 } from "node:path";
1965
+
1966
+ // ../lsp-core/src/lsp/workspace-edit-text.ts
1967
+ function comparePosition(left, right) {
1968
+ return left.line === right.line ? left.character - right.character : left.line - right.line;
1969
+ }
1970
+ function positionsEqual(left, right) {
1971
+ return left.line === right.line && left.character === right.character;
1972
+ }
1973
+ function rangesEqual(left, right) {
1974
+ return positionsEqual(left.start, right.start) && positionsEqual(left.end, right.end);
1975
+ }
1976
+ function isEmptyRange(range) {
1977
+ return positionsEqual(range.start, range.end);
1978
+ }
1979
+ function formatRange(range) {
1980
+ return `${range.start.line + 1}:${range.start.character + 1}-${range.end.line + 1}:${range.end.character + 1}`;
1981
+ }
1982
+ function validatePosition(position, label, context) {
1983
+ const { lines, changeIndex } = context;
1984
+ if (!Number.isInteger(position.line) || !Number.isInteger(position.character)) {
1985
+ throw new WorkspaceEditValidationError(changeIndex, `${label} position must use integer line and character`);
1986
+ }
1987
+ if (position.line < 0 || position.character < 0) {
1988
+ throw new WorkspaceEditValidationError(changeIndex, `${label} position cannot be negative`);
1989
+ }
1990
+ const line = lines[position.line];
1991
+ if (line === undefined) {
1992
+ throw new WorkspaceEditValidationError(changeIndex, `${label} line ${position.line} is outside the document`);
1993
+ }
1994
+ if (position.character > line.length) {
1995
+ throw new WorkspaceEditValidationError(changeIndex, `${label} character ${position.character} is outside line ${position.line}`);
1996
+ }
1997
+ }
1998
+ function validateRange(range, lines, changeIndex) {
1999
+ const context = { lines, changeIndex };
2000
+ validatePosition(range.start, "start", context);
2001
+ validatePosition(range.end, "end", context);
2002
+ if (comparePosition(range.start, range.end) > 0) {
2003
+ throw new WorkspaceEditValidationError(changeIndex, `range ${formatRange(range)} ends before it starts`);
2004
+ }
2005
+ }
2006
+ function sortAndDeduplicate(edits) {
2007
+ const sorted = edits.map((edit, index) => ({ edit, index })).sort((left, right) => {
2008
+ const positionOrder = comparePosition(right.edit.range.start, left.edit.range.start);
2009
+ return positionOrder === 0 ? right.index - left.index : positionOrder;
2010
+ });
2011
+ const unique = [];
2012
+ for (const entry of sorted) {
2013
+ const previous = unique.at(-1);
2014
+ if (previous !== undefined && !isEmptyRange(entry.edit.range) && rangesEqual(previous.range, entry.edit.range) && previous.newText === entry.edit.newText) {
2015
+ continue;
2016
+ }
2017
+ unique.push(entry.edit);
2018
+ }
2019
+ return unique;
2020
+ }
2021
+ function validateNoOverlap(edits, changeIndex) {
2022
+ for (let index = 0;index < edits.length - 1; index += 1) {
2023
+ const later = edits[index];
2024
+ const earlier = edits[index + 1];
2025
+ if (later === undefined || earlier === undefined)
2026
+ continue;
2027
+ if (comparePosition(earlier.range.end, later.range.start) > 0) {
2028
+ throw new WorkspaceEditValidationError(changeIndex, `overlapping edits ${formatRange(earlier.range)} and ${formatRange(later.range)}`);
2029
+ }
2030
+ }
2031
+ }
2032
+ function applyNormalizedTextEdits(content, edits) {
2033
+ const lines = content.split(`
2034
+ `);
2035
+ for (const edit of edits) {
2036
+ const { start, end } = edit.range;
2037
+ const startLine = lines[start.line];
2038
+ const endLine = lines[end.line];
2039
+ if (startLine === undefined || endLine === undefined)
2040
+ continue;
2041
+ const replacement = startLine.slice(0, start.character) + edit.newText + endLine.slice(end.character);
2042
+ lines.splice(start.line, end.line - start.line + 1, ...replacement.split(`
2043
+ `));
2044
+ }
2045
+ return lines.join(`
2046
+ `);
2047
+ }
2048
+ function normalizeTextEdits(content, edits, changeIndex) {
2049
+ const lines = content.split(`
2050
+ `);
2051
+ for (const edit of edits) {
2052
+ validateRange(edit.range, lines, changeIndex);
2053
+ }
2054
+ const normalized = sortAndDeduplicate(edits);
2055
+ validateNoOverlap(normalized, changeIndex);
2056
+ return { edits: normalized, text: applyNormalizedTextEdits(content, normalized) };
2057
+ }
2058
+
2059
+ // ../lsp-core/src/lsp/workspace-edit-simulation.ts
2060
+ function isSameOrDescendant2(candidate, parent) {
2061
+ const relativePath = relative3(parent, candidate);
2062
+ return relativePath === "" || !relativePath.startsWith("..") && relativePath !== "..";
2063
+ }
2064
+ function removeVirtualSubtree(virtual, path) {
2065
+ for (const candidate of [...virtual.keys()]) {
2066
+ if (isSameOrDescendant2(candidate, path))
2067
+ virtual.delete(candidate);
2068
+ }
2069
+ virtual.set(path, { kind: "missing" });
2070
+ }
2071
+ function moveVirtualSubtree(virtual, oldPath, newPath) {
2072
+ const moved = [...virtual.entries()].filter(([candidate]) => isSameOrDescendant2(candidate, oldPath));
2073
+ removeVirtualSubtree(virtual, oldPath);
2074
+ removeVirtualSubtree(virtual, newPath);
2075
+ for (const [candidate, entry] of moved) {
2076
+ const suffix = relative3(oldPath, candidate);
2077
+ virtual.set(suffix === "" ? newPath : resolve3(newPath, suffix), entry);
2078
+ }
2079
+ }
2080
+ function virtualDirectoryHasChildren(virtual, path) {
2081
+ for (const [candidate, entry] of virtual) {
2082
+ if (candidate !== path && entry.kind !== "missing" && isSameOrDescendant2(candidate, path))
2083
+ return true;
2084
+ }
2085
+ return false;
2086
+ }
2087
+ function requireVirtualParent(virtual, path, changeIndex) {
2088
+ if (virtual.get(dirname2(path))?.kind !== "directory") {
2089
+ throw new WorkspaceEditValidationError(changeIndex, `parent directory does not exist for ${path}`);
2090
+ }
2091
+ }
2092
+ function simulateOperations(parsed, snapshots) {
2093
+ const virtual = new Map(snapshots);
2094
+ const planned = [];
2095
+ const failures = [];
2096
+ for (const operation of parsed) {
2097
+ try {
2098
+ planned.push(simulateOperation(operation, virtual));
2099
+ } catch (error) {
2100
+ if (error instanceof WorkspaceEditValidationError) {
2101
+ failures.push({ changeIndex: operation.changeIndex, message: error.detail });
2102
+ continue;
2103
+ }
2104
+ throw error;
2105
+ }
2106
+ }
2107
+ return { operations: planned, failures };
2108
+ }
2109
+ function simulateOperation(operation, virtual) {
2110
+ switch (operation.kind) {
2111
+ case "text":
2112
+ return simulateText(operation, virtual);
2113
+ case "create":
2114
+ return simulateCreate(operation, virtual);
2115
+ case "rename":
2116
+ return simulateRename(operation, virtual);
2117
+ case "delete":
2118
+ return simulateDelete(operation, virtual);
2119
+ }
2120
+ }
2121
+ function rejectSymbolicLink(operation) {
2122
+ if (operation.followedSymbolicLink) {
2123
+ throw new WorkspaceEditValidationError(operation.changeIndex, "resource operations through symbolic links are unsupported");
2124
+ }
2125
+ }
2126
+ function simulateText(operation, virtual) {
2127
+ const entry = virtual.get(operation.path);
2128
+ if (entry?.kind !== "file")
2129
+ throw new WorkspaceEditValidationError(operation.changeIndex, `${operation.path} is not a file`);
2130
+ const normalized = normalizeTextEdits(entry.content, operation.edits, operation.changeIndex);
2131
+ virtual.set(operation.path, { kind: "file", content: normalized.text });
2132
+ return {
2133
+ kind: "text",
2134
+ changeIndex: operation.changeIndex,
2135
+ path: operation.path,
2136
+ beforeText: entry.content,
2137
+ afterText: normalized.text,
2138
+ editCount: normalized.edits.length,
2139
+ documentVersion: operation.version
2140
+ };
2141
+ }
2142
+ function simulateCreate(operation, virtual) {
2143
+ rejectSymbolicLink(operation);
2144
+ requireVirtualParent(virtual, operation.path, operation.changeIndex);
2145
+ const target = virtual.get(operation.path) ?? { kind: "missing" };
2146
+ if (target.kind !== "missing") {
2147
+ if (operation.overwrite && target.kind === "file") {
2148
+ virtual.set(operation.path, { kind: "file", content: "" });
2149
+ return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: true };
2150
+ }
2151
+ if (operation.ignoreIfExists)
2152
+ return { kind: "noop", changeIndex: operation.changeIndex };
2153
+ throw new WorkspaceEditValidationError(operation.changeIndex, `create target already exists: ${operation.path}`);
2154
+ }
2155
+ virtual.set(operation.path, { kind: "file", content: "" });
2156
+ return { kind: "create", changeIndex: operation.changeIndex, path: operation.path, replaced: false };
2157
+ }
2158
+ function simulateRename(operation, virtual) {
2159
+ rejectSymbolicLink(operation);
2160
+ const source = virtual.get(operation.oldPath) ?? { kind: "missing" };
2161
+ if (source.kind === "missing") {
2162
+ throw new WorkspaceEditValidationError(operation.changeIndex, `rename source does not exist: ${operation.oldPath}`);
2163
+ }
2164
+ if (operation.oldPath === operation.newPath)
2165
+ return { kind: "noop", changeIndex: operation.changeIndex };
2166
+ if (isSameOrDescendant2(operation.newPath, operation.oldPath)) {
2167
+ throw new WorkspaceEditValidationError(operation.changeIndex, "cannot rename a path into its own subtree");
2168
+ }
2169
+ requireVirtualParent(virtual, operation.newPath, operation.changeIndex);
2170
+ const destination = virtual.get(operation.newPath) ?? { kind: "missing" };
2171
+ if (destination.kind !== "missing" && !operation.overwrite) {
2172
+ if (operation.ignoreIfExists)
2173
+ return { kind: "noop", changeIndex: operation.changeIndex };
2174
+ throw new WorkspaceEditValidationError(operation.changeIndex, `rename target already exists: ${operation.newPath}`);
2175
+ }
2176
+ moveVirtualSubtree(virtual, operation.oldPath, operation.newPath);
2177
+ return {
2178
+ kind: "rename",
2179
+ changeIndex: operation.changeIndex,
2180
+ oldPath: operation.oldPath,
2181
+ newPath: operation.newPath,
2182
+ sourceKind: source.kind,
2183
+ replaceDestination: destination.kind !== "missing"
2184
+ };
2185
+ }
2186
+ function simulateDelete(operation, virtual) {
2187
+ rejectSymbolicLink(operation);
2188
+ const target = virtual.get(operation.path) ?? { kind: "missing" };
2189
+ if (target.kind === "missing") {
2190
+ if (operation.ignoreIfNotExists)
2191
+ return { kind: "noop", changeIndex: operation.changeIndex };
2192
+ throw new WorkspaceEditValidationError(operation.changeIndex, `delete target does not exist: ${operation.path}`);
2193
+ }
2194
+ if (target.kind === "directory" && !operation.recursive && virtualDirectoryHasChildren(virtual, operation.path)) {
2195
+ throw new WorkspaceEditValidationError(operation.changeIndex, `directory is not empty: ${operation.path}`);
2196
+ }
2197
+ removeVirtualSubtree(virtual, operation.path);
2198
+ return {
2199
+ kind: "delete",
2200
+ changeIndex: operation.changeIndex,
2201
+ path: operation.path,
2202
+ targetKind: target.kind,
2203
+ recursive: operation.recursive
2204
+ };
2205
+ }
2206
+
2207
+ // ../lsp-core/src/lsp/workspace-edit-snapshot.ts
2208
+ import { existsSync as existsSync4, lstatSync as lstatSync3, readdirSync as readdirSync2 } from "node:fs";
2209
+ import { dirname as dirname3, resolve as resolve4 } from "node:path";
2210
+ class WorkspaceSnapshotBuilder {
2211
+ workspaceRoot;
2212
+ snapshots = new Map;
2213
+ constructor(workspaceRoot) {
2214
+ this.workspaceRoot = workspaceRoot;
2215
+ }
2216
+ build(operations) {
2217
+ this.add(this.workspaceRoot, false);
2218
+ for (const operation of operations) {
2219
+ switch (operation.kind) {
2220
+ case "rename":
2221
+ this.add(operation.oldPath, true);
2222
+ this.add(operation.newPath, true);
2223
+ break;
2224
+ case "delete":
2225
+ this.add(operation.path, true);
2226
+ break;
2227
+ case "text":
2228
+ case "create":
2229
+ this.add(operation.path, false);
2230
+ break;
2231
+ }
2232
+ }
2233
+ return this.snapshots;
2234
+ }
2235
+ add(path, includeChildren) {
2236
+ let candidate = path;
2237
+ while (true) {
2238
+ const existing = this.snapshots.get(candidate);
2239
+ if (existing === undefined || includeChildren && existing.kind === "directory" && existing.children === undefined) {
2240
+ this.snapshots.set(candidate, snapshotPath(candidate, includeChildren && candidate === path));
2241
+ }
2242
+ if (candidate === this.workspaceRoot)
2243
+ break;
2244
+ candidate = dirname3(candidate);
2245
+ }
2246
+ if (!includeChildren || !existsSync4(path) || !lstatSync3(path).isDirectory())
2247
+ return;
2248
+ for (const child of readdirSync2(path))
2249
+ this.add(resolve4(path, child), true);
2250
+ }
2251
+ }
2252
+ function snapshotOperations(operations, workspaceRoot) {
2253
+ return new WorkspaceSnapshotBuilder(workspaceRoot).build(operations);
2254
+ }
2255
+
2256
+ // ../lsp-core/src/lsp/workspace-edit-plan.ts
2257
+ class PlanPathIndex {
2258
+ firstChangeByPath = new Map;
2259
+ reportedPathByCanonical = new Map;
2260
+ build(operations) {
2261
+ for (const operation of operations) {
2262
+ switch (operation.kind) {
2263
+ case "rename":
2264
+ this.add(operation.oldPath, operation.reportedOldPath, operation.changeIndex);
2265
+ this.add(operation.newPath, operation.reportedNewPath, operation.changeIndex);
2266
+ break;
2267
+ case "text":
2268
+ case "create":
2269
+ case "delete":
2270
+ this.add(operation.path, operation.reportedPath, operation.changeIndex);
2271
+ break;
2272
+ }
2273
+ }
2274
+ }
2275
+ add(path, reportedPath2, changeIndex) {
2276
+ if (!this.firstChangeByPath.has(path))
2277
+ this.firstChangeByPath.set(path, changeIndex);
2278
+ if (!this.reportedPathByCanonical.has(path))
2279
+ this.reportedPathByCanonical.set(path, reportedPath2);
2280
+ }
2281
+ }
2282
+ function fingerprintWorkspaceEdit(edit, workspaceRoot) {
2283
+ const root = canonicalWorkspaceRoot(workspaceRoot);
2284
+ if (!root.success)
2285
+ return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
2286
+ const parsed = parseWorkspaceEdit(edit, root.path);
2287
+ if (parsed.failures.length > 0)
2288
+ return { success: false, result: failureResult(parsed.failures) };
2289
+ return { success: true, fingerprint: canonicalFingerprint(parsed.operations) };
2290
+ }
2291
+ function planWorkspaceEdit(edit, workspaceRoot) {
2292
+ const root = canonicalWorkspaceRoot(workspaceRoot);
2293
+ if (!root.success)
2294
+ return { success: false, result: failureResult([{ changeIndex: 0, message: root.error }]) };
2295
+ const parsed = parseWorkspaceEdit(edit, root.path);
2296
+ if (parsed.failures.length > 0)
2297
+ return { success: false, result: failureResult(parsed.failures) };
2298
+ let snapshots;
2299
+ try {
2300
+ snapshots = snapshotOperations(parsed.operations, root.path);
2301
+ } catch (error) {
2302
+ return {
2303
+ success: false,
2304
+ result: failureResult([{ changeIndex: 0, message: error instanceof Error ? error.message : String(error) }])
2305
+ };
2306
+ }
2307
+ const simulated = simulateOperations(parsed.operations, snapshots);
2308
+ if (simulated.failures.length > 0)
2309
+ return { success: false, result: failureResult(simulated.failures) };
2310
+ const paths = new PlanPathIndex;
2311
+ paths.build(parsed.operations);
2312
+ const plan = {
2313
+ workspaceRoot: root.path,
2314
+ operations: simulated.operations,
2315
+ snapshots,
2316
+ firstChangeByPath: paths.firstChangeByPath,
2317
+ reportedPathByCanonical: paths.reportedPathByCanonical,
2318
+ fingerprint: canonicalFingerprint(parsed.operations)
2319
+ };
2320
+ return { success: true, plan };
2321
+ }
2322
+
2323
+ // ../lsp-core/src/lsp/workspace-mutation-controller.ts
2324
+ function failure(message, failedChange, base) {
2325
+ return {
2326
+ success: false,
2327
+ filesModified: base?.filesModified ?? [],
2328
+ totalEdits: base?.totalEdits ?? 0,
2329
+ errors: [message],
2330
+ ...failedChange === undefined ? {} : { failedChange },
2331
+ ...base?.lateAbort ? { lateAbort: true } : {}
2332
+ };
2333
+ }
2334
+ function responseFor(result) {
2335
+ if (result.success)
2336
+ return { applied: true };
2337
+ return {
2338
+ applied: false,
2339
+ failureReason: result.errors[0] ?? "workspace edit failed",
2340
+ ...result.failedChange === undefined ? {} : { failedChange: result.failedChange }
2341
+ };
2342
+ }
2343
+ function isRecord3(value) {
2344
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2345
+ }
2346
+
2347
+ class WorkspaceMutationController {
2348
+ workspaceRoot;
2349
+ documents;
2350
+ activeLease = null;
2351
+ nextLeaseId = 1;
2352
+ io;
2353
+ constructor(workspaceRoot, documents) {
2354
+ this.workspaceRoot = workspaceRoot;
2355
+ this.documents = documents;
2356
+ }
2357
+ setIo(io) {
2358
+ this.io = io;
2359
+ }
2360
+ acquire(signal) {
2361
+ if (this.activeLease)
2362
+ return { success: false, result: failure("workspace mutation is already in progress") };
2363
+ if (signal?.aborted)
2364
+ return { success: false, result: failure("cancelled before mutating request") };
2365
+ const lease = {
2366
+ id: this.nextLeaseId,
2367
+ phase: "idle",
2368
+ ...signal === undefined ? {} : { signal }
2369
+ };
2370
+ this.nextLeaseId += 1;
2371
+ this.activeLease = lease;
2372
+ return { success: true, lease };
2373
+ }
2374
+ release(lease) {
2375
+ if (this.activeLease?.id !== lease.id)
2376
+ return;
2377
+ this.activeLease.phase = "sealed";
2378
+ this.activeLease = null;
2379
+ }
2380
+ isBeforeCommit(lease) {
2381
+ return this.activeLease?.id === lease.id && this.activeLease.phase === "idle";
2382
+ }
2383
+ async handleApplyEdit(params) {
2384
+ const lease = this.activeLease;
2385
+ if (!lease)
2386
+ return { applied: false, failureReason: "workspace/applyEdit requires an active workspace mutation" };
2387
+ if (lease.phase !== "idle") {
2388
+ return {
2389
+ applied: false,
2390
+ failureReason: workspaceApplyEditConcurrentFailureReason(lease.phase === "applying" ? "applying" : "settled")
2391
+ };
2392
+ }
2393
+ lease.phase = "applying";
2394
+ lease.applyCompletion = new Promise((resolve5) => {
2395
+ lease.resolveApply = resolve5;
2396
+ });
2397
+ const edit = isRecord3(params) ? params["edit"] : undefined;
2398
+ const record = edit === undefined ? { fingerprint: null, result: failure("workspace/applyEdit params.edit is required", 0) } : await this.applyEdit(edit, lease);
2399
+ lease.serverApply = record;
2400
+ lease.phase = "settled";
2401
+ lease.resolveApply?.();
2402
+ return responseFor(record.result);
2403
+ }
2404
+ async reconcileRename(leaseToken, edit) {
2405
+ const lease = this.requireActiveLease(leaseToken);
2406
+ if (!lease)
2407
+ return { edit, apply: failure("workspace mutation lease ended before rename reconciliation") };
2408
+ if (lease.phase === "applying")
2409
+ await lease.applyCompletion;
2410
+ if (lease.serverApply)
2411
+ return this.reconcileServerApply(lease.serverApply, edit);
2412
+ lease.phase = "sealed";
2413
+ if (!edit)
2414
+ return { edit, apply: failure("No edit provided") };
2415
+ const applied = await this.applyEdit(edit, lease);
2416
+ return { edit, apply: applied.result };
2417
+ }
2418
+ reconcileServerApply(record, edit) {
2419
+ if (!edit)
2420
+ return { edit, apply: record.result };
2421
+ const fingerprint = fingerprintWorkspaceEdit(edit, this.workspaceRoot);
2422
+ if (fingerprint.success && record.fingerprint !== null && fingerprint.fingerprint === record.fingerprint) {
2423
+ return { edit, apply: record.result };
2424
+ }
2425
+ return {
2426
+ edit,
2427
+ apply: failure("rename result conflicts with server-applied workspace edit", 0, record.result)
2428
+ };
2429
+ }
2430
+ async applyEdit(edit, lease) {
2431
+ const planned = planWorkspaceEdit(edit, this.workspaceRoot);
2432
+ if (!planned.success)
2433
+ return { fingerprint: null, result: planned.result };
2434
+ const versionFailure = this.documents.validateVersions(planned.plan.operations);
2435
+ if (versionFailure) {
2436
+ return {
2437
+ fingerprint: planned.plan.fingerprint,
2438
+ result: failure(versionFailure.message, versionFailure.changeIndex)
2439
+ };
2440
+ }
2441
+ const commit = commitWorkspaceEditPlan(planned.plan, {
2442
+ ...lease.signal === undefined ? {} : { signal: lease.signal },
2443
+ ...this.io === undefined ? {} : { io: this.io }
2444
+ });
2445
+ let result = commit.result;
2446
+ if (commit.delta.operations.length > 0) {
2447
+ try {
2448
+ await this.documents.synchronize(commit.delta);
2449
+ } catch (error) {
2450
+ const message = error instanceof Error ? error.message : String(error);
2451
+ result = failure(`document synchronization failed after filesystem commit: ${message}`, undefined, result);
2452
+ }
2453
+ }
2454
+ if (lease.signal?.aborted && !result.lateAbort)
2455
+ result = { ...result, lateAbort: true };
2456
+ return { fingerprint: planned.plan.fingerprint, result };
2457
+ }
2458
+ requireActiveLease(lease) {
2459
+ return this.activeLease?.id === lease.id ? this.activeLease : null;
2460
+ }
2461
+ }
2462
+
2463
+ // ../lsp-core/src/lsp/client.ts
2464
+ var DIAGNOSTICS_FRESHNESS_TIMEOUT_MS = 3000;
2465
+ var VERSIONLESS_PUBLISH_QUIESCENCE_MS = 250;
2466
+
2467
+ class LspClient extends LspClientConnection {
2468
+ diagnosticPullErrors = [];
2469
+ documents;
2470
+ workspaceMutations;
2471
+ diagnosticsFreshnessTimeoutMs;
2472
+ constructor(root, server, options = {}) {
2473
+ super(root, server, options);
2474
+ this.diagnosticsFreshnessTimeoutMs = options.diagnosticsFreshnessTimeoutMs ?? DIAGNOSTICS_FRESHNESS_TIMEOUT_MS;
2475
+ this.documents = new WorkspaceDocumentState((method, params) => this.sendNotification(method, params), (uri) => this.diagnosticsStore.delete(uri), {
2476
+ versionlessPublishQuiescenceMs: options.versionlessPublishQuiescenceMs ?? VERSIONLESS_PUBLISH_QUIESCENCE_MS
2477
+ });
2478
+ this.workspaceMutations = new WorkspaceMutationController(root, this.documents);
2479
+ this.setWorkspaceApplyEditHandler((params) => this.workspaceMutations.handleApplyEdit(params));
2480
+ }
2481
+ getDiagnosticPullErrors() {
2482
+ return this.diagnosticPullErrors;
2483
+ }
2484
+ async openFile(filePath) {
2485
+ const absPath = this.resolveWorkspacePath(filePath);
2486
+ await this.documents.openFile(absPath);
2487
+ }
2488
+ getOpenDocumentVersion(filePath) {
2489
+ return this.documents.getVersion(this.resolveWorkspacePath(filePath));
2490
+ }
2491
+ getStoredDiagnostics(uri) {
2492
+ return [...this.documents.getStoredDiagnostics(uri)];
2493
+ }
2494
+ setWorkspaceEditIo(io) {
2495
+ this.workspaceMutations.setIo(io);
2496
+ }
2497
+ handlePublishDiagnostics(params) {
2498
+ super.handlePublishDiagnostics(params);
2499
+ this.documents.recordPublishedDiagnostics(params);
2500
+ }
2501
+ async definition(filePath, line, character, signal) {
2502
+ const absPath = this.resolveWorkspacePath(filePath);
2503
+ await this.openFile(absPath);
2504
+ const options = signal === undefined ? {} : { signal };
2505
+ return this.sendRequest("textDocument/definition", {
2506
+ textDocument: { uri: pathToFileURL3(absPath).href },
2507
+ position: { line: line - 1, character }
2508
+ }, options);
2509
+ }
2510
+ async references(filePath, line, character, includeDeclaration = true, signal) {
2511
+ const absPath = this.resolveWorkspacePath(filePath);
2512
+ await this.openFile(absPath);
2513
+ const options = signal === undefined ? {} : { signal };
2514
+ return this.sendRequest("textDocument/references", {
2515
+ textDocument: { uri: pathToFileURL3(absPath).href },
2516
+ position: { line: line - 1, character },
2517
+ context: { includeDeclaration }
2518
+ }, options);
2519
+ }
2520
+ async documentSymbols(filePath, signal) {
2521
+ const absPath = this.resolveWorkspacePath(filePath);
2522
+ await this.openFile(absPath);
2523
+ const options = signal === undefined ? {} : { signal };
2524
+ return this.sendRequest("textDocument/documentSymbol", {
2525
+ textDocument: { uri: pathToFileURL3(absPath).href }
2526
+ }, options);
2527
+ }
2528
+ async workspaceSymbols(query, signal) {
2529
+ const options = signal === undefined ? {} : { signal };
2530
+ return this.sendRequest("workspace/symbol", { query }, options);
2531
+ }
2532
+ isUnsupportedDiagnosticPullError(error) {
2533
+ if (!(error instanceof Error))
2534
+ return false;
2535
+ const code = "code" in error && typeof error.code === "number" ? error.code : undefined;
2536
+ if (code === -32601)
2537
+ return true;
2538
+ return /unsupported|not supported|method not found|unknown request/i.test(error.message);
2539
+ }
2540
+ freshnessTimeout(absPath) {
2541
+ return {
2542
+ items: [],
2543
+ transientError: {
2544
+ kind: "freshness_timeout",
2545
+ message: `Timed out waiting for fresh diagnostics for ${absPath} within ${this.diagnosticsFreshnessTimeoutMs}ms.`
2546
+ }
2547
+ };
2548
+ }
2549
+ parseDiagnosticPullReport(value) {
2550
+ if (value.kind === "unchanged") {
2551
+ return {
2552
+ type: "unchanged",
2553
+ ...value.resultId === undefined ? {} : { resultId: value.resultId }
2554
+ };
2555
+ }
2556
+ return {
2557
+ type: "full",
2558
+ diagnostics: value.items ?? [],
2559
+ ...value.resultId === undefined ? {} : { resultId: value.resultId }
2560
+ };
2561
+ }
2562
+ async diagnostics(filePath, signal) {
2563
+ signal?.throwIfAborted();
2564
+ const absPath = this.resolveWorkspacePath(filePath);
2565
+ const uri = pathToFileURL3(absPath).href;
2566
+ await this.openFile(absPath);
2567
+ const deadlineAt = Date.now() + this.diagnosticsFreshnessTimeoutMs;
2568
+ for (;; ) {
2569
+ signal?.throwIfAborted();
2570
+ const snapshot = this.documents.captureDiagnosticSnapshot(absPath);
2571
+ if (!snapshot)
2572
+ return this.freshnessTimeout(absPath);
2573
+ const push = this.documents.resolvePushDiagnostics(snapshot);
2574
+ if (push.status === "ready")
2575
+ return { items: [...push.diagnostics] };
2576
+ let pushFallbackOnly = !this.isDiagnosticPullSupported();
2577
+ if (!pushFallbackOnly) {
2578
+ const cached = this.documents.getPullCache(snapshot);
2579
+ try {
2580
+ const remainingMs2 = deadlineAt - Date.now();
2581
+ if (remainingMs2 <= 0)
2582
+ return this.freshnessTimeout(absPath);
2583
+ const result = await this.sendRequest("textDocument/diagnostic", {
2584
+ textDocument: { uri },
2585
+ ...cached?.resultId === undefined ? {} : { previousResultId: cached.resultId }
2586
+ }, { timeoutMs: remainingMs2, ...signal === undefined ? {} : { signal } });
2587
+ if (!this.documents.isCurrentSnapshot(snapshot))
2588
+ continue;
2589
+ const report = this.parseDiagnosticPullReport(result);
2590
+ if (report.type === "full") {
2591
+ this.documents.recordPullDiagnostics(snapshot, {
2592
+ kind: "full",
2593
+ diagnostics: report.diagnostics,
2594
+ ...report.resultId === undefined ? {} : { resultId: report.resultId }
2595
+ });
2596
+ return { items: [...report.diagnostics] };
2597
+ }
2598
+ if (cached !== null && cached.documentVersion === snapshot.version && cached.resultId === report.resultId) {
2599
+ return { items: [...cached.diagnostics] };
2600
+ }
2601
+ } catch (error) {
2602
+ if (this.isUnsupportedDiagnosticPullError(error)) {
2603
+ this.setDiagnosticPullSupported(false);
2604
+ pushFallbackOnly = true;
2605
+ } else if (error instanceof LspRequestTimeoutError) {
2606
+ pushFallbackOnly = true;
2607
+ } else {
2608
+ this.diagnosticPullErrors.push(error instanceof Error ? error : new Error(String(error)));
2609
+ throw error;
2610
+ }
2611
+ }
2612
+ }
2613
+ if (!pushFallbackOnly)
2614
+ continue;
2615
+ const remainingMs = deadlineAt - Date.now();
2616
+ if (remainingMs <= 0)
2617
+ return this.freshnessTimeout(absPath);
2618
+ const waitMs = push.status === "wait" ? Math.min(push.waitMs, remainingMs) : remainingMs;
2619
+ await waitForDiagnosticsActivity(this.documents.waitForDiagnosticsActivity(snapshot, waitMs), signal);
2620
+ }
2621
+ }
2622
+ async prepareRename(filePath, line, character, signal) {
2623
+ const absPath = this.resolveWorkspacePath(filePath);
2624
+ await this.openFile(absPath);
2625
+ const options = signal === undefined ? {} : { signal };
2626
+ return this.sendRequest("textDocument/prepareRename", {
2627
+ textDocument: { uri: pathToFileURL3(absPath).href },
2628
+ position: { line: line - 1, character }
2629
+ }, options);
2630
+ }
2631
+ async rename(filePath, line, character, newName, signal) {
2632
+ const absPath = this.resolveWorkspacePath(filePath);
2633
+ await this.openFile(absPath);
2634
+ const acquired = this.workspaceMutations.acquire(signal);
2635
+ if (!acquired.success)
2636
+ return { edit: null, apply: acquired.result };
2637
+ const preCommitSignal = createPreCommitAbortSignal(signal, () => this.workspaceMutations.isBeforeCommit(acquired.lease));
2638
+ try {
2639
+ const renameParams = {
2640
+ textDocument: { uri: pathToFileURL3(absPath).href },
2641
+ position: { line: line - 1, character },
2642
+ newName
2643
+ };
2644
+ const edit = preCommitSignal === undefined ? await this.sendRequest("textDocument/rename", renameParams) : await this.sendRequest("textDocument/rename", renameParams, {
2645
+ signal: preCommitSignal.signal
2646
+ });
2647
+ return await this.workspaceMutations.reconcileRename(acquired.lease, edit);
2648
+ } finally {
2649
+ preCommitSignal?.dispose();
2650
+ this.workspaceMutations.release(acquired.lease);
2651
+ }
2652
+ }
2653
+ resolveWorkspacePath(filePath) {
2654
+ return resolve5(this.root, filePath);
2655
+ }
2656
+ }
2657
+ function waitForDiagnosticsActivity(wait, signal) {
2658
+ if (!signal)
2659
+ return wait;
2660
+ if (signal.aborted)
2661
+ return Promise.reject(abortError2(signal));
2662
+ return new Promise((resolve6, reject) => {
2663
+ const onAbort = () => {
2664
+ signal.removeEventListener("abort", onAbort);
2665
+ reject(abortError2(signal));
2666
+ };
2667
+ signal.addEventListener("abort", onAbort, { once: true });
2668
+ wait.then(() => {
2669
+ signal.removeEventListener("abort", onAbort);
2670
+ resolve6();
2671
+ }, (error) => {
2672
+ signal.removeEventListener("abort", onAbort);
2673
+ reject(error);
2674
+ });
2675
+ });
2676
+ }
2677
+ function createPreCommitAbortSignal(source, isBeforeCommit) {
2678
+ if (!source)
2679
+ return;
2680
+ const controller = new AbortController;
2681
+ const onAbort = () => {
2682
+ if (isBeforeCommit() && !controller.signal.aborted)
2683
+ controller.abort(preCommitAbortReason(source));
2684
+ };
2685
+ if (source.aborted)
2686
+ onAbort();
2687
+ else
2688
+ source.addEventListener("abort", onAbort, { once: true });
2689
+ return {
2690
+ signal: controller.signal,
2691
+ dispose: () => source.removeEventListener("abort", onAbort)
2692
+ };
2693
+ }
2694
+ function preCommitAbortReason(source) {
2695
+ const reason = source.reason;
2696
+ if (reason instanceof Error && reason.name !== "AbortError")
2697
+ return reason;
2698
+ return new Error("LSP request cancelled before workspace edit commit");
2699
+ }
2700
+ function abortError2(signal) {
2701
+ const reason = signal.reason;
2702
+ if (reason instanceof Error)
2703
+ return reason;
2704
+ const error = new Error(typeof reason === "string" ? reason : "operation cancelled");
2705
+ error.name = "AbortError";
2706
+ return error;
2707
+ }
2708
+
2709
+ // ../lsp-core/src/lsp/process-signal-cleanup.ts
2710
+ function installProcessSignalCleanup(cleanup) {
2711
+ const signals = process.platform === "win32" ? ["SIGINT", "SIGTERM", "SIGBREAK"] : ["SIGINT", "SIGTERM"];
2712
+ const handler = () => {
2713
+ cleanup().catch((error) => {
2714
+ reportBestEffortCleanupError("signal cleanup", error);
2715
+ });
2716
+ };
2717
+ for (const signal of signals) {
2718
+ process.on(signal, handler);
2719
+ }
2720
+ return () => {
2721
+ for (const signal of signals) {
2722
+ process.removeListener(signal, handler);
2723
+ }
2724
+ };
2725
+ }
2726
+
2727
+ // ../lsp-core/src/lsp/manager.ts
2728
+ async function stopClientBestEffort(client) {
2729
+ try {
2730
+ await client.stop();
2731
+ } catch (error) {
2732
+ reportBestEffortCleanupError("client stop", error);
2733
+ }
2734
+ }
2735
+ function awaitWithSignal(promise, signal) {
2736
+ if (!signal)
2737
+ return promise;
2738
+ return new Promise((resolve6, reject) => {
2739
+ let settled = false;
2740
+ const onAbort = () => {
2741
+ if (settled)
2742
+ return;
2743
+ settled = true;
2744
+ reject(new DOMException("Aborted", "AbortError"));
2745
+ };
2746
+ if (signal.aborted) {
2747
+ onAbort();
2748
+ return;
2749
+ }
2750
+ signal.addEventListener("abort", onAbort, { once: true });
2751
+ promise.then((value) => {
2752
+ if (settled)
2753
+ return;
2754
+ settled = true;
2755
+ signal.removeEventListener("abort", onAbort);
2756
+ resolve6(value);
2757
+ }, (err) => {
2758
+ if (settled)
2759
+ return;
2760
+ settled = true;
2761
+ signal.removeEventListener("abort", onAbort);
2762
+ reject(err);
2763
+ });
2764
+ });
2765
+ }
2766
+
2767
+ class LspManager {
2768
+ clients = new Map;
2769
+ reaperHandle = null;
2770
+ signalDisposer = null;
1178
2771
  disposed = false;
1179
2772
  idleTimeoutMs;
1180
2773
  initTimeoutMs;
@@ -1414,6 +3007,177 @@ async function disposeDefaultLspManager() {
1414
3007
  }
1415
3008
  // src/daemon-client.ts
1416
3009
  import { connect as connect2 } from "node:net";
3010
+ import { homedir as homedir3 } from "node:os";
3011
+ import { join as join7 } from "node:path";
3012
+
3013
+ // ../lsp-core/src/request-context.ts
3014
+ import { AsyncLocalStorage } from "node:async_hooks";
3015
+ import { existsSync as existsSync5, realpathSync as realpathSync3, statSync as statSync2 } from "node:fs";
3016
+ import { homedir } from "node:os";
3017
+ import { basename as basename2, delimiter as delimiter2, dirname as dirname4, isAbsolute as isAbsolute2, join as join2, relative as relative4, resolve as resolve6 } from "node:path";
3018
+
3019
+ class LspRequestContextParseError extends Error {
3020
+ code;
3021
+ name = "LspRequestContextParseError";
3022
+ constructor(code, message) {
3023
+ super(message);
3024
+ this.code = code;
3025
+ }
3026
+ }
3027
+
3028
+ class LspRequestContextUnavailableError extends Error {
3029
+ name = "LspRequestContextUnavailableError";
3030
+ constructor() {
3031
+ super("LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).");
3032
+ }
3033
+ }
3034
+ var storage = new AsyncLocalStorage;
3035
+ var CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
3036
+ var CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
3037
+ function runWithRequestContext(context, fn) {
3038
+ return storage.run(context, fn);
3039
+ }
3040
+ function lspRequestContext() {
3041
+ const context = storage.getStore();
3042
+ if (!context)
3043
+ throw new LspRequestContextUnavailableError;
3044
+ return context;
3045
+ }
3046
+ function contextCwd() {
3047
+ return lspRequestContext().cwd;
3048
+ }
3049
+ function createStandaloneMcpRequestContext(input = {}) {
3050
+ const env = input.env ?? process.env;
3051
+ const cwd = canonicalCwd(input.cwd ?? process.cwd());
3052
+ const home = input.homeDir ?? homedir();
3053
+ const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
3054
+ const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
3055
+ const installDecisionsPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"], home, ".codex/lsp-install-decisions.json");
3056
+ return parseLspRequestContext({
3057
+ cwd,
3058
+ projectConfigPaths,
3059
+ userConfigPath,
3060
+ installDecisionsPath,
3061
+ capabilities: { installDecisionTool: true }
3062
+ });
3063
+ }
3064
+ function parseLspRequestContext(value) {
3065
+ if (!isRecord4(value)) {
3066
+ throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
3067
+ }
3068
+ rejectUnknownFields(value, CONTEXT_FIELDS, "context");
3069
+ const cwd = stringField(value, "cwd");
3070
+ const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
3071
+ const userConfigPath = stringField(value, "userConfigPath");
3072
+ const installDecisionsPath = stringField(value, "installDecisionsPath");
3073
+ const capabilities = capabilitiesField(value["capabilities"]);
3074
+ const canonical = canonicalCwd(cwd);
3075
+ for (const path of projectConfigPaths) {
3076
+ requireAbsolutePath(path, "projectConfigPaths");
3077
+ const projectPath = canonicalizeExistingOrNearestAncestor(path);
3078
+ if (!isPathInside(canonical, projectPath)) {
3079
+ throw new LspRequestContextParseError("project_config_outside_cwd", `Project LSP config path must be inside cwd: ${path}`);
3080
+ }
3081
+ }
3082
+ requireAbsolutePath(userConfigPath, "userConfigPath");
3083
+ requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
3084
+ return {
3085
+ cwd: canonical,
3086
+ projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
3087
+ userConfigPath,
3088
+ installDecisionsPath,
3089
+ capabilities
3090
+ };
3091
+ }
3092
+ function translateProjectConfigEnv(value, cwd) {
3093
+ if (value === undefined || value.length === 0)
3094
+ return [join2(cwd, ".codex", "lsp-client.json")];
3095
+ return value.split(delimiter2).filter((entry) => entry.length > 0).map((entry) => isAbsolute2(entry) ? entry : join2(cwd, entry));
3096
+ }
3097
+ function translateHomeConfigEnv(value, home, fallback) {
3098
+ if (value === undefined || value.length === 0)
3099
+ return join2(home, fallback);
3100
+ return isAbsolute2(value) ? value : join2(home, value);
3101
+ }
3102
+ function canonicalCwd(cwd) {
3103
+ const resolved = resolve6(cwd);
3104
+ if (!existsSync5(resolved) || !statSync2(resolved).isDirectory()) {
3105
+ throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
3106
+ }
3107
+ return realpathSync3(resolved);
3108
+ }
3109
+ function canonicalizeExistingOrNearestAncestor(path) {
3110
+ let current = resolve6(path);
3111
+ const suffix = [];
3112
+ while (true) {
3113
+ try {
3114
+ const existing = realpathSync3(current);
3115
+ return suffix.length === 0 ? existing : join2(existing, ...suffix);
3116
+ } catch (error) {
3117
+ if (!isMissingPathError(error))
3118
+ throw error;
3119
+ const parent = dirname4(current);
3120
+ if (parent === current)
3121
+ throw error;
3122
+ suffix.unshift(basename2(current));
3123
+ current = parent;
3124
+ }
3125
+ }
3126
+ }
3127
+ function capabilitiesField(value) {
3128
+ if (!isRecord4(value)) {
3129
+ throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
3130
+ }
3131
+ rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
3132
+ const installDecisionTool = value["installDecisionTool"];
3133
+ if (typeof installDecisionTool !== "boolean") {
3134
+ throw new LspRequestContextParseError("invalid_install_decision_capability", "LSP request capabilities.installDecisionTool must be a boolean.");
3135
+ }
3136
+ return { installDecisionTool };
3137
+ }
3138
+ function stringField(value, field) {
3139
+ const fieldValue = value[field];
3140
+ if (typeof fieldValue !== "string" || fieldValue.length === 0) {
3141
+ throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
3142
+ }
3143
+ return fieldValue;
3144
+ }
3145
+ function stringArrayField(value, field) {
3146
+ const fieldValue = value[field];
3147
+ if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
3148
+ throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string array.`);
3149
+ }
3150
+ return fieldValue;
3151
+ }
3152
+ function requireAbsolutePath(path, field) {
3153
+ if (!isAbsolute2(path)) {
3154
+ throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
3155
+ }
3156
+ }
3157
+ function isPathInside(parent, child) {
3158
+ const childPath = resolve6(child);
3159
+ const relativePath = relative4(parent, childPath);
3160
+ return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute2(relativePath);
3161
+ }
3162
+ function isMissingPathError(error) {
3163
+ const code = errorCode(error);
3164
+ return code === "ENOENT" || code === "ENOTDIR";
3165
+ }
3166
+ function rejectUnknownFields(value, allowed, scope) {
3167
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
3168
+ if (unknown.length > 0) {
3169
+ throw new LspRequestContextParseError("unknown_field", `Unknown LSP request ${scope} field: ${unknown.join(", ")}`);
3170
+ }
3171
+ }
3172
+ function isRecord4(value) {
3173
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3174
+ }
3175
+ function errorCode(error) {
3176
+ if (!error || typeof error !== "object" || !("code" in error))
3177
+ return;
3178
+ const code = Reflect.get(error, "code");
3179
+ return typeof code === "string" ? code : undefined;
3180
+ }
1417
3181
 
1418
3182
  // ../mcp-stdio-core/src/record.ts
1419
3183
  function isPlainRecord(value) {
@@ -1422,75 +3186,404 @@ function isPlainRecord(value) {
1422
3186
 
1423
3187
  // src/ensure-daemon.ts
1424
3188
  import { spawn as spawn2 } from "node:child_process";
1425
- import { closeSync as closeSync2, existsSync as existsSync2, mkdirSync as mkdirSync2, openSync as openSync2 } from "node:fs";
3189
+ import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2 } from "node:fs";
1426
3190
  import { connect } from "node:net";
1427
- import { dirname as dirname2 } from "node:path";
3191
+ import { dirname as dirname6 } from "node:path";
1428
3192
  import { execPath } from "node:process";
1429
- import { fileURLToPath } from "node:url";
1430
3193
 
1431
- // src/lock.ts
1432
- import { closeSync, mkdirSync, openSync, readFileSync as readFileSync2, unlinkSync, writeSync } from "node:fs";
1433
- import { dirname } from "node:path";
1434
- function isProcessAlive(pid) {
1435
- if (!Number.isInteger(pid) || pid <= 0)
1436
- return false;
3194
+ // src/ipc-protocol.ts
3195
+ import { randomBytes, timingSafeEqual } from "node:crypto";
3196
+ import {
3197
+ chmodSync,
3198
+ closeSync,
3199
+ constants,
3200
+ fchmodSync,
3201
+ fstatSync,
3202
+ lstatSync as lstatSync4,
3203
+ mkdirSync,
3204
+ openSync,
3205
+ readFileSync as readFileSync3,
3206
+ unlinkSync,
3207
+ writeSync
3208
+ } from "node:fs";
3209
+ import { dirname as dirname5 } from "node:path";
3210
+ var OMO_DAEMON_PROTOCOL_VERSION = 1;
3211
+ var AUTH_ERROR_CODE = -32001;
3212
+ var PROTOCOL_ERROR_CODE = -32002;
3213
+ var AUTH_TOKEN_BYTES = 32;
3214
+
3215
+ class UnsafePrivateDirectoryError extends Error {
3216
+ path;
3217
+ reason;
3218
+ name = "UnsafePrivateDirectoryError";
3219
+ code = "unsafe_private_directory";
3220
+ constructor(path, reason) {
3221
+ super(`unsafe private directory ${path}: ${reason}`);
3222
+ this.path = path;
3223
+ this.reason = reason;
3224
+ }
3225
+ }
3226
+ function authEnvelope(token) {
3227
+ return { protocolVersion: OMO_DAEMON_PROTOCOL_VERSION, token };
3228
+ }
3229
+ function readAuthToken(paths) {
1437
3230
  try {
1438
- process.kill(pid, 0);
1439
- return true;
3231
+ const token = readFileSync3(paths.auth, "utf8").trim();
3232
+ return token.length > 0 ? token : null;
1440
3233
  } catch (error) {
1441
- return error.code === "EPERM";
3234
+ if (error instanceof Error)
3235
+ return null;
3236
+ throw error;
1442
3237
  }
1443
3238
  }
1444
- function readLockPid(lockPath) {
3239
+ function readOrCreateAuthToken(paths) {
3240
+ const existing = readAuthToken(paths);
3241
+ if (existing)
3242
+ return existing;
3243
+ return createAuthToken(paths);
3244
+ }
3245
+ function rotateAuthToken(paths) {
1445
3246
  try {
1446
- const pid = Number.parseInt(readFileSync2(lockPath, "utf8").trim(), 10);
1447
- return Number.isInteger(pid) ? pid : null;
1448
- } catch {
1449
- return null;
3247
+ unlinkSync(paths.auth);
3248
+ } catch (error) {
3249
+ if (!(error instanceof Error))
3250
+ throw error;
1450
3251
  }
3252
+ return createAuthToken(paths);
1451
3253
  }
1452
- function tryAcquireLock(lockPath, ownerPid = process.pid) {
1453
- mkdirSync(dirname(lockPath), { recursive: true });
1454
- for (let attempt = 0;attempt < 2; attempt += 1) {
1455
- const handle = writeLockFile(lockPath, ownerPid);
1456
- if (handle)
1457
- return handle;
1458
- if (!reapStaleLock(lockPath))
1459
- return null;
3254
+ function authenticateMessage(raw, expectedToken) {
3255
+ const id = jsonRpcId(raw);
3256
+ if (!isPlainRecord(raw))
3257
+ return authError(id);
3258
+ const params = raw["params"];
3259
+ if (!isPlainRecord(params))
3260
+ return authError(id);
3261
+ const envelope = params["_omo"];
3262
+ if (!isPlainRecord(envelope))
3263
+ return authError(id);
3264
+ const protocolVersion = envelope["protocolVersion"];
3265
+ if (protocolVersion !== OMO_DAEMON_PROTOCOL_VERSION)
3266
+ return protocolError(id);
3267
+ const token = envelope["token"];
3268
+ if (typeof token !== "string" || !tokenMatches(token, expectedToken))
3269
+ return authError(id);
3270
+ const cleanParams = { ...params };
3271
+ delete cleanParams["_omo"];
3272
+ return { input: { ...raw, params: cleanParams }, id, method: typeof raw["method"] === "string" ? raw["method"] : undefined };
3273
+ }
3274
+ function isAuthErrorResponse(message) {
3275
+ if (!isPlainRecord(message))
3276
+ return false;
3277
+ const error = message["error"];
3278
+ if (!isPlainRecord(error))
3279
+ return false;
3280
+ const data = error["data"];
3281
+ return error["code"] === AUTH_ERROR_CODE && isPlainRecord(data) && data["code"] === "daemon_authentication_failed";
3282
+ }
3283
+ function writePrivateFile(path, data) {
3284
+ const fd = openSync(path, "w", 384);
3285
+ try {
3286
+ writeSync(fd, data);
3287
+ } finally {
3288
+ closeSync(fd);
1460
3289
  }
1461
- return null;
3290
+ setPrivateFileMode(path);
1462
3291
  }
1463
- function writeLockFile(lockPath, ownerPid) {
3292
+ function ensurePrivateDirectory(path, options = {}) {
1464
3293
  try {
1465
- const fd = openSync(lockPath, "wx");
1466
- writeSync(fd, `${ownerPid}
1467
- `);
3294
+ mkdirSync(path, { recursive: true, mode: 448 });
3295
+ } catch (error) {
3296
+ if (errorCode2(error) !== "EEXIST")
3297
+ throw error;
3298
+ }
3299
+ if (process.platform === "win32")
3300
+ return;
3301
+ const before = validatePrivateDirectory(path, options);
3302
+ const fd = openSync(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
3303
+ try {
3304
+ fchmodSync(fd, 448);
3305
+ const after = validatePrivateDirectory(path, options);
3306
+ const openStats = fstatSync(fd);
3307
+ if (!sameDirectory(before, after) || !sameDirectory(before, openStats)) {
3308
+ throw new UnsafePrivateDirectoryError(path, "changed_during_chmod");
3309
+ }
3310
+ } finally {
1468
3311
  closeSync(fd);
1469
- return { release: () => unlinkQuietly(lockPath) };
3312
+ }
3313
+ }
3314
+ function setPrivateFileMode(path) {
3315
+ if (process.platform !== "win32")
3316
+ chmodSync(path, 384);
3317
+ }
3318
+ function createAuthToken(paths) {
3319
+ ensurePrivateDirectory(dirname5(paths.auth));
3320
+ const token = randomBytes(AUTH_TOKEN_BYTES).toString("base64url");
3321
+ let fd;
3322
+ try {
3323
+ fd = openSync(paths.auth, "wx", 384);
1470
3324
  } catch (error) {
1471
- if (error.code === "EEXIST")
1472
- return null;
3325
+ if (errorCode2(error) === "EEXIST") {
3326
+ const existing = readAuthToken(paths);
3327
+ if (existing)
3328
+ return existing;
3329
+ }
1473
3330
  throw error;
1474
3331
  }
3332
+ try {
3333
+ writeSync(fd, `${token}
3334
+ `);
3335
+ } finally {
3336
+ closeSync(fd);
3337
+ }
3338
+ setPrivateFileMode(paths.auth);
3339
+ return token;
1475
3340
  }
1476
- function reapStaleLock(lockPath) {
1477
- const pid = readLockPid(lockPath);
1478
- if (pid !== null && isProcessAlive(pid))
1479
- return false;
1480
- unlinkQuietly(lockPath);
1481
- return true;
3341
+ function errorCode2(error) {
3342
+ if (!error || typeof error !== "object" || !("code" in error))
3343
+ return;
3344
+ const code = Reflect.get(error, "code");
3345
+ return typeof code === "string" ? code : undefined;
3346
+ }
3347
+ function validatePrivateDirectory(path, options) {
3348
+ const stats = options.lstat ? options.lstat(path) : lstatPrivateDirectory(path);
3349
+ if (stats.isSymbolicLink())
3350
+ throw new UnsafePrivateDirectoryError(path, "symlink");
3351
+ if (!stats.isDirectory())
3352
+ throw new UnsafePrivateDirectoryError(path, "not_directory");
3353
+ const currentUid = options.currentUid ? options.currentUid() : process.getuid?.();
3354
+ if (currentUid !== undefined && stats.uid !== currentUid) {
3355
+ throw new UnsafePrivateDirectoryError(path, "wrong_owner");
3356
+ }
3357
+ return stats;
3358
+ }
3359
+ function lstatPrivateDirectory(path) {
3360
+ return lstatSync4(path);
3361
+ }
3362
+ function sameDirectory(a, b) {
3363
+ return a.dev === b.dev && a.ino === b.ino;
3364
+ }
3365
+ function tokenMatches(candidate, expected) {
3366
+ const candidateBytes = Buffer.from(candidate);
3367
+ const expectedBytes = Buffer.from(expected);
3368
+ return candidateBytes.length === expectedBytes.length && timingSafeEqual(candidateBytes, expectedBytes);
3369
+ }
3370
+ function jsonRpcId(raw) {
3371
+ if (!isPlainRecord(raw))
3372
+ return null;
3373
+ const id = raw["id"];
3374
+ return typeof id === "string" || typeof id === "number" || id === null ? id : null;
3375
+ }
3376
+ function authError(id) {
3377
+ return {
3378
+ jsonrpc: "2.0",
3379
+ id,
3380
+ error: { code: AUTH_ERROR_CODE, message: "daemon authentication failed", data: { code: "daemon_authentication_failed" } }
3381
+ };
3382
+ }
3383
+ function protocolError(id) {
3384
+ return {
3385
+ jsonrpc: "2.0",
3386
+ id,
3387
+ error: { code: PROTOCOL_ERROR_CODE, message: "daemon protocol mismatch", data: { code: "daemon_protocol_mismatch" } }
3388
+ };
3389
+ }
3390
+
3391
+ // src/paths.ts
3392
+ import { createHash as createHash2 } from "node:crypto";
3393
+ import { createRequire } from "node:module";
3394
+ import { homedir as homedir2, tmpdir, userInfo } from "node:os";
3395
+ import * as path from "node:path";
3396
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
3397
+
3398
+ // src/runtime-contract.ts
3399
+ import { statSync as statSync3 } from "node:fs";
3400
+ import { isAbsolute as isAbsolute3 } from "node:path";
3401
+ var OMO_LSP_DAEMON_DIR = "OMO_LSP_DAEMON_DIR";
3402
+ var OMO_LSP_DAEMON_CLI = "OMO_LSP_DAEMON_CLI";
3403
+ var OMO_LSP_DAEMON_VERSION = "OMO_LSP_DAEMON_VERSION";
3404
+ var DAEMON_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
3405
+
3406
+ class InvalidRuntimeOverrideError extends Error {
3407
+ code = "invalid_runtime_override";
3408
+ reason;
3409
+ constructor(reason, message) {
3410
+ super(message);
3411
+ this.name = "InvalidRuntimeOverrideError";
3412
+ this.reason = reason;
3413
+ }
1482
3414
  }
1483
- function unlinkQuietly(path) {
3415
+
3416
+ class InvalidDaemonVersionError extends Error {
3417
+ code = "invalid_daemon_version";
3418
+ version;
3419
+ constructor(version) {
3420
+ super("LSP daemon version must match [A-Za-z0-9][A-Za-z0-9._+-]{0,127}");
3421
+ this.name = "InvalidDaemonVersionError";
3422
+ this.version = version;
3423
+ }
3424
+ }
3425
+ function validateDaemonVersion(version) {
3426
+ if (!DAEMON_VERSION_PATTERN.test(version))
3427
+ throw new InvalidDaemonVersionError(version);
3428
+ return version;
3429
+ }
3430
+ function resolveDaemonRuntime(env, defaults) {
3431
+ const cliOverride = env[OMO_LSP_DAEMON_CLI];
3432
+ const versionOverride = env[OMO_LSP_DAEMON_VERSION];
3433
+ const hasCliOverride = cliOverride !== undefined;
3434
+ const hasVersionOverride = versionOverride !== undefined;
3435
+ if (hasCliOverride !== hasVersionOverride) {
3436
+ throw new InvalidRuntimeOverrideError("paired_values_required", `${OMO_LSP_DAEMON_CLI} and ${OMO_LSP_DAEMON_VERSION} must be set together`);
3437
+ }
3438
+ if (!hasCliOverride || !hasVersionOverride) {
3439
+ if (!isAbsolute3(defaults.cliPath)) {
3440
+ throw new InvalidRuntimeOverrideError("packaged_cli_must_be_absolute", "Packaged LSP daemon CLI path must be absolute");
3441
+ }
3442
+ return { cliPath: defaults.cliPath, version: validateDaemonVersion(defaults.version) };
3443
+ }
3444
+ if (!isAbsolute3(cliOverride)) {
3445
+ throw new InvalidRuntimeOverrideError("cli_must_be_absolute", `${OMO_LSP_DAEMON_CLI} must be an absolute path to an existing regular file`);
3446
+ }
3447
+ let cliStats;
1484
3448
  try {
1485
- unlinkSync(path);
1486
- } catch (error) {}
3449
+ cliStats = statSync3(cliOverride);
3450
+ } catch (error) {
3451
+ if (!(error instanceof Error))
3452
+ throw error;
3453
+ throw new InvalidRuntimeOverrideError("cli_not_found", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
3454
+ }
3455
+ if (!cliStats.isFile()) {
3456
+ throw new InvalidRuntimeOverrideError("cli_not_file", `${OMO_LSP_DAEMON_CLI} must name an existing regular file`);
3457
+ }
3458
+ return { cliPath: cliOverride, version: validateDaemonVersion(versionOverride) };
3459
+ }
3460
+
3461
+ // src/paths.ts
3462
+ var requireFromHere = createRequire(import.meta.url);
3463
+ var MAX_SOCKET_PATH_LENGTH = 100;
3464
+
3465
+ class InvalidDaemonDirectoryError extends Error {
3466
+ code = "invalid_daemon_directory";
3467
+ directory;
3468
+ constructor(directory) {
3469
+ super(`${OMO_LSP_DAEMON_DIR} must be an absolute path`);
3470
+ this.name = "InvalidDaemonDirectoryError";
3471
+ this.directory = directory;
3472
+ }
3473
+ }
3474
+ function resolveDaemonVersion(requireFn = requireFromHere) {
3475
+ for (const candidate of ["./package.json", "../package.json"]) {
3476
+ let loaded;
3477
+ try {
3478
+ loaded = requireFn(candidate);
3479
+ } catch (error) {
3480
+ if (!(error instanceof Error))
3481
+ throw error;
3482
+ continue;
3483
+ }
3484
+ if (typeof loaded === "object" && loaded !== null && "version" in loaded) {
3485
+ const version = Reflect.get(loaded, "version");
3486
+ if (typeof version === "string")
3487
+ return validateDaemonVersion(version);
3488
+ }
3489
+ }
3490
+ return "0";
3491
+ }
3492
+ function packagedRuntimeDefaults() {
3493
+ return {
3494
+ cliPath: fileURLToPath2(new URL("./cli.js", import.meta.url)),
3495
+ version: resolveDaemonVersion()
3496
+ };
3497
+ }
3498
+ function daemonBaseDir(env = process.env, platform = defaultDaemonPlatform()) {
3499
+ const override = env[OMO_LSP_DAEMON_DIR];
3500
+ if (override !== undefined) {
3501
+ if (!platform.path.isAbsolute(override))
3502
+ throw new InvalidDaemonDirectoryError(override);
3503
+ return platform.path.resolve(override);
3504
+ }
3505
+ return platform.path.resolve(platform.path.join(platform.homedir(), ".omo", "lsp-daemon"));
3506
+ }
3507
+ function daemonPaths(env = process.env, runtimeDefaults = packagedRuntimeDefaults(), platform = defaultDaemonPlatform()) {
3508
+ const runtime = resolveDaemonRuntime(env, runtimeDefaults);
3509
+ const baseDir = daemonBaseDir(env, platform);
3510
+ const dir = platform.path.resolve(platform.path.join(baseDir, `v${runtime.version}`));
3511
+ return {
3512
+ version: runtime.version,
3513
+ cliPath: runtime.cliPath,
3514
+ dir,
3515
+ socket: resolveSocketPath(dir, runtime.version, platform),
3516
+ lock: platform.path.join(dir, "daemon.lock"),
3517
+ pid: platform.path.join(dir, "daemon.pid"),
3518
+ auth: platform.path.join(dir, "daemon.auth"),
3519
+ endpoint: platform.path.join(dir, "daemon.endpoint"),
3520
+ owner: platform.path.join(dir, "daemon.owner"),
3521
+ log: platform.path.join(dir, "daemon.log")
3522
+ };
3523
+ }
3524
+ function defaultDaemonPlatform() {
3525
+ return {
3526
+ platform: process.platform,
3527
+ homedir: homedir2,
3528
+ tmpdir,
3529
+ getuid: () => typeof process.getuid === "function" ? process.getuid() : undefined,
3530
+ username: () => userInfo().username,
3531
+ path
3532
+ };
3533
+ }
3534
+ function resolveSocketPath(dir, version, platform) {
3535
+ const canonicalVersionDir = platform.path.resolve(dir);
3536
+ if (platform.platform === "win32") {
3537
+ const currentUserDiscriminator = `${platform.getuid() ?? "win"}:${platform.username()}:${platform.path.resolve(platform.homedir())}`;
3538
+ const digest = shortDigest(`${canonicalVersionDir}\x00${currentUserDiscriminator}`);
3539
+ return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
3540
+ }
3541
+ const natural = platform.path.join(canonicalVersionDir, "daemon.sock");
3542
+ if (natural.length < MAX_SOCKET_PATH_LENGTH)
3543
+ return natural;
3544
+ return platform.path.join(platform.tmpdir(), `omo-lsp-${version}-${shortDigest(canonicalVersionDir)}`, "daemon.sock");
3545
+ }
3546
+ function shortDigest(value) {
3547
+ return createHash2("sha256").update(value).digest("hex").slice(0, 16);
3548
+ }
3549
+
3550
+ // src/socket-jsonrpc.ts
3551
+ function encodeJsonLine(message) {
3552
+ return `${JSON.stringify(message)}
3553
+ `;
3554
+ }
3555
+ function createLineDecoder(onMessage, onParseError) {
3556
+ let buffer = "";
3557
+ return {
3558
+ push(chunk) {
3559
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
3560
+ let index = buffer.indexOf(`
3561
+ `);
3562
+ while (index !== -1) {
3563
+ const raw = buffer.slice(0, index).trim();
3564
+ buffer = buffer.slice(index + 1);
3565
+ if (raw.length > 0) {
3566
+ try {
3567
+ onMessage(JSON.parse(raw));
3568
+ } catch (error) {
3569
+ if (error instanceof Error) {
3570
+ onParseError?.(raw, error);
3571
+ } else {
3572
+ throw error;
3573
+ }
3574
+ }
3575
+ }
3576
+ index = buffer.indexOf(`
3577
+ `);
3578
+ }
3579
+ }
3580
+ };
1487
3581
  }
1488
3582
 
1489
3583
  // src/ensure-daemon.ts
1490
3584
  var PROBE_TIMEOUT_MS = 500;
1491
3585
  var DEFAULT_READY_TIMEOUT_MS = 5000;
1492
3586
  var DEFAULT_POLL_INTERVAL_MS = 100;
1493
- var CODEX_LSP_DAEMON_CLI_ENV = "CODEX_LSP_DAEMON_CLI";
1494
3587
 
1495
3588
  class DaemonUnreachableError extends Error {
1496
3589
  constructor(socketPath) {
@@ -1498,61 +3591,62 @@ class DaemonUnreachableError extends Error {
1498
3591
  this.name = "DaemonUnreachableError";
1499
3592
  }
1500
3593
  }
1501
- async function ensureDaemonRunning(paths, deps = defaultEnsureDaemonDeps(), options = {}) {
1502
- const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
1503
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
1504
- if (await deps.probe(paths.socket))
1505
- return;
1506
- const lock = deps.acquireLock(paths.lock);
1507
- if (!lock) {
1508
- await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
1509
- return;
1510
- }
1511
- try {
1512
- if (await deps.probe(paths.socket))
1513
- return;
1514
- deps.cleanupStaleSocket(paths.socket);
1515
- deps.spawnDaemon(paths);
1516
- await waitUntilReachable(paths.socket, deps, readyTimeoutMs, pollIntervalMs);
1517
- } finally {
1518
- lock.release();
1519
- }
1520
- }
1521
- async function waitUntilReachable(socketPath, deps, readyTimeoutMs, pollIntervalMs) {
3594
+ async function ensureDaemonRunning(paths, deps = defaultEnsureDaemonDeps(), options = {}) {
3595
+ const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
3596
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
3597
+ if (await deps.probe(paths))
3598
+ return;
3599
+ deps.spawnDaemon(paths);
3600
+ await waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs);
3601
+ }
3602
+ async function waitUntilReachable(paths, deps, readyTimeoutMs, pollIntervalMs) {
1522
3603
  const deadline = deps.now() + readyTimeoutMs;
1523
3604
  for (;; ) {
1524
- if (await deps.probe(socketPath))
3605
+ if (await deps.probe(paths))
1525
3606
  return;
1526
3607
  if (deps.now() >= deadline)
1527
- throw new DaemonUnreachableError(socketPath);
3608
+ throw new DaemonUnreachableError(paths.socket);
1528
3609
  await deps.sleep(pollIntervalMs);
1529
3610
  }
1530
3611
  }
1531
- function probeSocket(socketPath, timeoutMs = PROBE_TIMEOUT_MS) {
1532
- return new Promise((resolve2) => {
1533
- const socket = connect(socketPath);
1534
- const finish = (ok) => {
3612
+ async function probeDaemon(paths, timeoutMs = PROBE_TIMEOUT_MS) {
3613
+ const token = readAuthToken(paths);
3614
+ if (!token)
3615
+ return false;
3616
+ return await pingDaemon(paths, token, timeoutMs) !== null;
3617
+ }
3618
+ function pingDaemon(paths, token, timeoutMs = PROBE_TIMEOUT_MS) {
3619
+ return new Promise((resolve7) => {
3620
+ const socket = connect(paths.socket);
3621
+ let settled = false;
3622
+ const finish = (value) => {
3623
+ if (settled)
3624
+ return;
3625
+ settled = true;
1535
3626
  socket.destroy();
1536
- resolve2(ok);
3627
+ resolve7(value);
1537
3628
  };
1538
- const timer = setTimeout(() => finish(false), timeoutMs);
3629
+ const timer = setTimeout(() => finish(null), timeoutMs);
1539
3630
  timer.unref?.();
1540
- socket.once("connect", () => {
3631
+ const decoder = createLineDecoder((message) => {
1541
3632
  clearTimeout(timer);
1542
- finish(true);
3633
+ finish(parsePingResponse(message));
3634
+ });
3635
+ socket.once("connect", () => {
3636
+ socket.write(encodeJsonLine({ jsonrpc: "2.0", id: 1, method: "omo/ping", params: { _omo: authEnvelope(token) } }));
1543
3637
  });
3638
+ socket.on("data", (chunk) => decoder.push(chunk));
1544
3639
  socket.once("error", () => {
1545
3640
  clearTimeout(timer);
1546
- finish(false);
3641
+ finish(null);
1547
3642
  });
1548
3643
  });
1549
3644
  }
1550
3645
  function spawnDaemonProcess(paths) {
1551
- mkdirSync2(dirname2(paths.log), { recursive: true });
3646
+ mkdirSync2(dirname6(paths.log), { recursive: true });
1552
3647
  const logFd = openSync2(paths.log, "a");
1553
3648
  try {
1554
- const cliPath = resolveDaemonCliPath();
1555
- const child = spawn2(execPath, [cliPath, "daemon"], {
3649
+ const child = spawn2(execPath, [paths.cliPath, "daemon"], {
1556
3650
  detached: true,
1557
3651
  stdio: ["ignore", logFd, logFd]
1558
3652
  });
@@ -1561,81 +3655,44 @@ function spawnDaemonProcess(paths) {
1561
3655
  closeSync2(logFd);
1562
3656
  }
1563
3657
  }
1564
- function resolveDaemonCliPath(env = process.env) {
1565
- const override = env[CODEX_LSP_DAEMON_CLI_ENV]?.trim();
1566
- if (override)
1567
- return override;
1568
- return fileURLToPath(new URL("./cli.js", import.meta.url));
1569
- }
1570
3658
  function defaultEnsureDaemonDeps() {
1571
3659
  return {
1572
- probe: (socketPath) => probeSocket(socketPath),
1573
- acquireLock: (lockPath) => tryAcquireLock(lockPath),
1574
- cleanupStaleSocket: (socketPath) => {
1575
- if (existsSync2(socketPath))
1576
- unlinkQuietly(socketPath);
1577
- },
3660
+ probe: (paths) => probeDaemon(paths),
1578
3661
  spawnDaemon: (paths) => spawnDaemonProcess(paths),
1579
- sleep: (ms) => new Promise((resolve2) => {
1580
- setTimeout(resolve2, ms);
3662
+ sleep: (ms) => new Promise((resolve7) => {
3663
+ setTimeout(resolve7, ms);
1581
3664
  }),
1582
3665
  now: () => Date.now()
1583
3666
  };
1584
3667
  }
1585
-
1586
- // src/paths.ts
1587
- import { createHash } from "node:crypto";
1588
- import { createRequire } from "node:module";
1589
- import { homedir, tmpdir } from "node:os";
1590
- import { join as join2 } from "node:path";
1591
- var requireFromHere = createRequire(import.meta.url);
1592
- var MAX_SOCKET_PATH_LENGTH = 100;
1593
- var CODEX_LSP_DAEMON_VERSION_ENV = "CODEX_LSP_DAEMON_VERSION";
1594
- function resolveDaemonVersion(requireFn = requireFromHere) {
1595
- for (const candidate of ["./package.json", "../package.json"]) {
1596
- try {
1597
- const pkg = requireFn(candidate);
1598
- if (typeof pkg.version === "string" && pkg.version.length > 0)
1599
- return pkg.version;
1600
- } catch {}
1601
- }
1602
- return "0";
1603
- }
1604
- function daemonBaseDir(env = process.env) {
1605
- const explicit = env["CODEX_LSP_DAEMON_DIR"]?.trim();
1606
- if (explicit)
1607
- return explicit;
1608
- const pluginData = env["PLUGIN_DATA"]?.trim();
1609
- if (pluginData)
1610
- return join2(pluginData, "daemon");
1611
- const codexHome = env["CODEX_HOME"]?.trim();
1612
- const home = codexHome && codexHome.length > 0 ? codexHome : join2(homedir(), ".codex");
1613
- return join2(home, "codex-lsp", "daemon");
1614
- }
1615
- function daemonPaths(env = process.env, version = resolveDaemonVersionFromEnv(env) ?? resolveDaemonVersion()) {
1616
- const dir = join2(daemonBaseDir(env), `v${version}`);
1617
- return {
1618
- version,
1619
- dir,
1620
- socket: resolveSocketPath(dir, version),
1621
- lock: join2(dir, "daemon.lock"),
1622
- pid: join2(dir, "daemon.pid"),
1623
- log: join2(dir, "daemon.log")
1624
- };
1625
- }
1626
- function resolveDaemonVersionFromEnv(env = process.env) {
1627
- const version = env[CODEX_LSP_DAEMON_VERSION_ENV]?.trim();
1628
- return version && version.length > 0 ? version : null;
1629
- }
1630
- function resolveSocketPath(dir, version) {
1631
- const digest = createHash("sha256").update(dir).digest("hex").slice(0, 16);
1632
- if (process.platform === "win32") {
1633
- return `\\\\.\\pipe\\omo-lsp-${version}-${digest}`;
3668
+ function parsePingResponse(message) {
3669
+ if (!message || typeof message !== "object" || Array.isArray(message))
3670
+ return null;
3671
+ const result = Reflect.get(message, "result");
3672
+ if (!result || typeof result !== "object" || Array.isArray(result))
3673
+ return null;
3674
+ const pid = Reflect.get(result, "pid");
3675
+ const nonce = Reflect.get(result, "nonce");
3676
+ const startedAt = Reflect.get(result, "startedAt");
3677
+ const endpoint = Reflect.get(result, "endpoint");
3678
+ if (typeof pid !== "number" || typeof nonce !== "string" || typeof startedAt !== "string")
3679
+ return null;
3680
+ if (!endpoint || typeof endpoint !== "object" || Array.isArray(endpoint))
3681
+ return null;
3682
+ const path2 = Reflect.get(endpoint, "path");
3683
+ const kind = Reflect.get(endpoint, "kind");
3684
+ if (typeof path2 !== "string")
3685
+ return null;
3686
+ if (kind === "windows")
3687
+ return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
3688
+ if (kind === "missing")
3689
+ return { pid, nonce, startedAt, endpoint: { kind, path: path2 } };
3690
+ const dev = Reflect.get(endpoint, "dev");
3691
+ const ino = Reflect.get(endpoint, "ino");
3692
+ if (kind === "unix" && typeof dev === "number" && typeof ino === "number") {
3693
+ return { pid, nonce, startedAt, endpoint: { kind, path: path2, dev, ino } };
1634
3694
  }
1635
- const natural = join2(dir, "daemon.sock");
1636
- if (natural.length < MAX_SOCKET_PATH_LENGTH)
1637
- return natural;
1638
- return join2(tmpdir(), `omo-lsp-${version}-${digest}.sock`);
3695
+ return null;
1639
3696
  }
1640
3697
  // ../mcp-stdio-core/src/responses.ts
1641
3698
  function successResponse(id, result) {
@@ -1644,12 +3701,9 @@ function successResponse(id, result) {
1644
3701
  function errorResponse(id, code, message, data) {
1645
3702
  return { jsonrpc: "2.0", id, error: data === undefined ? { code, message } : { code, message, data } };
1646
3703
  }
1647
- function jsonRpcId(value) {
3704
+ function jsonRpcId2(value) {
1648
3705
  return typeof value === "string" || typeof value === "number" || value === null ? value : null;
1649
3706
  }
1650
- function messageFromError(error) {
1651
- return error instanceof Error ? error.message : String(error);
1652
- }
1653
3707
  // ../mcp-stdio-core/src/transport.ts
1654
3708
  var HEADER_SEPARATOR2 = Buffer.from(`\r
1655
3709
  \r
@@ -1796,7 +3850,7 @@ function handleParseError(message, config, log) {
1796
3850
  }
1797
3851
  async function handleRequest(message, config, log) {
1798
3852
  const parsed = message.payload;
1799
- const id = isPlainRecord(parsed) ? jsonRpcId(parsed["id"]) : null;
3853
+ const id = isPlainRecord(parsed) ? jsonRpcId2(parsed["id"]) : null;
1800
3854
  const method = isPlainRecord(parsed) && typeof parsed["method"] === "string" ? parsed["method"] : null;
1801
3855
  log("request", { id: id === null ? null : String(id), method });
1802
3856
  try {
@@ -1836,29 +3890,22 @@ function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
1836
3890
  closed: () => isClosed
1837
3891
  };
1838
3892
  }
1839
- // ../lsp-core/src/tools/diagnostics.ts
1840
- import { resolve as resolve4 } from "node:path";
1841
-
1842
3893
  // ../lsp-core/src/lsp/client-wrapper.ts
1843
- import { existsSync as existsSync6, statSync as statSync2 } from "node:fs";
1844
- import { dirname as dirname4, join as join6, resolve as resolve2 } from "node:path";
3894
+ import { existsSync as existsSync9, statSync as statSync4 } from "node:fs";
3895
+ import { dirname as dirname8, join as join4, resolve as resolve7 } from "node:path";
1845
3896
 
1846
3897
  // ../lsp-core/src/lsp/server-install-state.ts
1847
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, writeFileSync } from "node:fs";
1848
- import { homedir as homedir2 } from "node:os";
1849
- import { dirname as dirname3, isAbsolute, join as join3 } from "node:path";
3898
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync4, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "node:fs";
3899
+ import { dirname as dirname7 } from "node:path";
1850
3900
  function getInstallDecisionsPath() {
1851
- const override = contextEnv("LSP_TOOLS_MCP_INSTALL_DECISIONS");
1852
- if (!override)
1853
- return join3(homedir2(), ".codex", "lsp-install-decisions.json");
1854
- return isAbsolute(override) ? override : join3(homedir2(), override);
3901
+ return lspRequestContext().installDecisionsPath;
1855
3902
  }
1856
3903
  function loadInstallDecisions() {
1857
- const path = getInstallDecisionsPath();
1858
- if (!existsSync3(path))
3904
+ const path2 = getInstallDecisionsPath();
3905
+ if (!existsSync6(path2))
1859
3906
  return {};
1860
3907
  try {
1861
- const parsed = JSON.parse(readFileSync3(path, "utf8"));
3908
+ const parsed = JSON.parse(readFileSync4(path2, "utf8"));
1862
3909
  return isInstallDecisions(parsed) ? parsed : {};
1863
3910
  } catch {
1864
3911
  return {};
@@ -1876,29 +3923,27 @@ function isInstallDecision(value) {
1876
3923
  return value === "declined" || value === "allowed";
1877
3924
  }
1878
3925
  function writeInstallDecisions(decisions) {
1879
- const path = getInstallDecisionsPath();
1880
- mkdirSync3(dirname3(path), { recursive: true });
1881
- const tmpPath = `${path}.tmp`;
1882
- writeFileSync(tmpPath, `${JSON.stringify(decisions, null, 2)}
3926
+ const path2 = getInstallDecisionsPath();
3927
+ mkdirSync3(dirname7(path2), { recursive: true });
3928
+ const tmpPath = `${path2}.tmp`;
3929
+ writeFileSync2(tmpPath, `${JSON.stringify(decisions, null, 2)}
1883
3930
  `, "utf8");
1884
- renameSync(tmpPath, path);
3931
+ renameSync2(tmpPath, path2);
1885
3932
  }
1886
3933
  function isInstallDecisions(value) {
1887
- return isRecord2(value) && Object.values(value).every(isInstallDecisionRecord);
3934
+ return isRecord5(value) && Object.values(value).every(isInstallDecisionRecord);
1888
3935
  }
1889
3936
  function isInstallDecisionRecord(value) {
1890
- if (!isRecord2(value))
3937
+ if (!isRecord5(value))
1891
3938
  return false;
1892
3939
  return isInstallDecision(value["decision"]) && typeof value["decidedAt"] === "string";
1893
3940
  }
1894
- function isRecord2(value) {
3941
+ function isRecord5(value) {
1895
3942
  return typeof value === "object" && value !== null && !Array.isArray(value);
1896
3943
  }
1897
3944
 
1898
3945
  // ../lsp-core/src/lsp/config-loader.ts
1899
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
1900
- import { homedir as homedir3 } from "node:os";
1901
- import { delimiter as delimiter2, isAbsolute as isAbsolute2, join as join4 } from "node:path";
3946
+ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
1902
3947
 
1903
3948
  // ../lsp-core/src/lsp/server-definitions.ts
1904
3949
  var LSP_INSTALL_HINTS = {
@@ -2048,27 +4093,17 @@ var BUILTIN_SERVERS = {
2048
4093
  };
2049
4094
 
2050
4095
  // ../lsp-core/src/lsp/config-loader.ts
2051
- function resolveProjectConfigPath(path) {
2052
- return isAbsolute2(path) ? path : join4(contextCwd(), path);
2053
- }
2054
4096
  function getProjectConfigPaths() {
2055
- const projectOverride = contextEnv("LSP_TOOLS_MCP_PROJECT_CONFIG");
2056
- if (projectOverride) {
2057
- return projectOverride.split(delimiter2).filter(Boolean).map(resolveProjectConfigPath);
2058
- }
2059
- return [join4(contextCwd(), ".codex", "lsp-client.json")];
4097
+ return lspRequestContext().projectConfigPaths;
2060
4098
  }
2061
4099
  function getUserConfigPath() {
2062
- const userOverride = contextEnv("LSP_TOOLS_MCP_USER_CONFIG");
2063
- if (!userOverride)
2064
- return join4(homedir3(), ".codex", "lsp-client.json");
2065
- return isAbsolute2(userOverride) ? userOverride : join4(homedir3(), userOverride);
4100
+ return lspRequestContext().userConfigPath;
2066
4101
  }
2067
- function loadJsonFile(path) {
2068
- if (!existsSync4(path))
4102
+ function loadJsonFile(path2) {
4103
+ if (!existsSync7(path2))
2069
4104
  return null;
2070
4105
  try {
2071
- const parsed = JSON.parse(readFileSync4(path, "utf-8"));
4106
+ const parsed = JSON.parse(readFileSync5(path2, "utf-8"));
2072
4107
  return isConfigJson(parsed) ? parsed : null;
2073
4108
  } catch {
2074
4109
  return null;
@@ -2085,8 +4120,8 @@ function loadAllConfigs() {
2085
4120
  return configs;
2086
4121
  }
2087
4122
  function loadFirstJsonFile(paths) {
2088
- for (const path of paths) {
2089
- const config = loadJsonFile(path);
4123
+ for (const path2 of paths) {
4124
+ const config = loadJsonFile(path2);
2090
4125
  if (config)
2091
4126
  return config;
2092
4127
  }
@@ -2207,16 +4242,16 @@ function applyOptionalServerFields(server2, entry) {
2207
4242
  }
2208
4243
  }
2209
4244
  function isConfigJson(value) {
2210
- if (!isRecord3(value))
4245
+ if (!isRecord6(value))
2211
4246
  return false;
2212
4247
  const lsp = value["lsp"];
2213
- return lsp === undefined || isRecord3(lsp);
4248
+ return lsp === undefined || isRecord6(lsp);
2214
4249
  }
2215
4250
  function parseLspEntry(value) {
2216
4251
  return isLspEntry(value) ? value : null;
2217
4252
  }
2218
4253
  function isLspEntry(value) {
2219
- if (!isRecord3(value))
4254
+ if (!isRecord6(value))
2220
4255
  return false;
2221
4256
  const disabled = value["disabled"];
2222
4257
  const command = value["command"];
@@ -2224,15 +4259,15 @@ function isLspEntry(value) {
2224
4259
  const priority = value["priority"];
2225
4260
  const env = value["env"];
2226
4261
  const initialization = value["initialization"];
2227
- return (disabled === undefined || typeof disabled === "boolean") && (command === undefined || isStringArray(command)) && (extensions === undefined || isStringArray(extensions)) && (priority === undefined || typeof priority === "number") && (env === undefined || isStringRecord(env)) && (initialization === undefined || isRecord3(initialization));
4262
+ return (disabled === undefined || typeof disabled === "boolean") && (command === undefined || isStringArray(command)) && (extensions === undefined || isStringArray(extensions)) && (priority === undefined || typeof priority === "number") && (env === undefined || isStringRecord(env)) && (initialization === undefined || isRecord6(initialization));
2228
4263
  }
2229
4264
  function isStringArray(value) {
2230
4265
  return Array.isArray(value) && value.every((item) => typeof item === "string");
2231
4266
  }
2232
4267
  function isStringRecord(value) {
2233
- return isRecord3(value) && Object.values(value).every((item) => typeof item === "string");
4268
+ return isRecord6(value) && Object.values(value).every((item) => typeof item === "string");
2234
4269
  }
2235
- function isRecord3(value) {
4270
+ function isRecord6(value) {
2236
4271
  return typeof value === "object" && value !== null && !Array.isArray(value);
2237
4272
  }
2238
4273
  function getDisabledServerIds() {
@@ -2253,8 +4288,8 @@ function getDisabledServerIds() {
2253
4288
  }
2254
4289
 
2255
4290
  // ../lsp-core/src/lsp/server-installation.ts
2256
- import { existsSync as existsSync5 } from "node:fs";
2257
- import { delimiter as delimiter3, join as join5 } from "node:path";
4291
+ import { existsSync as existsSync8 } from "node:fs";
4292
+ import { delimiter as delimiter3, join as join3 } from "node:path";
2258
4293
  function isServerInstalled(command, _workingDirectory) {
2259
4294
  if (command.length === 0)
2260
4295
  return false;
@@ -2262,7 +4297,7 @@ function isServerInstalled(command, _workingDirectory) {
2262
4297
  if (!cmd)
2263
4298
  return false;
2264
4299
  if (cmd.includes("/") || cmd.includes("\\")) {
2265
- if (existsSync5(cmd))
4300
+ if (existsSync8(cmd))
2266
4301
  return true;
2267
4302
  }
2268
4303
  const isWindows = process.platform === "win32";
@@ -2283,7 +4318,7 @@ function isServerInstalled(command, _workingDirectory) {
2283
4318
  const paths = pathEnv.split(delimiter3);
2284
4319
  for (const p of paths) {
2285
4320
  for (const suffix of exts) {
2286
- if (existsSync5(join5(p, cmd + suffix))) {
4321
+ if (existsSync8(join3(p, cmd + suffix))) {
2287
4322
  return true;
2288
4323
  }
2289
4324
  }
@@ -2382,39 +4417,50 @@ function getAllServers() {
2382
4417
  var WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
2383
4418
  function isDirectoryPath(filePath) {
2384
4419
  try {
2385
- return statSync2(filePath).isDirectory();
4420
+ return statSync4(filePath).isDirectory();
2386
4421
  } catch {
2387
4422
  return false;
2388
4423
  }
2389
4424
  }
2390
4425
  function findWorkspaceRoot(filePath) {
2391
- const abs = resolve2(contextCwd(), filePath);
4426
+ const abs = resolvePathInsideContext(filePath);
2392
4427
  let dir = abs;
2393
4428
  if (!isDirectoryPath(dir)) {
2394
- dir = dirname4(dir);
4429
+ dir = dirname8(dir);
2395
4430
  }
2396
4431
  let prevDir = "";
2397
4432
  while (dir !== prevDir) {
2398
4433
  for (const marker of WORKSPACE_MARKERS) {
2399
- if (existsSync6(join6(dir, marker))) {
4434
+ if (existsSync9(join4(dir, marker))) {
2400
4435
  return dir;
2401
4436
  }
2402
4437
  }
2403
4438
  prevDir = dir;
2404
- dir = dirname4(dir);
4439
+ dir = dirname8(dir);
4440
+ }
4441
+ return dirname8(abs);
4442
+ }
4443
+ function resolvePathInsideContext(filePath) {
4444
+ const cwd = contextCwd();
4445
+ const abs = resolve7(cwd, filePath);
4446
+ const canonical = canonicalizeExistingOrNearestAncestor(abs);
4447
+ if (!isPathInside(cwd, canonical)) {
4448
+ throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
2405
4449
  }
2406
- return dirname4(abs);
4450
+ return canonical;
2407
4451
  }
2408
4452
  function formatServerLookupError(result) {
2409
4453
  if (result.status === "not_installed") {
2410
4454
  return formatNotInstalled(result);
2411
4455
  }
4456
+ const context = lspRequestContext();
4457
+ const firstProjectConfigPath = context.projectConfigPaths[0] ?? "<project lsp config>";
2412
4458
  return [
2413
4459
  `No LSP server configured for extension: ${result.extension}`,
2414
4460
  "",
2415
4461
  `Available servers: ${result.availableServers.slice(0, 10).join(", ")}${result.availableServers.length > 10 ? "..." : ""}`,
2416
4462
  "",
2417
- "Configure a custom server in '.codex/lsp-client.json':",
4463
+ `Configure a custom server in '${firstProjectConfigPath}' or '${context.userConfigPath}':`,
2418
4464
  " {",
2419
4465
  ' "lsp": {',
2420
4466
  ' "my-server": {',
@@ -2430,6 +4476,7 @@ function formatNotInstalled(result) {
2430
4476
  const { server: server2, installHint } = result;
2431
4477
  const extensions = server2.extensions.join(", ");
2432
4478
  const decision = loadInstallDecision(server2.id)?.decision;
4479
+ const context = lspRequestContext();
2433
4480
  if (decision === "declined") {
2434
4481
  return `LSP server '${server2.id}' (${extensions}) is NOT INSTALLED; user previously declined installation — proceed without LSP.`;
2435
4482
  }
@@ -2445,6 +4492,17 @@ function formatNotInstalled(result) {
2445
4492
  "The user has pre-authorized LSP installation. Run the install command, then retry this tool:",
2446
4493
  ` ${installHint}`
2447
4494
  ].join(`
4495
+ `);
4496
+ }
4497
+ if (!context.capabilities.installDecisionTool) {
4498
+ return [
4499
+ ...header,
4500
+ "To install, run:",
4501
+ ` ${installHint}`,
4502
+ "",
4503
+ "ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
4504
+ "Install-decision recording is unavailable in this harness; proceed without LSP if the user declines."
4505
+ ].join(`
2448
4506
  `);
2449
4507
  }
2450
4508
  return [
@@ -2456,6 +4514,7 @@ function formatNotInstalled(result) {
2456
4514
  "- If the user agrees: run the install command above, then retry this tool.",
2457
4515
  "- If the user declines, OR has NOT explicitly asked for LSP installation:",
2458
4516
  ` call lsp_install_decision { server_id: "${server2.id}", decision: "declined" },`,
4517
+ ` which writes to ${context.installDecisionsPath},`,
2459
4518
  " then ignore this message and proceed WITHOUT LSP."
2460
4519
  ].join(`
2461
4520
  `);
@@ -2469,14 +4528,14 @@ var READ_ONLY_RETRY_TOOLS = new Set([
2469
4528
  "prepareRename"
2470
4529
  ]);
2471
4530
  async function withLspClient(filePath, fn, toolName, options = {}) {
2472
- const absPath = resolve2(contextCwd(), filePath);
4531
+ const absPath = resolvePathInsideContext(filePath);
2473
4532
  if (isDirectoryPath(absPath)) {
2474
4533
  throw new LspInvalidPathError("Directory paths are not supported by this LSP tool. " + "Use lsp.diagnostics with a directory path for directory diagnostics.");
2475
4534
  }
2476
4535
  const ext = effectiveExtension(absPath);
2477
4536
  const result = findServerForExtension(ext);
2478
4537
  if (result.status !== "found") {
2479
- throw new LspServerLookupError(formatServerLookupError(result));
4538
+ throw new LspServerLookupError(formatServerLookupError(result), result);
2480
4539
  }
2481
4540
  const server2 = result.server;
2482
4541
  const root = findWorkspaceRoot(absPath);
@@ -2504,11 +4563,11 @@ async function withLspClient(filePath, fn, toolName, options = {}) {
2504
4563
  }
2505
4564
 
2506
4565
  // ../lsp-core/src/lsp/directory-diagnostics.ts
2507
- import { existsSync as existsSync7, lstatSync, readdirSync } from "node:fs";
2508
- import { join as join7, resolve as resolve3 } from "node:path";
4566
+ import { existsSync as existsSync10, lstatSync as lstatSync5, readdirSync as readdirSync3 } from "node:fs";
4567
+ import { join as join5, resolve as resolve8 } from "node:path";
2509
4568
 
2510
4569
  // ../lsp-core/src/lsp/formatters.ts
2511
- import { fileURLToPath as fileURLToPath2 } from "node:url";
4570
+ import { fileURLToPath as fileURLToPath3 } from "node:url";
2512
4571
  var DIAGNOSTIC_SEVERITY_FILTERS = {
2513
4572
  error: 1,
2514
4573
  warning: 2,
@@ -2516,7 +4575,7 @@ var DIAGNOSTIC_SEVERITY_FILTERS = {
2516
4575
  hint: 4
2517
4576
  };
2518
4577
  function uriToPath(uri) {
2519
- return fileURLToPath2(uri);
4578
+ return fileURLToPath3(uri);
2520
4579
  }
2521
4580
  function formatLocation(loc) {
2522
4581
  if ("targetUri" in loc) {
@@ -2602,6 +4661,9 @@ function formatApplyResult(result) {
2602
4661
  for (const file of result.filesModified) {
2603
4662
  lines.push(` - ${file}`);
2604
4663
  }
4664
+ if (result.lateAbort) {
4665
+ lines.push("Cancellation arrived after the filesystem commit began; the committed edit completed.");
4666
+ }
2605
4667
  } else {
2606
4668
  lines.push("Failed to apply some changes:");
2607
4669
  for (const err of result.errors) {
@@ -2617,6 +4679,7 @@ function formatApplyResult(result) {
2617
4679
 
2618
4680
  // ../lsp-core/src/lsp/directory-diagnostics.ts
2619
4681
  var SKIP_DIRECTORIES = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
4682
+ var DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY = 4;
2620
4683
  function collectFilesWithExtension(dir, extension, maxFiles) {
2621
4684
  const files = [];
2622
4685
  function walk(currentDir) {
@@ -2624,17 +4687,17 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
2624
4687
  return;
2625
4688
  let entries = [];
2626
4689
  try {
2627
- entries = readdirSync(currentDir);
4690
+ entries = readdirSync3(currentDir);
2628
4691
  } catch {
2629
4692
  return;
2630
4693
  }
2631
4694
  for (const entry of entries) {
2632
4695
  if (files.length >= maxFiles)
2633
4696
  return;
2634
- const fullPath = join7(currentDir, entry);
4697
+ const fullPath = join5(currentDir, entry);
2635
4698
  let stat;
2636
4699
  try {
2637
- stat = lstatSync(fullPath);
4700
+ stat = lstatSync5(fullPath);
2638
4701
  } catch {
2639
4702
  continue;
2640
4703
  }
@@ -2652,52 +4715,65 @@ function collectFilesWithExtension(dir, extension, maxFiles) {
2652
4715
  walk(dir);
2653
4716
  return files;
2654
4717
  }
2655
- async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES) {
4718
+ async function aggregateDiagnosticsForDirectory(directory, extension, severity, maxFiles = DEFAULT_MAX_DIRECTORY_FILES, options = {}) {
2656
4719
  if (!extension.startsWith(".")) {
2657
4720
  throw new LspInvalidPathError(`Extension must start with a dot (e.g., ".ts", not "${extension}"). Use ".${extension}" instead.`);
2658
4721
  }
2659
- const absDir = resolve3(contextCwd(), directory);
2660
- if (!existsSync7(absDir)) {
4722
+ const absDir = resolve8(options.workspaceRoot ?? contextCwd(), directory);
4723
+ if (!existsSync10(absDir)) {
2661
4724
  throw new LspInvalidPathError(`Directory does not exist: ${absDir}`);
2662
4725
  }
2663
- const serverResult = findServerForExtension(extension);
4726
+ const serverResult = options.server === undefined ? findServerForExtension(extension) : { status: "found", server: options.server };
2664
4727
  if (serverResult.status !== "found") {
2665
4728
  throw new LspServerLookupError(formatServerLookupError(serverResult));
2666
4729
  }
2667
4730
  const server2 = serverResult.server;
2668
- const allFiles = collectFilesWithExtension(absDir, extension, maxFiles + 1);
4731
+ const allFiles = (options.listFiles ?? collectFilesWithExtension)(absDir, extension, maxFiles + 1);
2669
4732
  const wasCapped = allFiles.length > maxFiles;
2670
4733
  const filesToProcess = allFiles.slice(0, maxFiles);
2671
4734
  if (filesToProcess.length === 0) {
2672
- return [
4735
+ const output = [
2673
4736
  `Directory: ${absDir}`,
2674
4737
  `Extension: ${extension}`,
2675
4738
  "Files scanned: 0",
2676
4739
  `No files found with extension "${extension}".`
2677
4740
  ].join(`
2678
4741
  `);
4742
+ return { output, totalDiagnostics: 0, fileFailures: [] };
2679
4743
  }
2680
- const root = findWorkspaceRoot(absDir);
2681
- const manager = getLspManager();
4744
+ const root = options.workspaceRoot ?? findWorkspaceRoot(absDir);
4745
+ const manager = options.manager ?? getLspManager();
2682
4746
  const allDiagnostics = [];
2683
4747
  const fileErrors = [];
4748
+ const maxConcurrency = Math.max(1, options.maxConcurrency ?? DIRECTORY_DIAGNOSTICS_MAX_CONCURRENCY);
4749
+ options.signal?.throwIfAborted();
2684
4750
  const client = await manager.getClient(root, server2);
2685
4751
  try {
2686
- for (const file of filesToProcess) {
2687
- try {
2688
- const result = await client.diagnostics(file);
2689
- const filtered = filterDiagnosticsBySeverity(result.items, severity);
2690
- allDiagnostics.push(...filtered.map((diagnostic) => ({
2691
- filePath: file,
2692
- diagnostic
2693
- })));
2694
- } catch (e) {
2695
- fileErrors.push({
2696
- file,
2697
- error: e instanceof Error ? e.message : String(e)
2698
- });
4752
+ let nextIndex = 0;
4753
+ const workers = Array.from({ length: Math.min(maxConcurrency, filesToProcess.length) }, async () => {
4754
+ for (;; ) {
4755
+ if (options.signal?.aborted)
4756
+ return;
4757
+ const file = filesToProcess[nextIndex];
4758
+ nextIndex += 1;
4759
+ if (file === undefined)
4760
+ return;
4761
+ try {
4762
+ const result = await client.diagnostics(file, options.signal);
4763
+ const filtered = filterDiagnosticsBySeverity(result.items, severity);
4764
+ allDiagnostics.push(...filtered.map((diagnostic) => ({
4765
+ filePath: file,
4766
+ diagnostic
4767
+ })));
4768
+ } catch (e) {
4769
+ fileErrors.push({
4770
+ file,
4771
+ error: e instanceof Error ? e.message : String(e)
4772
+ });
4773
+ }
2699
4774
  }
2700
- }
4775
+ });
4776
+ await Promise.all(workers);
2701
4777
  } finally {
2702
4778
  manager.releaseClient(root, server2.id);
2703
4779
  }
@@ -2725,13 +4801,13 @@ async function aggregateDiagnosticsForDirectory(directory, extension, severity,
2725
4801
  lines.push("", `... (${allDiagnostics.length - DEFAULT_MAX_DIAGNOSTICS} more diagnostics not shown)`);
2726
4802
  }
2727
4803
  }
2728
- return lines.join(`
2729
- `);
4804
+ return { output: lines.join(`
4805
+ `), totalDiagnostics: allDiagnostics.length, fileFailures: fileErrors };
2730
4806
  }
2731
4807
 
2732
4808
  // ../lsp-core/src/lsp/infer-extension.ts
2733
- import { lstatSync as lstatSync2, readdirSync as readdirSync2 } from "node:fs";
2734
- import { join as join8 } from "node:path";
4809
+ import { lstatSync as lstatSync6, readdirSync as readdirSync4 } from "node:fs";
4810
+ import { join as join6 } from "node:path";
2735
4811
  var SKIP_DIRECTORIES2 = new Set(["node_modules", ".git", "dist", "build", ".next", "out"]);
2736
4812
  var MAX_SCAN_ENTRIES = 500;
2737
4813
  function inferExtensionFromDirectory(directory) {
@@ -2742,17 +4818,17 @@ function inferExtensionFromDirectory(directory) {
2742
4818
  return;
2743
4819
  let entries;
2744
4820
  try {
2745
- entries = readdirSync2(dir);
4821
+ entries = readdirSync4(dir);
2746
4822
  } catch {
2747
4823
  return;
2748
4824
  }
2749
4825
  for (const entry of entries) {
2750
4826
  if (scanned >= MAX_SCAN_ENTRIES)
2751
4827
  return;
2752
- const fullPath = join8(dir, entry);
4828
+ const fullPath = join6(dir, entry);
2753
4829
  let stat;
2754
4830
  try {
2755
- stat = lstatSync2(fullPath);
4831
+ stat = lstatSync6(fullPath);
2756
4832
  } catch {
2757
4833
  continue;
2758
4834
  }
@@ -2826,13 +4902,48 @@ function missingDependencyResult(error, details) {
2826
4902
  details: {
2827
4903
  ...details,
2828
4904
  error: message,
2829
- errorKind: "missing_dependency"
4905
+ errorKind: "missing_dependency",
4906
+ ...availabilityDetails(error)
2830
4907
  }
2831
4908
  };
2832
4909
  }
4910
+ function availabilityDetails(error) {
4911
+ const availability = missingDependencyAvailability(error);
4912
+ return availability === null ? {} : { availability };
4913
+ }
4914
+ function missingDependencyAvailability(error) {
4915
+ if (!(error instanceof LspServerLookupError) || error.lookup === undefined)
4916
+ return null;
4917
+ const context = lspRequestContext();
4918
+ switch (error.lookup.status) {
4919
+ case "not_configured":
4920
+ return {
4921
+ kind: "not_configured",
4922
+ extension: error.lookup.extension,
4923
+ availableServers: [...error.lookup.availableServers],
4924
+ projectConfigPaths: [...context.projectConfigPaths],
4925
+ userConfigPath: context.userConfigPath,
4926
+ installDecisionTool: context.capabilities.installDecisionTool
4927
+ };
4928
+ case "not_installed":
4929
+ return {
4930
+ kind: "not_installed",
4931
+ serverId: error.lookup.server.id,
4932
+ command: [...error.lookup.server.command],
4933
+ extensions: [...error.lookup.server.extensions],
4934
+ installHint: error.lookup.installHint,
4935
+ installDecisionTool: context.capabilities.installDecisionTool,
4936
+ installDecisionsPath: context.installDecisionsPath
4937
+ };
4938
+ default: {
4939
+ const exhaustive = error.lookup;
4940
+ return exhaustive;
4941
+ }
4942
+ }
4943
+ }
2833
4944
 
2834
4945
  // ../lsp-core/src/tools/parameters.ts
2835
- function isRecord4(value) {
4946
+ function isRecord7(value) {
2836
4947
  return typeof value === "object" && value !== null && !Array.isArray(value);
2837
4948
  }
2838
4949
  function requireString(params, key) {
@@ -2889,7 +5000,7 @@ async function executeLspDiagnostics(params, signal) {
2889
5000
  const filePath = requireString(params, "filePath");
2890
5001
  const severity = severityFilter(params);
2891
5002
  try {
2892
- const absPath = resolve4(contextCwd(), filePath);
5003
+ const absPath = resolvePathInsideContext(filePath);
2893
5004
  if (isDirectoryPath(absPath)) {
2894
5005
  const extension = inferExtensionFromDirectory(absPath);
2895
5006
  if (!extension) {
@@ -2906,18 +5017,33 @@ async function executeLspDiagnostics(params, signal) {
2906
5017
  };
2907
5018
  return text(message, details3);
2908
5019
  }
2909
- const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity);
5020
+ const output2 = await aggregateDiagnosticsForDirectory(absPath, extension, severity, undefined, signal === undefined ? {} : { signal });
2910
5021
  const details2 = {
2911
5022
  filePath,
2912
5023
  severity,
2913
5024
  mode: "directory",
2914
5025
  diagnostics: [],
5026
+ totalDiagnostics: output2.totalDiagnostics,
5027
+ truncated: false,
5028
+ fileFailures: [...output2.fileFailures]
5029
+ };
5030
+ return text(output2.output, details2);
5031
+ }
5032
+ const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath, signal), "diagnostics", clientOptions(signal));
5033
+ if (result.transientError) {
5034
+ const message = result.transientError.message;
5035
+ const details2 = {
5036
+ filePath,
5037
+ severity,
5038
+ mode: "file",
5039
+ diagnostics: [],
2915
5040
  totalDiagnostics: 0,
2916
- truncated: false
5041
+ truncated: false,
5042
+ error: message,
5043
+ errorKind: result.transientError.kind
2917
5044
  };
2918
- return text(output2, details2);
5045
+ return text(message, details2, true);
2919
5046
  }
2920
- const result = await withLspClient(filePath, async (client) => client.diagnostics(filePath), "diagnostics", clientOptions(signal));
2921
5047
  const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
2922
5048
  const total = diagnostics.length;
2923
5049
  const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
@@ -2978,7 +5104,7 @@ async function executeLspGotoDefinition(params, signal) {
2978
5104
  const line = requireNumber(params, "line");
2979
5105
  const character = requireNumber(params, "character");
2980
5106
  try {
2981
- const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character), "definition", clientOptions(signal));
5107
+ const result = await withLspClient(filePath, async (client) => client.definition(filePath, line, character, signal), "definition", clientOptions(signal));
2982
5108
  const locations = !result ? [] : Array.isArray(result) ? result : [result];
2983
5109
  const details = { filePath, line, character, locations };
2984
5110
  if (locations.length === 0)
@@ -3003,7 +5129,7 @@ async function executeLspFindReferences(params, signal) {
3003
5129
  const character = requireNumber(params, "character");
3004
5130
  const includeDeclaration = optionalBoolean(params, "includeDeclaration") ?? true;
3005
5131
  try {
3006
- const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration), "references", clientOptions(signal));
5132
+ const result = await withLspClient(filePath, async (client) => client.references(filePath, line, character, includeDeclaration, signal), "references", clientOptions(signal));
3007
5133
  const references = Array.isArray(result) ? result : [];
3008
5134
  const total = references.length;
3009
5135
  const truncated = total > DEFAULT_MAX_REFERENCES;
@@ -3039,181 +5165,13 @@ async function executeLspFindReferences(params, signal) {
3039
5165
  }
3040
5166
  }
3041
5167
 
3042
- // ../lsp-core/src/lsp/workspace-edit.ts
3043
- import { existsSync as existsSync8, readFileSync as readFileSync5, realpathSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "node:fs";
3044
- import { dirname as dirname5, isAbsolute as isAbsolute3, relative, resolve as resolve5 } from "node:path";
3045
- import { fileURLToPath as fileURLToPath3 } from "node:url";
3046
- function errorMessage2(error) {
3047
- return error instanceof Error ? error.message : String(error);
3048
- }
3049
- function isPathInsideWorkspace(filePath, workspaceRoot) {
3050
- const relativePath = relative(workspaceRoot, filePath);
3051
- return relativePath === "" || !relativePath.startsWith("..") && !isAbsolute3(relativePath);
3052
- }
3053
- function realpathForValidation(filePath) {
3054
- if (existsSync8(filePath))
3055
- return realpathSync(filePath);
3056
- const parent = dirname5(filePath);
3057
- return resolve5(realpathSync(parent), relative(parent, filePath));
3058
- }
3059
- function uriToWorkspacePath(uri, workspaceRoot) {
3060
- let filePath;
3061
- try {
3062
- filePath = fileURLToPath3(uri);
3063
- } catch (error) {
3064
- return { success: false, error: `non-file URI ${uri}: ${errorMessage2(error)}` };
3065
- }
3066
- let validatedPath;
3067
- try {
3068
- validatedPath = realpathForValidation(filePath);
3069
- } catch (error) {
3070
- return { success: false, error: `${filePath}: ${errorMessage2(error)}` };
3071
- }
3072
- if (!isPathInsideWorkspace(validatedPath, workspaceRoot)) {
3073
- return { success: false, error: `${filePath}: outside workspace ${workspaceRoot}` };
3074
- }
3075
- return { success: true, path: filePath };
3076
- }
3077
- function applyTextEditsToFile(filePath, edits) {
3078
- try {
3079
- const content = readFileSync5(filePath, "utf-8");
3080
- const lines = content.split(`
3081
- `);
3082
- const sortedEdits = [...edits].sort((a, b) => {
3083
- if (b.range.start.line !== a.range.start.line) {
3084
- return b.range.start.line - a.range.start.line;
3085
- }
3086
- return b.range.start.character - a.range.start.character;
3087
- });
3088
- for (const edit of sortedEdits) {
3089
- const startLine = edit.range.start.line;
3090
- const startChar = edit.range.start.character;
3091
- const endLine = edit.range.end.line;
3092
- const endChar = edit.range.end.character;
3093
- if (startLine === endLine) {
3094
- const line = lines[startLine] ?? "";
3095
- lines[startLine] = line.substring(0, startChar) + edit.newText + line.substring(endChar);
3096
- } else {
3097
- const firstLine = lines[startLine] ?? "";
3098
- const lastLine = lines[endLine] ?? "";
3099
- const newContent = firstLine.substring(0, startChar) + edit.newText + lastLine.substring(endChar);
3100
- lines.splice(startLine, endLine - startLine + 1, ...newContent.split(`
3101
- `));
3102
- }
3103
- }
3104
- writeFileSync2(filePath, lines.join(`
3105
- `), "utf-8");
3106
- return { success: true, editCount: edits.length };
3107
- } catch (err) {
3108
- return {
3109
- success: false,
3110
- editCount: 0,
3111
- error: err instanceof Error ? err.message : String(err)
3112
- };
3113
- }
3114
- }
3115
- function applyWorkspaceEdit(edit, options = {}) {
3116
- if (!edit) {
3117
- return { success: false, filesModified: [], totalEdits: 0, errors: ["No edit provided"] };
3118
- }
3119
- const result = { success: true, filesModified: [], totalEdits: 0, errors: [] };
3120
- const workspaceRoot = realpathSync(options.workspaceRoot ?? contextCwd());
3121
- if (edit.changes) {
3122
- for (const [uri, edits] of Object.entries(edit.changes)) {
3123
- const validatedPath = uriToWorkspacePath(uri, workspaceRoot);
3124
- if (!validatedPath.success) {
3125
- result.success = false;
3126
- result.errors.push(validatedPath.error);
3127
- continue;
3128
- }
3129
- const applyResult = applyTextEditsToFile(validatedPath.path, edits);
3130
- if (applyResult.success) {
3131
- result.filesModified.push(validatedPath.path);
3132
- result.totalEdits += applyResult.editCount;
3133
- } else {
3134
- result.success = false;
3135
- result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
3136
- }
3137
- }
3138
- }
3139
- if (edit.documentChanges) {
3140
- for (const change of edit.documentChanges) {
3141
- if (!("kind" in change)) {
3142
- const validatedPath = uriToWorkspacePath(change.textDocument.uri, workspaceRoot);
3143
- if (!validatedPath.success) {
3144
- result.success = false;
3145
- result.errors.push(validatedPath.error);
3146
- continue;
3147
- }
3148
- const applyResult = applyTextEditsToFile(validatedPath.path, change.edits);
3149
- if (applyResult.success) {
3150
- result.filesModified.push(validatedPath.path);
3151
- result.totalEdits += applyResult.editCount;
3152
- } else {
3153
- result.success = false;
3154
- result.errors.push(`${validatedPath.path}: ${applyResult.error}`);
3155
- }
3156
- continue;
3157
- }
3158
- if (change.kind === "create") {
3159
- try {
3160
- const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
3161
- if (!validatedPath.success) {
3162
- result.success = false;
3163
- result.errors.push(`Create ${change.uri}: ${validatedPath.error}`);
3164
- continue;
3165
- }
3166
- writeFileSync2(validatedPath.path, "", "utf-8");
3167
- result.filesModified.push(validatedPath.path);
3168
- } catch (err) {
3169
- result.success = false;
3170
- result.errors.push(`Create ${change.uri}: ${String(err)}`);
3171
- }
3172
- } else if (change.kind === "rename") {
3173
- try {
3174
- const oldPath = uriToWorkspacePath(change.oldUri, workspaceRoot);
3175
- const newPath = uriToWorkspacePath(change.newUri, workspaceRoot);
3176
- if (!oldPath.success || !newPath.success) {
3177
- const error = oldPath.success ? newPath.success ? "invalid URI" : newPath.error : oldPath.error;
3178
- result.success = false;
3179
- result.errors.push(`Rename ${change.oldUri}: ${error}`);
3180
- continue;
3181
- }
3182
- const content = readFileSync5(oldPath.path, "utf-8");
3183
- writeFileSync2(newPath.path, content, "utf-8");
3184
- unlinkSync2(oldPath.path);
3185
- result.filesModified.push(newPath.path);
3186
- } catch (err) {
3187
- result.success = false;
3188
- result.errors.push(`Rename ${change.oldUri}: ${String(err)}`);
3189
- }
3190
- } else if (change.kind === "delete") {
3191
- try {
3192
- const validatedPath = uriToWorkspacePath(change.uri, workspaceRoot);
3193
- if (!validatedPath.success) {
3194
- result.success = false;
3195
- result.errors.push(`Delete ${change.uri}: ${validatedPath.error}`);
3196
- continue;
3197
- }
3198
- unlinkSync2(validatedPath.path);
3199
- result.filesModified.push(validatedPath.path);
3200
- } catch (err) {
3201
- result.success = false;
3202
- result.errors.push(`Delete ${change.uri}: ${String(err)}`);
3203
- }
3204
- }
3205
- }
3206
- }
3207
- return result;
3208
- }
3209
-
3210
5168
  // ../lsp-core/src/tools/rename.ts
3211
5169
  async function executeLspPrepareRename(params, signal) {
3212
5170
  const filePath = requireString(params, "filePath");
3213
5171
  const line = requireNumber(params, "line");
3214
5172
  const character = requireNumber(params, "character");
3215
5173
  try {
3216
- const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character), "prepareRename", clientOptions(signal));
5174
+ const result = await withLspClient(filePath, async (client) => client.prepareRename(filePath, line, character, signal), "prepareRename", clientOptions(signal));
3217
5175
  const details = { filePath, line, character, result };
3218
5176
  return text(formatPrepareRenameResult(result), details);
3219
5177
  } catch (error) {
@@ -3234,13 +5192,9 @@ async function executeLspRename(params, signal) {
3234
5192
  const character = requireNumber(params, "character");
3235
5193
  const newName = requireString(params, "newName");
3236
5194
  try {
3237
- const edit = await withLspClient(filePath, async (client, workspaceRoot) => ({
3238
- edit: await client.rename(filePath, line, character, newName),
3239
- workspaceRoot
3240
- }), "rename", clientOptions(signal));
3241
- const apply = applyWorkspaceEdit(edit.edit, { workspaceRoot: edit.workspaceRoot });
3242
- const details = { filePath, line, character, newName, apply, edit: edit.edit };
3243
- return text(formatApplyResult(apply), details, !apply.success);
5195
+ const result = await withLspClient(filePath, async (client) => client.rename(filePath, line, character, newName, signal), "rename", clientOptions(signal));
5196
+ const details = { filePath, line, character, newName, apply: result.apply, edit: result.edit };
5197
+ return text(formatApplyResult(result.apply), details, !result.apply.success);
3244
5198
  } catch (error) {
3245
5199
  const missingDependency = missingDependencyResult(error, {
3246
5200
  filePath,
@@ -3315,10 +5269,10 @@ async function executeLspSymbols(params, signal) {
3315
5269
  errorKind: "missing_query"
3316
5270
  });
3317
5271
  }
3318
- const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query), "workspaceSymbols", clientOptions(signal));
5272
+ const symbols2 = await withLspClient(filePath, async (client) => client.workspaceSymbols(query, signal), "workspaceSymbols", clientOptions(signal));
3319
5273
  return formatSymbolsResult(filePath, scope, symbols2, limit, query);
3320
5274
  }
3321
- const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath), "documentSymbols", clientOptions(signal));
5275
+ const symbols = await withLspClient(filePath, async (client) => client.documentSymbols(filePath, signal), "documentSymbols", clientOptions(signal));
3322
5276
  return formatSymbolsResult(filePath, scope, symbols, limit);
3323
5277
  } catch (error) {
3324
5278
  const query = optionalString(params, "query");
@@ -3484,16 +5438,16 @@ function matchesToolName(tool, name) {
3484
5438
  return tool.name === name || (tool.aliases?.includes(name) ?? false);
3485
5439
  }
3486
5440
  function coerceToolArguments(value) {
3487
- return isRecord4(value) ? value : {};
5441
+ return isRecord7(value) ? value : {};
3488
5442
  }
3489
5443
  // ../lsp-core/src/mcp.ts
3490
5444
  var SERVER_NAME = "lsp";
3491
5445
  var SERVER_VERSION = "0.1.0";
3492
- async function handleLspMcpRequest(input) {
5446
+ async function handleLspMcpRequest(input, options = {}) {
3493
5447
  if (!isPlainRecord(input)) {
3494
5448
  return errorResponse(null, -32600, "Invalid Request");
3495
5449
  }
3496
- const id = jsonRpcId(input["id"]);
5450
+ const id = jsonRpcId2(input["id"]);
3497
5451
  const method = input["method"];
3498
5452
  if (method === "notifications/initialized")
3499
5453
  return;
@@ -3511,24 +5465,27 @@ async function handleLspMcpRequest(input) {
3511
5465
  return successResponse(id, { tools: LSP_MCP_TOOLS.map(describeTool) });
3512
5466
  }
3513
5467
  if (method === "tools/call") {
3514
- return handleToolCall(id, input["params"]);
5468
+ return handleToolCall(id, input["params"], options.signal);
3515
5469
  }
3516
5470
  return errorResponse(id, -32601, `Method not found: ${String(method)}`);
3517
5471
  }
3518
- async function handleToolCall(id, params) {
5472
+ async function handleToolCall(id, params, signal) {
3519
5473
  if (!isPlainRecord(params) || typeof params["name"] !== "string") {
3520
5474
  return errorResponse(id, -32602, "tools/call requires params.name");
3521
5475
  }
3522
5476
  try {
3523
- const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]));
5477
+ const result = await executeLspTool(params["name"], coerceToolArguments(params["arguments"]), signal);
3524
5478
  return successResponse(id, {
3525
5479
  content: result.content,
3526
5480
  isError: result.isError ?? false,
3527
5481
  details: result.details
3528
5482
  });
3529
5483
  } catch (error) {
5484
+ if (!(error instanceof Error)) {
5485
+ throw error;
5486
+ }
3530
5487
  return successResponse(id, {
3531
- content: [{ type: "text", text: messageFromError(error) }],
5488
+ content: [{ type: "text", text: error.message }],
3532
5489
  isError: true
3533
5490
  });
3534
5491
  }
@@ -3549,77 +5506,91 @@ function requestedProtocolVersion(params) {
3549
5506
 
3550
5507
  // src/request-routing.ts
3551
5508
  var CONTEXT_KEY = "_context";
5509
+
5510
+ class InvalidDaemonRequestError extends Error {
5511
+ name = "InvalidDaemonRequestError";
5512
+ }
3552
5513
  function extractRequestContext(raw) {
3553
5514
  if (!isPlainRecord(raw) || raw["method"] !== "tools/call")
3554
5515
  return { input: raw, context: undefined };
3555
5516
  const params = raw["params"];
3556
5517
  if (!isPlainRecord(params))
3557
- return { input: raw, context: undefined };
5518
+ throw new InvalidDaemonRequestError("Daemon tools/call params must be an object.");
3558
5519
  const args = params["arguments"];
3559
5520
  if (!isPlainRecord(args))
3560
- return { input: raw, context: undefined };
5521
+ throw new InvalidDaemonRequestError("Daemon tools/call arguments must be an object.");
5522
+ if (!Object.hasOwn(args, CONTEXT_KEY)) {
5523
+ throw new InvalidDaemonRequestError("Daemon tools/call arguments must include _context.");
5524
+ }
3561
5525
  const context = parseContext(args[CONTEXT_KEY]);
3562
- if (!context)
3563
- return { input: raw, context: undefined };
3564
5526
  const cleanedArgs = { ...args };
3565
5527
  delete cleanedArgs[CONTEXT_KEY];
3566
5528
  const cleaned = { ...raw, params: { ...params, arguments: cleanedArgs } };
3567
5529
  return { input: cleaned, context };
3568
5530
  }
3569
- function handleDaemonMessage(raw) {
3570
- const { input, context } = extractRequestContext(raw);
3571
- if (context)
3572
- return runWithRequestContext(context, () => handleLspMcpRequest(input));
3573
- return handleLspMcpRequest(input);
3574
- }
3575
- function parseContext(value) {
3576
- if (!isPlainRecord(value))
3577
- return;
3578
- const context = {};
3579
- const cwd = value["cwd"];
3580
- if (typeof cwd === "string")
3581
- context.cwd = cwd;
3582
- const env = value["env"];
3583
- if (isStringRecord2(env))
3584
- context.env = env;
3585
- return context.cwd === undefined && context.env === undefined ? undefined : context;
5531
+ function handleDaemonMessage(raw, state) {
5532
+ const authenticated = authenticateMessage(raw, state.token);
5533
+ if ("error" in authenticated)
5534
+ return Promise.resolve(authenticated);
5535
+ if (authenticated.method === "omo/ping") {
5536
+ return Promise.resolve({
5537
+ jsonrpc: "2.0",
5538
+ id: authenticated.id,
5539
+ result: { protocolVersion: OMO_DAEMON_PROTOCOL_VERSION, ...state.owner }
5540
+ });
5541
+ }
5542
+ if (authenticated.method === "$/cancelRequest") {
5543
+ const targetId = cancellationTargetId(authenticated.input);
5544
+ if (targetId !== undefined)
5545
+ state.activeRequests?.get(String(targetId))?.abort();
5546
+ return Promise.resolve(undefined);
5547
+ }
5548
+ let routed;
5549
+ try {
5550
+ routed = extractRequestContext(authenticated.input);
5551
+ } catch (error) {
5552
+ const message = error instanceof Error ? error.message : "invalid daemon request";
5553
+ return Promise.resolve({
5554
+ jsonrpc: "2.0",
5555
+ id: authenticated.id,
5556
+ error: { code: -32602, message, data: { code: "invalid_daemon_request" } }
5557
+ });
5558
+ }
5559
+ const { input, context } = routed;
5560
+ const key = routeRequestKey(authenticated.id);
5561
+ if (key === undefined || !state.activeRequests) {
5562
+ if (context)
5563
+ return runWithRequestContext(context, () => handleLspMcpRequest(input));
5564
+ return handleLspMcpRequest(input);
5565
+ }
5566
+ const controller = new AbortController;
5567
+ state.activeRequests.set(key, controller);
5568
+ const options = { signal: controller.signal };
5569
+ const run = context ? runWithRequestContext(context, () => handleLspMcpRequest(input, options)) : handleLspMcpRequest(input, options);
5570
+ return run.finally(() => {
5571
+ if (state.activeRequests?.get(key) === controller)
5572
+ state.activeRequests.delete(key);
5573
+ });
3586
5574
  }
3587
- function isStringRecord2(value) {
3588
- return isPlainRecord(value) && Object.values(value).every((item) => typeof item === "string");
5575
+ function routeRequestKey(id) {
5576
+ return typeof id === "string" || typeof id === "number" ? String(id) : undefined;
3589
5577
  }
3590
-
3591
- // src/socket-jsonrpc.ts
3592
- function encodeJsonLine(message) {
3593
- return `${JSON.stringify(message)}
3594
- `;
5578
+ function cancellationTargetId(input) {
5579
+ const params = input["params"];
5580
+ if (!isPlainRecord(params))
5581
+ return;
5582
+ const id = params["id"];
5583
+ return typeof id === "string" || typeof id === "number" ? id : undefined;
3595
5584
  }
3596
- function createLineDecoder(onMessage, onParseError) {
3597
- let buffer = "";
3598
- return {
3599
- push(chunk) {
3600
- buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
3601
- let index = buffer.indexOf(`
3602
- `);
3603
- while (index !== -1) {
3604
- const raw = buffer.slice(0, index).trim();
3605
- buffer = buffer.slice(index + 1);
3606
- if (raw.length > 0) {
3607
- try {
3608
- onMessage(JSON.parse(raw));
3609
- } catch (error) {
3610
- onParseError?.(raw, error);
3611
- }
3612
- }
3613
- index = buffer.indexOf(`
3614
- `);
3615
- }
3616
- }
3617
- };
5585
+ function parseContext(value) {
5586
+ if (!isPlainRecord(value))
5587
+ throw new InvalidDaemonRequestError("LSP request _context must be an object.");
5588
+ return parseLspRequestContext(value);
3618
5589
  }
3619
5590
 
3620
5591
  // src/daemon-client.ts
3621
5592
  var DEFAULT_REQUEST_TIMEOUT_MS = 30000;
3622
- var REQUEST_ID = 1;
5593
+ var nextProxyRequestId = 1;
3623
5594
 
3624
5595
  class DaemonRequestError extends Error {
3625
5596
  requestWritten;
@@ -3629,44 +5600,70 @@ class DaemonRequestError extends Error {
3629
5600
  this.requestWritten = requestWritten;
3630
5601
  }
3631
5602
  }
3632
- async function callToolViaDaemon(name, args, options = {}) {
5603
+
5604
+ class DaemonAuthenticationRejectedError extends DaemonRequestError {
5605
+ constructor() {
5606
+ super("daemon authentication failed before dispatch", true);
5607
+ this.name = "DaemonAuthenticationRejectedError";
5608
+ }
5609
+ }
5610
+
5611
+ class DaemonRequestCancelledError extends DaemonRequestError {
5612
+ constructor(requestWritten) {
5613
+ super("daemon request cancelled", requestWritten);
5614
+ this.name = "DaemonRequestCancelledError";
5615
+ }
5616
+ }
5617
+ async function callToolViaDaemon(name, args, options) {
5618
+ const context = requireContext(options.context);
3633
5619
  const paths = options.paths ?? daemonPaths();
3634
5620
  const ensure = options.ensure ?? ensureDaemonRunning;
3635
5621
  const timeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
3636
- const requestArgs = withContext(args, options.context);
5622
+ const requestArgs = withContext(args, context);
3637
5623
  let lastError;
3638
- for (let attempt = 0;attempt < 2; attempt += 1) {
5624
+ let authRefreshUsed = false;
5625
+ for (let attempt = 0;attempt < 3; attempt += 1) {
3639
5626
  try {
3640
5627
  await ensure(paths);
3641
- return await sendToolCall(paths.socket, name, requestArgs, timeoutMs);
5628
+ const token = readAuthToken(paths);
5629
+ if (!token)
5630
+ throw new DaemonRequestError("daemon auth token missing", false);
5631
+ const sendOptions = options.signal === undefined ? { timeoutMs } : { timeoutMs, signal: options.signal };
5632
+ return await sendToolCall(paths, token, name, requestArgs, sendOptions);
3642
5633
  } catch (error) {
3643
5634
  lastError = error;
3644
- if (error instanceof DaemonRequestError && error.requestWritten)
5635
+ if (error instanceof DaemonAuthenticationRejectedError && !authRefreshUsed) {
5636
+ authRefreshUsed = true;
5637
+ continue;
5638
+ }
5639
+ if (error instanceof DaemonRequestCancelledError)
5640
+ break;
5641
+ if (error instanceof DaemonRequestError && (error.requestWritten || !isRetryableTool(name)))
3645
5642
  break;
3646
5643
  }
3647
5644
  }
3648
5645
  return daemonUnreachableResult(paths, lastError);
3649
5646
  }
3650
- function callDiagnosticsViaDaemon(filePath, options = {}) {
5647
+ function callDiagnosticsViaDaemon(filePath, options) {
3651
5648
  return callToolViaDaemon("diagnostics", { filePath, severity: "error" }, options);
3652
5649
  }
3653
- var FORWARDED_ENV_KEYS = [
3654
- "LSP_TOOLS_MCP_PROJECT_CONFIG",
3655
- "LSP_TOOLS_MCP_USER_CONFIG",
3656
- "LSP_TOOLS_MCP_INSTALL_DECISIONS"
3657
- ];
3658
5650
  function currentRequestContext(env = process.env) {
3659
- const forwarded = {};
3660
- for (const key of FORWARDED_ENV_KEYS) {
3661
- const value = env[key];
3662
- if (value !== undefined)
3663
- forwarded[key] = value;
3664
- }
3665
- return { cwd: process.cwd(), env: forwarded };
5651
+ const cwd = process.cwd();
5652
+ const home = env["HOME"] ?? homedir3();
5653
+ return parseLspRequestContext({
5654
+ cwd,
5655
+ projectConfigPaths: [join7(cwd, ".codex", "lsp-client.json")],
5656
+ userConfigPath: join7(home, ".codex", "lsp-client.json"),
5657
+ installDecisionsPath: join7(home, ".codex", "lsp-install-decisions.json"),
5658
+ capabilities: { installDecisionTool: true }
5659
+ });
5660
+ }
5661
+ function requireContext(context) {
5662
+ if (!context)
5663
+ throw new DaemonRequestError("daemon tool context is required", false);
5664
+ return parseLspRequestContext(context);
3666
5665
  }
3667
5666
  function withContext(args, context) {
3668
- if (!context || context.cwd === undefined && context.env === undefined)
3669
- return args;
3670
5667
  return { ...args, [CONTEXT_KEY]: context };
3671
5668
  }
3672
5669
  function daemonUnreachableResult(paths, error) {
@@ -3680,39 +5677,84 @@ function daemonUnreachableResult(paths, error) {
3680
5677
  `);
3681
5678
  return { content: [{ type: "text", text: text2 }], isError: true };
3682
5679
  }
3683
- function sendToolCall(socketPath, name, args, timeoutMs) {
3684
- return new Promise((resolve6, reject) => {
3685
- const socket = connect2(socketPath);
5680
+ function sendToolCall(paths, token, name, args, options) {
5681
+ return new Promise((resolve9, reject) => {
5682
+ const socket = connect2(paths.socket);
5683
+ const requestId = allocateProxyRequestId();
3686
5684
  let settled = false;
3687
5685
  let requestWritten = false;
5686
+ let cancelAfterWrite = false;
5687
+ const cancelPayload = () => encodeJsonLine({
5688
+ jsonrpc: "2.0",
5689
+ method: "$/cancelRequest",
5690
+ params: { _omo: authEnvelope(token), id: requestId }
5691
+ });
3688
5692
  const finish = (run) => {
3689
5693
  if (settled)
3690
5694
  return;
3691
5695
  settled = true;
3692
5696
  clearTimeout(timer);
3693
- socket.destroy();
5697
+ options.signal?.removeEventListener("abort", onAbort);
5698
+ destroyAfterCancel();
3694
5699
  run();
3695
5700
  };
3696
- const timer = setTimeout(() => finish(() => reject(new DaemonRequestError("daemon request timed out", requestWritten))), timeoutMs);
5701
+ const sendCancel = () => {
5702
+ if (!requestWritten) {
5703
+ cancelAfterWrite = true;
5704
+ return;
5705
+ }
5706
+ if (!socket.writable)
5707
+ return;
5708
+ socket.write(cancelPayload());
5709
+ };
5710
+ const destroyAfterCancel = () => {
5711
+ socket.destroy();
5712
+ };
5713
+ const onAbort = () => {
5714
+ sendCancel();
5715
+ finish(() => reject(new DaemonRequestCancelledError(requestWritten)));
5716
+ };
5717
+ const timer = setTimeout(() => {
5718
+ sendCancel();
5719
+ finish(() => reject(new DaemonRequestError("daemon request timed out", requestWritten)));
5720
+ }, options.timeoutMs);
3697
5721
  timer.unref();
5722
+ if (options.signal?.aborted) {
5723
+ onAbort();
5724
+ return;
5725
+ }
5726
+ options.signal?.addEventListener("abort", onAbort, { once: true });
3698
5727
  const decoder = createLineDecoder((message) => {
3699
- const result = toToolResult(message);
5728
+ if (isAuthErrorResponse(message)) {
5729
+ finish(() => reject(new DaemonAuthenticationRejectedError));
5730
+ return;
5731
+ }
5732
+ const result = toToolResult(message, requestId);
3700
5733
  if (result)
3701
- finish(() => resolve6(result));
5734
+ finish(() => resolve9(result));
3702
5735
  else
3703
5736
  finish(() => reject(new DaemonRequestError("invalid daemon response", requestWritten)));
3704
5737
  });
3705
5738
  socket.once("connect", () => {
3706
- requestWritten = true;
3707
- socket.write(encodeJsonLine({ jsonrpc: "2.0", id: REQUEST_ID, method: "tools/call", params: { name, arguments: args } }));
5739
+ const payload = encodeJsonLine({
5740
+ jsonrpc: "2.0",
5741
+ id: requestId,
5742
+ method: "tools/call",
5743
+ params: { _omo: authEnvelope(token), name, arguments: args }
5744
+ });
5745
+ socket.write(payload, () => {
5746
+ requestWritten = true;
5747
+ if (cancelAfterWrite && socket.writable)
5748
+ socket.write(cancelPayload());
5749
+ });
3708
5750
  });
3709
5751
  socket.on("data", (chunk) => decoder.push(chunk));
3710
5752
  socket.once("error", (error) => finish(() => reject(new DaemonRequestError(error.message, requestWritten))));
3711
5753
  socket.once("close", () => finish(() => reject(new DaemonRequestError("daemon connection closed", requestWritten))));
3712
5754
  });
3713
5755
  }
3714
- function toToolResult(message) {
3715
- if (!isPlainRecord(message) || message["id"] !== REQUEST_ID)
5756
+ function toToolResult(message, requestId) {
5757
+ if (!isPlainRecord(message) || message["id"] !== requestId)
3716
5758
  return null;
3717
5759
  const result = message["result"];
3718
5760
  if (!isPlainRecord(result) || !Array.isArray(result["content"]))
@@ -3723,21 +5765,41 @@ function toToolResult(message) {
3723
5765
  details: result["details"]
3724
5766
  };
3725
5767
  }
5768
+ function allocateProxyRequestId() {
5769
+ const id = nextProxyRequestId;
5770
+ nextProxyRequestId += 1;
5771
+ if (nextProxyRequestId > Number.MAX_SAFE_INTEGER)
5772
+ nextProxyRequestId = 1;
5773
+ return id;
5774
+ }
5775
+ function isRetryableTool(name) {
5776
+ return name !== "rename" && name !== "lsp_rename";
5777
+ }
3726
5778
  function errorText(error) {
3727
5779
  return error instanceof Error ? error.message : String(error);
3728
5780
  }
3729
5781
  // src/proxy.ts
5782
+ import { existsSync as existsSync11, realpathSync as realpathSync4 } from "node:fs";
5783
+ import { basename as basename3, delimiter as delimiter4, dirname as dirname9, isAbsolute as isAbsolute4 } from "node:path";
3730
5784
  async function runMcpStdioProxy(options = {}) {
3731
5785
  const input = options.input ?? process.stdin;
3732
5786
  const output = options.output ?? process.stdout;
3733
5787
  const paths = options.paths ?? daemonPaths();
3734
- const context = options.context ?? currentRequestContext();
5788
+ const env = options.env ?? process.env;
5789
+ const cwd = options.cwd ?? inferOpenCodeProjectCwd(env["LSP_TOOLS_MCP_PROJECT_CONFIG"]);
5790
+ const contextEnv = cwd === undefined ? env : canonicalizeContextEnv(env);
5791
+ const contextInput = {
5792
+ env: contextEnv,
5793
+ ...cwd === undefined ? {} : { cwd },
5794
+ ...options.homeDir === undefined ? {} : { homeDir: options.homeDir }
5795
+ };
5796
+ const context = options.context ?? createStandaloneMcpRequestContext(contextInput);
3735
5797
  const callOptions = { paths, context, ...options.ensure ? { ensure: options.ensure } : {} };
3736
5798
  await runJsonRpcStdioServer({
3737
5799
  input,
3738
5800
  output,
3739
5801
  idleTimeoutMs: 0,
3740
- handler: handleProxyRequest,
5802
+ handler: (request, requestOptions) => runWithRequestContext(context, () => handleProxyRequest(request, requestOptions)),
3741
5803
  handlerOptions: callOptions,
3742
5804
  onHandlerError: (error) => {
3743
5805
  process.stderr.write(`[lsp-daemon] proxy error: ${error instanceof Error ? error.message : String(error)}
@@ -3759,14 +5821,60 @@ function asToolCall(parsed) {
3759
5821
  if (!isPlainRecord(params) || typeof params["name"] !== "string")
3760
5822
  return null;
3761
5823
  const args = params["arguments"];
3762
- return { id: jsonRpcId(parsed["id"]), name: params["name"], args: isPlainRecord(args) ? args : {} };
5824
+ return { id: jsonRpcId2(parsed["id"]), name: params["name"], args: isPlainRecord(args) ? args : {} };
5825
+ }
5826
+ function inferOpenCodeProjectCwd(projectConfigEnv) {
5827
+ if (!projectConfigEnv)
5828
+ return;
5829
+ for (const entry of projectConfigEnv.split(delimiter4)) {
5830
+ const projectRoot = projectRootFromOpenCodeConfigPath(entry);
5831
+ if (projectRoot)
5832
+ return projectRoot;
5833
+ }
5834
+ return;
5835
+ }
5836
+ function canonicalizeContextEnv(env) {
5837
+ return {
5838
+ ...env,
5839
+ LSP_TOOLS_MCP_PROJECT_CONFIG: canonicalizePathList(env["LSP_TOOLS_MCP_PROJECT_CONFIG"]),
5840
+ LSP_TOOLS_MCP_USER_CONFIG: canonicalizePath(env["LSP_TOOLS_MCP_USER_CONFIG"]),
5841
+ LSP_TOOLS_MCP_INSTALL_DECISIONS: canonicalizePath(env["LSP_TOOLS_MCP_INSTALL_DECISIONS"])
5842
+ };
5843
+ }
5844
+ function canonicalizePathList(value) {
5845
+ if (value === undefined)
5846
+ return;
5847
+ return value.split(delimiter4).map((entry) => canonicalizePath(entry) ?? entry).join(delimiter4);
5848
+ }
5849
+ function canonicalizePath(value) {
5850
+ if (value === undefined || !isAbsolute4(value) || !existsSync11(value))
5851
+ return value;
5852
+ return realpathSync4(value);
5853
+ }
5854
+ function projectRootFromOpenCodeConfigPath(path2) {
5855
+ if (basename3(path2) !== "lsp.json" && basename3(path2) !== "lsp-client.json")
5856
+ return;
5857
+ const configDir = dirname9(path2);
5858
+ const configDirName = basename3(configDir);
5859
+ if (configDirName !== ".opencode" && configDirName !== ".omo")
5860
+ return;
5861
+ return dirname9(configDir);
3763
5862
  }
3764
5863
  export {
5864
+ validateDaemonVersion,
3765
5865
  runMcpStdioProxy,
5866
+ resolveDaemonRuntime,
5867
+ probeDaemon,
3766
5868
  ensureDaemonRunning,
3767
5869
  disposeDefaultLspManager,
3768
5870
  daemonPaths,
3769
5871
  currentRequestContext,
3770
5872
  callToolViaDaemon,
3771
- callDiagnosticsViaDaemon
5873
+ callDiagnosticsViaDaemon,
5874
+ OMO_LSP_DAEMON_VERSION,
5875
+ OMO_LSP_DAEMON_DIR,
5876
+ OMO_LSP_DAEMON_CLI,
5877
+ InvalidRuntimeOverrideError,
5878
+ InvalidDaemonVersionError,
5879
+ InvalidDaemonDirectoryError
3772
5880
  };